diff --git a/examples/semantic-vocabulary-drift-smoke.py b/examples/semantic-vocabulary-drift-smoke.py index 687dcd3d0..053a2b3b0 100755 --- a/examples/semantic-vocabulary-drift-smoke.py +++ b/examples/semantic-vocabulary-drift-smoke.py @@ -145,8 +145,8 @@ TWIN_ROOT_ANCHOR = "loopx/control_plane" TWIN_BUDGET_ANCHOR = 43 BUDGET_ANCHOR = { - "same_runtime_forks": 25, - "same_runtime_fork_definitions": 58, + "same_runtime_forks": 20, + "same_runtime_fork_definitions": 47, "conflicting_values": 18, "conflicting_definitions": 59, "schema_version_same_runtime_forks": 7, @@ -154,7 +154,7 @@ "multi_value_forks": 4, "multi_value_forks_semantic": 3, "multi_value_fork_definitions": 10, - "same_runtime_forks_semantic": 18, + "same_runtime_forks_semantic": 13, "conflicting_values_semantic": 0, } # Budgets for the legacy should-run decision fields, anchored the same way so a diff --git a/loopx/semantics/vocabulary_v0.json b/loopx/semantics/vocabulary_v0.json index b1fa138ff..c5ac93859 100644 --- a/loopx/semantics/vocabulary_v0.json +++ b/loopx/semantics/vocabulary_v0.json @@ -886,15 +886,15 @@ }, "inventory_ratchets": { "meaning": "Counts read from the generated inventory. A same-runtime fork is one constant name with one value defined in two or more modules of the same runtime; a conflicting value is one name with different values. Both the number of affected names and the number of definitions are budgets, so a third spelling of an already-conflicting name is still a regression.", - "same_runtime_forks": 25, - "same_runtime_fork_definitions": 58, + "same_runtime_forks": 20, + "same_runtime_fork_definitions": 47, "conflicting_values": 18, "conflicting_definitions": 59, "schema_version_same_runtime_forks": 7, "multi_value_twins": 19, "multi_value_forks": 4, "multi_value_fork_definitions": 10, - "same_runtime_forks_semantic": 18, + "same_runtime_forks_semantic": 13, "conflicting_values_semantic": 0, "multi_value_meaning": "Enums, named closed sets, Literal aliases, and TypeScript as-const arrays are vocabulary exactly as a NAME = \"value\" constant is, so they get the same collision rule. One name defined in two modules with identical values is a twin; with different values it is a fork. The semantic multi-value-fork budget excludes only names declared in scope_declarations.", "multi_value_forks_note": "The 4 counted forks include SOURCE_SURFACES, whose four definitions are four CLI commands each listing its own data sources; that is bounded-context reuse of one name, not drift. It stays in the budget until M0.5 adds a scope field (RFC Section 5) and must not be removed by renaming.", diff --git a/loopx/status.py b/loopx/status.py index d31aaaa2d..f7c12377b 100644 --- a/loopx/status.py +++ b/loopx/status.py @@ -9,9 +9,23 @@ StatusCollectionContext, collect_status as _collect_status_read_model, ) +from .control_plane.status.active_state_projection import ( + STATE_EVENT_LOG_BASENAME, +) +from .control_plane.status.contract_projection import ( + STATUS_CONTRACT_RELOAD_HINT, +) +from .control_plane.status.goal_attention_projection import ( + PLANNED_CONTROLLER_OPT_IN_RECOMMENDED_ACTION, +) # 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. +# owns it because it feeds the monitor/attention read models; this module keeps +# re-exporting each name for existing callers instead of restating its value. +from .control_plane.status.monitor_display_projection import ( + MONITOR_DISPLAY_FALLBACK_ACTION, + MONITOR_DISPLAY_STOP_CONDITION, + MONITOR_SIGNAL_WAITING_ON, +) from .control_plane.status.registry_health_projection import ( SOURCE_REGISTRY_SHADOW_FINDINGS, ) @@ -224,11 +238,18 @@ "todo_projection_sort_key": "loopx.control_plane.todos.todo_summary", "normalize_todo_task_class": "loopx.control_plane.todos.contract", "todo_item_is_expired_monitor": "loopx.control_plane.todos.todo_semantics", + # Refs #4447: single-sourced monitor/status vocabulary. Each name was already + # defined here and again in the control_plane projection that owns it; these + # entries keep the facade exporting one definition instead of a second copy. + "MONITOR_DISPLAY_STOP_CONDITION": "loopx.control_plane.status.monitor_display_projection", + "MONITOR_DISPLAY_FALLBACK_ACTION": "loopx.control_plane.status.monitor_display_projection", + "STATUS_CONTRACT_RELOAD_HINT": "loopx.control_plane.status.contract_projection", + "STATE_EVENT_LOG_BASENAME": "loopx.control_plane.status.active_state_projection", + "PLANNED_CONTROLLER_OPT_IN_RECOMMENDED_ACTION": "loopx.control_plane.status.goal_attention_projection", } STATUS_NEUTRAL_CLASSIFICATIONS = HISTORY_STATUS_NEUTRAL_CLASSIFICATIONS -STATE_EVENT_LOG_BASENAME = "events.jsonl" STATUS_CONTROL_PLANE_CONTEXT_LIMIT = 20 AGENT_LANE_PROGRESS_SCOPE = "agent_lane" REGISTRY_WAITING_ON_OVERRIDES = { @@ -241,18 +262,9 @@ "await_", "external_evidence_observation_", ) -MONITOR_SIGNAL_WAITING_ON = "monitor_signal" MONITOR_DISPLAY_SCHEMA_VERSION = "monitor_quiet_display_v0" -MONITOR_DISPLAY_STOP_CONDITION = ( - "stop until a material monitor transition, regression, or concrete blocker appears" -) -MONITOR_DISPLAY_FALLBACK_ACTION = ( - "No immediate agent work; keep the monitor quiet until a material monitor " - "transition, regression, or concrete blocker appears." -) STATUS_CONTRACT_SCHEMA_VERSION = 2 MINIMUM_DASHBOARD_STATUS_CONTRACT_SCHEMA_VERSION = 2 -STATUS_CONTRACT_RELOAD_HINT = "scripts/macos-dashboard-launchagent.sh restart" STATUS_CONTRACT_SIGNAL_LIMIT = 3 MONITOR_WRITEBACK_CONTRACT_SCHEMA_VERSION = _MONITOR_WRITEBACK_CONTRACT_SCHEMA_VERSION EVENT_LEDGER_DECISION_CLASSIFICATIONS = USER_OR_CONTROLLER_CLASSIFICATIONS | { @@ -293,9 +305,6 @@ CONNECTED_DELIVERY_ADAPTER_STATUSES = { "connected-delivery", } -PLANNED_CONTROLLER_OPT_IN_RECOMMENDED_ACTION = ( - "先在 LoopX 完成 operator 判断;同意后项目 Agent 只执行 read-only map dry-run" -) RUN_COMPACT_FIELDS = RUN_BASE_COMPACT_FIELDS LIFECYCLE_PRIORITY = ( "controller_ready", diff --git a/tests/architecture/test_control_plane_import_boundaries.py b/tests/architecture/test_control_plane_import_boundaries.py index 1a9418e0a..acdd362bf 100644 --- a/tests/architecture/test_control_plane_import_boundaries.py +++ b/tests/architecture/test_control_plane_import_boundaries.py @@ -156,13 +156,34 @@ def _public_contract_evidence() -> dict[tuple[str, str], set[str]]: for root in PUBLIC_CONTRACT_ROOTS: for path in root.rglob("*.py"): tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path)) + # Aliases such as ``from loopx import status as status_module`` are a + # real consumer of a facade export, reached by attribute access + # rather than a from-import. Without this map the audit would report + # a live consumer as missing evidence. + module_aliases: dict[str, str] = {} for node in ast.walk(tree): - if not isinstance(node, ast.ImportFrom): - continue - if node.module not in PUBLIC_COMPAT_FACADES: + if not isinstance(node, ast.ImportFrom) or node.level: continue + module = node.module or "" for alias in node.names: - evidence.setdefault((node.module, alias.name), set()).add( + # ``from loopx import status`` binds the facade itself; a + # from-import of a facade also binds the facade's exports. + candidate = alias.name if not module else f"{module}.{alias.name}" + if candidate in PUBLIC_COMPAT_FACADES: + module_aliases[alias.asname or alias.name] = candidate + continue + if module in PUBLIC_COMPAT_FACADES: + evidence.setdefault((module, alias.name), set()).add( + str(path.relative_to(REPOSITORY_ROOT)) + ) + if not module_aliases: + continue + for node in ast.walk(tree): + if not isinstance(node, ast.Attribute) or not isinstance(node.ctx, ast.Load): + continue + facade_name = module_aliases.get(getattr(node.value, "id", "")) + if facade_name is not None: + evidence.setdefault((facade_name, node.attr), set()).add( str(path.relative_to(REPOSITORY_ROOT)) ) for path in (REPOSITORY_ROOT / "docs").rglob("*.md"):