Research: Meta.json Fail-Closed Read Routing
Phase 0 decisions. Most were resolved by the pre-spec research pass and the post-spec adversarial squad (structure / anti-laziness / live-gate / sequencing lenses); this consolidates them in Decision / Rationale / Alternatives form.
D1 — Home of the L1 decode primitive and the unified comparator
- Decision: Place the L1
decode_metaprimitive, its typed error, and the unified VCS-lock comparator + named field-set insrc/kernel/(new moduleskernel/meta_decode.py,kernel/vcs_lock.py). - Rationale:
src/kernel/is the CI-enforced zero-dependency root (kernel <- doctrine <- charter <- specify_cli,test_layer_rules.py). Site A lives ingit/ref_advance.py, which is git plumbing and must not importspecify_cli(documented atref_advance.py:37-38). L2 (_parse_meta_text) lives inmission_metadata.py, which importsspecify_cli.core.*— so co-locating L1 there makes site A un-routable. Kernel is the only layer both plumbing and application can depend on. Bonus: it letscore.pathsandmission_metadataboth depend downward on kernel, relieving the existing deferred-import cycle (mission_metadata.py:480-494). - Alternatives considered: (a) L1 in
mission_metadata.pybeside L2 — REJECTED: re-breaks C-003 for site A. (b) Comparator incore/vcs/types.py(already declaresvcs_locked_at) — REJECTED: still underspecify_cli, unreachable by plumbing.
D2 — L1 signature and the malformed-vs-empty boundary
- Decision:
decode_meta(raw: str | bytes, *, on_malformed: Literal["raise","empty","none"] = "raise") -> dict[str, Any] | None, pure (no I/O).None/typed-raise means malformed only (json.JSONDecodeError,UnicodeDecodeErroron explicit utf-8 decode, or non-object top level). Empty/whitespace-only content is not L1's concern: it stays a benign short-circuit at L2 or the caller (→ {}where currently contracted, e.g.merge_driver). - Rationale: The three current parsers disagree on empty input —
merge_drivershort-circuits empty→{}beforejson.loads, whileref_advance/implement_coresletjson.loads("")raise and fold empty into the same silentNoneas corruption. If L1's singleNonechannel meant "empty or malformed", routing would convertmerge_driver's benign empty→{}into a loud failure (violating FR-005) and make a red-first "empty at merge_driver" test non-deterministic. Keeping empty benign at L2/caller preserves each site's contract while L1 owns malformed exclusively. - Alternatives considered: three-state return distinguishing empty from malformed at L1 — REJECTED as over-broad; the empty→benign decision is caller-specific, not a property of the bytes.
D3 — Routed-census growth requires extending ROUTED_CALLEES
- Decision: Add the new decode symbols (kernel L1 + the public L2 entry) to
ROUTED_CALLEESintest_inline_meta_read_gate.pyin the same change that routes the sites, then re-deriveROUTED_LOAD_META_FLOORfrom a fresh live measurement. - Rationale:
scan_routed_load_meta_callscounts only calls to the fixedROUTED_CALLEESset (load_meta,load_meta_strict,load_meta_or_empty,load_meta_fail_closed,_load_meta_fail_closed,_require_meta) — all dir-level. The blob-fed sites route onto the new pure-decode symbols, which are invisible to the census unless added. Live census measured on this branch = 134; floor = 130, margin = 4 → the gate sits at its exact ceiling (134−130=4). Without extendingROUTED_CALLEES, routing does not raise the census and FR-008 is a no-op. - Alternatives considered: leave census unchanged and rely on the inline-read gate — REJECTED: the inline gate is a ceiling on un-routed reads and would not credit the routing.
D4 — Floor re-derivation is verifiable via the existing margin, not an honor-system "not copied"
- Decision: Express the floor requirement as: after routing,
ROUTED_LOAD_META_FLOORsits withinROUTED_LOAD_META_FLOOR_MARGIN(4) of the freshly-measured live census and strictly below it (live - MARGIN <= floor < live), by the establishedfloor = live - 3convention. - Rationale: A stored integer is byte-identical whether measured or pasted, so "0 copied constants" is not machine-checkable. The existing gate already enforces the checkable control:
test_routed_load_meta_floorassertslen(routed) >= floor,len(routed) > floor(anti-vacuity), andlen(routed) - floor <= MARGIN. Anchoring the requirement to that gate makes it enforceable. - Alternatives considered: a bespoke "derivation provenance" record — REJECTED as unverifiable ceremony; the margin gate already is the control.
D5 — Enforce "one decoder" and "all reads routed" with gates, not a hand count
- Decision: Add (FR-010) an architectural check that fails on any
json.loads/json.loadapplied tometa.jsoncontent outside the kernel L1, plus a completeness check that no un-routed bypass read hides beyond the enumerated 5. Close the NFR-002 inline-literal hole by asserting no inline VCS-lock field-set literal + exactly one named declaration. - Rationale: The census/allow-list gates do not count decoders; a hidden hand-rolled
json.loadsover meta content would pass them. "5 sites" is a human count; the routed floor is a floor (≥) and cannot detect a 6th un-routed site. Machine enforcement makes NFR-001/SC-001/SC-003 non-fakeable. - Alternatives considered: rely on review — REJECTED per anti-laziness lens (fakeable).
D6 — Red-first is a captured deliverable with typed + identifier assertions
- Decision: Each site's diagnosability test is captured red against pre-routing code (record the failing output / a known-absorbing stub), and asserts BOTH the shared kernel typed error type AND a message naming
meta.json+ the site's source identifier (filesystem path for A-worktree/C;ref:pathblob spec for the committed reads). At site E, tests reflect the honest per-parser starting state (one arm already names the path; one raises unnamedJSONDecodeError). - Rationale: "red-first" as an adjective is fakeable (write green-first, relabel).
assert "meta.json" in str(exc)is trivially satisfiable (any path ends inmeta.json). Pinning the type + a site-appropriate identifier closes both holes. - Alternatives considered: message-substring only — REJECTED as fakeable.
D7 — Sequencing: per-module atomic, serialized on the floor constant
- Decision: Foundation first (kernel L1 + public L2 + L2/L3 re-express, census-neutral, green alone). Then per-module atomic routing units (ref_advance / implement_cores+implement / merge_driver), each deleting its parser AND rewiring its callers together. Routing units serialize on the single
ROUTED_LOAD_META_FLOOR; a final closeout unit pins the cumulative floor and lands the gates + governance record. - Rationale: Deleting a private parser breaks its in-module callers at compile/import time; a naive "all deletions, then all rewiring" split leaves the tree non-importable between WPs. Two routing units editing the floor in parallel each compute a different partial-correct floor and collide. Serial-with-final-pin is the only green path.
- Alternatives considered: one monolithic routing WP — viable but un-reviewable and loses per-module isolation; rejected in favor of atomic-per-module.
D8 — Governance (#3240) resolved by deviation record
- Decision: Record the #3240 deviation (do not add a
_baselines.yamlcount baseline). Document thattest_allowlist_matches_floor(equality) +test_allowlist_shrink_onlyare strictly stronger than a<=count baseline and add stale-entry eviction the baseline lacks. - Rationale: Operator decision (confirmed during discovery). Adding a second count baseline duplicates governance; the compensating controls already exist and are stronger.
- Alternatives considered: register the baseline — available but redundant; not chosen.
Confirmed-sound premises (live-verified by the squad)
- All three named gates exist and are green now:
test_inline_meta_read_floor,test_routed_load_meta_floor,test_no_unaccounted_load_meta_call_sites. - Site A (
ref_advance._meta_change_is_vcs_lock_only) is genuinely unrouted (parses via local_parse_meta_object→ silentNone). - The two diagnosability test files are genuinely absent on this branch (must be authored fresh).
- Every site has a practical red-first injection seam (merge_driver reads a path; implement_cores via injectable
GitPort; only ref_advance's committed arm needs agit_repofixture).