refactor(semantics): single-source five duplicated multi-value vocabularies - #4617
Conversation
…laries Track A for loopx-project#4447: five closed sets were defined twice with identical values, once in the module that owns the concept and again in a consumer. The values match today, so neither module fails on its own; a later value change would have two places to land and a reader would find only one. Import the owner's name in the four consumers: TERMINAL_PR_STATES and BRANCH_REPLAN_MERGE_STATES from pr_lifecycle, SUPPORT_ASPECTS from repository_memory, and CAPTURE_SCOPES from the control-plane operator inbox contract. Each consumer already imported from that module, so no dependency direction moves. The fifth copy is dead rather than duplicated: the goal_state hook bound its own REGISTRY_DIRS but never read it, because its find_registry already delegates to goal_mode_context.find_registry. Delete it rather than re-export an unused name; nothing imports the constant from the hook. Semantic multi-value twins 18 -> 13. The residual is the six status-facade duplicates owned by the status single-sourcing batch and the seven registered cross_runtime pairs whose Python and TypeScript owners both carry the value set by contract. Budgets and the smoke anchor move together, and the ratchet note records that split so a reviewer cannot read the residual as removable slack. Refs loopx-project#4447 Signed-off-by: song <liusongstep@gmail.com>
…ource-multi-value-vocabularies Signed-off-by: song <22676124+songoow@users.noreply.github.com>
exact-head 复核(
|
…ource-multi-value-vocabularies Signed-off-by: song <22676124+songoow@users.noreply.github.com>
本 PR 在 #4447 计划中的位置issue #4447 现在有一节统一协调(中英双语),把这 13 个在开 PR 作为一个计划列出:各自修什么、为何必要、以及实测出的合并顺序。 冲突实测:对全部 78 对做了试合并,9 对冲突,分四簇,每一处都是文本相邻,没有一处是语义分歧。
建议顺序(代价从低到高):#4628 → #4625、#4626 → #4627 → #4619、#4621 → #4630 → #4614 → #4631 → #4629 → #4617 → #4606 → #4608。四个棘轮 PR 放最后,因为每落地一个,下一个的数字就从估算变成确定值。 全部 13 个 PR 现已同步到 |
huangruiteng
left a comment
There was a problem hiding this comment.
详细评审 — exact head 227d81e4a89cf17f65df79ee9152f5caa8022d57
动机
Track A(#4447):五个封闭集合被定义了两次——一次在拥有该概念的模块,一次在消费者——值今天一致,所以两边单独看都不会报错,但未来改动有两个落点、读者只会找到其中一个。作者的判断准确,而且这次特别点出了第五个的性质不同:它不是重复,而是死绑定。
改动思路
四处用 owner 的导入替换复制,且不移动依赖方向(每个消费者本来就从 owner 导入);第五处 loopx/claude_goal_mode/hooks/goal_state.py 的 REGISTRY_DIRS 从未被读取(注册表目录常量真正由 goal_mode_context.py 拥有与使用,hook 的 find_registry 只是委派),因此选择删除而不是保留死再导出。同时把 multi_value_twins 锚点从 19 下调到 13,并在注册表里新增 multi_value_twins_note,说明残余的 13 条为什么不是"可继续砍的余量"(6 条属于状态门面批次、7 条是 I3 要求的 Python/TypeScript cross_runtime 对)——这条 note 很关键,它避免后来的评审把残余读成松懈。
具体改动
loopx/capabilities/issue_fix/pr_gate_reconcile.py:导入TERMINAL_PR_STATES,删除本地{"MERGED","CLOSED"}。loopx/capabilities/issue_fix/outcome_projection.py:导入BRANCH_REPLAN_MERGE_STATES,删除本地{"BEHIND","DIRTY"}。loopx/capabilities/issue_fix/repository_context.py:导入SUPPORT_ASPECTS,删除本地五元集合。loopx/extensions/lark/event_inbox.py:从control_plane/work_items/operator_inbox导入CAPTURE_SCOPES,删除本地{"addressed_only","configured_chat_all"}。loopx/claude_goal_mode/hooks/goal_state.py:删除从未读取的REGISTRY_DIRS。examples/semantic-vocabulary-drift-smoke.py、loopx/semantics/vocabulary_v0.json:锚点 19→13,新增说明性 note。
验证(都在本 head):导入 owner 与消费者后逐个比对集合——TERMINAL_PR_STATES = {CLOSED, MERGED}、BRANCH_REPLAN_MERGE_STATES = {BEHIND, DIRTY}、SUPPORT_ASPECTS = 五元集合、CAPTURE_SCOPES = {addressed_only, configured_chat_all},四处完全一致;全仓库检索确认没有任何模块从被改的消费者模块导入这些名字,也没有任何地方引用被删掉的 REGISTRY_DIRS(真正 owner 仍在 goal_mode_context.py:14/24 定义并使用)。测试:三个架构文件 106 passed、issue-fix/Lark/goal-mode 相关能力与扩展用例 83 passed;drift smoke ok,实测 multi_value_twins=13/13(旧锚点 19)。
对主干的风险
唯一的模块级 API 变化是删掉 hook 里那个从未读取的常量,我已按"所有导入方"逐个核对(无引用),且模块在本 head 正常导入。四处的值一致性由直接导入比对确认,依赖方向未变,锚点下调方向是收紧。残余 13 条 twin 由新增 note 明确归因,不会让下一个读者误以为还能继续砍。
我的整体评价
APPROVE。 范围小、验证可复算、方向正确;把"重复"和"死代码"区分开处理,并给下调后的预算配了说明性 note,这是这类改动应有的完整形态。合并权仍在维护者。
English verdict: APPROVE - reviewed exact head 227d81e. Four consumers now import their vocabulary from the owning module and the fifth copy is deleted because it was dead rather than duplicated. I verified the owners and consumers expose identical sets at this head (TERMINAL_PR_STATES {CLOSED,MERGED}, BRANCH_REPLAN_MERGE_STATES {BEHIND,DIRTY}, SUPPORT_ASPECTS five members, CAPTURE_SCOPES {addressed_only,configured_chat_all}), that no module imports those names from the consumer modules, and that the deleted REGISTRY_DIRS has no importer while its real owner goal_mode_context still defines and uses it (the hook's find_registry already delegates). Validation at this head: 106 passed across the semantic drift, semantic production and import-boundary tests, 83 passed across the issue-fix, Lark and goal-mode suites, and the drift smoke reports multi_value_twins=13/13 against the lowered anchor (19 before). The new multi_value_twins_note explains why the residual is not removable slack, which is the right companion for a lowered budget.
…ratchets loopx-project#4606 and loopx-project#4617 merged while this branch was open, and both sides tightened ratchets in the same two anchored blocks. Resolved by taking the tighter side of each, the only resolution budgets_only_decrease allows: - same_runtime_forks 25 -> 20 and same_runtime_fork_definitions 58 -> 47 from main (loopx-project#4606 single-sourced six status constants). - conflicting_values 18 -> 16 and conflicting_definitions 59 -> 55 from this branch, which is what it exists to lock. multi_value_twins takes main's 13 from loopx-project#4617, which is tighter than the 19 this branch left in place for exactly that reason. Registry and BUDGET_ANCHOR carry the same values, as the equality check requires. Measured after the merge: every counter this branch owns is at budget, conflicting_values=16/16 and conflicting_definitions=55/55. Signed-off-by: song <22676124+songoow@users.noreply.github.com>
conflicting_values 18->16 and conflicting_definitions 59->55 pin the measured values #4499 earned; both sides of each budget move in this one diff so the equality anchor holds. multi_value_twins stays 19 here: the multi-value single-source batch (#4617) owns that counter and locks it to 13. The inventory report line now discloses unlocked headroom (slack=key=N). The guard only fails on overflow, so a merge that reverts a tightened budget (registry and anchor move back together, the merge-trap shape) used to pass silently; now the reopened headroom is visible in the smoke output and in the PR diff of any run after it. Signed-off-by: song <liusongstep@gmail.com>
动机
Track A(收敛已测量的语义债)后继项,承接 #4606(status façade 六个常量)与 #4608(Todo task-class 词汇)。
主干上 inventory 的
multi_value_twins(同名、同值、多份定义的 closed set / Literal / as-const 数组)是 18 个,其中 6 个属于 status façade 批次、7 个是已注册的cross_runtime词汇(Python 与 TypeScript 两个 owner 按契约各自持有同一值集,属于迁移 RFC 的范围)。剩下 5 个是纯重复:值和 owner 逐字一致,只是消费方各自又声明了一遍。代价不在今天——五处值相同,读起来一致,没有任何东西会失败;代价在未来:改一处不会被另一处发现,而这种复制无法通过阅读任一模块察觉。RFC 第 5 节的 multi-value 冲突规则针对的正是"同一概念多种拼写"。
改动思路
入口是五处重复声明。四个消费方本来就在从 owner 所在模块导入,所以改为导入不引入新依赖、不动分层、不产生循环:
TERMINAL_PR_STATEScapabilities/issue_fix/pr_lifecycle.pycapabilities/issue_fix/pr_gate_reconcile.pyfrom .pr_lifecycle import ...BRANCH_REPLAN_MERGE_STATEScapabilities/issue_fix/pr_lifecycle.pycapabilities/issue_fix/outcome_projection.pypr_lifecycle只依赖public_safety与metadata_preview,无环)SUPPORT_ASPECTScapabilities/issue_fix/repository_memory.pycapabilities/issue_fix/repository_context.pyfrom .repository_memory import ...CAPTURE_SCOPEScontrol_plane/work_items/operator_inbox.pyextensions/lark/event_inbox.pyfrom ...control_plane.work_items.operator_inbox import ...REGISTRY_DIRSgoal_mode_context.pyclaude_goal_mode/hooks/goal_state.pyfind_registry as _find_registry第五处与前四处不同,值得单独说明:hook 里的
REGISTRY_DIRS从未被读取——hook 自己的find_registry只是return _find_registry(cwd),登记目录的顺序由 owner 决定。所以正确的修法是删除该死代码,而不是 re-export 一个没人用的名字(保留会触发 F401);仓库内也没有任何模块从该 hook 导入这个常量。具体改动
7 文件、+13/−16(净减 3 行):
goal_state.py的REGISTRY_DIRS)loopx/semantics/vocabulary_v0.json:inventory_ratchets.multi_value_twins19 → 13,同时收紧到实测值(顺带吃掉 1 个单位的陈旧余量),并新增multi_value_twins_note记录残留的 13 个由哪两类构成,防止后续 reviewer 把它读成可以随便删的余量examples/semantic-vocabulary-drift-smoke.py:BUDGET_ANCHOR["multi_value_twins"]19 → 13关键代码讲解
pr_gate_reconcile.py:TERMINAL_PR_STATES原本紧跟在 schema 版本常量之后,容易让人以为它是本模块的词汇。删除后并入from .pr_lifecycle import (...),与build_issue_fix_pr_lifecycle_monitor_packet同源。outcome_projection.py:BRANCH_REPLAN_MERGE_STATES = {"BEHIND", "DIRTY"}用于判断"分支落后或有冲突"→branch_stale_or_conflicted。该判定本质是 PR lifecycle 状态机的语义,owner 在pr_lifecycle;本模块只是消费它。repository_context.py:SUPPORT_ASPECTS与同文件的REQUIRED_FIX_ASPECTS是两个不同集合,只有前者重复(后者本就是本模块自己的子集约束),所以只删前者、保留后者。goal_state.py:只删常量、不动find_registry委托,行为零变化。对主干的风险
无阻塞项。
examples/semantic-vocabulary-drift-smoke.pyok;multi_value_twins=13/13(改前 18/19)is断言 True(4/4);hook 侧确认REGISTRY_DIRS已不再绑定,find_registry仍委托 ownerruff check五个改动源文件pytest tests/architecturepytest(issue_fix / goal_mode / lark / change_quality 定向)loopx canary premerge --from-git-diff --git-diff-base upstream/mainpassed,19 项检查,failures=0,manual_holds=0覆盖率说明:
pr_gate_reconcile、outcome_projection、repository_context、larkevent_inbox、goal_state、pr_lifecycle、operator_inbox各自都有 catalog canary 或定向测试命中(issue-fix-pr-lifecycle、issue-fix-outcome-projection、issue-fix-repository-context、lark-event-inbox、lark-event-collector-lifecycle、dsh/kunluncode goal mode),因此本次用"改动文件编译 + catalog 命中 + 定向用例集"而不是另加断言重复的测试。一项 advisory(非本 diff 引入):
examples/control_plane/control-plane-maintainability-ratchet-smoke.py报module_metric_budget:loopx/chat_actions.py。我在干净upstream/main(809f0cfc3)上单独跑了同一 smoke,输出逐字相同,且本 diff 不触碰loopx/chat_actions.py与module_metric_baseline.json。负向走查:若再次在任一消费方复制这 5 个名字之一,该名字在 inventory 里重新变成"同名多份定义",
multi_value_twins变为 14,超过收紧后的预算 13,smoke 失败并打印inventory multi_value_twins grew to 14; budget is 13 (unreviewed)。最小修复是删除复制、改用 owner 导入,而不是提高预算。变更体量相称:净减 5 处重复声明、机制成本为零(无新依赖、无新检查、无行为变更)。
English verdict: APPROVE — exact head
668d5385a. Five closed sets were restated in a consumer while the owning module already held them:TERMINAL_PR_STATESandBRANCH_REPLAN_MERGE_STATESbelong topr_lifecycle,SUPPORT_ASPECTStorepository_memory,CAPTURE_SCOPESto the operator-inbox contract, and the goal-state hook'sREGISTRY_DIRSwas dead code because itsfind_registryalready delegates togoal_mode_context. All values were byte-identical, so nothing failed; the defect is that a future change has two places to land and a reader finds only one. Every consumer already imported from the owning module, so no dependency direction moves, and each import resolves to the owner's object (verified withis). Semantic multi-value twins 18→13; budgets and the smoke anchor move together, and the new ratchet note records why the residual pairs are not removable slack. The one smoke failure in the gate (control-plane-maintainability-ratchet-smoke.py,loopx/chat_actions.py) reproduces byte-identically on cleanupstream/mainand is unrelated.Refs #4447
🤖 Generated with pi