Skip to content

Materialize the Census SPM independence role in US builds - #959

Merged
MaxGhenis merged 28 commits into
mainfrom
us-spm-role-stage
Sep 23, 2026
Merged

MaxGhenis merged 28 commits into
mainfrom
us-spm-role-stage

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Sep 19, 2026 •

Copy link
Copy Markdown
Contributor

ASEC bases can lose the Census relationship fields needed to classify minor-only SPM units. Restore is_spm_independent_minor_role through an explicit source stage that joins the pinned complete ASEC person files by income year and PERIDNUM, using the existing reviewed derivation. The base builder runs it before support cloning; releases from older ASEC bases rederive and reconcile it.

The adapter persists the engine-declared source input without manufacturing a default. Source mismatches, missing observations, stale provenance and unresolved units refuse the build. Gate details now survive JSON/checkpoint serialization; existing nonconstant role columns cannot bypass source checks.

Targets main (its original base, #948, merged). This does not qualify ACS-origin rows: prepared stacked pools without a sourced role fail before calibration. The ACS source/partition work and national/CD release qualification remain separate requirements.

Re-level on main (22–23 September)

Three merges of origin/main (f69389ee0 at 4d773a478, 0d3c40a03 at e6dede2e4, a222b904c at e66d6f6c8), then two fixes for contracts main gained while this branch was stacked.

Textual conflicts and resolutions:

Fixes for main's newer contracts (121ea4119, ec5d701e6, 4ef635715):

  • Serializer registry (frame_serializer_registry, e7bccc3): main forbids a bare DataFrame.to_hdf sink in production code. The stage's scratch parent H5 for derive_spm_role_source is now written by _write_role_projection (pd.HDFStore plus put_frame_table, fixed format). It is registered as spm_role_derivation_projection and has a nullable-boolean round-trip adapter, so the registry now lists ten serializers. The derivation still reads the projection with pd.read_hdf. The re-level did not change spm_role_source.py. The archive reader below changed it later, in 47976be6c.
  • Spine blindness: person.get(NATIVE_SPM_ROLE) failed closed because the scanner does not resolve imported constants. It is now an explicit column subscript. The multispine pool tool's runtime import graph is re-pinned from 70 to 73 modules, adding spm_independence_role, spm_role_source and spm_composition, all three already classified and scanned. Both failures were already present at the pre-merge head 96761c635: a scan of that tree shows the same offender and 73 modules.
  • H3 graph parity pin (microcosm-graph): CI engine-us (3.14, us-am) on ec5d701e6 failed test_h3_us_post_transfer_parity. The pinned us_post_transfer.json carries multispine_pool's remaining-stage manifest and engine-input projection/defaults digests, which this PR re-pins when the pool projection learns the role is a required source input. It was regenerated with tools/graph_us_post_transfer_fixture.py. Only those three digests change; sources/, expected.csv, NORMALIZATION.md and PRODUCED_BY.txt are byte-identical.

Archive reader (47976be6c) and producer identity

The base stage passes the official Census archives (asecpub23/24/25csv.zip) through --asec-education-source, which has always accepted either the archive or its extracted CSV. spm_role_source._load_source read only a CSV, so it would have refused those archives at the CSV byte-length check. 47976be6c adds _read_archive_member, which does the following:

  • requires the archive to match the pinned SHA-256;
  • requires exactly one member with the pinned name, whose size and streamed SHA-256 equal the CSV pins;
  • reads that member with the same pd.read_csv call the CSV path uses;
  • re-hashes the archive after reading.

The pins are the existing ASEC_EDUCATION_ASSISTANCE_ARCHIVES values. The extracted-CSV path is unchanged statement for statement, and everything after the read is shared and unchanged. In a local review run on all three vintages, the archive and the CSV gave equal frames and equal source checks.

Consequence: spm_role_source.py is in source_enrichment.PRODUCER_SOURCE_FILES. From a checkout with this change, _check_producer_source_identity refuses to certify or publish a native-SPM-role candidate recorded before it. Such a candidate must be re-validated from a checkout at its recorded producer commit. This is not a new requirement for populace-us-2024-spm-20260915: it recorded 743683b0e, and source_enrichment.py and contract.py have both changed on main since. Its 2026-09-15 recertification already ran certify, validate and publish from a clean worktree detached at 743683b0e. See docs/us-spm-role-stage.md §2.

Follow-up on 23 September (behaviour-neutral)

  • Docs and docstrings now describe the archive reader. They no longer claim spm_role_source.py is byte-identical to main.
  • SPM_COMPOSITION_REMEDY no longer offers the head/spouse fallback as a remedy. A release now requires the source role; only the message text changed.
  • Dated notes in docs/us-spm-role-for-a-fresh-base.md and docs/us-spm-role-stage.md §4 record what this PR changed.
  • The two derivation receipts now report the count of offending SPM units instead of 20 internal unit ids. Each records its pre-redaction SHA-256, and the proof script emits the count.
  • Merged origin/main at b78f5a88e (Preflight a new-lineage US release without a selection source #988) with no conflicts. The merged release_gate_preflight.py is main's file, less the definitions this PR moved to spm_composition.py (AST-checked).
  • The non-doc diff in packages/ and tools/ since 47976be6c has two sources. The first is Preflight a new-lineage US release without a selection source #988's three files, identical to main's change. The second is docstring and comment edits plus the SPM_COMPOSITION_REMEDY string: an AST comparison with docstrings stripped shows no other difference in spm_role_source.py, spm_independence_role.py or spm_composition.py.
  • Unit-id lists removed from the two receipts remain in this branch's git history.

Validation

Re-level (local, policyengine-us 2.2.1, Python 3.14):

  • 46 test files pass on their latest local run, with 0 failures. They are the test files this PR touches plus the tests that import the modules it touches: the role stage (75 tests), base builder, fiscal-refresh builder, spine blindness, serializer registry, release input coverage, spec-engine, bundle and multispine suites, SPM role source/enrichment, preflight and the microcosm-data importers. Every file that exercises the role stage, the registry or the blindness scanner was re-run after the last code change (ec5d701e6); test_acceptance_h_parity.py passes at 4ef635715.
  • ruff check . is clean. ruff format --check passes on every changed file except test_spec_engine_field_usage.py and test_us_bundle_core_contracts.py, whose drift is identical on origin/main and outside this PR's hunks. CI lint runs ruff check only. tools/ci_test_groups.py --verify passes, and the generated-variable audit --check is current.
  • PR CI is the gate. Local checks do not replace it.

Follow-up (23 September, merged tree at the new head):

  • 1,880 tests across 25 files pass, with 0 failures. They cover the role stage, role source, composition engine, preflight (including Preflight a new-lineage US release without a selection source #988's tests), Build P enrichment builder and source_enrichment, h5_enrichment, spine blindness, serializer registry, release input coverage, the base builder, the multispine pool tool, the spec-engine seed, coverage and inventory tests, bundle pins, and the fiscal-refresh SPM and gate subset.
  • ruff check . is clean, and ruff format --check passes on the changed Python files. ci_test_groups.py --verify, the bundle --check (spec_sha256=1c46dda9…), the generated-variable audit --check and spec_engine_coverage.py --check (42,180/42,180) all pass.

Original (pre-re-level):

  • Actual wrapper and signal gate on pinned Build P (166,321 people) and base-q3 (907,382): all roles agree with the original derivation; unresolved SPM units fall from 28 and 222 to zero. Existing tables, columns, weights, strata, mass logs and input files are preserved. Final receipts also serialize the full gate details.
  • Fable's second source review closed its eight initial findings and found no new high/medium defect. Dtype-sensitive binding, pre-clone metadata scope and JSON-only provenance limitations are documented in docs/us-spm-role-stage.md.
  • No calibration result, incumbent no-worse comparison, release certification or publication is claimed. No end-to-end base-stage run with this stage has been made; the wrapper receipts above ran the stage on existing base outputs.

Aggregate receipts and reproducible proof scripts are committed under experiments/; population files are not.

🤖 Generated with Claude Code

MaxGhenis and others added 17 commits September 18, 2026 15:37
…section

The proof derives the role for a population through the certified derivation
(derive_spm_role_source, unchanged) from the pinned complete Census ASEC
person CSVs, attaches it to the frame the release tool itself loads, and
records check_spm_composition before and after, plus the invariants the
brief names: no unit re-grouped, no person invented, every offending unit
resolved by its own 15-to-17-year-old, support clones agreeing with their
source person. The buildp subcommand compares the same derivation against the
enrichment lane's pinned reference evidence person for person.

The journal's first-session State/Next are historicized in place and a
Step 2 section records what step 1 found on arrival (#948 already green).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…uild P byte for byte

Phase-2 base (base-q3, 352,932 households / 907,382 persons / 367,306 SPM
units), read-only, through the unchanged derive_spm_role_source: before, 222
units without a classified adult (head-only fallback); after, 0. All 238
units with no member aged 18 or over are resolved by their own 15-to-17-year-
old carrying the source role (240 such persons); person ids, SPM membership,
the spm_unit table and every age are unchanged; zero unmatched persons; zero
count-reconciliation mismatches; the 432,523 distinct source persons repeated
across the two support channels agree on the role in every case.

Build P parent (pinned digest, read-only): the same derivation reproduces the
enrichment lane's reference evidence byte for byte (SHA-256 22b5968d…),
166,321 of 166,321 roles equal, every EXPECTED_COUNTS value matched.

The proof loader symlinks a suffix-less Hugging Face blob under a temporary
directory because the engine's dataset loader insists on .h5.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Answers from code at this head: the rule's columns never enter the pool (no
frozen census_cps vintage carries SPM_HEAD; only 2024 carries A_FAMTYP and
A_FAMREL), so the stage restores the role from the pinned complete Census
person CSVs by exact (source_year, PERIDNUM) join, the same repair class as
LKWEEKS/ED_VAL/PAW_TYP, and calls derive_spm_role_source unchanged; how clones
inherit it (many_to_one across native units, refused within one; zero
disagreements on 432,523 source persons); why ACS-origin rows are refused
rather than defaulted; the carve-out in both classification paths, driven by
policyengine_us.spm.DATASET_SOURCE_INPUTS; and the moved-pin table with each
artifact's generator. Corrects two claims in the earlier note: the zero-adult
refusal already exists in _reconcile_units, and the engine does not assert
DATASET_SOURCE_INPUTS disjoint from REJECTED_DATASET_INPUTS.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A manifest stage shaped like relationship_inputs (read_table person ->
derive_spm_independence_role) whose derivation body is a call into the
certified derive_spm_role_source, unchanged: the handler projects the frame's
identity, age and Census count columns and the spm_unit table into a
temporary H5, hashes it, and runs the derivation against the pinned complete
Census ASEC person CSVs. Every refusal of that derivation (no ASEC origin, a
repeated or mixed source unit, more or fewer than one SPM_HEAD, a unit left
without a classified adult, a Census count that does not reconcile) surfaces
as a SourceRuntimeError naming the stage; the stage adds its own required-
column check first. The idempotence guard, person_id alignment, coverage
refusal, summary and signal gate follow the model; the gate additionally
runs check_spm_composition on the produced frame and requires zero
unresolved units from the source column.

Registered in US_DONORS, US_STAGE_NAMES (after relationship_inputs, before
puf_support), source_stages.json, the sources schema, the allowed operation
kinds and the US handler registry. The pool operator order is deliberately
untouched: ACS-origin rows carry no PERIDNUM and the stage refuses to
default them (docs/us-spm-role-stage.md §3, §7).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…leaves

policyengine_us/spm.py declares DATASET_SOURCE_INPUTS = {is_spm_independent_
minor_role} and says a producer must retain its observed boolean rather than
treat the fallback formula as ownership. The adapter classified the role by
formula presence alone, in both of its paths, so write_dataset refused the
one input the engine asks a producer to deliver.

Live engine: _engine_dataset_source_inputs reads the declaration off
policyengine_us.spm, requires a non-empty frozenset of names disjoint from
REJECTED_DATASET_INPUTS (the engine itself does not assert that), and
variables(), formula_owned_outputs() and _engine_computed_columns() treat
those names as leaves. Import-free metadata index: the generated audit block
is regenerated by tools/refresh_us_generated_variable_audit.py, which now
reads the same declaration through the same validator, emits formula_owned=
False for a declared source input, refuses a declared name the snapshot does
not cover, and pins spm.py as a policyengine-us activation file so an
upstream change to the declaration fails the audit closed. No name list of
microcosm's own is involved.

Observed at policyengine-us 2.2.1 / spm-calculator 1.0.0: 926 input leaves
(was 925), 5,241 formula-owned (was 5,242), in both paths.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…e export

POST_REFERENCE_ECPS_REQUIRED_INPUTS gains is_spm_independent_minor_role in
the module and the generator, with a note explaining the engine's
declaration and why no reviewed exclusion is possible (the anti-rot check
fails an exclusion whose column carries signal, and the certified default's
does, through the Build P enrichment). Manifest regenerated:
164 required, 7 reviewed exclusions, 41 probes. The L0/refit export's
required non-constant person columns gain the same leaf.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…engine contracts

The pool's projection receipt requires the default-value surface to equal
variables() exactly; default_values still skipped the role by formula
presence. With the same declaration-driven rule applied there,
tools/repin_us_pool_engine_contracts.py re-derived the five pinned
quantities against the installed 2.2.1 engine: 926 inputs and defaults
(was 925), the projection and defaults digests, the provision counts (the
role is a declared-absent pool input: the pool has no producer for it, see
docs/us-spm-role-stage.md §3), the 1,059-row remaining-stage manifest
digest, and the regenerated engine ABI lock.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…d snapshot

The metadata-index pins move to the observed values under the regenerated
audit: seven spm-calculator formula-owned outputs, two input leaves, 926
inputs, 5,241 formula-owned. 17 passed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…elease tool

Base builder: both paths (the byte-compatible monolithic _run_all and the
checkpointed pre_clone_enrichment stage) run with_us_spm_independence_role
immediately after relationship_inputs, on the ASEC-only frame before
cloning, with the pinned Census person CSV paths resolved from
--asec-education-source or the verified cache; the signal gate is checked
and recorded as spm_independence_role_signal in the summary; the stage
joins the pre_clone_enrichment boundary list.

Release tool: the stage runs on the pool_frame-is-None path after the
relationship gate (a no-op on a base that already carries the role) and
its signal gate is evaluated on every path, with telemetry, like the other
with_us_* stages. The composition report's docstring no longer claims the
role cannot be written, and SPM_COMPOSITION_REMEDY names the stage as
remedy (a) for a base built from raw sources.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The stage's gate reuses check_spm_composition, but release_gate_preflight
also hosts the preflight runbook, whose _release_tool_module imports the
release tool by name; the primary-QRF worker identity walks every import
statement in the package's static closure, so reaching the check from a
stage put 'build_us_fiscal_refresh_release' into the worker's external
roots and the bundle generator refused. The check, its role resolution,
CheckResult and the remedy text move verbatim into us_runtime/
spm_composition.py, which imports only the Frame; release_gate_preflight
re-exports them (same objects, same public names, __all__ unchanged) and
the stage imports the pure module directly. 78 preflight and engine drift
tests pass unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The frozen generation-0 source_stages.json digest moves with its bytes, as
in the 2.2.1 bump (afaffae):
7935d891ed16eac618f04813a7b8a3ceb9a971c8b4c4273683594ba61e7dd631
-> 9f4f983091fb18dea8f4524cc100be20f9fca2b006435461592f8edf53c9a5d8
in tools/generate_us_bundle_from_constants.py and its two pin tests.
tools/generate_us_bundle_from_constants.py then regenerated spec/
sources.yaml (the engine ABI lock had already been regenerated by the pool
contract re-pin). The US bundle spec_sha256 is now
53a24962e567a9c44193385da7e80346e6704069306dc34cf198b878b3f0d4f2.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…e-blindness inventory

Both join the reviewed non-population inventory beside relationship_inputs:
the composition check reads only age and the role, and the stage restores a
measured Census field by exact identity. 497 passed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…eded agreement sweep

Pins the stage's manifest and plan wiring, every refusal the brief names (a
missing source column, a unit with more or fewer than one SPM_HEAD, a unit
left without a classified adult, a person with no ASEC origin, a native unit
mixing two source units, a Census count that does not reconcile, an unpinned
CSV), the frame integration and idempotence, the signal gate, the release
coverage and export contract, the adapter's persistence of the role as an
input leaf (engine-gated), and a 12-seed agreement sweep showing the stage's
role equals the raw rule on each person's Census row and derive_spm_role_source
run directly on the same frame.

Recovered from the third session's salvage ref and made to run on pandas 3:
factorize takes a MultiIndex rather than a list of tuples, the sweep draws one
weight per household, and dropping source_year is refused by the frame wrapper
(ValueError) before the handler (SourceRuntimeError) — both are asserted.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…le stage moves

The new stage adds 18 configuration fields to the US bundle (10 authored
under the source_stages claim, 8 resolved under generated_authorities for
the remaining-stage input manifest row the role now owns), and
us_runtime.source_runtime — an attested kernel module — gained the handler
registration, so the seed protocol and compiled seed map digests move.

Observed at this head and written verbatim, never hand-derived:
- field_usage: 42,174 configuration / 32,394 authored / 9,780 resolved
  (was 42,156 / 32,384 / 9,772); source_stages claim 1,712 pointers,
  generated_authorities claim 8,614 pointers, each with its observed digest.
- inventory_coverage: seed_protocol, seed_map, source_manifest,
  full_checkpoint and late_resource_semantics digests; source_stages 38.
- tools/spec_engine_coverage.py pointer-inventory digest;
  docs/evidence/spec-engine/us-f0-coverage.json regenerated by that tool
  (42174/42174 fields, 41/41 inventory checks, --check green).
- test pins: coverage-tool and field-usage counts, the minimal-loader
  golden spec_sha256, and the pool tool's bundle spec_sha256
  (53a24962…, as tools/generate_us_bundle_from_constants.py --check prints).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@MaxGhenis
MaxGhenis changed the base branch from spm-composition-preflight to main September 23, 2026 03:03
MaxGhenis and others added 9 commits September 22, 2026 23:44
#948, this branch's base, merged into main with a review round the branch
predates. Conflicts and how they were resolved:

- release_gate_preflight.py / spm_composition.py: this branch moved the SPM
  composition check into spm_composition.py so build stages can import it;
  main hardened the check in place (report cap and its hard ceiling, age
  bands instead of exact ages, the person-to-unit join guard). The check
  stays in spm_composition.py and carries main's hardened bodies verbatim;
  the preflight module re-exports MAX_REPORTED_SPM_UNITS_HARD_CAP from there.
  The branch's updated remedy text (naming the new stage) is kept.
- Generated spec-engine pins (field_usage counts, inventory seed digests,
  the pointer-inventory digest, us-f0-coverage.json, the loader golden
  vector, the pool tool's bundle spec_sha256): both sides moved them, so
  each was re-derived at the merged tree, not picked from either side:
  42,180 fields (32,400 authored / 9,780 resolved), 41/41 inventory checks,
  bundle spec_sha256 1c46dda9...
- PROGRESS-spm-composition.md: both sides' historical notes kept, with a
  dated note that #948 merged and #959 was re-levelled.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflicts (release input coverage) resolved as a union of both sides:
is_spm_independent_minor_role (this branch, #893) and the three ASEC
reported-receipt inputs receives_wic/snap/tanf (#980, #978 option 1) are
all required with no reviewed exclusion; manifest counts 167 required /
174 total; derivation text names both additions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
No textual conflicts. build_us_fiscal_refresh_release.py (restated
age-band ledger filters, #986) and test_us_spine_blindness.py
(spm_universe_source.py, #977) auto-merged alongside this branch's
spm_independence_role stage and module registrations.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two test_us_spine_blindness failures that already existed at the
pre-merge head 96761c6 (the stacked draft never ran CI):

- spm_independence_role.py read the existing role with
  person.get(NATIVE_SPM_ROLE); the scanner resolves only module-local
  string constants, so a .get() keyed by the imported name fails closed.
  Read it as an explicit column subscript guarded by a columns check,
  which is the same lookup and keeps spm_role_source.NATIVE_SPM_ROLE the
  single source of the name.
- build_us_multispine_pool.py now reaches 73 runtime modules, not 70:
  the pool's engine-input projection imports the role's output columns,
  which pulls in spm_independence_role, spm_role_source and
  spm_composition. All three are already classified and scanned; re-pin
  the count with a note naming them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main's serializer registry (e7bccc3) forbids a bare DataFrame.to_hdf
sink in production code and requires every writable HDF site to be
classified. The role stage wrote its scratch parent H5 for the certified
derive_spm_role_source with two to_hdf calls, so the registry test failed
after the re-level.

The projection now goes through _write_role_projection: one
pd.HDFStore(mode="w") with put_frame_table (the shared nullable-boolean
boundary, pandas fixed format), returning the digest. It is registered as
spm_role_derivation_projection with a round-trip adapter in the registry
test, which now pins ten serializers. derive_spm_role_source is unchanged
and still reads the projection with pd.read_hdf.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CI engine-us (3.14, us-am) failed test_h3_us_post_transfer_parity: the
pinned graph declaration carries multispine_pool's remaining-stage
manifest and engine-input projection/defaults digests, which this branch
re-pinned when the pool projection learned the SPM role is a required
source input (926 inputs, 1,059 rows). Regenerated with
tools/graph_us_post_transfer_fixture.py: only those three digests in
us_post_transfer.json change; sources/, expected.csv, NORMALIZATION.md
and PRODUCED_BY.txt are byte-identical, so the derive/seed/materialize
outputs are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…stage passes it

The base stage hands the role stage its --asec-education-source paths, which
name the official Census archives (asecpub23csv.zip and so on); the education
loader accepts an archive or its extracted person CSV. The role loader only
accepted the CSV, so a production base-stage run refused every year with
"CSV byte length mismatch" (route A's first run, 2026-09-23 03:58). The loader
now reads the pinned member from inside an archive: the archive's SHA-256 must
equal the pin, it must hold exactly one pinned member whose size and SHA-256
equal the CSV pins, and it is re-hashed after reading. A CSV path is checked
exactly as before. Verified on the three real archives (146,133 / 144,265 /
142,125 persons, the pinned counts); tests for archive equivalence and each
refusal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
47976be taught spm_role_source.py to read the pinned person member from
the official Census archive, so the design note's "byte-identical to main"
and "Build P lane untouched" claims no longer held. The note now describes
the reader (archive SHA-256 pin, exactly one pinned member, member size and
SHA-256 pins, re-hash after reading; the extracted-CSV path unchanged) and
its consequence: spm_role_source.py is in PRODUCER_SOURCE_FILES, so a role
enrichment recorded before it must be certified from its recorded producer
commit, as the 2026-09-15 recertification already was.

SPM_COMPOSITION_REMEDY no longer offers household-structure fallback as a
remedy, since a release now requires the source role. Dated notes mark what
this PR changed in both SPM role design notes and the journal. Docstrings
and comments name archive-or-CSV sources. No code path, condition or value
changes; only the remedy message text differs at runtime.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The two derivation receipts listed up to 20 internal spm_unit_id values of
minor-only SPM units in gated populations. They now carry the count
(n_offending_units_reported), as main's composition receipt reports only
counts, and each records its pre-redaction SHA-256, which the wrapper
receipts' historical_receipt_sha256 names. The proof script emits the count,
so a re-run produces the same shape.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MaxGhenis
MaxGhenis marked this pull request as ready for review September 23, 2026 09:29
@MaxGhenis
MaxGhenis merged commit 4305a7d into main Sep 23, 2026
24 checks passed
MaxGhenis added a commit that referenced this pull request Sep 23, 2026
voluntary_filing, sipp_head_start and ssi_disability_criteria are seed sites,
so the seed protocol's implementation digest and the compiled seed map digest
move with their source, and CI's engine-shared job refused #992 merged with
main (test_us_inventory_is_structure_exact_and_complete,
test_semantic_hash_has_golden_vector_and_surface_separation and the coverage
tool's committed-report checks). Observed at this head and written verbatim,
never hand-derived, as #959 did:
- inventory_coverage EXPECTED_HASHES: seed_protocol 56f832aa…, seed_map 18594e59…
- the minimal-loader golden spec_sha256 db3fdab1…
- docs/evidence/spec-engine/us-f0-coverage.json regenerated by
  tools/spec_engine_coverage.py (42,180/42,180 fields, 41/41 inventory checks;
  --check green)
Spec-engine and touched-stage suites pass on this merge of origin/main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis added a commit that referenced this pull request Sep 26, 2026
The native input_coverage_profile froze the release coverage manifest at
05d254a (163 required inputs). Main since requires 167: the SPM
independence role (#959) and the reported receipts receives_snap,
receives_tanf and receives_wic (#978). The profile's anti-rot test failed on
every lane. The historical roster is re-extracted in manifest order with the
manifest digest re-pinned. Because the identifiers encode their counts, they
move to us_release_167_v1, us_national_cd_165_v1 and
us_native_national_cd_163_v1. The derivation rules are unchanged: national/CD
omits block/tract, and native also omits the prior-year family. The
survey-enrichment coverage count moves 161 -> 165 with the national/CD
profile.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant