From 904345178454664823d54a02464d5a05b989c1df Mon Sep 17 00:00:00 2001 From: song <22676124+songoow@users.noreply.github.com> Date: Sat, 19 Sep 2026 21:54:07 -0400 Subject: [PATCH 1/4] fix(semantics): report the formal detail ratios from what the checks walked ``summarise_formal_domains`` printed ``projections=N/N`` with both sides read from ``len(registry['projections'])``. The ratio was therefore 100% by construction: a registered projection that ``check_projections`` never imports still counted itself as its own evidence. The invariant above that line had already been fixed. F5's domain selector counts only the projections in the code-owned ``EXECUTED_PROJECTIONS`` set, precisely so a declaration could not credit itself, and ``check_invariant_domain`` validates the registry's declared ``verified`` against it. The detail path was left recomputing its own number, so the report and the invariant could disagree while both stayed green -- and the report is what a reader sees first. Both ratios now come from the same selectors. Adding an unregistered-owner projection to the registry reports ``projections=1/2`` here and reported ``2/2`` before, which is the regression added to ``tests/architecture/test_semantic_vocabulary_drift.py``; a second test pins each printed ratio to the selector whose pair ``check_invariant_domain`` validates, so the two cannot drift apart again. ``declared_contexts`` is left reading 100% and the reason is stated where it is computed rather than left to look like the same defect: ``check_scope_declarations`` validates every declared context against the modules the inventory actually found, or raises, so the pair is a fail-closed count of what the check had to clear, not a ratio that could show a gap. Reported as defect 3 in discussion #4738. No gate and no decision depends on it: the main producer and projection checks were already fail-closed, and this changes only what the smoke prints. ``tests/architecture`` 672 passed, the drift smoke reports the same formal_domain line as main, and Ruff reports the same ten findings on the changed files as main does. Co-Authored-By: Claude Opus 5 (1M context) Signed-off-by: song <22676124+songoow@users.noreply.github.com> --- examples/semantic-vocabulary-drift-smoke.py | 21 ++++++++--- .../test_semantic_vocabulary_drift.py | 35 +++++++++++++++++++ 2 files changed, 52 insertions(+), 4 deletions(-) diff --git a/examples/semantic-vocabulary-drift-smoke.py b/examples/semantic-vocabulary-drift-smoke.py index 77700d9d84..073aa94826 100755 --- a/examples/semantic-vocabulary-drift-smoke.py +++ b/examples/semantic-vocabulary-drift-smoke.py @@ -680,15 +680,28 @@ def summarise_formal_domains(registry: dict[str, Any], sources: list[SourceFile] covered = [name for name in kernel if 'producers' in vocabularies[name]] unverified = [name for name in cross_runtime if 'producers' not in vocabularies[name]] scanned, tracked = producer_scan_reach(sources) - projections = len(registry['projections']) - contexts = sum(len(entry['contexts']) for entry in registry['scope_declarations'].values()) + # Both sides of these ratios came from one expression, so they printed 100% + # by construction: ``projections={len(registry['projections'])}`` over + # itself reported full coverage however many registered projections the + # check never executed. The pair now comes from the same code-owned + # selector ``check_invariant_domain`` validates the declared domain + # against, so the detail line cannot disagree with the invariant above it. + projections_walked, projections_registered = FORMAL_DOMAIN_SELECTORS["projections[*]"](registry) + # F4's selector still derives both sides from the declaration list. That is + # not a second self-satisfying ratio but a fail-closed count: + # ``check_scope_declarations`` validates every declared context against the + # modules the inventory really found, or raises. The ratio therefore reads + # 100% whenever the smoke gets far enough to print it, and what it reports + # is how many contexts that check had to clear. + contexts_walked, contexts_registered = FORMAL_DOMAIN_SELECTORS["scope_declarations[*].contexts"](registry) return ( f"formal_domain={sizes} (verified/registered)\n" f" formal_domain_bounds: kernel_with_producers={len(covered)}/{len(kernel)}" f" cross_runtime_unverified={len(unverified)}/{len(cross_runtime)}" f" producer_scan_reach={scanned}/{tracked}_files" - f" projections={projections}/{projections}" - f" scope_declarations={len(registry['scope_declarations'])} declared_contexts={contexts}/{contexts}" + f" projections={projections_walked}/{projections_registered}" + f" scope_declarations={len(registry['scope_declarations'])}" + f" declared_contexts={contexts_walked}/{contexts_registered}" ) diff --git a/tests/architecture/test_semantic_vocabulary_drift.py b/tests/architecture/test_semantic_vocabulary_drift.py index 212b1db2a5..10875bbe33 100644 --- a/tests/architecture/test_semantic_vocabulary_drift.py +++ b/tests/architecture/test_semantic_vocabulary_drift.py @@ -516,6 +516,41 @@ def test_f1_f2_domain_names_exactly_the_vocabularies_the_producer_check_walks() assert domain["evidence_bound"] == "producer_scan_reach" +def test_the_formal_detail_line_cannot_report_a_projection_it_never_executed() -> None: + """A ratio whose two sides come from one expression cannot show a gap. + + ``projections`` was printed as ``len(registry['projections'])`` on both + sides of the slash, so the detail line read 100% however many registered + projections ``check_projections`` never imports. The invariant above it had + already been fixed to count only executed projections, so the report and + the invariant could disagree while both stayed green. + """ + smoke = runpy.run_path(str(SMOKE)) + registry = copy.deepcopy(smoke["load_registry"]()) + assert "projections=1/1" in smoke["summarise_formal_domains"](registry, []) + + registry["projections"]["never_executed"] = { + "owner": "loopx/nowhere.py::absent", + "mapping": {}, + } + detail = smoke["summarise_formal_domains"](registry, []) + assert "projections=1/2" in detail, detail + + +def test_the_formal_detail_line_agrees_with_the_selectors_it_reports() -> None: + """The detail line reports the pair the invariant's domain is checked on.""" + smoke = runpy.run_path(str(SMOKE)) + registry = smoke["load_registry"]() + selectors = smoke["FORMAL_DOMAIN_SELECTORS"] + detail = smoke["summarise_formal_domains"](registry, []) + for selector, label in ( + ("projections[*]", "projections"), + ("scope_declarations[*].contexts", "declared_contexts"), + ): + walked, registered = selectors[selector](registry) + assert f"{label}={walked}/{registered}" in detail, (label, detail) + + @pytest.mark.parametrize("invariant_id", sorted({ "F1_producer_closedness", "F2_canonical_value_liveness", From b2d10a6dcd295cf46398c52313d525a546227333 Mon Sep 17 00:00:00 2001 From: song <22676124+songoow@users.noreply.github.com> Date: Sun, 20 Sep 2026 02:34:49 -0400 Subject: [PATCH 2/4] test(chat): characterize missing actions and persisted Lark routing modes Signed-off-by: song <22676124+songoow@users.noreply.github.com> --- .../test_lark_goal_topic_connections.py | 69 +++++++++++++++++++ tests/test_chat_server_cors.py | 33 +++++++++ 2 files changed, 102 insertions(+) diff --git a/tests/extensions/test_lark_goal_topic_connections.py b/tests/extensions/test_lark_goal_topic_connections.py index 70d24bb28c..2add32f6ff 100644 --- a/tests/extensions/test_lark_goal_topic_connections.py +++ b/tests/extensions/test_lark_goal_topic_connections.py @@ -2122,6 +2122,75 @@ def _prep_goal_channel_target(root: Path) -> Path: return target_path +@pytest.mark.parametrize( + ("routing", "expected"), + [ + ({}, ("addressed_only", "direct_session", "topic_reply")), + ( + {"incoming_mode": "all"}, + ("configured_chat_all", "direct_session", "topic_reply"), + ), + ( + { + "incoming_mode": "all", + "capture_scope": " ADDRESSED_ONLY ", + "ingress_mode": " SESSION_QUEUE ", + "reply_mode": " TOPIC_REPLY ", + }, + ("addressed_only", "session_queue", "topic_reply"), + ), + ({"capture_scope": "invalid"}, None), + ({"ingress_mode": "async-inbox"}, None), + ({"reply_mode": "invalid"}, None), + ], +) +def test_connection_readback_and_event_route_share_persisted_mode_rules( + tmp_path: Path, + routing: dict[str, str], + expected: tuple[str, str, str] | None, +) -> None: + target_path = _prep_goal_channel_target(tmp_path) + binding_path = tmp_path / "binding.json" + payload = _legacy_v0_binding_payload("om_topic_alpha", "agent-alpha") + payload["bindings"]["goal-alpha"]["routing"] = routing + write_goal_channel_binding(binding_path, payload) + before = binding_path.read_bytes() + rows = list_lark_connections( + registry=_registry(tmp_path), + target_path=target_path, + binding_paths={"goal-alpha": binding_path}, + runner=_runner({}), + ) + decision = decide_lark_topic_event( + target_payload=read_goal_channel_targets(target_path), + binding_payloads={"goal-alpha": read_goal_channel_binding(binding_path)}, + event={ + "chat_id": CHAT_ID, + "root_id": "om_topic_alpha", + "message_id": "om_incoming", + "content": "@mew bot hello", + }, + ) + assert len(rows) == 1 + if expected is None: + assert rows[0]["reply_ready"] is False + assert rows[0]["health_error_code"] == "invalid_routing_state" + assert decision == { + "matched": False, + "reason": "invalid_routing_state", + "route": None, + } + else: + assert rows[0]["reply_ready"] is True + assert decision["matched"] is True + for key, value in zip( + ("capture_scope", "ingress_mode", "reply_mode"), expected + ): + assert rows[0][key] == value + assert decision["route"][key] == value + assert binding_path.read_bytes() == before + + def test_reconnect_after_upgrade_reuses_legacy_topic_root_without_resend( tmp_path: Path, ) -> None: diff --git a/tests/test_chat_server_cors.py b/tests/test_chat_server_cors.py index 92b9f02fa3..7521619925 100644 --- a/tests/test_chat_server_cors.py +++ b/tests/test_chat_server_cors.py @@ -213,6 +213,39 @@ def test_chat_action_context_cannot_persist_or_emit_overflowed_float( server.server_close() +@pytest.mark.parametrize( + "action", ["snapshot", "apply", "cancel", "regenerate", "reject", "defer"] +) +def test_missing_action_returns_the_same_http_error( + tmp_path: Path, action: str +) -> None: + server, thread = _start_server() + server.action_store = ChatActionStore(tmp_path / "actions") + server.action_service = ChatActionService( + store=server.action_store, registry_path=tmp_path / "registry.json" + ) + try: + response = _request( + server.server_address[1], + method="GET" if action == "snapshot" else "POST", + origin=None, + path="/api/actions/missing" + + ("" if action == "snapshot" else f"/{action}"), + body=None if action == "snapshot" else b"{}", + ) + assert response.status == 404 + assert json.loads(response.read()) == { + "ok": False, + "error": "typed Chat action proposal was not found", + "error_code": "action_not_found", + } + assert server.action_store.list() == [] + finally: + server.shutdown() + thread.join(timeout=5) + server.server_close() + + def test_chat_status_forwards_valid_goal_activation_scope(monkeypatch) -> None: calls: list[dict[str, object]] = [] From d231971ec01b04263bd6921a047318f1b87a1a85 Mon Sep 17 00:00:00 2001 From: song <22676124+songoow@users.noreply.github.com> Date: Sun, 20 Sep 2026 02:34:49 -0400 Subject: [PATCH 3/4] refactor(chat): deduplicate action errors and Lark routing defaults Signed-off-by: song <22676124+songoow@users.noreply.github.com> --- loopx/chat_server.py | 31 +++++------- .../extensions/lark/goal_topic_connections.py | 49 ++----------------- loopx/extensions/lark/goal_topic_routing.py | 31 ++++++++++++ 3 files changed, 45 insertions(+), 66 deletions(-) diff --git a/loopx/chat_server.py b/loopx/chat_server.py index de9c8c7d33..c0f3ebc762 100644 --- a/loopx/chat_server.py +++ b/loopx/chat_server.py @@ -1055,6 +1055,13 @@ def _action_preview(self) -> None: status=201, ) + def _action_not_found(self) -> None: + self._send_error( + "typed Chat action proposal was not found", + status=404, + error_code="action_not_found", + ) + def _action_snapshot(self, proposal_id: str) -> None: try: proposal = self.server.action_service.load(proposal_id) @@ -1062,11 +1069,7 @@ def _action_snapshot(self, proposal_id: str) -> None: self._send_error(str(exc), status=400, error_code="invalid_proposal_id") return if proposal is None: - self._send_error( - "typed Chat action proposal was not found", - status=404, - error_code="action_not_found", - ) + self._action_not_found() return self._send_json( { @@ -1107,11 +1110,7 @@ def _action_cancel(self, proposal_id: str) -> None: raise ValueError("action cancel request must be empty") proposal = self.server.action_service.cancel(proposal_id) except KeyError: - self._send_error( - "typed Chat action proposal was not found", - status=404, - error_code="action_not_found", - ) + self._action_not_found() return except ActionConflictError as exc: self._send_error(str(exc), status=409, error_code="action_conflict") @@ -1144,11 +1143,7 @@ def _action_transition(self, proposal_id: str, transition: str) -> None: else: raise ValueError("unsupported action transition") except KeyError: - self._send_error( - "typed Chat action proposal was not found", - status=404, - error_code="action_not_found", - ) + self._action_not_found() return except ActionConflictError as exc: self._send_error(str(exc), status=409, error_code="action_conflict") @@ -1190,11 +1185,7 @@ def _action_apply(self, proposal_id: str) -> None: ) return except KeyError: - self._send_error( - "typed Chat action proposal was not found", - status=404, - error_code="action_not_found", - ) + self._action_not_found() return except ActionConflictError as exc: self._send_error(str(exc), status=409, error_code="action_conflict") diff --git a/loopx/extensions/lark/goal_topic_connections.py b/loopx/extensions/lark/goal_topic_connections.py index 55d7bd3932..48dc33e006 100644 --- a/loopx/extensions/lark/goal_topic_connections.py +++ b/loopx/extensions/lark/goal_topic_connections.py @@ -92,6 +92,7 @@ IngressMode, ReplyMode, _routing_value, + _connection_routing_modes, decide_lark_topic_route_event, ) from .presentation.kanban import ( @@ -1035,29 +1036,7 @@ def list_lark_connections( ) connector_status: dict[str, Any] | None = None try: - capture_scope = _routing_value( - CaptureScope, - routing.get("capture_scope") - or ( - "configured_chat_all" - if routing.get("incoming_mode") == "all" - else "addressed_only" - ), - default=CaptureScope.ADDRESSED_ONLY.value, - field="capture_scope", - ) - ingress_mode = _routing_value( - IngressMode, - routing.get("ingress_mode"), - default=IngressMode.DIRECT_SESSION.value, - field="ingress_mode", - ) - reply_mode = _routing_value( - ReplyMode, - routing.get("reply_mode"), - default=ReplyMode.TOPIC_REPLY.value, - field="reply_mode", - ) + capture_scope, ingress_mode, reply_mode = _connection_routing_modes(routing) raw_connector = binding.get("connector") if raw_connector is not None: if not isinstance(raw_connector, Mapping): @@ -1216,29 +1195,7 @@ def decide_lark_topic_event( else {} ) try: - capture_scope = _routing_value( - CaptureScope, - routing.get("capture_scope") - or ( - "configured_chat_all" - if routing.get("incoming_mode") == "all" - else "addressed_only" - ), - default=CaptureScope.ADDRESSED_ONLY.value, - field="capture_scope", - ) - ingress_mode = _routing_value( - IngressMode, - routing.get("ingress_mode"), - default=IngressMode.DIRECT_SESSION.value, - field="ingress_mode", - ) - reply_mode = _routing_value( - ReplyMode, - routing.get("reply_mode"), - default=ReplyMode.TOPIC_REPLY.value, - field="reply_mode", - ) + capture_scope, ingress_mode, reply_mode = _connection_routing_modes(routing) connector = binding.get("connector") if connector is not None: if not isinstance(connector, Mapping): diff --git a/loopx/extensions/lark/goal_topic_routing.py b/loopx/extensions/lark/goal_topic_routing.py index 81b8e0cccf..09caab8c50 100644 --- a/loopx/extensions/lark/goal_topic_routing.py +++ b/loopx/extensions/lark/goal_topic_routing.py @@ -47,6 +47,37 @@ def _routing_value( raise ValueError(f"{field} must be one of: {allowed}") from exc +def _connection_routing_modes( + routing: Mapping[str, Any], +) -> tuple[str, str, str]: + """Normalize persisted modes for both connection readback and event routing.""" + + capture_scope = _routing_value( + CaptureScope, + routing.get("capture_scope") + or ( + "configured_chat_all" + if routing.get("incoming_mode") == "all" + else "addressed_only" + ), + default=CaptureScope.ADDRESSED_ONLY.value, + field="capture_scope", + ) + ingress_mode = _routing_value( + IngressMode, + routing.get("ingress_mode"), + default=IngressMode.DIRECT_SESSION.value, + field="ingress_mode", + ) + reply_mode = _routing_value( + ReplyMode, + routing.get("reply_mode"), + default=ReplyMode.TOPIC_REPLY.value, + field="reply_mode", + ) + return capture_scope, ingress_mode, reply_mode + + def _normalize_mention_name(name: str) -> str: cleaned = str(name or "").strip() if cleaned.startswith("@"): From 277f43ab282653a534c383a046e40fe68e70c061 Mon Sep 17 00:00:00 2001 From: song <22676124+songoow@users.noreply.github.com> Date: Sun, 20 Sep 2026 04:07:53 -0400 Subject: [PATCH 4/4] test(lark): freeze the context clock in dated runtime fixtures Signed-off-by: song <22676124+songoow@users.noreply.github.com> --- .../extensions/test_lark_goal_topic_runtime.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/extensions/test_lark_goal_topic_runtime.py b/tests/extensions/test_lark_goal_topic_runtime.py index a5e664726e..2367bf870f 100644 --- a/tests/extensions/test_lark_goal_topic_runtime.py +++ b/tests/extensions/test_lark_goal_topic_runtime.py @@ -5,6 +5,7 @@ import subprocess import threading from collections.abc import Mapping +from datetime import UTC, datetime from pathlib import Path from typing import Any @@ -283,6 +284,21 @@ def test_mention_uses_existing_inbox_reply_and_ack_path(tmp_path: Path) -> None: assert projection["processed_count"] == 1 +@pytest.fixture +def manager_context_clock(monkeypatch: pytest.MonkeyPatch) -> None: + """Keep the dated fixture within retention without disabling compaction.""" + from loopx.extensions.lark import manager_context + + class FixtureDatetime(datetime): + @classmethod + def now(cls, tz=None): + instant = datetime(2026, 9, 13, 6, 1, tzinfo=UTC) + return instant.astimezone(tz) if tz is not None else instant.replace(tzinfo=None) + + monkeypatch.setattr(manager_context, "datetime", FixtureDatetime) + + +@pytest.mark.usefixtures("manager_context_clock") def test_manager_captures_unaddressed_context_without_granting_turn_authority( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: @@ -467,6 +483,7 @@ def decision(**options: Any) -> dict[str, Any]: assert answer_calls == [] +@pytest.mark.usefixtures("manager_context_clock") def test_manager_authorized_turn_quietly_recovers_history_as_context( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: