Source-data hygiene: FNS-sourced SNAP, loud bundle drift, label year keys (#77, #78, #79)#81
Merged
Merged
Conversation
…#77) db/etl_snap.py hardcoded a SNAP_DATA dict of 2021-2023 national and "top 10 states" values that were suspiciously round with no per-value citation, and shipped into the legacy targets DB via `ledger load snap` (and `ledger load all`, which CI runs on every push). Spot-checks against the real FNS FY24 workbook confirm the values were fabricated (e.g. 2023 CA households 2,891k vs parsed 3,129k; national benefits 112,848M vs 93,847M). The loader is live, not dead: it is wired through db/cli.py and re-exported by ledger/targets/loaders.py and ledger/jurisdictions/us, so per #77 the fix is to replace the hardcoded values with FNS-parsed figures carrying provenance, not to delete the loader. load_snap_targets now sources household, participant, and benefit values from the trustworthy packages/usda_snap/fy69_to_current source package, which parses the FY24 workbook cell-by-cell with guard cells and a checksum-locked artifact. Each target records per-value provenance (source file, vintage, sha256, R2 URI, source record id) in its notes. The `years` argument now filters by fiscal-year reference period, so a year the package does not publish yields nothing rather than mislabeling FY2024 data. Tests assert against the parsed source facts instead of any hardcoded table, so they can no longer bless fabricated numbers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
_render_string eagerly computed filing_year=year + 1 before formatting, so
any string value rendered under a non-integer files:/column_by_year key
(a release or vintage label such as the SSA Table 7.B1 "extracted_targets"
entry) crashed with "TypeError: can only concatenate str (not int) to str"
at source_package.py:1735 -- even for values with no {year} placeholder,
like source_table and sheet_name. This blocked the affected (package, key)
builds and would bite the first manifest where a labeled key is the only
source of facts.
Non-integer keys are labels, not years: there is no year to interpolate and
year + 1 is undefined. _render_string now skips {year}/{filing_year}
templating when year is not an int and returns the value unchanged, so a
label build resolves literal record-set periods (e.g. SSA period 2024)
instead of failing. Integer years still template both {year} and
{filing_year}, preserving the SOI tax-year/filing-year headers.
Tests reproduce the crash first (red), then assert: integer years template
filing_year; label years pass strings through and leave stray {year}
placeholders literal; and build_facts("extracted_targets") on the real SSA
package builds facts instead of raising.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
) build-bundle iterates a hand-maintained SOURCE_PACKAGE_ALIASES map, so a packages/* directory that was added, renamed, or consolidated without a matching alias entry was silently dropped from the merged consumer_facts.jsonl instead of failing. A silently-incomplete export then surfaced far downstream as "reference did not match a Ledger fact identifier" failures in the populace build. Add drift detection over the packages/* authoring surface and make a default build-bundle fail loudly when the alias map and layout disagree: - discover_source_package_dirs() scans packages/<source>/<package> dirs. - find_alias_map_drift() reports (missing_dirs, unmapped_dirs): stale alias targets with no directory, and on-disk packages with no alias entry. - assert_alias_map_covers_packages() raises SourcePackageAliasDriftError naming every offender, so drift can never produce a partial bundle. build_bundle() runs the assertion for the default (non-explicit) source list, before building, so the export is either complete or a clear error -- never silently partial. Explicit --sources builds are unaffected. Tests plant an unmapped package directory and assert build_bundle raises, plus unit-test discovery and drift detection against temp package trees. A regression test pins that the committed alias map covers every committed package directory. 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
Three source-data hygiene fixes, one commit each. Closes #77, #78, #79.
#77 — Replace fabricated
SNAP_DATAindb/etl_snap.pywith parsed FNS factsdb/etl_snap.pyhardcoded aSNAP_DATAdict of 2021–2023 national and"top 10 states by participation" values that were suspiciously round with no
per-value citation. The loader is live, not dead: it is wired through
db/cli.py(ledger load snap/ledger load all, which CI runs on everypush at
.github/workflows/ci.yml) and re-exported byledger/targets/loaders.pyandledger/jurisdictions/us/__init__.py.Spot-checks against the real FNS FY24 workbook confirm the values were
fabricated (2023 CA households 2,891k vs parsed 3,129k; national benefits
112,848M vs 93,847M).
Per the issue's decision tree (live ⇒ replace with FNS-parsed figures +
provenance),
load_snap_targetsnow sources household, participant, andbenefit values from the trustworthy
packages/usda_snap/fy69_to_currentsource package, which parses the FY24 workbook cell-by-cell with guard cells
and a checksum-locked artifact. Each target records per-value provenance
(source file, vintage, sha256, R2 URI, source record id) in its notes. The
yearsargument now filters by fiscal-year reference period, so a year thepackage does not publish yields nothing rather than mislabeling FY2024 data.
#78 — Fail loudly on source-package alias/directory drift in
build-bundlebuild-bundleiterates the hand-maintainedSOURCE_PACKAGE_ALIASESmap, so apackages/*directory added/renamed/consolidated without a matching aliasentry was silently dropped from the merged
consumer_facts.jsonl— thefailure mode that produced an incomplete populace fiscal-refresh build.
New drift detection over the
packages/*authoring surface makes a defaultbuild-bundlefail loudly when the alias map and layout disagree:discover_source_package_dirs(),find_alias_map_drift(), andassert_alias_map_covers_packages()(raisingSourcePackageAliasDriftError).build_bundle()runs the assertion for the default (non-explicit) source listbefore building, so the export is either complete or a clear error — never
silently partial. Explicit
--sourcesbuilds are unaffected.Note: the committed alias map is currently already in sync with
packages/*(63 ↔ 63), so the specific drift examples in the issue have since been
reconciled; this fixes the mechanism that allowed the silent drift.
#79 — Treat non-integer manifest year keys as labels in
source_package.py_render_string(source_package.py:1735) eagerly computedfiling_year = year + 1before formatting, so any string value rendered undera non-integer
files:/column_by_yearkey (a release/vintage label such asthe SSA Table 7.B1
extracted_targetsentry) crashed withTypeError: can only concatenate str (not "int") to str— even for valueswith no
{year}placeholder, likesource_tableandsheet_name.Non-integer keys are labels, not years: there is no year to interpolate.
_render_stringnow skips{year}/{filing_year}templating whenyearisnot an int and returns the value unchanged, so a label build resolves literal
record-set periods (e.g. SSA period 2024) instead of failing. Integer years
still template both
{year}and{filing_year}, preserving the SOItax-year/filing-year headers.
Ledger Governance
This PR touches source-data surfaces (a jurisdiction ETL and the source-package
build/render path), so noting the governance context:
ledger-source-ingestor(source-data loaderfidelity + bundle completeness)
pytest -qsuite (559 passed, 1 pre-existingskip) and CI-equivalent
ruff check ledger policyengine_ledger db scripts tests(clean); the FNS source package still validates and builds(
ledger validate-package/build-suite), andledger load allsucceeds.ledger-source-fidelity: SNAP values now trace cell-by-cell to the FNSFY24 workbook instead of an uncited hardcoded dict.
ledger-target-profile: n/a — no target profile changes.ledger-contract: n/a — no consumer-contract schema changes; build-bundle silently skips packages whose aliases drifted from the db/data layout #78 makesthe bundle export complete-or-fail rather than silently partial.
ledger-boundary: preserved — every SNAP target value is a published FNSfigure; no reconciliation, aging, or imputation added.
Boundary impact: #77 replaces fabricated numbers with published FNS values
(strictly improves provenance); #78 and #79 are build/render robustness fixes
that add no new facts and change no published values.
Tests
tests/test_etl_snap.pyrewritten to assert against the parsed sourcefacts (via
load_snap_facts) instead of any hardcoded table, so it can nolonger bless fabricated numbers. 12 tests.
tests/test_source_package_alias_drift.py(new) plants an unmappedpackage directory and asserts
build_bundleraises, plus unit-testsdiscovery and drift detection against temp package trees, and pins that the
committed alias map covers every committed package directory. 7 tests.
Existing
tests/test_ledger_bundle.py(6 tests, incl. the 38,939-rowexact-count fixture) still pass — the guard does not change in-sync output.
tests/test_ledger_source_package.pyreproduces theyear + 1crashfirst (red), then asserts integer years template
filing_year, label yearspass strings through / leave stray
{year}literal, andbuild_facts("extracted_targets")on the real SSA package builds facts.Full suite: 559 passed, 1 skipped;
ruffclean on the CI surface.🤖 Generated with Claude Code