Contracts
meta-read-ratchet.md
Contract — Inline meta.json Read Ratchet (new gate)
Requirement: FR-006 · NFR-002 · IC-06
Purpose
Establish the first architectural gate that keeps inline json.loads(<meta_path>) reads drained after IC-05 routes them onto load_meta. Modeled on the existing test_resolution_authority_gates.py + resolution_gate_allowlist.yaml machinery — do NOT invent a weaker shape.
The scanner
An AST/heuristic scanner over src/ that flags an inline meta read: a json.loads(...)/json.load(open(...)) whose argument derives from a meta.json path (var names meta_path|meta_file|meta_json|target_meta_path, or a <dir> / "meta.json" join), excluding mission_metadata.py internals and the task_utils adapter.
Gate mechanics (all four required — this is the non-vacuity contract)
1. Integer floor — INLINE_META_READ_FLOOR = <post-drain count>; the live count MUST be ≤ floor. 2. Margin — a FLOOR_MARGIN; live − margin ≤ floor < live (prevents a floor pinned far above the live count from masking regressions). 3. Routed-count floor (anti-mass-allow-list) — mirrors ROUTED_CANONICALIZER_FLOOR: the number of routed (load_meta) sites has its own floor and can only rise. An attempt to "drain" by mass-allow-listing rather than routing trips this floor. 4. Composite-key allow-list with stale-entry detection — each remaining/deferred site is a {key, rationale, issue} entry. allowlist_keys − live_keys non-empty ⇒ FAIL (a routed-away entry must be deleted, never left masking). Every deferred m_0_13_ entry carries a filed follow-up issue number.
Invariants
of a whole tree (the migration/ blanket-exclude is rejected — only m_0_13_* is deferred, per-entry).
- INV-C2: a newly-introduced inline meta read goes RED (self-test plants one and asserts the gate bites).
- Shrink-only: the floor may only decrease across missions; a raise requires an explicit justified edit.
- No vacuous exemption: a deferral is an allow-list entry (rationale + issue), never a scanner path-exclude
Self-tests (ship with the gate)
test_new_inline_meta_read_is_flagged— plant → RED.test_allowlist_entries_are_still_live— stale-entry twin-guard.test_routed_count_floor_blocks_mass_allowlist— mass-allow-list attempt → RED.
partition-aware-commit-seam.md
Contract — Partition-Aware Commit Seam (commit_for_mission)
Requirement: FR-007 · Constraint: C-006 (close the class at the seam), C-002 (no partition change) · IC-01
Current (defective) behaviour
commit_for_mission(repo_root, mission_slug, files, , kind, message, …) resolves ONE placement for the whole batch: resolve_placement_only(repo_root, mission_slug, kind=kind) (commit_router.py:152). Any caller that passes a mixed-partition batch under a single kind misroutes every file to that kind's partition. Known offenders: spec_commit_cmd.py (kind=SPEC, PRIMARY) and mission_finalize.py:1320 (kind=TASKS_INDEX, PRIMARY) — both commit COORD artifacts (acceptance-matrix.json, issue-matrix.md, status.) to the primary branch, so accept reads a stale coord copy (#2404).
Contract (post-fix)
commit_for_mission MUST resolve placement per file, not per batch:
1. For each file in files, classify: kind_f = kind_for_mission_file(file, mission_slug=mission_slug); surface_f = PRIMARY if is_primary_artifact_kind(kind_f) else PLACEMENT. 2. Group files by surface_f. 3. Commit each group to its own CommitTarget (resolve_placement_only(kind=<representative kind of group>)), using the existing materialize-then-retry path for a PLACEMENT (coord) group.
Chosen shape (WP pins one, red-first):
existing callers by construction with no caller change.
must submit single-partition batches. Only if (a) proves infeasible for a caller's atomicity needs.
- (a) Split-and-commit — the batch is transparently split into per-partition commits. Preferred: fixes
- (b) Guard-reject —
commit_for_missionraises a structuredMixedPartitionBatcherror and each caller
Invariants
submits a mixed batch and asserts each file lands on its partition's ref.)
- INV-C1: no file is committed to a ref other than its own partition's. (Pinned by a red-first test that
- C-002: the
kind → partitionmapping is unchanged; this contract only changes routing, not membership. - No fast-path regression: a single-partition batch commits exactly as today (one commit, one ref).
By-construction consequences (no per-caller patch)
acceptance-matrix.json now routes that file to COORD.
acceptance-matrix.json/issue-matrix.md/status.* route to COORD.
spec_commit_cmd.py(kind=SPEC): a spec-only batch still lands PRIMARY; a batch that includesmission_finalize.py:1320(kind=TASKS_INDEX): tasks/WP files land PRIMARY; the batched
Out of contract
entirely (raw git commit) and is routed + surface-reconciled there.
(COORD) — NOT in scope.
accept.py's residual commit is a separate concern (IC-02/FR-008) — it bypassescommit_for_missionmission_record_analysis.pyalready commitsanalysis-report.mdwith the correctkind=ANALYSIS_REPORT