Enforce consumer-artifact integrity: schema-validated rows, real profile hashes, dimension cross-tab resolution#87
Open
MaxGhenis wants to merge 3 commits into
Open
Enforce consumer-artifact integrity: schema-validated rows, real profile hashes, dimension cross-tab resolution#87MaxGhenis wants to merge 3 commits into
MaxGhenis wants to merge 3 commits into
Conversation
Ship the pinned consumer_fact.v1 schema inside the wheel as the single source of truth for artifact validation. A new policyengine_ledger.schema module loads the packaged copy and exposes the parsed schema, its sha256, and a row validator that raises with a precise path, line, and JSON location. A test pins the packaged copy byte-for-byte to docs/schemas, and the wheel-smoke import test confirms the schema ships in the wheel. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Schema-validate every fact row at build and load, always and with no public opt-out, and reject rows with a duplicate aggregate_fact_key. Record the schema sha256 in the manifest so a load rejects an unknown schema version loudly; older manifests without the field still get full row validation. Hash each profile over the exact bytes written, including the trailing newline, and re-verify every profile file against the manifest on load. A pre-fix manifest whose hash omitted the newline matches only through an explicit legacy_profile_hash path, recorded per profile on the loaded artifact; tampered profile bytes never match and fail the load. Add a dimensions selector that matches a row's dimension identity as an order-insensitive exact set, so the ONS SIC-by-turnover and SIC-by-employment firm cross-tab targets resolve strictly. Adversarial tests exercise each guard, a packaged-profile gate rejects any profile whose selector uses an unsupported key, and a fixtures test resolves both cross-tab targets end to end through the artifact. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sol round-2 finding 7. Consumer artifact load was trusting declared content; now it recomputes and cross-checks: - Every fact row's aggregate_fact_key is recomputed from the row's own component keys and raw aggregation/period/geography/entity/assertion via the shared _hash_key; a declared key that does not hash the row is rejected. Schema validation only checks key SYNTAX, so a syntactically valid all-zero identity key previously loaded clean. - Rows parse with a parse_constant that rejects NaN/Infinity/-Infinity, and every numeric value (nested included) must be finite — Python's default json.loads accepts those non-JSON tokens. - The manifest's fact_row_count and each profile's target_count are compared with the actually-loaded rows/targets and a mismatch is rejected. Full recomputation of the leaf component keys from raw fields (rebuilding an AggregateFact from the projected consumer row) is the remaining depth for internally-consistent forged keys; the top-level recompute closes the demonstrated all-zero forgery. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Finding #8 of the 2026-07-10 ledger architecture review ("artifact schema and profile integrity are nominal rather than end-to-end enforced"): the consumer artifact's stated integrity guarantees become enforced ones.
jsonschemabecomes a runtime dependency; the pinnedconsumer_fact.v1schema ships inside the wheel (policyengine_ledger/schemas/, byte-identity withdocs/schemas/enforced by test) and every row is validated on bothbuild_consumer_artifactandload_consumer_artifactwith no public opt-out. Duplicateaggregate_fact_keyrows are rejected. The manifest recordsconsumer_fact_schema_sha256; a manifest declaring an unknown schema hash is rejected loudly.profile_jsonwithout the trailing newline it wrote to disk, and the loader never checked profile hashes at all — dead, wrong integrity metadata. Build now hashes exact file bytes; load re-hashes every profile against the manifest. Pre-fix manifests (no schema-sha field) may match via the legacy pre-newline semantics, recorded explicitly on the loaded artifact (profile_hash_semantics) — never silently. Tampered profile bytes fail either way.dimensionsselector support. The two shippeduk_firmscross-tab targets (SIC × turnover band, SIC × employment band) used a selector key the resolver vocabulary didn't have, so they could never resolve strictly.dimensionsis now a supported selector: strict, order-insensitive exact match on the row's dimension-variable set. A fixtures test resolves both targets throughresolve_profile_targetsstrict mode, and a packaged-profiles gate asserts every shipped profile's selectors use only supported keys.Ledger Governance
ledger-contract-maintainerscope (policyengine_ledger/**,docs/schemas/**untouched except packaging a byte-identical copy; no source-package or fact-value changes). The twouk_firmsprofile targets are NOT edited — the resolver vocabulary was extended to support their existing, correct intent.ledger-contract,ledger-boundary): not run — this PR was prepared as part of the cross-institution hardening sprint at Max's direction; flagging here explicitly rather than claiming verdicts. No Populace-side logic enters Ledger (boundary rules honored: validation/identity only).Tests
uv sync --locked --all-extras;uv run ruff check ledger policyengine_ledger db scripts testsclean;uv run pytest -q→ 576 passed, 1 skipped (skip pre-existing). CI's DB build + wheel-smoke steps run locally, with the wheel verified to contain and load the packaged schema. Each new adversarial test was verified to FAIL against the pre-fix code and pass after: missing required field named in the error; unknown extra field rejected; unknown manifest schema-sha rejected; tampered profile byte detected; legacy pre-newline hash accepted only via the explicit path; duplicateaggregate_fact_keyrejected; unsupported selector key still fails loudly.Pairs with PolicyEngine/populace#391 (consumer-side pins/validation/vendoring) and #86 (thesis-facts append gate).
🤖 Generated with Claude Code