refactor(status): single-source the source-registry shadow findings vocabulary - #4517
Conversation
…ocabulary Refs loopx-project#4447 (Track A) SOURCE_REGISTRY_SHADOW_FINDINGS had two active definitions with identical values: one in loopx/status.py and one in the control_plane status projection. Both were passed as the source_registry_shadow_findings argument and both reached the only reader, work_items/attention_queue.py:77, so the fork is same_semantics rather than two independent contracts. The projection keeps the only definition and loopx/status.py imports it. The dependency direction stays outer -> control_plane: status.py already imports sibling control_plane.status modules at module level and the projection has no import back, so no cycle is introduced. status.SOURCE_REGISTRY_SHADOW_FINDINGS is deliberately kept as a public name because examples/control_plane/attention-queue-readmodel-smoke.py reads it; removing the re-export would break an existing caller for no gain. Values are unchanged, so no behavior changes. The two further copies under deprecate/benchmark-legacy/ are outside the active scan scope and are left alone. The remaining forks in this checklist row are not classified here. Signed-off-by: YZJF <195568136+YZJF@users.noreply.github.com>
huangruiteng
left a comment
There was a problem hiding this comment.
动机
#4447 Track A 把 SOURCE_REGISTRY_SHADOW_FINDINGS 判为同名同值的 fork:loopx/status.py 与 loopx/control_plane/status/registry_health_projection.py 各自持有一份内容完全相同的两值集合,而它们喂给的是 attention-queue read model 的同一个 source_registry_shadow_findings 参数。两份字面量只要有一处以后被改动,读模型接受的 finding 就会和生产者发出的种类不一致。
改动思路
既然语义相同、且投影本来就是该 read model 输入词汇的 owner,就把定义收在投影里(registry_health_projection.py:28 保持不变),loopx/status.py 改为 re-export 同一个对象,保证既有调用方(examples/control_plane/attention-queue-readmodel-smoke.py 通过 status_module.SOURCE_REGISTRY_SHADOW_FINDINGS 取值)不需要改动。新增 3 个用例分别钉住取值集合、对象同一性(is)以及"status 里不得再出现第二份定义"。
具体改动
loopx/status.py(+6/-4):删除本地字面量,改为从投影导入并在原位置继续使用(第 16 行导入、第 1093 行传入 read model),并加注释说明这是 re-export 而不是第二份定义。tests/control_plane/test_source_registry_shadow_findings.py(新增 42 行):test_vocabulary_is_unchanged、test_status_shares_the_single_definition(is同一性)、test_status_defines_no_second_copy(源码里不得再出现stale_source_registry)。
关键代码讲解
loopx/control_plane/status/registry_health_projection.py:28—SOURCE_REGISTRY_SHADOW_FINDINGS = ('source_registry_missing', 'stale_source_registry'):现在是全仓唯一的活动定义,并且在同文件第 53 行直接喂给 read model,因此"owner 与消费点在同一模块"这件事成立。loopx/status.py:16—from .control_plane.status.registry_health_projection import SOURCE_REGISTRY_SHADOW_FINDINGS:re-export 是这次改动的兼容性关键。我确认了消费点仍在loopx/status.py:1093,且 shipped smokeexamples/control_plane/attention-queue-readmodel-smoke.py:62/:95仍以status_module.SOURCE_REGISTRY_SHADOW_FINDINGS读取——如果直接删掉名字而不 re-export,这个 smoke 会立刻失败。tests/control_plane/test_source_registry_shadow_findings.py:35—status.SOURCE_REGISTRY_SHADOW_FINDINGS is registry_health_projection.SOURCE_REGISTRY_SHADOW_FINDINGS:用对象同一性而不是值相等来防 fork,这一点比"值仍然相等"强得多,因为未来的漂移往往从"又是一份字面量"开始。tests/control_plane/test_source_registry_shadow_findings.py:41—assert "stale_source_registry" not in inspect.getsource(status):这条最直接,但也是我唯一提出的问题(见下):它检查的是源码文本,而不是赋值结构。
对主干的风险
没有阻塞项。 这是一次同语义去重:取值集合未变(('source_registry_missing', 'stale_source_registry')),loopx/status.py:1093 与 read-model smoke 的消费路径都保留;我全仓搜索确认活动代码里只剩投影这一处声明(其余命中是 deprecate/benchmark-legacy/ 下的归档副本、global_registry_health.py 里作为生产者发出的 kind、以及 example smoke 里作为数据使用的字符串,都不构成第二份词汇表)。分支与当前 main 合并干净。
P3(非阻断,断言形式):test_status_defines_no_second_copy 断言的是 "stale_source_registry" not in inspect.getsource(status)。这意味着以后有人在 loopx/status.py 的注释或 docstring 里提到这个取值(例如解释 re-export 的来源),测试会因为散文而失败,而不是因为真的出现了第二份定义——这类假阳性会诱使后来者删注释而不是复核 fork。最小修复是改成检查解析后的模块级赋值目标(或把检查范围限定在赋值语句),保留"重新引入字面量就失败"的能力。
验证(全部在 be53db7d9 上跑):pytest -q tests/control_plane/test_source_registry_shadow_findings.py → 3 passed;python examples/control_plane/attention-queue-readmodel-smoke.py → attention-queue-readmodel-smoke ok;git merge-tree --write-tree HEAD origin/main → 无冲突。按本 lane 配置不拉取 CI。
我的整体评价
APPROVE。这是"删除重复权威"这一类里最干净的一种:owner 已经在正确的模块(投影就是 read model 输入的来源),改动只是把另一份相同字面量换成 re-export,并用对象同一性测试把 fork 的可能性真正关掉;同时它照顾到了既有消费方,因此没有连带 churn。我有意把它与"值相等"型断言区分开评价:is 断言是这次最值钱的三行。唯一的 P3 是那条源码文本断言的形式,属于可随手收紧的细节,不影响正确性。
English verdict: APPROVE — exact head be53db7d9ffe1313e8999e2aeb2fb0db0d1b24d0 of #4517. The duplicate SOURCE_REGISTRY_SHADOW_FINDINGS vocabulary is merged into its existing owner (loopx/control_plane/status/registry_health_projection.py:28) and loopx/status.py now re-exports the same object, so the attention-queue read model keeps receiving identical findings while a second literal can no longer drift. Validation at this head: the 3 new tests pass (value set, is identity, no re-introduced literal), the shipped consumer smoke examples/control_plane/attention-queue-readmodel-smoke.py prints ok, and the branch merges cleanly with current main. A tree-wide search shows no remaining active declaration outside the projection. One non-blocking P3: the anti-fork assertion inspects module source text, so a future comment mentioning the finding name would fail it without a real re-fork.
Refs #4447 (Track A) — checklist row: "Classify the 19 historical multi-value twins and 18 semantic string forks; retain justified independent contracts."
This is one bounded sub-item, not completion of the row and not completion of the tracker. It must not close #4447.
Classification:
SOURCE_REGISTRY_SHADOW_FINDINGSissame_semanticsThe name has four definitions on
main(a28562e97):loopx/status.py:289{source_registry_missing, stale_source_registry}loopx/control_plane/status/registry_health_projection.py:28deprecate/benchmark-legacy/.../benchmark_status_runner.py:218deprecate/benchmark-legacy/.../benchmark_status_compaction.py:185Evidence that the active pair is one concept rather than two contracts:
{source_registry_missing, stale_source_registry}.source_registry_shadow_findingskeyword argument.work_items/attention_queue.py:77, which is the only place the value is read (if finding.get("kind") in source_registry_shadow_findings and live_items).Per the issue's own rule, only confirmed
same_semanticsgroups become merge work — this one qualifies, so it is merged.Change
registry_health_projection.pykeeps the only definition;loopx/status.pyimports and re-exports it.control_plane.status.pyalready imports.control_plane.status.collectionand.control_plane.status.runtime_summariesat module level, andregistry_health_projection.pyhas no import back intostatus.py, so no cycle is introduced.status.SOURCE_REGISTRY_SHADOW_FINDINGSis kept as a public name on purpose:examples/control_plane/attention-queue-readmodel-smoke.py:62,95reads it asstatus_module.SOURCE_REGISTRY_SHADOW_FINDINGS. Removing the re-export would be an unforced break of an existing caller, which the row's "caller parity" requirement argues against.Verification
tests/control_plane/test_source_registry_shadow_findings.pylocks three things: the admitted values are unchanged,loopx.statusexposes the same object as the projection rather than a copy, and no literal can be reintroduced intoloopx.status.examples/control_plane/attention-queue-readmodel-smoke.py→ ok (this is the caller that reads the re-exported name).ruff checkclean on both changed files.tests/control_plane -k "status or registry or attention or queue"— no new failures versus baseline.Out of scope
The remaining 18 semantic forks and 19 multi-value twins in that row are not classified here; each needs its own caller check. The two
deprecate/benchmark-legacy/copies are left alone. Basea28562e97, Python 3.13.12.