From fe7a5d7e6003bf845190b025c02b29e5a1c2c6be Mon Sep 17 00:00:00 2001 From: huangruiteng Date: Wed, 9 Sep 2026 13:02:18 +0800 Subject: [PATCH 1/2] refactor(delivery): project history and follow-through in TypeScript Signed-off-by: huangruiteng --- .../delivery-signals-readmodel-smoke.py | 117 +++------------ .../control_plane/status-markdown-smoke.py | 36 ++--- examples/delivery-outcome-enum-smoke.py | 22 +-- examples/delivery-turn-kind-enum-smoke.py | 8 +- .../outcome-followthrough-policy-smoke.py | 6 +- .../project-handoff-readmodel-smoke.py | 5 +- loopx/canary/planner.py | 3 +- loopx/canary/qualification_profiles.py | 3 +- .../control_plane/effect_runtime_handlers.ts | 2 + .../control_plane/handoff/project_handoff.py | 20 +-- .../work_items/delivery_history.py | 72 ++++++++++ .../work_items/delivery_history.ts | 135 ++++++++++++++++++ .../work_items/delivery_outcome.py | 49 +------ .../work_items/delivery_signals.py | 77 ---------- .../work_items/outcome_followthrough.py | 69 --------- .../work_items/work_lane_context.py | 5 +- loopx/status.py | 105 ++++---------- .../test_delivery_history_projection.py | 102 +++++++++++++ .../control_plane/test_delivery_semantics.py | 86 +++++++++-- .../test_delivery_semantics_cli.py | 6 +- .../control_plane_ts/delivery_history.test.ts | 103 +++++++++++++ tsconfig.control-plane.json | 1 + 22 files changed, 586 insertions(+), 446 deletions(-) create mode 100644 loopx/control_plane/work_items/delivery_history.py create mode 100644 loopx/control_plane/work_items/delivery_history.ts delete mode 100644 loopx/control_plane/work_items/delivery_signals.py delete mode 100644 loopx/control_plane/work_items/outcome_followthrough.py create mode 100644 tests/control_plane/test_delivery_history_projection.py create mode 100644 tests/control_plane_ts/delivery_history.test.ts diff --git a/examples/control_plane/delivery-signals-readmodel-smoke.py b/examples/control_plane/delivery-signals-readmodel-smoke.py index 80ba31e589..63a7dc05b0 100644 --- a/examples/control_plane/delivery-signals-readmodel-smoke.py +++ b/examples/control_plane/delivery-signals-readmodel-smoke.py @@ -1,112 +1,31 @@ #!/usr/bin/env python3 -from __future__ import annotations - +"""The public status batch preserves typed streaks and unknown boundaries.""" from pathlib import Path import sys - ROOT = Path(__file__).resolve().parents[2] -if str(ROOT) not in sys.path: - sys.path.insert(0, str(ROOT)) - -from loopx import status as status_module # noqa: E402 -from loopx.control_plane.work_items import delivery_signals as delivery_signal_read_model # noqa: E402 - - -PROFILE = { - "outcome_floor": { - "outcome_markers": ["merged", "validated"], - "surface_only_hints": ["doc_only", "contract"], - } -} - - -def _projection_scale(run: dict[str, object]) -> str: - return delivery_signal_read_model.delivery_batch_scale_for_run(run) - - - -def _projection_outcome(run: dict[str, object], profile: dict[str, object] | None = None) -> str: - return delivery_signal_read_model.delivery_outcome_for_run( - run, - profile, - execution_profile_outcome_floor=status_module.execution_profile_outcome_floor, - ) - - -def _projection_outcome_floor_configured(profile: dict[str, object] | None) -> bool: - return delivery_signal_read_model.outcome_floor_configured( - profile, - execution_profile_outcome_floor=status_module.execution_profile_outcome_floor, - ) - +sys.path.insert(0, str(ROOT)) -def _projection_outcome_gap_streak(runs: list[dict[str, object]], profile: dict[str, object] | None) -> int: - return delivery_signal_read_model.outcome_gap_streak( - runs, - profile, - delivery_outcome_for_run=_projection_outcome, - outcome_floor_configured=_projection_outcome_floor_configured, - ) +from loopx.status import project_post_handoff_history # noqa: E402 -def _projection_small_scale_streak(runs: list[dict[str, object]]) -> int: - return delivery_signal_read_model.small_delivery_batch_scale_streak( - runs, - delivery_batch_scale_for_run=_projection_scale, - small_delivery_batch_scales=status_module.SMALL_DELIVERY_BATCH_SCALES, - ) - - -def assert_delivery_signal_wrapper_parity() -> None: - scale_cases = [ - {"delivery_batch_scale": "multi_surface", "classification": "ignored_smoke"}, - {"delivery_batch_scale": "future_scale", "classification": "ignored_smoke"}, - {"classification": "owner_handoff_consumer_test"}, - {"classification": "delivery_ranker_readiness_batch"}, - {"classification": "feedback_reranker_adapter_slice"}, - {"classification": "single_note"}, - {}, - ] - for run in scale_cases: - assert status_module.delivery_batch_scale_for_run(run) == _projection_scale(run), run - - outcome_cases = [ - ({"delivery_outcome": "outcome_progress", "classification": "ignored"}, PROFILE), - ({"delivery_outcome": "future_outcome", "classification": "ignored"}, PROFILE), - ({"classification": "adapter_validated"}, PROFILE), - ({"classification": "doc_only_contract"}, PROFILE), - ({"classification": "needs_real_outcome"}, PROFILE), - ({"classification": "adapter_validated"}, None), - ({}, PROFILE), - ] - for run, profile in outcome_cases: - assert status_module.delivery_outcome_for_run(run, profile) == _projection_outcome(run, profile), run - - assert status_module.outcome_floor_configured(PROFILE) == _projection_outcome_floor_configured(PROFILE) - assert status_module.outcome_floor_configured(None) == _projection_outcome_floor_configured(None) - +def main() -> None: + profile = {"outcome_floor": {"outcome_markers": ["validated"]}} runs = [ - {"classification": "needs_real_outcome"}, - {"classification": "doc_only_contract"}, - {"classification": "adapter_validated"}, - {"classification": "needs_real_outcome"}, - ] - assert status_module.outcome_gap_streak(runs, PROFILE) == _projection_outcome_gap_streak(runs, PROFILE) - assert status_module.outcome_gap_streak(runs, None) == _projection_outcome_gap_streak(runs, None) - - small_runs = [ - {"classification": "single_note"}, - {"classification": "owner_handoff_consumer_test"}, - {"classification": "delivery_ranker_readiness_batch"}, + {"delivery_outcome": "surface_only", "delivery_batch_scale": "single_segment"}, + {"classification": "contract validated implementation"}, + {"delivery_outcome": "outcome_gap", "delivery_batch_scale": "test_only"}, ] - assert status_module.small_delivery_batch_scale_streak(small_runs) == _projection_small_scale_streak( - small_runs - ) - - -def main() -> None: - assert_delivery_signal_wrapper_parity() + result = project_post_handoff_history(runs, profile) + assert result["post_handoff_small_scale_streak"] == 1 + assert result["post_handoff_outcome_gap_streak"] == 1 + assert result["post_handoff_latest_run"]["delivery_batch_scale"] == "single_surface" + assert result["post_handoff_recent_runs"][1]["delivery_outcome"] == "unknown" + no_floor = project_post_handoff_history([{}]) + assert "post_handoff_outcome_gap_streak" not in no_floor + assert "delivery_outcome" not in no_floor["post_handoff_latest_run"] + assert no_floor["post_handoff_latest_run"]["delivery_turn_kind"] == "unknown" + print("delivery-signals-readmodel-smoke ok") if __name__ == "__main__": diff --git a/examples/control_plane/status-markdown-smoke.py b/examples/control_plane/status-markdown-smoke.py index 66982345ca..02c0f63613 100644 --- a/examples/control_plane/status-markdown-smoke.py +++ b/examples/control_plane/status-markdown-smoke.py @@ -18,12 +18,11 @@ if str(REPO_ROOT) not in sys.path: sys.path.insert(0, str(REPO_ROOT)) +from loopx.status import project_post_handoff_history # noqa: E402 from loopx.status import ( # noqa: E402 build_status_runtime_summaries, build_contract_health_projection, collect_status, - delivery_batch_scale_for_run, - delivery_outcome_for_run, project_asset_summary_is_public_safe, ) from loopx.presentation.renderers.status_markdown import render_status_markdown # noqa: E402 @@ -921,8 +920,11 @@ def assert_connected_delivery_surface_loop(payload: dict, markdown: str) -> None def assert_delivery_semantics_require_structured_fields() -> None: + def delivery_scale_signal(run: dict) -> str: + return project_post_handoff_history([run])["post_handoff_latest_run"]["delivery_batch_scale"] + assert ( - delivery_batch_scale_for_run( + delivery_scale_signal( { "classification": "dashboard_home_browser_smoke_regression", "delivery_batch_scale": "multi_surface", @@ -931,21 +933,21 @@ def assert_delivery_semantics_require_structured_fields() -> None: == "multi_surface" ) assert ( - delivery_batch_scale_for_run( + delivery_scale_signal( {"classification": "side_bypass_validation_plan_source_shape_consumer_test"} ) == "unknown" ) assert ( - delivery_batch_scale_for_run({"classification": "owner_handoff_consumer_test"}) + delivery_scale_signal({"classification": "owner_handoff_consumer_test"}) == "unknown" ) assert ( - delivery_batch_scale_for_run({"classification": "delivery_ranker_readiness_batch"}) + delivery_scale_signal({"classification": "delivery_ranker_readiness_batch"}) == "unknown" ) assert ( - delivery_batch_scale_for_run({"classification": "feedback_reranker_adapter_slice"}) + delivery_scale_signal({"classification": "feedback_reranker_adapter_slice"}) == "unknown" ) profile = { @@ -956,31 +958,19 @@ def assert_delivery_semantics_require_structured_fields() -> None: } } assert ( - delivery_outcome_for_run( - {"classification": "side_bypass_owner_drop_landing_forecast_implementation"}, - profile, - ) + project_post_handoff_history([{"classification": "side_bypass_owner_drop_landing_forecast_implementation"}], profile)["post_handoff_latest_run"]["delivery_outcome"] == "unknown" ) assert ( - delivery_outcome_for_run( - {"classification": "side_bypass_ranker_fit_metric_implementation"}, - profile, - ) + project_post_handoff_history([{"classification": "side_bypass_ranker_fit_metric_implementation"}], profile)["post_handoff_latest_run"]["delivery_outcome"] == "unknown" ) assert ( - delivery_outcome_for_run( - {"classification": "side_bypass_macro_evidence_segment_implementation"}, - profile, - ) + project_post_handoff_history([{"classification": "side_bypass_macro_evidence_segment_implementation"}], profile)["post_handoff_latest_run"]["delivery_outcome"] == "unknown" ) assert ( - delivery_outcome_for_run( - {"classification": "status_refresh_without_marker", "delivery_outcome": "primary_goal_outcome"}, - profile, - ) + project_post_handoff_history([{"classification": "status_refresh_without_marker", "delivery_outcome": "primary_goal_outcome"}], profile)["post_handoff_latest_run"]["delivery_outcome"] == "primary_goal_outcome" ) diff --git a/examples/delivery-outcome-enum-smoke.py b/examples/delivery-outcome-enum-smoke.py index 0ab4f1d360..e1fca6d526 100644 --- a/examples/delivery-outcome-enum-smoke.py +++ b/examples/delivery-outcome-enum-smoke.py @@ -14,17 +14,15 @@ if str(REPO_ROOT) not in sys.path: sys.path.insert(0, str(REPO_ROOT)) +from loopx.control_plane.work_items.delivery_history import project_delivery_history # noqa: E402 from loopx.control_plane.work_items.delivery_outcome import ( # noqa: E402 ACCOUNTABLE_DELIVERY_OUTCOMES, DELIVERY_OUTCOME_CHOICES, - DELIVERY_OUTCOME_UNKNOWN, DeliveryOutcome, - FOLLOWTHROUGH_REQUIRED_DELIVERY_OUTCOMES, normalize_delivery_outcome, require_delivery_outcome, ) from loopx.state_refresh import refresh_state_run # noqa: E402 -from loopx.status import delivery_outcome_for_run # noqa: E402 GOAL_ID = "delivery-outcome-enum-fixture" @@ -86,10 +84,6 @@ def assert_enum_sets() -> None: DeliveryOutcome.OUTCOME_PROGRESS, DeliveryOutcome.PRIMARY_GOAL_OUTCOME, } - assert FOLLOWTHROUGH_REQUIRED_DELIVERY_OUTCOMES == { - DeliveryOutcome.SURFACE_ONLY, - DeliveryOutcome.OUTCOME_GAP, - } try: require_delivery_outcome("contract_v0_delivered") except ValueError as exc: @@ -158,22 +152,18 @@ def assert_refresh_state_enforces_enum(registry_path: Path) -> None: def assert_status_uses_enum_not_classification() -> None: assert ( - delivery_outcome_for_run( - { + project_delivery_history([{ "classification": "runner_contract_v0_delivered", "delivery_outcome": DeliveryOutcome.OUTCOME_PROGRESS.value, - } - ) + }], outcome_floor_configured=False)["runs"][0]["delivery_outcome"] == DeliveryOutcome.OUTCOME_PROGRESS.value ) assert ( - delivery_outcome_for_run( - { + project_delivery_history([{ "classification": "runner_contract_v0_delivered", "delivery_outcome": "contract_v0_delivered", - } - ) - == DELIVERY_OUTCOME_UNKNOWN + }], outcome_floor_configured=False)["runs"][0]["delivery_outcome"] + == "unknown" ) diff --git a/examples/delivery-turn-kind-enum-smoke.py b/examples/delivery-turn-kind-enum-smoke.py index 1a9690f4fc..fbb33c9e8c 100644 --- a/examples/delivery-turn-kind-enum-smoke.py +++ b/examples/delivery-turn-kind-enum-smoke.py @@ -11,11 +11,11 @@ if str(REPO_ROOT) not in sys.path: sys.path.insert(0, str(REPO_ROOT)) +from loopx.control_plane.work_items.delivery_history import project_delivery_history # noqa: E402 from loopx.control_plane.work_items.delivery_outcome import ( # noqa: E402 DELIVERY_TURN_KIND_CHOICES, DeliveryOutcome, DeliveryTurnKind, - delivery_turn_kind_for_run, normalize_delivery_turn_kind, require_delivery_turn_kind, ) @@ -47,7 +47,7 @@ def assert_invalid_explicit_kind_does_not_fallback_to_classification() -> None: "delivery_outcome": DeliveryOutcome.SURFACE_ONLY.value, "delivery_turn_kind": "contract_v0_delivered", } - assert delivery_turn_kind_for_run(run) == DeliveryTurnKind.UNKNOWN.value + assert project_delivery_history([run])["runs"][0]["delivery_turn_kind"] == DeliveryTurnKind.UNKNOWN.value assert compact_post_handoff_run(run)["delivery_turn_kind"] == DeliveryTurnKind.UNKNOWN.value @@ -57,11 +57,11 @@ def assert_inference_still_works_when_kind_is_absent() -> None: "delivery_outcome": DeliveryOutcome.SURFACE_ONLY.value, } assert ( - delivery_turn_kind_for_run(contract_run) + project_delivery_history([contract_run])["runs"][0]["delivery_turn_kind"] == DeliveryTurnKind.CONTRACT_ONLY_PREPARATION.value ) assert ( - delivery_turn_kind_for_run({"delivery_outcome": DeliveryOutcome.PRIMARY_GOAL_OUTCOME.value}) + project_delivery_history([{"delivery_outcome": DeliveryOutcome.PRIMARY_GOAL_OUTCOME.value}])["runs"][0]["delivery_turn_kind"] == DeliveryTurnKind.PRODUCT_PATH_EXECUTION.value ) diff --git a/examples/outcome-followthrough-policy-smoke.py b/examples/outcome-followthrough-policy-smoke.py index 27a247a61d..17926c2825 100644 --- a/examples/outcome-followthrough-policy-smoke.py +++ b/examples/outcome-followthrough-policy-smoke.py @@ -7,7 +7,11 @@ REPO_ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(REPO_ROOT)) -from loopx.control_plane.work_items.outcome_followthrough import build_outcome_followthrough_hint # noqa: E402 +from loopx.control_plane.work_items.work_lane_context import outcome_followthrough_hint # noqa: E402 + + +def build_outcome_followthrough_hint(run): + return outcome_followthrough_hint({"handoff_readiness": {"post_handoff_latest_run": run}}) def assert_none(latest_run: dict[str, object] | None) -> None: diff --git a/examples/project/project-handoff-readmodel-smoke.py b/examples/project/project-handoff-readmodel-smoke.py index 7dc73145ca..99cbee669a 100644 --- a/examples/project/project-handoff-readmodel-smoke.py +++ b/examples/project/project-handoff-readmodel-smoke.py @@ -36,10 +36,7 @@ def _projection_state( is_handoff_ready_run=status_module.is_handoff_ready_run, is_custom_post_handoff_work_run=status_module.is_custom_post_handoff_work_run, is_status_neutral_run=status_module.is_status_neutral_run, - compact_post_handoff_run=status_module.compact_post_handoff_run, - small_delivery_batch_scale_streak=status_module.small_delivery_batch_scale_streak, - outcome_floor_configured=status_module.outcome_floor_configured, - outcome_gap_streak=status_module.outcome_gap_streak, + project_delivery_history=status_module.project_post_handoff_history, ) diff --git a/loopx/canary/planner.py b/loopx/canary/planner.py index c9b6d2e01d..129e80837c 100644 --- a/loopx/canary/planner.py +++ b/loopx/canary/planner.py @@ -311,7 +311,8 @@ "loopx/control_plane/todos/handoff_gate.py", "loopx/control_plane/work_items/execution_obligation.py", "loopx/control_plane/work_items/goal_route_hint.py", - "loopx/control_plane/work_items/outcome_followthrough.py", + "loopx/control_plane/work_items/delivery_history.py", + "loopx/control_plane/work_items/delivery_history.ts", "loopx/control_plane/work_items/work_lane.py", "loopx/control_plane/runtime/event_store_migration_bridge.py", ), diff --git a/loopx/canary/qualification_profiles.py b/loopx/canary/qualification_profiles.py index 32376a39ea..764284f87a 100644 --- a/loopx/canary/qualification_profiles.py +++ b/loopx/canary/qualification_profiles.py @@ -106,7 +106,8 @@ "loopx/control_plane/work_items/interaction_contract.py", "loopx/control_plane/work_items/execution_obligation.py", "loopx/control_plane/work_items/goal_route_hint.py", - "loopx/control_plane/work_items/outcome_followthrough.py", + "loopx/control_plane/work_items/delivery_history.py", + "loopx/control_plane/work_items/delivery_history.ts", "loopx/control_plane/work_items/work_lane.py", "loopx/control_plane/runtime/event_store_migration_bridge.py", "control-plane-integrated-canary-smoke.py", diff --git a/loopx/control_plane/effect_runtime_handlers.ts b/loopx/control_plane/effect_runtime_handlers.ts index 056688bff0..537e0e7318 100644 --- a/loopx/control_plane/effect_runtime_handlers.ts +++ b/loopx/control_plane/effect_runtime_handlers.ts @@ -94,6 +94,7 @@ import { qualifyActionSelection, } from "./work_items/action_portfolio.ts"; import { projectQuotaPlanningHorizon } from "./work_items/planning_horizon.ts"; +import { projectDeliveryHistory } from "./work_items/delivery_history.ts"; import { evaluateTaskLeaseAcquireDecision, evaluateTaskLeaseWriteScopesOverlap, @@ -406,6 +407,7 @@ export function createEffectRuntimeHandlers( ["work_item.planning_inventory.project", projectTodoPlanningInventory], ["work_item.planning_inventory.detail", projectTodoPlanningInventoryDetail], ["work_item.refresh_recommendation.resolve", resolveRefreshRecommendation], + ["work_item.delivery_history.project", projectDeliveryHistory], ["goal.vision_checkpoint.evaluate", buildVisionCheckpoint], ["goal.vision_wait.coverage", projectVisionWaitCoverage], ["goal.shared_goal_alignment.project", projectSharedGoalAlignment], diff --git a/loopx/control_plane/handoff/project_handoff.py b/loopx/control_plane/handoff/project_handoff.py index 7b39389ab3..acf5d71778 100644 --- a/loopx/control_plane/handoff/project_handoff.py +++ b/loopx/control_plane/handoff/project_handoff.py @@ -14,10 +14,7 @@ def project_asset_handoff_state( is_handoff_ready_run: Callable[[dict[str, Any]], bool], is_custom_post_handoff_work_run: Callable[[dict[str, Any]], bool], is_status_neutral_run: Callable[[dict[str, Any]], bool], - compact_post_handoff_run: Callable[[dict[str, Any], dict[str, Any] | None], dict[str, Any]], - small_delivery_batch_scale_streak: Callable[[list[dict[str, Any]]], int], - outcome_floor_configured: Callable[[dict[str, Any] | None], bool], - outcome_gap_streak: Callable[[list[dict[str, Any]], dict[str, Any] | None], int], + project_delivery_history: Callable[[list[dict[str, Any]], dict[str, Any] | None], dict[str, Any]], ) -> dict[str, Any]: runs = [run for run in latest_runs or [] if isinstance(run, dict)] profile = compact_execution_profile( @@ -97,21 +94,8 @@ def project_asset_handoff_state( state["handoff_ready_at"] = handoff_run.get("generated_at") if handoff_run and handoff_run.get("classification"): state["handoff_ready_classification"] = handoff_run.get("classification") - if post_handoff_run: - state["post_handoff_latest_run"] = compact_post_handoff_run(post_handoff_run, profile) if recent_post_handoff_runs: - state["post_handoff_recent_runs"] = [ - compact_post_handoff_run(run, profile) - for run in recent_post_handoff_runs - ] - state["post_handoff_small_scale_streak"] = small_delivery_batch_scale_streak( - recent_post_handoff_runs - ) - if outcome_floor_configured(profile): - state["post_handoff_outcome_gap_streak"] = outcome_gap_streak( - recent_post_handoff_runs, - profile, - ) + state.update(project_delivery_history(recent_post_handoff_runs, profile)) return state diff --git a/loopx/control_plane/work_items/delivery_history.py b/loopx/control_plane/work_items/delivery_history.py new file mode 100644 index 0000000000..09c430a041 --- /dev/null +++ b/loopx/control_plane/work_items/delivery_history.py @@ -0,0 +1,72 @@ +"""Compact transport for the typed delivery-history read model, never a writer.""" +from __future__ import annotations + +from collections.abc import Mapping +from enum import Enum +from typing import Any + +from ..effect_runtime import effect_runtime_result + + +def _text(value: Any) -> str: + # Preserve Python's legacy scalar/Enum transport without classifying prose. + return str((value.value if isinstance(value, Enum) else value) or "") + + +def _bounded_text(text: str) -> str: + # A non-whitespace invalid suffix survives downstream trim/normalization. + # A raw prefix could end with spaces and alias a valid enum or identifier. + return text if len(text) <= 128 else text[:128] + "!" + + +def _identifier_fact(value: Any) -> str: + text = _text(value) + # Keep one excess character as invalidity evidence, not a truncated valid id. + # Whitespace-only bindings stay present for the exactly-one-scope check. + return _bounded_text(text.strip()) or (" " if text else "") + + +def _run_facts(run: Mapping[str, Any]) -> dict[str, Any]: + observation = run.get("progress_observation") + compact_observation = None + if isinstance(observation, Mapping): + compact_observation = { + **{key: _bounded_text(_text(observation.get(key))) for key in ("schema_version", "result_class")}, + **{key: _identifier_fact(observation.get(key)) for key in ("work_item_id", "blocker_id")}, + } + evidence = observation.get("evidence_ids") + compact_observation["evidence_ids"] = ( + [_identifier_fact(value) for value in evidence] if isinstance(evidence, list) else None + ) + return { + **{key: _bounded_text(_text(run.get(key)).strip()) for key in ( + "delivery_outcome", "delivery_batch_scale", "delivery_turn_kind", + )}, + **{key: _identifier_fact(run.get(key)) for key in ("todo_id", "replan_obligation_id")}, + "outcome_followthrough_required": run.get("outcome_followthrough_required") is True, + "progress_observation": compact_observation, + } + + +def project_delivery_history( + runs: list[dict[str, Any]], *, outcome_floor_configured: bool = False, +) -> dict[str, Any]: + """Project a caller-selected history batch with one managed-runtime request. + + Do not send raw trajectories, evidence bodies, recommendations or profiles. + The profile adapter passes only whether the legacy outcome floor is enabled. + """ + result = effect_runtime_result("work_item.delivery_history.project", { + "schema_version": "delivery_history_request_v0", + "runs": [_run_facts(run) for run in runs], + "outcome_floor_configured": outcome_floor_configured, + }) + if (not isinstance(result, dict) or result.get("schema_version") != "delivery_history_v0" + or not isinstance(result.get("runs"), list) or len(result["runs"]) != len(runs)): + raise RuntimeError("TypeScript delivery history shape mismatch") + for run, signal in zip(runs, result["runs"], strict=True): + hint = signal["outcome_followthrough"] + if hint is not None: + # Display-only annotation after the decision; narrative never enters TS. + hint["latest_classification"] = _text(run.get("classification")).strip() + return result diff --git a/loopx/control_plane/work_items/delivery_history.ts b/loopx/control_plane/work_items/delivery_history.ts new file mode 100644 index 0000000000..f211b76633 --- /dev/null +++ b/loopx/control_plane/work_items/delivery_history.ts @@ -0,0 +1,135 @@ +import type { JsonObject } from "../effect_program.ts"; +import { EffectRuntimeRequestError } from "../effect_runtime_errors.ts"; +import { requireJsonObject } from "../runtime_decode.ts"; +import { DELIVERY_OUTCOMES, isTurnScopedSettlementOutcome, type DeliveryOutcome } from "./delivery_outcome.ts"; + +const TURN_KINDS = [ + "contract_only_preparation", "compact_evidence", "blocker_writeback", + "product_path_execution", "outcome_gap", "unknown", +] as const; +type TurnKind = typeof TURN_KINDS[number]; +type BatchScale = "test_only" | "single_surface" | "multi_surface" | "implementation" | "unknown"; +type OutcomeSignal = DeliveryOutcome | "unknown" | "not_configured"; + +interface DeliveryRun { + delivery_outcome: string; + delivery_batch_scale: string; + delivery_turn_kind: string; + todo_id: string; + replan_obligation_id: string; + outcome_followthrough_required: boolean; + progress_observation: JsonObject | null; +} + +interface DeliverySignal extends JsonObject { + delivery_outcome: OutcomeSignal; + delivery_batch_scale: BatchScale; + delivery_turn_kind: TurnKind; + outcome_followthrough: JsonObject | null; +} + +function decodeRun(value: unknown): DeliveryRun { + const raw = requireJsonObject(value, "delivery run"); + const text = (key: string): string => { + if (typeof raw[key] !== "string") throw new EffectRuntimeRequestError(`delivery run.${key} must be a string`); + return raw[key] as string; + }; + if (typeof raw.outcome_followthrough_required !== "boolean") { + throw new EffectRuntimeRequestError("delivery run.outcome_followthrough_required must be a boolean"); + } + return { + delivery_outcome: text("delivery_outcome"), + delivery_batch_scale: text("delivery_batch_scale"), + delivery_turn_kind: text("delivery_turn_kind"), + todo_id: text("todo_id"), + replan_obligation_id: text("replan_obligation_id"), + outcome_followthrough_required: raw.outcome_followthrough_required, + progress_observation: raw.progress_observation === null ? null : requireJsonObject(raw.progress_observation, "progress_observation"), + }; +} + +function batchScale(raw: string): BatchScale { + switch (raw.trim()) { + case "single_segment": case "bounded_segment": case "single_surface": return "single_surface"; + case "test_only": return "test_only"; + case "multi_surface": return "multi_surface"; + case "implementation": return "implementation"; + default: return "unknown"; + } +} + +function outcomeSignal(raw: string, floorConfigured: boolean): OutcomeSignal { + const value = raw.trim(); + const explicit = DELIVERY_OUTCOMES.find((candidate) => candidate === value); + return explicit ?? (value || floorConfigured ? "unknown" : "not_configured"); +} + +function turnKind(run: DeliveryRun, outcome: OutcomeSignal): TurnKind { + const explicit = run.delivery_turn_kind.trim(); + // An unsupported explicit kind stays unknown; narrative is never a fallback. + if (explicit) return TURN_KINDS.find((kind) => kind === explicit) ?? "unknown"; + const binding = Boolean(run.todo_id) !== Boolean(run.replan_obligation_id) + ? (run.todo_id || run.replan_obligation_id).trim() : null; + if (outcome === "outcome_gap" && isTurnScopedSettlementOutcome(outcome, run.progress_observation, binding)) { + return "blocker_writeback"; + } + switch (outcome) { + case "primary_goal_outcome": return "product_path_execution"; + case "outcome_progress": return "compact_evidence"; + case "surface_only": return "contract_only_preparation"; + case "outcome_gap": return "outcome_gap"; + default: return "unknown"; + } +} + +function needsFollowthrough(outcome: OutcomeSignal): boolean { + return outcome === "surface_only" || outcome === "outcome_gap"; +} + +function followthrough(run: DeliveryRun, outcome: OutcomeSignal, kind: TurnKind): JsonObject | null { + if (outcome === "primary_goal_outcome") return null; + if (!run.outcome_followthrough_required && ( + kind === "blocker_writeback" || (!needsFollowthrough(outcome) && kind !== "contract_only_preparation") + )) return null; + return { + source: "post_handoff_latest_run", + required: true, + latest_delivery_outcome: outcome === "unknown" || outcome === "not_configured" ? null : outcome, + latest_delivery_turn_kind: kind, + obligation: "advance_primary_outcome_or_write_blocker", + accepted_resolution_kinds: ["product_path_execution", "compact_evidence", "blocker_writeback"], + spend_policy: "do not spend for another contract/preparation-only slice; spend only after validated goal-outcome evidence or a precise blocker writeback", + }; +} + +function prefixLength(items: readonly T[], matches: (item: T) => boolean): number { + const boundary = items.findIndex((item) => !matches(item)); + return boundary < 0 ? items.length : boundary; +} + +/** One pure batch projection. History selection/order remains the caller's job; + * this read model grants neither progress nor a durable settlement receipt. */ +export function projectDeliveryHistory(value: unknown): JsonObject { + const input = requireJsonObject(value, "delivery history"); + if (input.schema_version !== "delivery_history_request_v0" || !Array.isArray(input.runs) + || typeof input.outcome_floor_configured !== "boolean") { + throw new EffectRuntimeRequestError("invalid delivery_history_request_v0"); + } + const floorConfigured = input.outcome_floor_configured; + const runs: DeliverySignal[] = input.runs.map(decodeRun).map((run) => { + const outcome = outcomeSignal(run.delivery_outcome, floorConfigured); + const kind = turnKind(run, outcome); + return { + delivery_outcome: outcome, + delivery_batch_scale: batchScale(run.delivery_batch_scale), + delivery_turn_kind: kind, + outcome_followthrough: followthrough(run, outcome, kind), + }; + }); + return { + schema_version: "delivery_history_v0", + runs, + small_scale_streak: prefixLength(runs, (run) => ["test_only", "single_surface"].includes(run.delivery_batch_scale)), + outcome_gap_streak: floorConfigured ? prefixLength(runs, (run) => needsFollowthrough(run.delivery_outcome)) : 0, + }; +} diff --git a/loopx/control_plane/work_items/delivery_outcome.py b/loopx/control_plane/work_items/delivery_outcome.py index 61df225e3d..8d795dce68 100644 --- a/loopx/control_plane/work_items/delivery_outcome.py +++ b/loopx/control_plane/work_items/delivery_outcome.py @@ -33,8 +33,6 @@ class DeliveryTurnKind(str, Enum): DELIVERY_OUTCOME_CHOICES = tuple(outcome.value for outcome in DeliveryOutcome) DELIVERY_TURN_KIND_CHOICES = tuple(kind.value for kind in DeliveryTurnKind) -DELIVERY_OUTCOME_UNKNOWN = "unknown" -DELIVERY_OUTCOME_NOT_CONFIGURED = "not_configured" MATERIAL_DELIVERY_OUTCOMES = frozenset( { @@ -49,12 +47,6 @@ class DeliveryTurnKind(str, Enum): DeliveryOutcome.PRIMARY_GOAL_OUTCOME, } ) -FOLLOWTHROUGH_REQUIRED_DELIVERY_OUTCOMES = frozenset( - { - DeliveryOutcome.SURFACE_ONLY, - DeliveryOutcome.OUTCOME_GAP, - } -) PROGRESS_DELIVERY_OUTCOMES = ACCOUNTABLE_DELIVERY_OUTCOMES @@ -86,7 +78,8 @@ def qualifies_turn_scoped_blocker_settlement( work_item_id or replan_obligation_id ) if ( - observation.get("result_class") != ProgressResultClass.BLOCKED.value + normalized_work_item_id is None + or observation.get("result_class") != ProgressResultClass.BLOCKED.value or normalize_progress_identifier(observation.get("blocker_id")) is None or normalize_progress_identifier(observation.get("work_item_id")) != normalized_work_item_id @@ -160,41 +153,3 @@ def require_delivery_turn_kind(value: Any) -> DeliveryTurnKind: if kind is None: raise ValueError("delivery_turn_kind must be one of: " + ", ".join(DELIVERY_TURN_KIND_CHOICES)) return kind - - -def delivery_turn_kind_for_run( - run: dict[str, Any], - *, - delivery_outcome: Any = None, -) -> str: - """Read explicit delivery semantics; narrative and evidence presence grant none. - - Missing or invalid historical fields remain unknown. A blocker needs an - explicit turn kind or a scoped typed observation; an outcome gap alone - does not prove blocker writeback. - """ - - raw_explicit = str(run.get("delivery_turn_kind") or "").strip() - if raw_explicit: - explicit = normalize_delivery_turn_kind(raw_explicit) - return explicit.value if explicit else DeliveryTurnKind.UNKNOWN.value - - outcome = normalize_delivery_outcome( - delivery_outcome if delivery_outcome is not None else run.get("delivery_outcome") - ) - if qualifies_turn_scoped_blocker_settlement( - outcome, - run.get("progress_observation"), - work_item_id=run.get("todo_id"), - replan_obligation_id=run.get("replan_obligation_id"), - ): - return DeliveryTurnKind.BLOCKER_WRITEBACK.value - if outcome == DeliveryOutcome.PRIMARY_GOAL_OUTCOME: - return DeliveryTurnKind.PRODUCT_PATH_EXECUTION.value - if outcome == DeliveryOutcome.OUTCOME_PROGRESS: - return DeliveryTurnKind.COMPACT_EVIDENCE.value - if outcome == DeliveryOutcome.SURFACE_ONLY: - return DeliveryTurnKind.CONTRACT_ONLY_PREPARATION.value - if outcome == DeliveryOutcome.OUTCOME_GAP: - return DeliveryTurnKind.OUTCOME_GAP.value - return DeliveryTurnKind.UNKNOWN.value diff --git a/loopx/control_plane/work_items/delivery_signals.py b/loopx/control_plane/work_items/delivery_signals.py deleted file mode 100644 index bb448259a8..0000000000 --- a/loopx/control_plane/work_items/delivery_signals.py +++ /dev/null @@ -1,77 +0,0 @@ -from __future__ import annotations - -from collections.abc import Callable -from typing import Any - -from .delivery_batch_scale import UNKNOWN_DELIVERY_BATCH_SCALE, normalize_delivery_batch_scale -from .delivery_outcome import ( - DELIVERY_OUTCOME_NOT_CONFIGURED, - DELIVERY_OUTCOME_UNKNOWN, - FOLLOWTHROUGH_REQUIRED_DELIVERY_OUTCOMES, - normalize_delivery_outcome, -) - - -def delivery_batch_scale_for_run(run: dict[str, Any]) -> str: - explicit = normalize_delivery_batch_scale(run.get("delivery_batch_scale")) - return explicit.value if explicit else UNKNOWN_DELIVERY_BATCH_SCALE - - -def delivery_outcome_for_run( - run: dict[str, Any], - profile: dict[str, Any] | None = None, - *, - execution_profile_outcome_floor: Callable[[dict[str, Any] | None], dict[str, Any]], -) -> str: - explicit = normalize_delivery_outcome(run.get("delivery_outcome")) - if explicit: - return explicit.value - if str(run.get("delivery_outcome") or "").strip(): - return DELIVERY_OUTCOME_UNKNOWN - if not outcome_floor_configured( - profile, execution_profile_outcome_floor=execution_profile_outcome_floor - ): - return DELIVERY_OUTCOME_NOT_CONFIGURED - return DELIVERY_OUTCOME_UNKNOWN - - -def outcome_floor_configured( - profile: dict[str, Any] | None, - *, - execution_profile_outcome_floor: Callable[[dict[str, Any] | None], dict[str, Any]], -) -> bool: - floor = execution_profile_outcome_floor(profile) - return bool(floor.get("outcome_markers") or floor.get("surface_only_hints")) - - -def outcome_gap_streak( - runs: list[dict[str, Any]], - profile: dict[str, Any] | None = None, - *, - delivery_outcome_for_run: Callable[[dict[str, Any], dict[str, Any] | None], str], - outcome_floor_configured: Callable[[dict[str, Any] | None], bool], -) -> int: - if not outcome_floor_configured(profile): - return 0 - streak = 0 - for run in runs: - outcome = delivery_outcome_for_run(run, profile) - normalized = normalize_delivery_outcome(outcome) - if normalized not in FOLLOWTHROUGH_REQUIRED_DELIVERY_OUTCOMES: - break - streak += 1 - return streak - - -def small_delivery_batch_scale_streak( - runs: list[dict[str, Any]], - *, - delivery_batch_scale_for_run: Callable[[dict[str, Any]], str], - small_delivery_batch_scales: set[str], -) -> int: - streak = 0 - for run in runs: - if delivery_batch_scale_for_run(run) not in small_delivery_batch_scales: - break - streak += 1 - return streak diff --git a/loopx/control_plane/work_items/outcome_followthrough.py b/loopx/control_plane/work_items/outcome_followthrough.py deleted file mode 100644 index 5b3cc16119..0000000000 --- a/loopx/control_plane/work_items/outcome_followthrough.py +++ /dev/null @@ -1,69 +0,0 @@ -from __future__ import annotations - -from typing import Any - -from .delivery_outcome import ( - DeliveryOutcome, - DeliveryTurnKind, - FOLLOWTHROUGH_REQUIRED_DELIVERY_OUTCOMES, - delivery_turn_kind_for_run, - normalize_delivery_outcome, -) - - -def build_outcome_followthrough_hint( - latest_run: dict[str, Any] | None, -) -> dict[str, Any] | None: - """Build the follow-through obligation for a post-handoff latest run. - - The helper intentionally preserves the quota payload shape. It only moves - the policy decision out of the quota builder so future rule changes can be - tested without touching the broader status/quota extraction path. - """ - - if not isinstance(latest_run, dict) or not latest_run: - return None - - explicit_required = latest_run.get("outcome_followthrough_required") is True - delivery_outcome = normalize_delivery_outcome(latest_run.get("delivery_outcome")) - delivery_turn_kind = delivery_turn_kind_for_run( - latest_run, - delivery_outcome=delivery_outcome, - ) - if delivery_outcome == DeliveryOutcome.PRIMARY_GOAL_OUTCOME: - return None - if ( - not explicit_required - and delivery_turn_kind == DeliveryTurnKind.BLOCKER_WRITEBACK.value - ): - return None - - classification = str(latest_run.get("classification") or "").strip() - kind_requires_followthrough = ( - delivery_turn_kind == DeliveryTurnKind.CONTRACT_ONLY_PREPARATION.value - ) - if ( - not explicit_required - and delivery_outcome not in FOLLOWTHROUGH_REQUIRED_DELIVERY_OUTCOMES - and not kind_requires_followthrough - ): - return None - - return { - "source": "post_handoff_latest_run", - "required": True, - "latest_classification": classification, - "latest_delivery_outcome": delivery_outcome.value if delivery_outcome else None, - "latest_delivery_turn_kind": delivery_turn_kind, - "obligation": "advance_primary_outcome_or_write_blocker", - "accepted_resolution_kinds": [ - DeliveryTurnKind.PRODUCT_PATH_EXECUTION.value, - DeliveryTurnKind.COMPACT_EVIDENCE.value, - DeliveryTurnKind.BLOCKER_WRITEBACK.value, - ], - "spend_policy": ( - "do not spend for another contract/preparation-only slice; spend only " - "after validated goal-outcome evidence or a " - "precise blocker writeback" - ), - } diff --git a/loopx/control_plane/work_items/work_lane_context.py b/loopx/control_plane/work_items/work_lane_context.py index 953c83babc..d562826cea 100644 --- a/loopx/control_plane/work_items/work_lane_context.py +++ b/loopx/control_plane/work_items/work_lane_context.py @@ -14,7 +14,7 @@ todo_summary_monitor_schedule_gap_items, todo_summary_open_task_counts, ) -from .outcome_followthrough import build_outcome_followthrough_hint +from .delivery_history import project_delivery_history from .work_lane import ( build_work_lane_contract, due_monitor_preempts_advancement, @@ -73,7 +73,8 @@ def post_handoff_latest_run(item: dict[str, Any]) -> dict[str, Any]: def outcome_followthrough_hint(item: dict[str, Any]) -> dict[str, Any] | None: - return build_outcome_followthrough_hint(post_handoff_latest_run(item)) + run = post_handoff_latest_run(item) + return project_delivery_history([run])["runs"][0]["outcome_followthrough"] if run else None def next_action_requires_advancement(item: dict[str, Any]) -> bool: diff --git a/loopx/status.py b/loopx/status.py index e48ef3967e..40a39200d1 100644 --- a/loopx/status.py +++ b/loopx/status.py @@ -14,13 +14,6 @@ build_status_runtime_summaries as _build_status_runtime_summaries_read_model, ) from .contract import check_contract -from .control_plane.work_items.delivery_batch_scale import ( - SMALL_DELIVERY_BATCH_SCALES as STRUCTURED_SMALL_DELIVERY_BATCH_SCALES, -) -from .control_plane.work_items.delivery_outcome import ( - DELIVERY_OUTCOME_NOT_CONFIGURED, - delivery_turn_kind_for_run, -) from .doctor import ( PROMOTION_READINESS_CLASSIFICATIONS, PROMOTION_READINESS_FRESHNESS_HOURS, @@ -95,13 +88,7 @@ from .control_plane.work_items.backlog_hygiene import ( MAX_BACKLOG_HYGIENE_EVIDENCE_ITEMS as _MAX_BACKLOG_HYGIENE_EVIDENCE_ITEMS_READ_MODEL, ) -from .control_plane.work_items.delivery_signals import ( - delivery_batch_scale_for_run as _delivery_batch_scale_for_run_read_model, - delivery_outcome_for_run as _delivery_outcome_for_run_read_model, - outcome_floor_configured as _outcome_floor_configured_read_model, - outcome_gap_streak as _outcome_gap_streak_read_model, - small_delivery_batch_scale_streak as _small_delivery_batch_scale_streak_read_model, -) +from .control_plane.work_items.delivery_history import project_delivery_history from .control_plane.runtime.run_compaction import ( RUN_BASE_COMPACT_FIELDS, attach_run_summary_projections as _attach_run_summary_projections_read_model, @@ -291,9 +278,6 @@ ) -SMALL_DELIVERY_BATCH_SCALES = { - scale.value for scale in STRUCTURED_SMALL_DELIVERY_BATCH_SCALES -} CONNECTED_ADAPTER_STATUSES = { "connected", "connected-read-only", @@ -584,62 +568,38 @@ def is_custom_post_handoff_work_run(run: dict[str, Any]) -> bool: ) -def delivery_batch_scale_for_run(run: dict[str, Any]) -> str: - return _delivery_batch_scale_for_run_read_model(run) - - -def delivery_outcome_for_run(run: dict[str, Any], profile: dict[str, Any] | None = None) -> str: - return _delivery_outcome_for_run_read_model( - run, - profile, - execution_profile_outcome_floor=execution_profile_outcome_floor, - ) - - -def outcome_floor_configured(profile: dict[str, Any] | None) -> bool: - return _outcome_floor_configured_read_model( - profile, - execution_profile_outcome_floor=execution_profile_outcome_floor, - ) - - -def outcome_gap_streak(runs: list[dict[str, Any]], profile: dict[str, Any] | None = None) -> int: - return _outcome_gap_streak_read_model( - runs, - profile, - delivery_outcome_for_run=delivery_outcome_for_run, - outcome_floor_configured=outcome_floor_configured, - ) - - def compact_post_handoff_run(run: dict[str, Any], profile: dict[str, Any] | None = None) -> dict[str, Any]: - compact: dict[str, Any] = {} - for field in ("generated_at", "classification", "health_check", "json_exists", "markdown_exists"): - if field in run: - compact[field] = run[field] - compact["delivery_batch_scale"] = delivery_batch_scale_for_run(run) - outcome = delivery_outcome_for_run(run, profile) - if outcome != DELIVERY_OUTCOME_NOT_CONFIGURED: - compact["delivery_outcome"] = outcome - compact["delivery_turn_kind"] = delivery_turn_kind_for_run( - run, - delivery_outcome=outcome, - ) - return _attach_run_summary_projections_read_model( - compact, - run, - compact_session_runtime_projection_from_run=( - compact_session_runtime_projection_from_run - ), - ) + return project_post_handoff_history([run], profile)["post_handoff_latest_run"] -def small_delivery_batch_scale_streak(runs: list[dict[str, Any]]) -> int: - return _small_delivery_batch_scale_streak_read_model( - runs, - delivery_batch_scale_for_run=delivery_batch_scale_for_run, - small_delivery_batch_scales=SMALL_DELIVERY_BATCH_SCALES, - ) +def project_post_handoff_history( + runs: list[dict[str, Any]], profile: dict[str, Any] | None = None, +) -> dict[str, Any]: + if not runs: + return {} + floor = execution_profile_outcome_floor(profile) + floor_configured = bool(floor.get("outcome_markers") or floor.get("surface_only_hints")) + projection = project_delivery_history(runs, outcome_floor_configured=floor_configured) + compact_runs = [] + for run, signal in zip(runs, projection["runs"], strict=True): + compact = {field: run[field] for field in ( + "generated_at", "classification", "health_check", "json_exists", "markdown_exists", + ) if field in run} + compact.update({field: signal[field] for field in ("delivery_batch_scale", "delivery_turn_kind")}) + if signal["delivery_outcome"] != "not_configured": + compact["delivery_outcome"] = signal["delivery_outcome"] + compact_runs.append(_attach_run_summary_projections_read_model( + compact, run, + compact_session_runtime_projection_from_run=compact_session_runtime_projection_from_run, + )) + result = { + "post_handoff_latest_run": compact_runs[0], + "post_handoff_recent_runs": compact_runs, + "post_handoff_small_scale_streak": projection["small_scale_streak"], + } + if floor_configured: + result["post_handoff_outcome_gap_streak"] = projection["outcome_gap_streak"] + return result def project_asset_handoff_state( @@ -657,10 +617,7 @@ def project_asset_handoff_state( is_handoff_ready_run=is_handoff_ready_run, is_custom_post_handoff_work_run=is_custom_post_handoff_work_run, is_status_neutral_run=is_status_neutral_run, - compact_post_handoff_run=compact_post_handoff_run, - small_delivery_batch_scale_streak=small_delivery_batch_scale_streak, - outcome_floor_configured=outcome_floor_configured, - outcome_gap_streak=outcome_gap_streak, + project_delivery_history=project_post_handoff_history, ) diff --git a/tests/control_plane/test_delivery_history_projection.py b/tests/control_plane/test_delivery_history_projection.py new file mode 100644 index 0000000000..dcc9255614 --- /dev/null +++ b/tests/control_plane/test_delivery_history_projection.py @@ -0,0 +1,102 @@ +"""Coarse production-call and compact transport contracts for delivery history.""" +from copy import deepcopy +import json + +import pytest + +from loopx import status +from loopx.control_plane.work_items import delivery_history + + +def test_status_selects_one_bounded_history_batch_before_crossing(monkeypatch) -> None: + calls = [] + actual = delivery_history.effect_runtime_result + + def record(method, request): + calls.append(request) + return actual(method, request) + + monkeypatch.setattr(delivery_history, "effect_runtime_result", record) + runs = [{"generated_at": f"2026-09-01T00:00:{i:02d}Z", "classification": "custom_delivery", + "delivery_outcome": "surface_only", "delivery_batch_scale": "test_only"} for i in range(50)] + before = deepcopy(runs) + result = status.project_asset_handoff_state(ready=True, + project_asset={"execution_profile": {"outcome_floor": {"outcome_markers": ["merged"]}}}, + latest_runs=runs) + assert len(calls) == 1 + assert len(calls[0]["runs"]) == 3 + assert result["post_handoff_small_scale_streak"] == 3 + assert result["post_handoff_outcome_gap_streak"] == 3 + assert result["post_handoff_latest_run"]["generated_at"].endswith("49Z") + assert len(result["post_handoff_recent_runs"]) == 3 + assert runs == before + + +def test_huge_untrusted_fields_do_not_enter_the_decision_request(monkeypatch) -> None: + calls = [] + actual = delivery_history.effect_runtime_result + + def record(method, request): + calls.append(request) + return actual(method, request) + + monkeypatch.setattr(delivery_history, "effect_runtime_result", record) + run = {"delivery_outcome": "surface_only", "classification": "Display label", "health_check": "x" * 3_000_000, + "recommended_action": "x" * 3_000_000, "compact_evidence": {"raw": "x" * 3_000_000}, + "progress_observation": {"payload": "x" * 3_000_000}} + result = delivery_history.project_delivery_history([run]) + assert len(json.dumps(calls[0])) < 1_000 + assert "classification" not in calls[0]["runs"][0] + assert result["runs"][0]["outcome_followthrough"]["latest_classification"] == "Display label" + run["classification"] = "a different narrative" * 100_000 + delivery_history.project_delivery_history([run]) + assert calls[0] == calls[1] + + +@pytest.mark.parametrize("identity", ["a" * 3_000_000, "a" * 128 + " " * 200 + "b"]) +def test_oversized_typed_values_stay_invalid_after_compaction(identity) -> None: + run = {"delivery_outcome": "outcome_gap", "todo_id": identity, + "progress_observation": {"schema_version": "typed_progress_observation_v0", "result_class": "blocked", + "work_item_id": identity, "blocker_id": "blocker-a", "evidence_ids": ["evidence-a"]}} + signal = delivery_history.project_delivery_history([run])["runs"][0] + assert signal["delivery_turn_kind"] == "outcome_gap" + assert signal["outcome_followthrough"]["required"] is True + + +def test_truncated_enum_cannot_alias_a_valid_prefix_after_trim() -> None: + signal = delivery_history.project_delivery_history([{ + "delivery_outcome": "primary_goal_outcome" + " " * 200 + "invalid", + "delivery_turn_kind": "blocker_writeback" + " " * 200 + "invalid", + "delivery_batch_scale": "implementation" + " " * 200 + "invalid", + "outcome_followthrough_required": True, + }])["runs"][0] + assert signal["delivery_outcome"] == "unknown" + assert signal["delivery_turn_kind"] == "unknown" + assert signal["delivery_batch_scale"] == "unknown" + assert signal["outcome_followthrough"]["required"] is True + + +def test_empty_status_history_does_not_start_runtime(monkeypatch) -> None: + def unexpected(*_args, **_kwargs): + raise AssertionError("empty history needs no runtime") + + monkeypatch.setattr(delivery_history, "effect_runtime_result", unexpected) + assert status.project_post_handoff_history([]) == {} + + +def test_unavailable_runtime_never_falls_back_to_python_classification(monkeypatch) -> None: + def unavailable(*_args, **_kwargs): + raise RuntimeError("isolated unavailable runtime") + + monkeypatch.setattr(delivery_history, "effect_runtime_result", unavailable) + rows = [{"delivery_outcome": "surface_only"}] + with pytest.raises(RuntimeError, match="isolated unavailable runtime"): + delivery_history.project_delivery_history(rows) + + +@pytest.mark.parametrize("result", [None, {}, {"schema_version": "delivery_history_v0", "runs": []}]) +def test_invalid_response_cannot_silently_drop_history(monkeypatch, result) -> None: + monkeypatch.setattr(delivery_history, "effect_runtime_result", lambda *_args: result) + rows = [{"delivery_outcome": "surface_only"}] + with pytest.raises(RuntimeError, match="shape mismatch"): + delivery_history.project_delivery_history(rows) diff --git a/tests/control_plane/test_delivery_semantics.py b/tests/control_plane/test_delivery_semantics.py index 2b6bd99afd..f2cbd433f6 100644 --- a/tests/control_plane/test_delivery_semantics.py +++ b/tests/control_plane/test_delivery_semantics.py @@ -4,15 +4,35 @@ import pytest -from loopx.control_plane.work_items.delivery_outcome import delivery_turn_kind_for_run -from loopx.control_plane.work_items.outcome_followthrough import build_outcome_followthrough_hint -from loopx.status import ( - compact_post_handoff_run, - delivery_batch_scale_for_run, - delivery_outcome_for_run, - outcome_gap_streak, - small_delivery_batch_scale_streak, -) +from loopx.control_plane.work_items.delivery_history import project_delivery_history +from loopx.control_plane.work_items.work_lane_context import outcome_followthrough_hint +from loopx.status import compact_post_handoff_run + + +# Test adapters retain the independently characterized assertions after retiring +# the single-field production readers. Production projects the selected batch. +def delivery_turn_kind_for_run(run): + return project_delivery_history([run])["runs"][0]["delivery_turn_kind"] + + +def delivery_batch_scale_for_run(run): + return project_delivery_history([run])["runs"][0]["delivery_batch_scale"] + + +def delivery_outcome_for_run(run, profile=None): + return project_delivery_history([run], outcome_floor_configured=bool(profile))["runs"][0]["delivery_outcome"] + + +def outcome_gap_streak(runs, profile=None): + return project_delivery_history(runs, outcome_floor_configured=bool(profile))["outcome_gap_streak"] + + +def small_delivery_batch_scale_streak(runs): + return project_delivery_history(runs)["small_scale_streak"] + + +def build_outcome_followthrough_hint(run): + return outcome_followthrough_hint({"handoff_readiness": {"post_handoff_latest_run": run}}) PROFILE = { @@ -136,3 +156,51 @@ def test_explicit_legacy_blocker_and_explicit_obligation_remain_readable() -> No assert build_outcome_followthrough_hint(run) is None run["outcome_followthrough_required"] = True assert build_outcome_followthrough_hint(run)["required"] is True + + +@pytest.mark.parametrize("required", (False, None, 0, 1, "true", [], {})) +def test_followthrough_requires_literal_true_not_truthy_metadata(required) -> None: + assert build_outcome_followthrough_hint({"outcome_followthrough_required": required}) is None + + +def test_primary_outcome_precedes_explicit_followthrough_and_unknown_kind_does_not_recover() -> None: + assert build_outcome_followthrough_hint({ + "delivery_outcome": "primary_goal_outcome", "outcome_followthrough_required": True, + }) is None + assert delivery_turn_kind_for_run({ + "delivery_outcome": "primary_goal_outcome", "delivery_turn_kind": "future_kind", + }) == "unknown" + + +@pytest.mark.parametrize("scale", ("single_segment", "bounded_segment", " single_surface ")) +def test_legacy_scale_aliases_keep_small_batch_meaning(scale) -> None: + run = {"delivery_batch_scale": scale, "delivery_outcome": "surface_only"} + assert delivery_batch_scale_for_run(run) == "single_surface" + assert small_delivery_batch_scale_streak([run, run]) == 2 + assert outcome_gap_streak([run, run], None) == 0 + + +@pytest.mark.parametrize("binding", ( + {"replan_obligation_id": "replan-a"}, + {"todo_id": "todo-a", "replan_obligation_id": "replan-a"}, + {}, +)) +def test_blocker_binding_is_exactly_one_todo_or_replan(binding) -> None: + run = {**binding, "delivery_outcome": "outcome_gap", "progress_observation": { + "schema_version": "typed_progress_observation_v0", "result_class": "blocked", + "work_item_id": "replan-a", "blocker_id": "blocker-a", "evidence_ids": ["evidence-a"], + }} + expected = "blocker_writeback" if len(binding) == 1 else "outcome_gap" + assert delivery_turn_kind_for_run(run) == expected + + +@pytest.mark.parametrize("invalid_id", ("bad id", " ", "x" * 129)) +def test_invalid_identifier_pair_cannot_prove_blocker_attribution(invalid_id) -> None: + from loopx.control_plane.work_items.delivery_outcome import qualifies_turn_scoped_blocker_settlement + + observation = {"schema_version": "typed_progress_observation_v0", "result_class": "blocked", + "work_item_id": invalid_id, "blocker_id": "blocker-a", "evidence_ids": ["evidence-a"]} + assert not qualifies_turn_scoped_blocker_settlement("outcome_gap", observation, work_item_id=invalid_id) + run = {"delivery_outcome": "outcome_gap", "todo_id": invalid_id, "progress_observation": observation} + assert delivery_turn_kind_for_run(run) == "outcome_gap" + assert build_outcome_followthrough_hint(run)["required"] is True diff --git a/tests/control_plane/test_delivery_semantics_cli.py b/tests/control_plane/test_delivery_semantics_cli.py index 4474dbb4f8..fb6a81e621 100644 --- a/tests/control_plane/test_delivery_semantics_cli.py +++ b/tests/control_plane/test_delivery_semantics_cli.py @@ -6,13 +6,17 @@ import subprocess import sys -from loopx.control_plane.work_items.outcome_followthrough import build_outcome_followthrough_hint +from loopx.control_plane.work_items.work_lane_context import outcome_followthrough_hint from loopx.status import compact_post_handoff_run ROOT = Path(__file__).resolve().parents[2] +def build_outcome_followthrough_hint(run): + return outcome_followthrough_hint({"handoff_readiness": {"post_handoff_latest_run": run}}) + + def test_refresh_history_preserves_labels_without_inventing_delivery(tmp_path: Path) -> None: state = tmp_path / "ACTIVE_GOAL_STATE.md" state.write_text( diff --git a/tests/control_plane_ts/delivery_history.test.ts b/tests/control_plane_ts/delivery_history.test.ts new file mode 100644 index 0000000000..3e43f5da47 --- /dev/null +++ b/tests/control_plane_ts/delivery_history.test.ts @@ -0,0 +1,103 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import type { JsonObject } from "../../loopx/control_plane/effect_program.ts"; +import { projectDeliveryHistory } from "../../loopx/control_plane/work_items/delivery_history.ts"; + +function run(fields: JsonObject = {}): JsonObject { + return { delivery_outcome: "", delivery_batch_scale: "", delivery_turn_kind: "", + todo_id: "", replan_obligation_id: "", outcome_followthrough_required: false, + progress_observation: null, ...fields }; +} + +function project(runs: JsonObject[], configured = true): JsonObject { + return projectDeliveryHistory({ schema_version: "delivery_history_request_v0", + runs, outcome_floor_configured: configured }); +} + +function signal(fields: JsonObject): JsonObject { + return (project([run(fields)]).runs as JsonObject[])[0]; +} + +test("typed outcomes have distinct follow-through meaning, independent of arbitrary narrative", () => { + const cases = [ + ["surface_only", "contract_only_preparation", true], + ["outcome_gap", "outcome_gap", true], + ["outcome_progress", "compact_evidence", false], + ["primary_goal_outcome", "product_path_execution", false], + ["future_outcome", "unknown", false], + ] as const; + for (const [outcome, kind, required] of cases) { + const fields = { delivery_outcome: outcome, classification: "blocked contract merged", + health_check: "unblocked", recommended_action: "ship", compact_evidence: { result: "success" } }; + const actual = signal(fields); + assert.equal(actual.delivery_turn_kind, kind); + assert.equal(actual.outcome_followthrough !== null, required); + assert.deepEqual(actual, signal({ delivery_outcome: outcome })); + } +}); + +test("missing floor is elided while unsupported evidence remains unknown", () => { + const rows = (project([run(), run({ delivery_outcome: "future" })], false).runs as JsonObject[]); + assert.equal(rows[0].delivery_outcome, "not_configured"); + assert.equal(rows[1].delivery_outcome, "unknown"); + assert.equal(rows[0].delivery_turn_kind, "unknown"); +}); + +test("unsupported kind suppresses inference; explicit obligations do not override primary outcome", () => { + assert.equal(signal({ delivery_outcome: "outcome_progress", delivery_turn_kind: "future" }).delivery_turn_kind, "unknown"); + assert.equal(signal({ delivery_outcome: "primary_goal_outcome", outcome_followthrough_required: true }).outcome_followthrough, null); + assert.notEqual(signal({ outcome_followthrough_required: true }).outcome_followthrough, null); + assert.equal(signal({ delivery_outcome: "outcome_gap", delivery_turn_kind: "blocker_writeback" }).outcome_followthrough, null); + assert.notEqual(signal({ delivery_outcome: "outcome_gap", delivery_turn_kind: "blocker_writeback", outcome_followthrough_required: true }).outcome_followthrough, null); +}); + +test("unknown breaks consecutive streaks; aliases retain their small-scale meaning", () => { + const gap = run({ delivery_outcome: "surface_only", delivery_batch_scale: "single_segment" }); + const small = run({ delivery_outcome: "outcome_gap", delivery_batch_scale: "bounded_segment" }); + assert.equal(project([gap, small]).small_scale_streak, 2); + assert.equal(project([gap, small]).outcome_gap_streak, 2); + assert.equal(project([gap, run(), small]).small_scale_streak, 1); + assert.equal(project([gap, run(), small]).outcome_gap_streak, 1); + assert.equal(project([gap, small], false).outcome_gap_streak, 0); + assert.equal(project([]).outcome_gap_streak, 0); + assert.equal(project([]).small_scale_streak, 0); +}); + +test("blocker evidence must bind exactly one valid Todo or replan identity", () => { + const observation = { schema_version: "typed_progress_observation_v0", result_class: "blocked", + work_item_id: "todo-a", blocker_id: "blocker-a", evidence_ids: ["evidence-a"] }; + const fields = { delivery_outcome: "outcome_gap", todo_id: "todo-a", progress_observation: observation }; + assert.equal(signal(fields).delivery_turn_kind, "blocker_writeback"); + assert.equal(signal({ ...fields, todo_id: "", replan_obligation_id: "todo-a" }).delivery_turn_kind, "blocker_writeback"); + for (const patch of [{ todo_id: "" }, { todo_id: "different" }, { replan_obligation_id: "todo-a" }]) { + assert.equal(signal({ ...fields, ...patch }).delivery_turn_kind, "outcome_gap"); + } + for (const patch of [ { work_item_id: "other" }, { evidence_ids: [] }, { evidence_ids: ["valid", "bad id"] }, + { blocker_id: "" }, { result_class: "advanced" }, { schema_version: "future" } ]) { + const actual = signal({ ...fields, progress_observation: { ...observation, ...patch } }); + assert.equal(actual.delivery_turn_kind, "outcome_gap"); + assert.notEqual(actual.outcome_followthrough, null); + } +}); + +test("batch is immutable, ordered and not limited to a presentation cap", () => { + const rows = Array.from({ length: 257 }, () => run({ delivery_outcome: "outcome_gap", delivery_batch_scale: "test_only" })); + const before = structuredClone(rows); + const result = project(rows); + assert.equal(result.small_scale_streak, 257); + assert.equal(result.outcome_gap_streak, 257); + assert.equal((result.runs as unknown[]).length, 257); + assert.deepEqual(rows, before); +}); + +test("wire faults fail closed, not a fallback to untyped history", () => { + for (const input of [null, {}, { schema_version: "future", runs: [] }, + { schema_version: "delivery_history_request_v0", runs: [], outcome_floor_configured: "true" }]) { + assert.throws(() => projectDeliveryHistory(input)); + } + for (const fields of [{ delivery_outcome: true }, { outcome_followthrough_required: 1 }, + { progress_observation: [] }, { todo_id: null }]) { + const rows = [run(fields)]; + assert.throws(() => project(rows)); + } +}); diff --git a/tsconfig.control-plane.json b/tsconfig.control-plane.json index 313ab8ac75..3789c32bc1 100644 --- a/tsconfig.control-plane.json +++ b/tsconfig.control-plane.json @@ -83,6 +83,7 @@ "tests/control_plane_ts/authority_store_readback_probe.ts", "tests/control_plane_ts/postgresql_authority_store.integration.test.ts", "tests/control_plane_ts/delivery_continuity.test.ts", + "tests/control_plane_ts/delivery_history.test.ts", "tests/control_plane_ts/delivery_workspace.test.ts", "tests/control_plane_ts/settlement_workspace_causality.test.ts", "tests/control_plane_ts/quota_settlement_readback.test.ts", From 9e6dcd79baae333188b4f015e22f62c92f40b013 Mon Sep 17 00:00:00 2001 From: huangruiteng Date: Wed, 9 Sep 2026 13:02:42 +0800 Subject: [PATCH 2/2] docs(rfc): record delivery read-policy closure and retained writer boundary Signed-off-by: huangruiteng --- ...shared-goal-authority-state-provider-v0.md | 6 +++ ...-goal-authority-state-provider-v0.zh-CN.md | 5 +++ .../typescript-control-plane-migration-v0.md | 42 +++++++++++-------- ...script-control-plane-migration-v0.zh-CN.md | 32 ++++++++------ .../references/repair-patterns.md | 1 + 5 files changed, 57 insertions(+), 29 deletions(-) diff --git a/docs/architecture/rfcs/shared-goal-authority-state-provider-v0.md b/docs/architecture/rfcs/shared-goal-authority-state-provider-v0.md index 973b8c9b99..a193a13ae0 100644 --- a/docs/architecture/rfcs/shared-goal-authority-state-provider-v0.md +++ b/docs/architecture/rfcs/shared-goal-authority-state-provider-v0.md @@ -2504,6 +2504,12 @@ owns business-rule unification and caller deletion; this RFC owns one durable truth, recovery and cutover. Native CLI conversion and a daemon are not prerequisites, and PostgreSQL deployment must not hold local adoption hostage. +Delivery history now shares one TS outcome/scale/follow-through read projection +across status and quota, deleting the replaced Python decisions. This advances +the TS RFC independently: it changes no provider, durable history, writer fence +or promotion eligibility. Markdown remains a readable projection; neither it +nor narrative history labels become an additional delivery authority. + ```text CLI / Agent / Dashboard → one TS Todo transaction owner → canonical authority ├ structured consumers diff --git a/docs/architecture/rfcs/shared-goal-authority-state-provider-v0.zh-CN.md b/docs/architecture/rfcs/shared-goal-authority-state-provider-v0.zh-CN.md index 6f25b45587..7bc0546a47 100644 --- a/docs/architecture/rfcs/shared-goal-authority-state-provider-v0.zh-CN.md +++ b/docs/architecture/rfcs/shared-goal-authority-state-provider-v0.zh-CN.md @@ -1985,6 +1985,11 @@ Markdown 是**长期保留的一等可读投影**。退役的是它的数据库 负责业务规则统一和 caller 删除;本 RFC 负责唯一 durable truth、恢复和 cutover。 CLI 原生 TS 化与 daemon 不是前提,PostgreSQL 部署不能阻塞本地采用。 +交付历史现已由 status 与 quota 共用一份 TS outcome/scale/follow-through 读投影, +删除被替代的 Python decision。这独立推进 TS RFC,不改变 provider、持久历史、 +writer fence 或 promotion 资格。Markdown 仍作为可读投影;它和历史叙述标签都 +不能成为额外的交付权威。 + ```text CLI / Agent / Dashboard → 唯一 TS Todo 事务 owner → canonical authority ├ structured consumers diff --git a/docs/architecture/rfcs/typescript-control-plane-migration-v0.md b/docs/architecture/rfcs/typescript-control-plane-migration-v0.md index a0babf4b19..578bf540ba 100644 --- a/docs/architecture/rfcs/typescript-control-plane-migration-v0.md +++ b/docs/architecture/rfcs/typescript-control-plane-migration-v0.md @@ -150,12 +150,18 @@ follow-through obligation, prove an outcome, or classify delivery scale. For example, `unblocked after dependency update` is not a blocker receipt and `implemented network protocol parser` is not preparation-only evidence. -The owning modules remain `control_plane/work_items/delivery_outcome.py`, -`delivery_signals.py`, and `outcome_followthrough.py`. This is a correctness -prerequisite inside the existing owner, not a new capability/provider or a -completed TypeScript transaction migration. It deletes keyword inference and -its status constants without adding a runtime crossing, schema, or service. -The existing typed blocker-settlement predicate is reused rather than copied. +`control_plane/work_items/delivery_history.ts` now owns the complete delivery +history-to-obligation read projection: outcome, turn kind, scale, consecutive +streaks and follow-through. Status selects one bounded history batch before one +`work_item.delivery_history.project` request; quota's latest-run consumer uses +the same projection with one row. This adds a managed-runtime crossing where +Python previously decided locally, not one request per field or historical row. +The Python bridge sends compact typed facts, never narrative or evidence bodies; +display-only classification is attached after the decision. The replaced +`delivery_signals.py`, `outcome_followthrough.py`, turn-kind inference and status +streak wrappers are deleted. Existing TS blocker binding is reused. Python enum +codecs and the settlement writer predicate still have real callers and remain; +this is not a writer/transaction or provider migration. The acceptance invariant is **narrative non-interference**: holding typed fields and configuration fixed, rewriting narrative or adding an unvalidated @@ -182,18 +188,20 @@ label; no legacy prediction is retained without a concrete display consumer. erroneous behavior. This intentionally changes status, handoff/review, and quota decisions previously derived from untyped historical labels. -Within this delivery domain, the migration unit is the complete -delivery-history-to-obligation projection, including scale/outcome streaks and -its status/quota consumers. This defines the slice boundary without displacing -the provider-first Todo sequence below. -It must cross at most once per bounded history batch, delete the replaced -Python decision path, preserve independently reviewed typed cases, and retain -narrative-mutation regressions through the real CLI. Transport-only golden -parity is insufficient because the old inference was incorrect. Separately -inventory writers still omitting material-result fields and retire obsolete +The migration preserves independently characterized legal typed behavior and +validates real refresh/history/status/quota entrypoints, batch cardinality and +narrative non-interference. One intentional correction is separate from parity: +two invalid work-item identifiers must not compare equal merely because both +normalize to a missing value. Such observations cannot infer blocker writeback +or discharge a follow-through obligation. The remaining Python writer predicate +rejects that case too; no active history is rewritten. + +Next, inventory writers still omitting material-result fields and retire obsolete marker/hint configuration with an explicit compatibility plan. Exact legacy -lifecycle classification codes and unrelated cadence policies are outside this -slice; they must not be reported as migrated or globally free of prose rules. +lifecycle classification codes, history selection and unrelated cadence policies +remain outside this slice. Do not claim all writers migrated or all prose rules +retired. This read-policy closure does not displace the provider-first Todo +sequence below or wait for a provider cutover. ### Legacy field-rule retirement checkpoint diff --git a/docs/architecture/rfcs/typescript-control-plane-migration-v0.zh-CN.md b/docs/architecture/rfcs/typescript-control-plane-migration-v0.zh-CN.md index caec32439a..6d9350879e 100644 --- a/docs/architecture/rfcs/typescript-control-plane-migration-v0.zh-CN.md +++ b/docs/architecture/rfcs/typescript-control-plane-migration-v0.zh-CN.md @@ -120,11 +120,16 @@ receipt 过期。 不能判定交付规模。例如,`unblocked after dependency update` 不构成 blocker receipt,`implemented network protocol parser` 不构成仅完成准备工作的证据。 -规则继续由 `control_plane/work_items/delivery_outcome.py`、`delivery_signals.py` -和 `outcome_followthrough.py` 持有。本批在既有 owner 中完成正确性前置修复,不新增 -capability/provider,也不宣称完成 TypeScript 事务迁移。删除关键词推断与 status -常量,不增加 runtime crossing、schema 或 service;复用已有 typed blocker -settlement 判定,不复制证据绑定规则。 +`control_plane/work_items/delivery_history.ts` 现在持有完整的交付历史到后续义务 +读投影:outcome、turn kind、scale、连续计数及 follow-through。Status 先选出一批 +有界历史,再调用一次 `work_item.delivery_history.project`;quota 的 latest-run +消费者以单行调用同一投影。相比此前 Python 本地判断,新增 managed-runtime crossing, +但不是每个字段或每条历史各跨一次。Python bridge 只发送紧凑 typed facts,不发送 +叙述或证据正文;classification 在决策之后作为展示标签附加。 +删除被替代的 `delivery_signals.py`、`outcome_followthrough.py`、turn-kind 推断 +及 status 连续计数 wrapper,复用已有 TS blocker 绑定规则。Python enum codec 和 +settlement writer predicate 仍有真实 caller,因此保留;本批不是 writer/事务或 +provider 迁移。 验收不变量是**叙述非干涉**:固定 typed fields 与配置,改写叙述或增加未经验证的 `compact_evidence` / `case_result` 对象,都不能改变交付语义与后续执行义务。 @@ -144,13 +149,16 @@ classification 保留为历史标签;没有明确展示消费者时,不保 词语不再分类 run。不改写持久历史,也不新增开关恢复错误行为。此前由未结构化 历史标签推导的 status、handoff/review 和 quota 决策会发生明确的行为变化。 -交付领域的迁移单元是完整的 delivery-history-to-obligation projection,包含规模/结果 -连续计数与 status/quota 消费者。这定义该领域的切片边界,不改变下文 provider-first -Todo 的交付顺序。每批有界历史最多跨 runtime 一次,删除被替代的 -Python decision,保留独立审阅的 typed case,并通过真实 CLI 验证叙述变异用例。 -旧推断本身错误,因此只有传输 golden parity 不够。另行盘点仍缺少 material-result -字段的 writer,并用明确兼容计划退役旧 marker/hint 配置。本批不迁移精确的旧 -lifecycle classification code 或其他 cadence policy,不能宣称全局已无文本规则。 +迁移保留独立刻画的合法 typed 行为,并验证真实 refresh/history/status/quota 入口、 +批次基数与叙述非干涉。有一项有意修正单独披露,不能混称 parity:两个非法 work-item +identifier 不能仅因都归一化为缺失值而被视为相等;此类 observation 不能推断出 +blocker writeback 或解除后续义务。仍在使用的 Python writer predicate 同样拒绝该 +情况,不改写任何活跃历史。 + +下一步另行盘点仍缺少 material-result 字段的 writer,并用明确兼容计划退役旧 +marker/hint 配置。精确的旧 lifecycle classification code、历史选取与其他 cadence +policy 不在本批范围内,不能宣称所有 writer 已迁移或全局已无文本规则。这一读策略 +闭合不改变下文 provider-first Todo 顺序,也不等待 provider cutover。 ### Legacy 字段规则退役检查点 diff --git a/skills/loopx-self-repair/references/repair-patterns.md b/skills/loopx-self-repair/references/repair-patterns.md index 01d412162d..5235238f8b 100644 --- a/skills/loopx-self-repair/references/repair-patterns.md +++ b/skills/loopx-self-repair/references/repair-patterns.md @@ -5,6 +5,7 @@ teaches a reusable control-plane lesson. | Pattern | Symptoms | Evidence To Read | Likely Root | Durable Repair | | --- | --- | --- | --- | --- | +| `invalid_identity_normalization_alias` | Two malformed binding ids appear to match after normalization, allowing unbound blocker evidence to count. | Raw synthetic ids, normalized results, and the same record through writer validation and the typed history projection. | Equality of two failed normalizations was mistaken for a valid identity match. | Require successful expected-id normalization before comparing identities; preserve oversized-id invalidity during compact transport and test malformed pairs independently from valid Todo/replan bindings. | | `causal_wait_coverage_gap` | One waiting route hides another open acceptance binding, or adding unrelated waiting Todos changes defer into replan. | Replay the same canonical Todo/vision facts through the real CLI with absent/runnable/waiting successors, varied display sizes, and reordered unrelated rows. | A union of related waits was treated as coverage of every causal binding, or compact display absence was treated as canonical absence. | Derive per-binding wait coverage from canonical Todo rows before display slicing; reuse resume/ownership evaluators and directed successor lineage. Keep incomplete evidence distinct from proven absence, and prove ordinary authoring and lifecycle paths before adding separately maintained declarations. | | `narrative_delivery_authority_leak` | Rewording a run label turns resolved work into a blocker or preparation into a required follow-through; untyped history accumulates delivery debt. | Explicit outcome/kind/scale, scoped progress observation, compact history, and status/quota decisions before and after narrative-only mutations. | Keyword inference and unknown-as-failure streaks promoted narrative into machine authority. | Use explicit typed semantics and the existing scoped blocker predicate; unknown breaks evidence streaks. Delete keyword fallbacks, preserve labels for display, and test both negative narrative mutations and positive typed obligations through the CLI. | | `periodic_report_todo_log_editorial_gap` | A stage report is generated and frozen, but the artifact is an English Todo chronology, report-building work displaces business findings, or the page lacks a clear overview-to-depth analysis mainline. | Frozen artifact and language, projected completed/open Todo facts, report-meta action kinds, editorial source and section order, cause/boundary details, fact references, and approval-gate creation time. | The governed consumer treated durable Todo prose as audience-ready copy and let a renderer infer narrative structure from timestamps or generic content kinds. Generation correctness therefore proved persistence and authority, but not editorial quality. | Freeze a compact public-safe fact request for the exact intent, require Agent-authored Chinese editorial with a typed `overview -> problem map -> causal analysis -> coverage/actions -> next actions` contract, validate language density and fact lineage in the consumer, and create no frozen artifact or approval gate until the editorial response passes. Keep report-meta work outside the business narrative and retain publication as a later exact-payload authority gate. |