Fail-closed ledger feeds: required pins, schema-validated rows, vendored bytes, typed units and periods#391
Fail-closed ledger feeds: required pins, schema-validated rows, vendored bytes, typed units and periods#391MaxGhenis wants to merge 5 commits into
Conversation
|
CI note: the matrix failure ( |
…its and periods Ledger architecture review 2026-07-10, findings 7-10. The release build no longer accepts a feed on faith: - load_ledger_consumer_artifact gains require_pins (bare feeds rejected; both external hash pins mandatory and verified) and validate_rows (every row validated against the vendored consumer_fact.v1 schema — stdlib validator that raises on constructs it does not implement — plus unique aggregate_fact_key). Profile bytes re-hash against the manifest, with the upstream pre-newline hash accepted only as an explicit legacy semantics recorded in provenance. - build_us_fiscal_refresh_release requires both pins, loads fail-closed, and vendors the exact verified artifact bytes into releases/<id>/ledger_artifact/ with a recomputed-hash manifest — a hash alone cannot reconstruct a feed whose source file disappears (the current certified US release's feed exists only on the build machine; this makes that impossible going forward). - Every target mapping declares its expected unit and scale: a fact whose unit contradicts the mapping is a hard error at compile time (the thousands-vs-millions class), and undeclared units never pass silently. - Latest-fact selection carries the full typed period: tax-year, calendar-year, and fiscal-year facts never collapse into one series; a reference matching multiple period types without declaring one is an ambiguity error. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sol round-2 findings 7 (numeric) and 12, populace side: - Consumer facts parse with a parse_constant that rejects NaN, Infinity, and -Infinity, and every numeric value must be finite. Python's default json.loads accepts those tokens even though they are not valid JSON, so a non-finite value could enter a schema-valid, hash-valid artifact. - The pre-newline profile hash is now accepted only when the manifest predates the fix (no consumer_fact_schema_sha256), matching Ledger's loader. Recording a downgrade in provenance is not the same as refusing it, and a post-fix pinned artifact must match exact bytes. - The release build passes require_pins=True, so a manifest-listed profile that is missing on disk fails the release instead of being silently skipped and then vendored incomplete. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The extracted _load_release_ledger_artifact helper was defined but never called, and referenced an unimported type. main() now loads through it, so the require_pins=True path is the only way a release reads its feed and is directly testable. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sol round-2 finding 4: the dynamic US fiscal reference builders set expected_unit=_measure_unit(fact) — copying the expectation from the very fact being checked, so a drifted-unit fact validated against itself. They now read _expected_unit_for_production_measure(fact), a committed measure->unit map keyed independently of the incoming fact; a fact whose unit differs is a hard error. value_scale must be explicitly declared (value_scale_declared) on a production reference — an undeclared or defaulted scale is rejected rather than silently applied. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nt unit Rebasing onto main (SNAP caseload targets, #371) surfaced two flaws in the finding-4 unit gate: - expected_unit was checked before the aggregation/value checks, so a fact that cannot compile at all (a mean fact with no time_mean contract) failed on the missing declaration rather than on its real cause. The gate now runs after the structural checks. - the committed expected-unit map assigned usd to every indicator measure, but USDA SNAP average-monthly caseload is a count. Count-based indicators now map to their real denomination (SNAP -> count, CMS -> people), so the household caseload fact (unit 'count') passes and a genuine drift still fails the gate. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
340490a to
8379b05
Compare
Steps 3 of the ledger-review hardening (
~/thesis-brief/ledger-architecture-review-sol-2026-07-10.md, findings 7–10). The release build no longer accepts a ledger feed on faith.Loader (
populace.build.ledger_artifact)require_pins=Truerejects bare consumer-facts files outright and makes BOTH external hash pins (expected_facts_sha256,expected_manifest_sha256) mandatory and verified (finding 7: the current certified US release was built from a bare, pin-less 131 MB feed that exists only on the build machine).validate_rows=True(default) validates every row against the vendoredconsumer_fact.v1JSON Schema before it can compile into a calibration target, plus uniqueaggregate_fact_key(finding 8). The validator (populace.build.ledger_schema) is stdlib-only per this module's duck-typing doctrine, implements exactly the constructs the schema uses, and raises at import on any construct it doesn't implement — a future schema revision cannot silently weaken the pin. The vendored schema's sha256 is pinned by test.legacy_pre_newlinesemantics recorded in provenance (never silently). Pairs with PolicyEngine/ledger's consumer-hardening PR which fixes the hash at build and verifies on load.Release CLI (
tools/build_us_fiscal_refresh_release.py)--ledger-facts-sha256and--ledger-manifest-sha256are now required; the loader runs fail-closed.releases/<id>/ledger_artifact/with avendored_manifest.json(per-file sha256 recomputed after copy; divergent copies fail the release), and the vendored block is recorded in both build and release manifests.Unit/scale gate (finding 9 — the thousands-vs-millions class)
LedgerTargetReferenceandLedgerTargetMappingmeasure declares its expected unit (+ explicitvalue_scale); a fact whose unit contradicts the declaration is a hard compile-time error naming both units, and an undeclared unit never passes silently. All checked-in references/mappings updated.Typed periods (finding 10)
_selector_period_invariant_keyno longer strips TY/CY/FY distinctions: tax-year, calendar-year, and fiscal-year facts never collapse into one "latest" series. References may declareperiod_type; matching multiple period types without declaring one is an ambiguity error, and compiled metadata records the fact's full typed period.Verification
uv run ruff check .clean;uv run pytest -qgreen except one pre-existing live-network failure on clean main (populace-datacertified-release pointer test, untouched by this branch).aggregate_fact_keyrejected.🤖 Generated with Claude Code