Contracts
compat-surface.md
Contract: Private-Symbol Compat Surface (FR-012 — the land-ability net)
The biggest land-ability risk. Tests bind to 50 distinct private symbols on runtime_bridge (imports + monkeypatches); relocating a symbol naively silently breaks patching (false-green). Full inventory + per-symbol decisions: research.md §Compat / rbresearch-C-compat.md.
The false-green mechanism (why re-export alone is insufficient)
A plain re-export makes monkeypatch.setattr(runtime_bridge, "_x", …) a no-op when _x's leaf is called by another function that moved into the same seam — the intra-seam call resolves via the seam module's own global, not the shim. The test then passes by coincidence, not isolation.
Per-symbol preservation decision (three strategies)
- KEEP-IN-PLACE — symbol stays in the thin residual. Mandatory for
_wrap_with_decision_git_log(also neutralizes the identity-trio + retrospective-pair risks)._advance_run_state_after_compositionis the reconciled case: its CC23 body is adapter-owned (moves toruntime_bridge_engine.py, reduced ≤15 for FR-004), but a thin residual delegate stays reachable atruntime_bridge._advance_run_state_after_compositionfor its heavy patch (8×) + attr (9×) compat surface — logic in the adapter, compat shim in the residual. - RE-EXPORT — symbol moves to a seam, re-imported into
runtime_bridge+ added to the guarded compat block. Sufficient only when NO intra-seam caller reaches it. - LAZY-ACCESSOR (
_wf-style, per #2464/merge.py) — required for names a sibling module calls through the shim; re-export alone is false-green for these. - 🔴 grounded high-risk:
_primary_runtime_feature_dir(patched 6×, intra-seam caller_resolve_mission_ulid),_build_discovery_context. ⚠ SPLIT-flag:_state_to_action,_compute_wp_progress,_build_prompt_or_error,_is_wp_iteration_step.
__all__
Introduce __all__ for the 8 public names (sibling merge.py parity). It governs import * ONLY — it does not preserve the 50 private symbols; those live in the explicit guarded compat re-export block.
The guard test — tests/runtime/test_bridge_compat_surface.py (two guards, FR-012/SC-006)
- (A) behavioral sentinel — for each of the 50 symbols, patch it on
runtime_bridge, drive the public entry that reaches it, andpytest.raiseson a sentinel the patch injects. If the patch is a no-op the sentinel never fires → the test FAILS (catches false-green). - Per-entry reach mapping is BINDING. A single-entry sentinel (all patches driven through
decide_next_via_runtime) is itself false-green for symbols reached only viaquery_current_stateoranswer_decision_via_runtime— the patched leaf is never executed, so the sentinel never fires and the guard passes vacuously. Each of the 50 symbols MUST be mapped to its reaching entry (decide_next/query/answer— several are reached from more than one; drive each reaching entry), and the sentinel driven through that entry. Symbols on the query/answer-only path (e.g. the_build_*_query_decisionfamily viaquery_current_state; the_map_runtime_decisioncluster viaanswer_decision_via_runtime) MUST have their sentinel driven through query/answer, notdecide_next. - (B) static AST guard — assert identity re-export (
rb.x is seam.x) for every relocated symbol, and forbid function-scope re-imports of compat names (the exact structural signature of false-green shadowing).
Gate semantics
This guard is built in WP-0b (its own deliverable, alongside the WP-0a parity oracle) and stays green through every extraction WP that relocates a patched symbol. A false-green (sentinel doesn't fire) or a broken identity check = reject the extraction.
parity-oracle.md
Contract: decide_next() Parity Oracle (the characterization safety net)
The load-bearing safety net for the whole behavior-preserving refactor. Built in WP-0a, proven green on unmodified source at the full coverage floor, then re-run as the acceptance gate of every extraction WP. Full derivation: research.md §Parity.
Scope — ALL THREE public entry points (not just decide_next)
The refactor touches three public entries, and the 29 Decision(...) sites are partitioned across them — an oracle driving decide_next_via_runtime alone leaves ~half the sites (including the CC33 _map_runtime_decision cluster) unexercised and WP-0 falsely green.
| Public entry | Site | Owns (Decision sites) | Own side effects |
|---|---|---|---|
decide_next_via_runtime | :2524 | ~15 sites reachable here (11 constructed directly :2545–3015 + the WP-iteration path) | sync emit :2556, coord commit :2563 |
query_current_state | :3199 | the *4 _build__query_decision sites — _build_finalized_override_query_decision:3059, _build_initial_query_decision:3111, _build_decision_required_query:3136, _build_runtime_query_decision:3166 — reached exclusively** from here | none (read-only query) |
answer_decision_via_runtime | :3355 | the remaining sites incl. the 10 _map_runtime_decision sites (:3582–3798, CC33) + _build_wp_iteration_decision (:3468–3536) | its OWN sync emitter (:3410), snapshot seed via _read_snapshot (:3418), coord-branch commit via _wrap_with_decision_git_log/DecisionGitLog (:3427) |
The oracle carries per-entry fixture sub-ledgers. Coverage floor is scoped per owning entry: every Decision site reached ≥1× from its owning entry, every guard branch reached ≥1×. Driving decide_next alone can never reach the query/answer-only sites.
Equality contract — canonical(decision, repo_root)
Over Decision.to_dict() (decision.py:82-166):
- MASK (drop before compare, but keep None-vs-present so a kind-shape change isn't blinded):
timestamp(bridge:2542),run_id,decision_id(ULIDs). - PATH-NORMALIZE (relativize to each run's own
repo_root— the copytree temp dirs differ per fixture run, so the normalizer takes the run's root, never a shared constant):workspace_path,prompt_file,reason(non-obvious carrier — embedsfeature_dir/excpaths),origin.mission_path(bridge:2575). NBorigin.mission_tierstays STABLE. - STABLE (compare as-is): everything else —
kind,agent, mission identity,state,action,wp_id,step_id,guard_failures(content and order),progress,question/options,is_query, …
assert_parity(before, after, repo_root) = canonical(before, root) == canonical(after, root).
reason normalizer meta-test (binding — guards against self-blinding)
Path-relativizing free text can swallow a genuine reason/field change. WP-0a MUST ship a normalizer meta-test proving canonical():
- COLLAPSES pure path noise (two runs under different copytree roots with the same logical decision → equal), AND
- does NOT collapse a semantic delta (a changed
reasonphrase, or any STABLE field flip) → the two must compare unequal.
Side-effect isolation — CAPTURE-and-assert (BINDING, not optional)
Comparing Decision.to_dict() alone is behavior-change-blind: a refactor that changes what is emitted/committed can still return an identical Decision and pass. Therefore the affected seams are captured and asserted, not merely stubbed. Each fixture runs once against a fresh copytree of a frozen repo snapshot with a fixed per-run repo_root:
- Run create/advance (
get_or_start_run) runs REAL on the throwaway copy (its ULID is masked). - CAPTURE-and-assert (binding equality on the recorded payloads before/after): the sync emitter (
bridge:2556), the coord-branchDecisionGitLogcommit (bridge:2563), the retrospectiveConfirm.askgate, and the answer-path emitter + coord commit (bridge:3410/:3427) plus the engine mutations relocated in IC-02 (_append_event/_write_snapshot). A change to emitted/committed content = a parity break = reject. - The runtime planner (
next_step) is never stubbed — it is the logic under test.
Fixture matrix + coverage floor (binding, per-entry)
- 29
Decision(...)sites (19 blocked / 4 step / 4 query / 1 terminal / 1 decision_required) across 7 orchestrator phases, partitioned by owning entry as above. - Both guards fully branched:
_check_cli_guards(:1057, ~10 branches incl. requirement-ref cross-check) and_check_composed_action_guard(:1515) across 3 mission families (software-dev / research / documentation), including both fail-closed defaults and the 4-waytaskslegacy_step_idunion. - Coverage floor: every Decision site ≥1× AND every guard branch ≥1× — each reached from its owning entry. Ledger ≈ 22–26 fixtures spread across the three entry sub-ledgers.
- Named highest-risk fixtures: the two fail-closed defaults + the
taskslegacy-union — each asserts identicalguard_failurescontent and order before/after.
Gate semantics
- WP-0a done ⇔ oracle green on unmodified source at full coverage-floor across all 3 entries. The coverage floor itself is a checkable assertion (site/branch reach tallied and asserted ≥ floor) — a hollow oracle is also green on unmodified source, so green-on-unmodified is necessary but not sufficient.
- Every subsequent extraction WP: oracle stays green (its acceptance gate). A parity break — Decision inequality OR a captured side-effect delta — = the extraction changed behavior = reject.