Skip to content

Fix Populace release and export gates - #37

Merged
MaxGhenis merged 4 commits into
mainfrom
codex/fix-open-populace-issues-20260614
Jun 14, 2026
Merged

MaxGhenis merged 4 commits into
mainfrom
codex/fix-open-populace-issues-20260614

Conversation

@MaxGhenis

Copy link
Copy Markdown
Contributor

Summary

This is the live-Populace cleanup after retiring Microplex/eCPS from the production repo surface. It folds the release contract/latest-pointer/calibration-diagnostics draft PRs into one branch and adds release/export gates that should block bad artifacts before publication.

Fixes #9, #10, #11, #18, #24, #25, and #36.
Refs #27, #28, #29, #33, and #34.
Supersedes draft PRs #12, #13, and #14 once merged.

What changed

  • require and validate build_manifest.json, release_manifest.json, and calibration_diagnostics.json before publication
  • publish latest.json last, validate exact contract paths on read, and reject partial/invalid uploads before touching the Hub
  • reject non-standard JSON constants like NaN in release contract files
  • add calibration diagnostics serialization for per-target rows, loss trajectory, skipped targets, and options
  • make PolicyEngine-US export fail before writing when formula-owned variables reach the output surface
  • add enum-domain validation so PE enum inputs cannot ship raw source codes
  • add export/source-coverage/SPM diagnostic gate helpers for the US target surface
  • support schema-v1 release manifests in TRACE, including per-artifact repo_id
  • update current PE-US support to policyengine-us==1.729.0; keep policyengine-us-data<1.115.4 because 1.115.4 still pins the older engine metadata
  • keep eCPS comparison/parity work out of live Populace; that belongs in PolicyEngine/populace-benchmarks

Validation

  • focused release/TRACE tests: 47 passed
  • Python 3.13: uv sync --python 3.13 --all-packages --all-extras --dev; ruff check .; pytest => 459 passed, 2 skipped
  • Python 3.14: same => 459 passed, 2 skipped
  • installed-wheel CI gate for Python 3.13 and 3.14 => 410 passed, 6 skipped each
  • git diff --check
  • no live references from rg -i "microplex|\\becps\\b|enhanced_cps|enhanced CPS|sound_ecps|mp[-_]?ecps|populace-score-work|score_driver" .
  • /cycle read-only review: final pass had no actionable findings

Notes

This intentionally does not merge PR #21's eCPS parity runner or PR #22's old build_us_candidate.py patch. #21 is benchmark-harness material now, and #22 targets a deleted historical build path; the partnership/S-corp values work should be ported to the current Populace build/profile surface separately.

PavelMakarchuk and others added 4 commits June 14, 2026 14:46
…e producer

The releases already on policyengine/populace-us disagree with each other:
1abddeb has no build_manifest.json and an unversioned release_manifest
schema, while later releases carry schema_version 1. Every consumer was
left to implement its own defensive filter.

validate_release_dir() is the single gate: required files, manifest schema
version, and build-id agreement between both manifests and the directory
name — raising ReleaseContractError that names every violation at once.
Verified against the real 9f1260b release (passes) and the real 1abddeb
release (rejected with six named failures).

Fixes #11.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Hub repo had no way to say which release is current: consumers listed
the releases/ tree and guessed, and same-day builds (the ids end in a
date) have no defined ordering.

publish_release() validates the release directory against the contract
(an invalid release uploads nothing), uploads its files, and writes the
latest.json pointer LAST so a reader never sees a pointer to files that
are not there yet. latest_release() is the consumer side: one call that
returns the typed pointer. The Hub client is injected so the suite tests
the real ordering against a fake, network-free.

Fixes #9. Stacked on #11's contract branch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…he artifact

CalibrationResult already carries everything an auditor needs — per-target
initial/final estimates and tolerance verdicts, the per-epoch loss
trajectory, every skipped target with its reason, and the solver options
actually used — but none of it left the build machine: the build pushed
diagnostics to telemetry and dropped them, and the published .npz kept
only closing scalars. "Skipped and reported, never dropped silently" is
only true if the report ships.

diagnostics_payload() renders the result as strict JSON (non-finite
floats become null; the writer passes allow_nan=False so anything that
escapes the scrub fails loudly), and write_calibration_diagnostics()
writes the calibration_diagnostics.json a release directory publishes
next to its manifests.

