Country-agnostic gate battery core: phased evaluation, unconditional reporting, per-country gate selection (#611) - #619
Conversation
…reporting, per-country selection (#611) The tooling half of #611 increment 1. populace.build.gate_battery evaluates a country's declared gate selection phase by phase, resolves every declared entry to exactly one of five statuses (passed / failed / not_applicable / evidence_absent / unreached), persists the full attested report before any blocking decision, and hashes every declared threshold into the policy digest. gates.json gains a per-country phases order and per-entry phase; thresholds live in entry parameters; a reviewed not_applicable reason turns a deliberately unselected gate into a reported receipt. The Belgian spec migrates mechanically (nine entries, all terminal) and a compatibility test proves it runs as declared, with unimplemented gate names surfacing as named evidence_absent gaps rather than crashes. The UK terminal battery is untouched; its migration onto this executor is the consumer half, sequenced after #610 and the #612 Frame carrier. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
MaxGhenis
left a comment
There was a problem hiding this comment.
This core is the right shape — phases as country data with a global vocabulary, unreached computed only from spec order, and the reviewed not_applicable receipt are exactly what the #578 battery consolidation needs, and the US-path neutrality holds in fixture diffs. Both blockers are in the trust boundary between the caller and the report, and they mirror what the #616 review rounds found in the US battery this same week — so there's a ready-made convergence path rather than a redesign.
1. A truncated substitute spec can claim canonical provenance and ship signed. GateBatteryRun (gate_battery.py:677) accepts a naked GatesManifest plus independently supplied spec_fingerprint and gates_manifest_sha256, verifying neither; completeness iterates only the supplied manifest (:786). Repro with the canonical nine-gate spec's digests over a one-gate substitute: reported_gate_count=1, claims_canonical_spec_fingerprint=True, signed=True, shippable=True. And gates_manifest_sha256 is optional — a signed release can ship without it. The report currently receipts a caller assertion, not the spec.
2. Post-evaluation mutation of the parameter dict attests a policy that was never evaluated. The frozen GateSelectionSpec holds a mutable dict (country_spec.py:363) and the policy digest reads it live after evaluation (gate_battery.py:807). Repro: evaluate at tolerance 0.5 (passes at 5% drift), mutate to 0.01, and the report signs hash_matches_strict=True, reported_status=passed for a policy that fails at 1%.
Both close with the same move: digests derived by the run from the objects it actually evaluated (never caller-supplied), spec/manifest content deep-frozen at construction, and the canonical spec loaded from an authoritative module path with any test substitution receipted as non-canonical. That's precisely the canonical-authority doctrine #616's fix round is landing on the US side — once it merges (expected within a day), this PR can consume the same primitives and the two battery efforts converge on one framework, which was the point of #611. The phase model and reporting core need no changes.
|
Pushed the two review blockers as fix commits on top of your branch (María — your phase model and reporting core are untouched; these only close the trust-boundary items):
Full workspace 5,095 passed / 0 failed; #583 guard 495 exact. The later convergence onto #616's canonical-authority primitives stays open as the follow-up we discussed — these fixes are deliberately self-contained so this PR doesn't wait on it. |
MaxGhenis
left a comment
There was a problem hiding this comment.
Review findings closed with the repros as committed regressions (receipts in the fix comment). Approving to dismiss the earlier request-changes.
Where this sits
The tooling half of #611 increment 1 — the country-agnostic core only, deliberately scoped to files neither #610 nor #617 touch so it can review in parallel with both. The UK migration (
uk/gates.json, theterminal_gates.pyrewrite,contract.pyschema 4 + per-country pins) is the consumer half and follows after #610 merges and the #612 Frame carrier lands, since its content consumes both.What this adds
country_spec.py: phases as country data.gates.jsongains aphasesheader declaring the country's phase order (UK national will declare[preflight, terminal]; the US pool builder[assembled, transferred, simulated]— its existingPOOL_CHECKPOINT_STAGE_ORDER) and every gate entry declares itsphase. The vocabulary (ALLOWED_GATE_PHASES) is global so reports stay comparable across countries; the order is spec data because theunreachedoutcome is only computable from it. Entries also gain a reviewednot_applicablereason — a deliberately unselected gate becomes a reported receipt instead of a silence (the shape #578 item 5 will use). The gate-function allowlist widens with the names the UK battery and pool builder need, kept country-neutral (weight_ess, notuk_weight_ess).gate_battery.py: one executor, two layers. The gate comparisons stay inpopulace.build.gates, untouched. The new binding layer (GateBinding/FunctionBinding) adapts declared specparametersinto gate keyword arguments and routes evidence out of a per-phaseEvidenceContext(frame, named artifacts, the loaded spec). Behaviour is lifted from the UK terminal battery, which already does it correctly:_evaluate_gate, verbatim fromuk_runtime/terminal_gates.py), and a result under the wrong name fails rather than impersonating another gate.passed/failed/not_applicable(reviewed spec reason) /evidence_absent(no implementation, or missing evidence — named, never silent; on release candidates it blocks, because a missing frozen reference is not a passing gate, while dev builds record it and continue) /unreached(an upstream phase blocked first). Conflatingunreachedwithnot_applicableis the failure mode the battery exists to prevent, sophases_evaluatedandblocked_at_phaseare report fields and the attestation covers both.run_phasepersists the full report — later phases asunreachedplaceholders — before returning;enforceis a separate call that can only run afterwards, with the two blocking modes the three existing call sites need:blocks_artifact(national releases — the raise precedes the H5 write) andmarks_artifact(the pool builder — the intermediate H5 is written and the manifest records the verdict).policy_sha256is the canonical hash of the resolved entries — ids, gates, phases, criticalities, parameters, exclusion reasons — so a threshold outside the hash is not attested, with no hand-threaded payload builder per country. Bindings may contribute per-entryevidence_sha256lines; evidence that cannot be canonicalized turns a pass into a failure rather than shipping unattested.POPULACE_{CC}_TERMINAL_GATE_SIGNING_KEY— the UK's existing name is theukinstance). The signature is valid over failed reports too: a signed failure is evidence, an unsigned one is a hole. An unsigned report is nevershippable.Belgium proved compatible, not asserted.
build/be/gates.jsongains its phase declarations (all nine entriesterminal— mechanical, no build exists) and a test loads the BE spec, runs the battery, and asserts a well-formed report in which every unimplemented gate name is a namedevidence_absentgap — never a crash, never a pass. The golden spec file is regenerated for the deliberate byte change.Registry, deliberately minimal. Bindings ship for the three gates whose evidence already travels as plain data (
weights_audit,input_mass_parity,tail_concentration— the #609 pair among them, exercised end-to-end in tests with declared thresholds reaching the gate). The rest of the registry arrives with the consumers; an incomplete registry cannot manufacture a pass.What this does not do
uk_runtime/is untouched; the migration PR asserts verdict identity against the June fixtures and the UK parity increment 4: weighted integrity gates (input-mass parity + QRF tail concentration) in the terminal battery (#609) #610 differential tests.contract.pypins arrive with the consumer half.Verification
test_gate_battery.py(18 tests): taxonomy exhaustiveness, report-on-disk-before-the-raise,unreachedvsnot_applicableunder a preflight block, fail-closed evaluators, evidence-absence semantics perrelease_candidate, phase-order enforcement, signature validity over a failed report recomputed independently, policy-hash movement with a threshold, BE compatibility.test_country_spec.py: eight new refusal tests for the phase andnot_applicablevalidation; BE phase assertions; golden regenerated.populace-buildsuite passes;ruff check .clean.Relates
parameters)🤖 Generated with Claude Code