From 370ece7709739006e8ffb7f611055bab98e4beef Mon Sep 17 00:00:00 2001 From: huangruiteng <14976749+huangruiteng@users.noreply.github.com> Date: Wed, 16 Sep 2026 09:23:29 +0800 Subject: [PATCH] fix(semantics): keep retired migration vocabulary out of the repository map The generated semantic inventory copied migration-only constants into the repository-wide vocabulary map. That misstated which module owns the vocabulary today and republished retired hierarchy tokens into a derived artifact outside the migration boundary, so examples/control_plane/peer-agent-hard-cut-boundary-smoke.py failed on main with LEGACY_HIERARCHY_ROLES and TODO_REMOVED_REVIEW_CONTINUATION_POLICY_VALUES. Name the retired carriers explicitly instead of matching on substrings, keep the omission visible in the rendered inventory, and pin the rule with a characterization test. Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com> --- loopx/semantics/inventory.py | 30 ++++++++++++ loopx/semantics/inventory_v0.json | 8 ++-- tests/architecture/test_semantic_inventory.py | 46 +++++++++++++++++++ 3 files changed, 81 insertions(+), 3 deletions(-) diff --git a/loopx/semantics/inventory.py b/loopx/semantics/inventory.py index 02a05716ea..48f43d7486 100644 --- a/loopx/semantics/inventory.py +++ b/loopx/semantics/inventory.py @@ -23,6 +23,23 @@ SOURCE_SUFFIXES = (".py", ".ts") SKIP_PARTS = frozenset({"__pycache__", "node_modules"}) +# Constants that exist only to read or reject vocabulary retired by a one-way +# migration. Their values are not current semantic ownership, so the repository +# map must not republish them: listing them misstates which module owns the +# vocabulary today and copies retired hierarchy tokens into a derived artifact +# outside the migration boundary the peer-agent guard enforces. Each pair is +# explicit rather than name-matched so a new carrier cannot be hidden by +# accident, and the skipped pairs stay visible in the rendered inventory. +RETIRED_VOCABULARY_CARRIERS = frozenset( + { + ("loopx/control_plane/agents/legacy_migration.py", "LEGACY_HIERARCHY_ROLES"), + ( + "loopx/control_plane/todos/contract.py", + "TODO_REMOVED_REVIEW_CONTINUATION_POLICY_VALUES", + ), + } +) + UPPER_NAME = re.compile(r"^[A-Z][A-Z0-9_]*$") SCHEMA_VERSION_NAME = re.compile(r"SCHEMA_VERSION$") # Names that are a per-module convention rather than shared vocabulary: every @@ -253,6 +270,10 @@ def _sorted_entries(entries: list[dict[str, Any]]) -> list[dict[str, Any]]: return sorted(entries, key=lambda entry: (entry["module"], entry["name"])) +def _is_retired_vocabulary(entry: dict[str, Any]) -> bool: + return (entry.get("module"), entry.get("name")) in RETIRED_VOCABULARY_CARRIERS + + def build_inventory( repo_root: Path, root: str = DEFAULT_ROOT, sources: list[SourceFile] | None = None ) -> dict[str, Any]: @@ -275,6 +296,12 @@ def build_inventory( const_arrays.extend(facts["const_arrays"]) string_constants.extend(facts["string_constants"]) + enums = [entry for entry in enums if not _is_retired_vocabulary(entry)] + closed_sets = [entry for entry in closed_sets if not _is_retired_vocabulary(entry)] + literal_aliases = [entry for entry in literal_aliases if not _is_retired_vocabulary(entry)] + const_arrays = [entry for entry in const_arrays if not _is_retired_vocabulary(entry)] + string_constants = [entry for entry in string_constants if not _is_retired_vocabulary(entry)] + multi_value = multi_value_carriers( enums=enums, closed_sets=closed_sets, @@ -307,6 +334,9 @@ def build_inventory( "Structural map only. Consumer counts are printed by the generator's --report " "and are not committed. Single-module string constants are counted, not listed." ), + "retired_vocabulary_excluded": _sorted_entries( + {"module": module, "name": name} for module, name in RETIRED_VOCABULARY_CARRIERS + ), "python_enums": _sorted_entries(enums), "python_closed_sets": _sorted_entries(closed_sets), "python_literal_aliases": _sorted_entries(literal_aliases), diff --git a/loopx/semantics/inventory_v0.json b/loopx/semantics/inventory_v0.json index 6e8bdf0ff3..27eda468f1 100644 --- a/loopx/semantics/inventory_v0.json +++ b/loopx/semantics/inventory_v0.json @@ -3,6 +3,10 @@ "root": "loopx", "generator": "scripts/generate_semantic_inventory.py", "advisory": "Structural map only. Consumer counts are printed by the generator's --report and are not committed. Single-module string constants are counted, not listed.", + "retired_vocabulary_excluded": [ + {"module": "loopx/control_plane/agents/legacy_migration.py", "name": "LEGACY_HIERARCHY_ROLES"}, + {"module": "loopx/control_plane/todos/contract.py", "name": "TODO_REMOVED_REVIEW_CONTINUATION_POLICY_VALUES"} + ], "python_enums": [ {"name": "BenchmarkContinuationDecision", "module": "loopx/capabilities/benchmark_toolkit/continuation.py", "values": ["continue", "stop_complete", "stop_progress_regression", "stop_prompt_mismatch", "stop_round_limit", "stop_task_shape_mismatch", "stop_time_budget"]}, {"name": "NetworkRequestScope", "module": "loopx/capabilities/benchmark_toolkit/integrity.py", "values": ["none", "loopback", "external"]}, @@ -325,7 +329,6 @@ {"name": "CAPABILITY_OWNER_GATE_HINTS", "module": "loopx/control_plane/agents/capability_gate.py", "container": "set", "values": ["credentials", "production_access"]}, {"name": "DELIVERY_WORKSPACE_IDENTITY_KINDS", "module": "loopx/control_plane/agents/delivery_workspace.py", "container": "frozenset", "values": ["git_repository", "local_goal"]}, {"name": "DELIVERY_WORKSPACE_KINDS", "module": "loopx/control_plane/agents/delivery_workspace.py", "container": "frozenset", "values": ["canonical_checkout", "independent_git_worktree", "local_goal_workspace"]}, - {"name": "LEGACY_HIERARCHY_ROLES", "module": "loopx/control_plane/agents/legacy_migration.py", "container": "set", "values": ["primary-agent", "side-agent"]}, {"name": "AGENT_PROFILE_FIELDS", "module": "loopx/control_plane/agents/profile.py", "container": "set", "values": ["schema_version", "agent_id", "profile_role", "scope_summary", "default_task_classes", "vision_requirement", "preferred_action_kinds", "avoid_action_kinds"]}, {"name": "AGENT_PROFILE_HIERARCHY_ROLES", "module": "loopx/control_plane/agents/profile.py", "container": "set", "values": ["leader", "manager", "supervisor", "worker"]}, {"name": "AGENT_PROFILE_VISION_REQUIREMENTS", "module": "loopx/control_plane/agents/profile.py", "container": "set", "values": ["optional", "required"]}, @@ -433,7 +436,6 @@ {"name": "TODO_DECISION_SCOPE_KIND_VALUES", "module": "loopx/control_plane/todos/contract.py", "container": "set", "values": ["private_read", "write_scope", "resource", "production", "public_claim", "direction", "other"]}, {"name": "TODO_LEGACY_TERMINAL_STATUS_VALUES", "module": "loopx/control_plane/todos/contract.py", "container": "set", "values": ["completed", "closed", "archived"]}, {"name": "TODO_MONITOR_METADATA_FIELDS", "module": "loopx/control_plane/todos/contract.py", "container": "tuple", "values": ["target_key", "monitor_effect_id", "cadence", "next_due_at", "expires_at", "last_checked_at", "result_hash", "consecutive_no_change", "material_change", "material_change_generation", "max_no_change_before_replan", "watch_only"]}, - {"name": "TODO_REMOVED_REVIEW_CONTINUATION_POLICY_VALUES", "module": "loopx/control_plane/todos/contract.py", "container": "set", "values": ["primary_review", "review_handoff"]}, {"name": "TODO_GATE_BLOCKING_STATES", "module": "loopx/control_plane/todos/decision_scope.py", "container": "frozenset", "values": ["gate_targets_todo", "gate_covers_action", "projection_repair_required"]}, {"name": "TODO_FRONTIER_DEADLINE_LANES", "module": "loopx/control_plane/todos/frontier_deadline.py", "container": "tuple", "values": ["current_agent_claimed_monitor_items", "monitor_open_items", "gate_open_items", "deferred_resume_candidates", "current_agent_deferred_resume_candidates", "resume_blocked_items", "current_agent_monitor_blocked_resume_candidates", "current_agent_handoff_gates"]}, {"name": "FRONTIER_REVISION_FIELDS", "module": "loopx/control_plane/todos/frontier_revision.py", "container": "tuple", "values": ["todo_id", "status", "done", "title", "text", "task_class", "claimed_by", "bound_agent", "blocks_agent", "excluded_agents", "priority", "action_kind", "task_domain", "task_repository", "capability_binding_ref", "required_capabilities", "target_capabilities", "target_key", "continuation_policy", "removed_continuation_policy", "decision_scope", "required_decision_scopes", "decision_outcome", "replan_obligation_id", "unblocks_todo_id", "depends_on_todo_id", "depends_on_todo_ids", "resume_when", "no_followup", "successor_todo_ids", "completion_continuation"]}, @@ -902,7 +904,7 @@ "summary": { "source_files": 1181, "python_enums": 103, - "python_closed_sets": 493, + "python_closed_sets": 491, "python_literal_aliases": 8, "typescript_const_arrays": 40, "named_string_constants": 2059, diff --git a/tests/architecture/test_semantic_inventory.py b/tests/architecture/test_semantic_inventory.py index 2855793771..d57ca0671e 100644 --- a/tests/architecture/test_semantic_inventory.py +++ b/tests/architecture/test_semantic_inventory.py @@ -190,6 +190,52 @@ def test_render_is_deterministic_valid_json(repo: Path) -> None: assert ' {"name": "Kind", "module": "loopx/a.py", "values": ["one", "two"]}' in rendered +@pytest.fixture +def retirement_repo(tmp_path: Path) -> Path: + """A tree where retired migration vocabulary sits next to live vocabulary.""" + _write( + tmp_path, + "loopx/control_plane/agents/legacy_migration.py", + 'LEGACY_HIERARCHY_ROLES = {"primary-agent", "side-agent"}\n', + ) + _write( + tmp_path, + "loopx/control_plane/todos/contract.py", + 'TODO_REMOVED_REVIEW_CONTINUATION_POLICY_VALUES = {"primary_review", "review_handoff"}\n' + 'TODO_DECISION_SCOPE_KIND_VALUES = {"private_read", "write_scope"}\n', + ) + subprocess.run(["git", "init", "-q", str(tmp_path)], check=True) + subprocess.run(["git", "-C", str(tmp_path), "add", "loopx"], check=True) + return tmp_path + + +def test_retired_migration_vocabulary_stays_out_of_the_map(retirement_repo: Path) -> None: + """A migration module's constants describe retired vocabulary, not ownership. + + Republishing them would copy hierarchy tokens into a derived artifact + outside the peer migration boundary the repository guard enforces, and + would name a migration module as the current owner of a retired vocabulary. + The declared exclusions stay visible so a reviewer sees the omission. + """ + inventory = build_inventory(retirement_repo) + assert [entry["name"] for entry in inventory["python_closed_sets"]] == [ + "TODO_DECISION_SCOPE_KIND_VALUES" + ] + assert inventory["retired_vocabulary_excluded"] == [ + { + "module": "loopx/control_plane/agents/legacy_migration.py", + "name": "LEGACY_HIERARCHY_ROLES", + }, + { + "module": "loopx/control_plane/todos/contract.py", + "name": "TODO_REMOVED_REVIEW_CONTINUATION_POLICY_VALUES", + }, + ] + duplicates = inventory["duplicate_definitions"] + assert duplicates["multi_value_twins"] == [] + assert duplicates["multi_value_forks"] == [] + + def test_committed_inventory_matches_the_tree() -> None: result = subprocess.run( [sys.executable, str(REPO_ROOT / "scripts" / "generate_semantic_inventory.py"), "--check"],