Fixes #10.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@MaxGhenis
MaxGhenis merged commit 810e593 into main Jun 14, 2026
4 checks passed
@MaxGhenis
MaxGhenis deleted the codex/fix-open-populace-issues-20260614 branch June 14, 2026 19:49
MaxGhenis added a commit to TheAxiomFoundation/axiom-oracles that referenced this pull request Oct 15, 2026
The July entry eitc-earned-income-8614-partnership-scorp-split selected
every tax unit's EITC rows (case_id_prefix "ecps-tax_unit_"), and its
mechanism was wrong. It said PolicyEngine-US 1.729.0 predates #8614, but
1.729.0 contains #8614.

Every EITC row of the 2026-10-14 run is now attributed by counterfactual.
Each mechanism is one change to Axiom's inputs that makes Axiom see what
PolicyEngine-US 1.729.0 sees, and a row belongs to the smallest set of
changes whose re-run clears it. The 17,424 EITC rows split into five
entries, each listing its rows by output (case_ids_by_output) and bound to
them (selector_binding):

- 7,627 rows, bridge_artifact: the bridge's 32(c)(2) self-employment
  projection (axiom-oracles#640).
- 4,618 rows, bridge_artifact: the Populace artifact stores
  partnership_se_income, which 1.729.0 no longer reads
  (PolicyEngine/microcosm#35).
- 4,086 rows, bridge_artifact: the Populace artifact stores a
  dividend_income that disagrees with its components
  (PolicyEngine/microcosm#37).
- 890 rows, upstream_engine_gap: 1.729.0's 32(i)(2) investment-income
  definition (PolicyEngine/policyengine-us#9635 and #9567; Axiom debt
  rulespec-us#1416).
- 203 rows, upstream_engine_gap: 1.729.0's childless age test counts
  dependents (PolicyEngine/policyengine-us#9131; Axiom debt
  rulespec-us#1776).

The tax-before-credits, capital-gain and CTC entries are narrowed to their
2,131 rows the same way, and each row's signature is re-verified.

Evidence, tooling and tests:

- The evidence report (reports/fiit-ecps-eitc-attribution-2026-10-14.json)
  records every row's stage attributions. The FULL v2 report the
  dispositions bind to is committed beside it.
- scripts/fiit_ecps_eitc_attribution/ holds the three stage scripts, which
  now share minimal_sets.attribute. Re-applied to the variants stages 1 and
  2 stored, that function reproduces their attributions exactly.
- scripts/build_fiit_ecps_eitc_dispositions.py rebuilds the evidence byte
  for byte from the stage outputs. Its --check mode audits the evidence,
  the dispositions and the bound report against each other, and
  tests/test_fiit_ecps_eitc_attribution.py runs it with mutation cases.
- Stage 3's base check now accepts rows only the counterfactual has, and
  records them.

The PE-Axiom ratchet rises by one, from 81 to 82, as an approved and
recorded debt_raises entry. The grandfathered entry is closed, and the two
PolicyEngine attributions declare their Axiom side as debt instead.

docs/federal-income-tax-comparison.md now gives the October residuals and
the suite's manual lane.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis added a commit to TheAxiomFoundation/axiom-oracles that referenced this pull request Oct 17, 2026
…ctual attribution (#635)

* Run fiit-ecps against this checkout's bridge and a commit-pinned PolicyEngine-US 1.729.0

PyPI no longer serves policyengine-us 1.729.0, so the suite pins its
release commit's archive (byte-identical to the PyPI wheel) and the runner
checks the installed version against the label before a population run.
The tax-ecps runner now overlays this checkout's bridge via PYTHONPATH, as
the UK EFRS runner does, so a run validates the current bridge rather than
the axiom-oracles commit axiom-encode pins.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Bind non-v2 harness reports through a full v2 report; republish a run with --from-report

tax-populace-compare writes its own JSON, so the reports/ file a premerged
dashboard copy pointed at was not a v2 report and apply_dispositions.py
--check could never accept it. The runner now publishes the adapted v2
report (every mismatch row, no cases) beside the raw one and binds the
dashboard copy to it. --from-report rebuilds a dashboard copy from an
earlier real run after a dispositions edit, executing nothing, and
stamp_selector_bindings.py binds against the report the dashboard names.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Correct fiit-ecps's #8614 rationale: 1.729.0 carries #8614; the pinned artifact stores the pre-#8614 column

PE-US 1.729.0's release commit (180410fe) descends from #8614's merge
(1de4bff6) and reads partnership_self_employment_net_earnings. The pinned
Populace artifact still stores partnership_se_income, so PolicyEngine reads
$0 while the bridge feeds Axiom the old column. Document the runner's
overlay, commit-pinned source, full v2 report and --from-report.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Let a population run raise the tax bridge's engine timeouts

One engine call evaluates a whole surface over the selected population.
On a loaded host the CTC call over all 87,519 Populace units outlasted the
fixed 600-second run timeout. AXIOM_ENGINE_RUN_TIMEOUT_SECONDS and
AXIOM_ENGINE_COMPILE_TIMEOUT_SECONDS raise them; the defaults are unchanged
and a malformed value is refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Request the engine's fast mode from the tax bridge

Fast returns exactly what explain returns except the trace, and the
bridge reads only output values. Since axiom-rules-engine#131, explain
builds each query's trace by scanning every cached value in the request,
so a population request's cost grows with the square of its tax units:
fiit-ecps's CTC surface ran past 36 minutes over 87,519 units. The
Medicaid and SNAP Populace bridges already request fast. Log the engine's
fallback reason when it runs explain instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Pin fiit-ecps to the engine line rulespec-us builds its artifacts with

Engine main cannot compile rulespec-us main's 26 USC 1(h), 1(j), 32 and
25A programs (module.source_verification.values, rulespec-us#1050). The
release/v0.1 engine rulespec-us pins as axiom_artifact_rules_engine_ref
(98af0dce, v0.1.2) compiles all thirteen fiit-ecps surfaces, so the suite
declares that revision and the runner refuses any other checkout.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Keep the full v2 binding report to the rows the binding check reads

fiit-ecps's adapted report carries one observed_outputs row per compared
value (about 3.9 million rows, 1 GB). apply_dispositions.py --check
re-derives a premerged dashboard copy only from summary and mismatches, so
the published binding report omits observed_outputs as well as cases.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Select disposition rows by output, so one case's rows can carry different mechanisms

fiit-ecps's EITC concept covers eleven compared outputs, so one tax unit
carries several rows under it, and the rerun shows units whose rows have
different mechanisms (an investment-income eligibility row beside a
partnership earned-income row). Case-level selectors would give all of a
unit's rows to the first matching entry. The tax-ecps adapter now names
each row's output; case_selector.case_ids_by_output selects exact
(output, case) pairs; the premerged binding check keys retained rows by
output too. Selector membership sets are built once per selector.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Publish the full v2 binding report only when a dashboard copy needs a binding

A suite whose dashboard copy keeps every mismatch row never binds to a
source, so writing a -v2.json beside each non-v2 raw report published an
extra file for every such run (four provenance tests counted the
published files). The dashboard writer now writes it only for a
premerged-slim copy whose named source is not already this suite's v2
report.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Keep population-scale observed outputs out of published fiit-ecps copies

A full fiit-ecps run records one observed-output row per compared value,
3,881,635 rows. The tax-ecps adapter has kept them since #368, so the first
full run after it wrote a 1.2 GB dashboard copy and a 999 MB overview.

Past 10,000 rows, the dashboard copy and the FULL v2 binding report now
drop observed_outputs whole and record the count in
observed_outputs_omitted. Each output's comparison count stays in
output_summary.

Attestation only credits an output when the report body shows its returned
values. So dropping the rows also empties the stamp's per-output claims and
sets outputs_complete to false. A copy with the rows gone and the stamp left
as it was fails attestation, and a test pins that.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Split fiit-ecps's blanket #8614 EITC disposition by per-row attribution

The July entry eitc-earned-income-8614-partnership-scorp-split selected
every tax unit's EITC rows (case_id_prefix "ecps-tax_unit_"), and its
mechanism was wrong. It said PolicyEngine-US 1.729.0 predates #8614, but
1.729.0 contains #8614.

Every EITC row of the 2026-10-14 run is now attributed by counterfactual.
Each mechanism is one change to Axiom's inputs that makes Axiom see what
PolicyEngine-US 1.729.0 sees, and a row belongs to the smallest set of
changes whose re-run clears it. The 17,424 EITC rows split into five
entries, each listing its rows by output (case_ids_by_output) and bound to
them (selector_binding):

- 7,627 rows, bridge_artifact: the bridge's 32(c)(2) self-employment
  projection (axiom-oracles#640).
- 4,618 rows, bridge_artifact: the Populace artifact stores
  partnership_se_income, which 1.729.0 no longer reads
  (PolicyEngine/microcosm#35).
- 4,086 rows, bridge_artifact: the Populace artifact stores a
  dividend_income that disagrees with its components
  (PolicyEngine/microcosm#37).
- 890 rows, upstream_engine_gap: 1.729.0's 32(i)(2) investment-income
  definition (PolicyEngine/policyengine-us#9635 and #9567; Axiom debt
  rulespec-us#1416).
- 203 rows, upstream_engine_gap: 1.729.0's childless age test counts
  dependents (PolicyEngine/policyengine-us#9131; Axiom debt
  rulespec-us#1776).

The tax-before-credits, capital-gain and CTC entries are narrowed to their
2,131 rows the same way, and each row's signature is re-verified.

Evidence, tooling and tests:

- The evidence report (reports/fiit-ecps-eitc-attribution-2026-10-14.json)
  records every row's stage attributions. The FULL v2 report the
  dispositions bind to is committed beside it.
- scripts/fiit_ecps_eitc_attribution/ holds the three stage scripts, which
  now share minimal_sets.attribute. Re-applied to the variants stages 1 and
  2 stored, that function reproduces their attributions exactly.
- scripts/build_fiit_ecps_eitc_dispositions.py rebuilds the evidence byte
  for byte from the stage outputs. Its --check mode audits the evidence,
  the dispositions and the bound report against each other, and
  tests/test_fiit_ecps_eitc_attribution.py runs it with mutation cases.
- Stage 3's base check now accepts rows only the counterfactual has, and
  records them.

The PE-Axiom ratchet rises by one, from 81 to 82, as an approved and
recorded debt_raises entry. The grandfathered entry is closed, and the two
PolicyEngine attributions declare their Axiom side as debt instead.

docs/federal-income-tax-comparison.md now gives the October residuals and
the suite's manual lane.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Close the Sol review of #635: output in digests, per-merge selector sets, a merge-faithful audit

Fixes the four major findings and the minor one from the round-1 review
(~/reviews/axiom-oracles-635/review-r1.md).

- Both binding digests now include each row's output when the row names
  one. Before this, two equal-valued rows of one case could trade entries
  without changing selector_binding or the premerged assignment digest.
  Output-less rows hash exactly as before, so the bindings of
  us-tariff-panel and fiit-taxsim-ecps are unchanged. fiit-ecps's bindings
  are restamped.
- A selector's membership sets are built once per merge pass and dropped
  after it, instead of being cached across merges by list identity and
  length. A same-length, in-place edit between merges is now seen.
- build_fiit_ecps_eitc_dispositions.py --check now does three more things:
  validates the dispositions document; replays the production merge over
  the bound report, which must give every row its evidence entry with
  nothing expired, orphaned or unexplained; and requires each entry's
  selector_binding to be its rows' binding. Mutation tests cover a changed
  kind, a changed concept, a corrupted digest, a removed binding and a
  removed mechanism.
- --from-report takes the publication lock before its first read and
  adapts the one snapshot it parsed, so a run replacing the report
  meanwhile cannot pair one run's results with another's provenance.
- The investment-definition entry no longer says its rows clear "only
  through" a definition switch. 171 of the 890 tie with the Populace
  dividend artifact alone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Report malformed audit inputs instead of raising; match outputs as strings

Two follow-ups from the round-2 review of #635
(~/reviews/axiom-oracles-635/review-r2.md), which approved 0363f2d:

- build_fiit_ecps_eitc_dispositions.py --check reports a committed file
  of the wrong shape as a problem, not a traceback. Tests cover a missing
  run block, a list in place of the rows, a row without its value and a
  string binding.
- case_ids_by_output compares outputs as strings, as the binding digest
  identifies them. Every current output is a string, so no selection
  changes.

The assignment-digest sort nit is left as it is. It can only reorder rows
whose entry ids are empty, which validation never admits, and changing it
would re-digest every premerged suite.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Reject unresolved FIIT EITC counterfactual attributions

Require supported clearing labels in every applicable downstream stage
and passing baseline/model checks before building or auditing dispositions.
Check original mismatches while allowing counterfactual-only extra rows.

Cover corrupt labels, failed checks, missing stage-3 labels, and an honestly
unresolved stage-2 run. Preserve saved evidence, counts, and row bindings.
Clarify selector expiry and correct the TAXSIM suite count in the PR body.

Co-Authored-By: GPT-6.1 Sol <noreply@openai.com>

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Correct two descriptions of fiit-ecps refresh and v2 report contents

The unattended regeneration scripts do attempt fiit-ecps; the runner refuses
an engine checkout that is not at the pinned commit. The v2 report drops
observed_outputs only above 10,000 rows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

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.

Publish a latest.json release pointer on policyengine/populace-us

2 participants