From be53db7d9ffe1313e8999e2aeb2fb0db0d1b24d0 Mon Sep 17 00:00:00 2001 From: YZJF <195568136+YZJF@users.noreply.github.com> Date: Wed, 16 Sep 2026 17:11:13 +0800 Subject: [PATCH] refactor(status): single-source the source-registry shadow findings vocabulary Refs #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> --- loopx/status.py | 10 +++-- .../test_source_registry_shadow_findings.py | 42 +++++++++++++++++++ 2 files changed, 48 insertions(+), 4 deletions(-) create mode 100644 tests/control_plane/test_source_registry_shadow_findings.py diff --git a/loopx/status.py b/loopx/status.py index fb021fdcd8..d0a671d727 100644 --- a/loopx/status.py +++ b/loopx/status.py @@ -9,6 +9,12 @@ StatusCollectionContext, collect_status as _collect_status_read_model, ) +# Refs #4447: one definition for this vocabulary. The control_plane projection +# owns it because it feeds the attention-queue read model; this module keeps +# re-exporting the name for existing callers. +from .control_plane.status.registry_health_projection import ( + SOURCE_REGISTRY_SHADOW_FINDINGS, +) from .control_plane.status.runtime_summaries import ( StatusRuntimeSummaryContext, build_status_runtime_summaries as _build_status_runtime_summaries_read_model, @@ -286,10 +292,6 @@ CONNECTED_DELIVERY_ADAPTER_STATUSES = { "connected-delivery", } -SOURCE_REGISTRY_SHADOW_FINDINGS = { - "source_registry_missing", - "stale_source_registry", -} PLANNED_CONTROLLER_OPT_IN_RECOMMENDED_ACTION = ( "先在 LoopX 完成 operator 判断;同意后项目 Agent 只执行 read-only map dry-run" ) diff --git a/tests/control_plane/test_source_registry_shadow_findings.py b/tests/control_plane/test_source_registry_shadow_findings.py new file mode 100644 index 0000000000..53988618ea --- /dev/null +++ b/tests/control_plane/test_source_registry_shadow_findings.py @@ -0,0 +1,42 @@ +"""Refs #4447 (Track A): one definition for the source-registry shadow vocabulary. + +`SOURCE_REGISTRY_SHADOW_FINDINGS` used to be defined in two active modules with +identical values, feeding the same `source_registry_shadow_findings` parameter of +the attention-queue read model. Classifying that fork as `same_semantics` made it +merge work: the control_plane projection now owns the only definition and +`loopx.status` re-exports it, so the two copies can no longer drift apart. + +`loopx.status` keeps re-exporting the name because +`examples/control_plane/attention-queue-readmodel-smoke.py` reads it as +`status_module.SOURCE_REGISTRY_SHADOW_FINDINGS`. +""" + +from __future__ import annotations + +import inspect + +from loopx import status +from loopx.control_plane.status import registry_health_projection + +EXPECTED_FINDINGS = {"source_registry_missing", "stale_source_registry"} + + +def test_vocabulary_is_unchanged() -> None: + """Merging the fork must not change the admitted values.""" + assert ( + set(registry_health_projection.SOURCE_REGISTRY_SHADOW_FINDINGS) + == EXPECTED_FINDINGS + ) + + +def test_status_shares_the_single_definition() -> None: + """`loopx.status` re-exports the projection's set instead of owning a copy.""" + assert ( + status.SOURCE_REGISTRY_SHADOW_FINDINGS + is registry_health_projection.SOURCE_REGISTRY_SHADOW_FINDINGS + ) + + +def test_status_defines_no_second_copy() -> None: + """A literal reintroduced into `loopx.status` would fork the vocabulary again.""" + assert "stale_source_registry" not in inspect.getsource(status)