Skip to content

refactor(semantics): single-source five duplicated multi-value vocabularies - #4617

Merged
huangruiteng merged 3 commits into
loopx-project:mainfrom
songoow:feat/track-a-single-source-multi-value-vocabularies
Sep 17, 2026
Merged

huangruiteng merged 3 commits into
loopx-project:mainfrom
songoow:feat/track-a-single-source-multi-value-vocabularies

Conversation

@songoow

@songoow songoow commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator

动机

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 所在模块导入,所以改为导入不引入新依赖、不动分层、不产生循环:

常量 owner(保留唯一定义) 消费方(改为导入 / 删除) 依赖方向
TERMINAL_PR_STATES capabilities/issue_fix/pr_lifecycle.py capabilities/issue_fix/pr_gate_reconcile.py 已有 from .pr_lifecycle import ...
BRANCH_REPLAN_MERGE_STATES capabilities/issue_fix/pr_lifecycle.py capabilities/issue_fix/outcome_projection.py 同包内新增一行导入(pr_lifecycle 只依赖 public_safety 与 metadata_preview,无环)
SUPPORT_ASPECTS capabilities/issue_fix/repository_memory.py capabilities/issue_fix/repository_context.py 已有 from .repository_memory import ...
CAPTURE_SCOPES control_plane/work_items/operator_inbox.py extensions/lark/event_inbox.py 已有 from ...control_plane.work_items.operator_inbox import ...
REGISTRY_DIRS goal_mode_context.py claude_goal_mode/hooks/goal_state.py hook 已导入 find_registry as _find_registry

第五处与前四处不同,值得单独说明:hook 里的 REGISTRY_DIRS 从未被读取——hook 自己的 find_registry 只是 return _find_registry(cwd),登记目录的顺序由 owner 决定。所以正确的修法是删除该死代码,而不是 re-export 一个没人用的名字(保留会触发 F401);仓库内也没有任何模块从该 hook 导入这个常量。

具体改动

7 文件、+13/−16(净减 3 行):

  • 4 个消费方删除本地字面量,改从 owner 导入,并把新名字并入既有的同源 import 语句,而不是新增一行——同一来源一个语句,避免读者以为是两个来源
  • 1 处死代码删除(goal_state.py 的 REGISTRY_DIRS)
  • loopx/semantics/vocabulary_v0.json:inventory_ratchets.multi_value_twins 19 → 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.py ok;multi_value_twins=13/13(改前 18/19)
四个名字与 owner 同对象 is 断言 True(4/4);hook 侧确认 REGISTRY_DIRS 已不再绑定,find_registry 仍委托 owner
ruff check 五个改动源文件 All checks passed(F401 不会再出现)
pytest tests/architecture 288 passed
pytest(issue_fix / goal_mode / lark / change_quality 定向) 212 passed
loopx canary premerge --from-git-diff --git-diff-base upstream/main passed,19 项检查,failures=0,manual_holds=0

覆盖率说明:pr_gate_reconcile、outcome_projection、repository_context、lark event_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_STATES and BRANCH_REPLAN_MERGE_STATES belong to pr_lifecycle, SUPPORT_ASPECTS to repository_memory, CAPTURE_SCOPES to the operator-inbox contract, and the goal-state hook's REGISTRY_DIRS was dead code because its find_registry already delegates to goal_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 with is). 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 clean upstream/main and is unrelated.

Refs #4447

🤖 Generated with pi

…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>
@songoow

songoow commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator Author

exact-head 复核(f83b05d33)— 同步 main,失败已消

之前三项红:test-shard (2) / pytest / merge-gate。根因不在本 PR:

unreviewed finding: module_metric_budget:loopx/chat_actions.py
module metric debt: refresh the reviewed ceiling in module_metric_baseline.json

loopx/chat_actions.py 的模块度量天花板在 main 上已由 #4618 刷新,而本分支落后 main 10 个提交、缺这个提交。本 PR 改的七个文件里没有 chat_actions.py。

合并 main(干净,无冲突)后在本地合并树上复跑:tests/canary/test_maintainability_ratchet.py 10 passed。合并提交带 DCO 签名。

…ource-multi-value-vocabularies

Signed-off-by: song <22676124+songoow@users.noreply.github.com>
@songoow

songoow commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator Author

本 PR 在 #4447 计划中的位置

issue #4447 现在有一节统一协调(中英双语),把这 13 个在开 PR 作为一个计划列出:各自修什么、为何必要、以及实测出的合并顺序。

冲突实测:对全部 78 对做了试合并,9 对冲突,分四簇,每一处都是文本相邻,没有一处是语义分歧。

冲突簇 涉及 PR 后合并者的解法
棘轮锚点 #4606、#4608、#4617、#4629 四者全部合并后的终态已实测:same_runtime_forks 18、same_runtime_fork_definitions 41、same_runtime_forks_semantic 11、conflicting_values 16、conflicting_definitions 55、multi_value_twins 13。registry 与 BUDGET_ANCHOR 两处都要带同一组值(该检查是相等而非 <=)
漂移测试文件 #4626、#4629、#4631 三者都在文件末尾追加,全部保留即可
RFC 双镜像 #4614、#4627、#4631 附录 B 决策行,按日期先后保留两条
清单与生成器 #4614、#4630 加法式:owner 对过滤与改名不变性证据同时保留

建议顺序(代价从低到高):#4628 → #4625、#4626 → #4627 → #4619、#4621 → #4630 → #4614 → #4631 → #4629 → #4617 → #4606 → #4608。四个棘轮 PR 放最后,因为每落地一个,下一个的数字就从估算变成确定值。

全部 13 个 PR 现已同步到 main、零失败检查。

@huangruiteng huangruiteng left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

详细评审 — 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.

@huangruiteng
huangruiteng merged commit e1a97c2 into loopx-project:main Sep 17, 2026
27 checks passed
songoow added a commit to songoow/loopx that referenced this pull request Sep 17, 2026
…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>
huangruiteng pushed a commit that referenced this pull request Sep 17, 2026
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>
@songoow
songoow deleted the feat/track-a-single-source-multi-value-vocabularies branch September 28, 2026 03:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants