Skip to content

Build and verify isolated wrapper release candidates - #520

Draft
MaxGhenis wants to merge 10 commits into
max/spm-canonical-wrapper-release-20260910from
max/wrapper-release-candidates-20260914
Draft

MaxGhenis wants to merge 10 commits into
max/spm-canonical-wrapper-release-20260910from
max/wrapper-release-candidates-20260914

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Sep 15, 2026 •

Copy link
Copy Markdown
Contributor

Keeps the release-candidate tooling from 818c894e and fixes the two defects it
carries, plus a test rewrite that no review examined.

This stack cannot stand on main. Verified against origin/main:
scripts/release_lock.py, scripts/check_release_credentials.py and
scripts/spm_bundle.py do not exist there; scripts/bundle.py exists but has no
--published-spm; and trace_tro.schema.json exists but its pe:emittedIn enum
is ["local", "github-actions", "policyengine-api"], without the
repository-bundle value release_build.py emits. Base is therefore
max/spm-canonical-wrapper-release-20260910, whose head is already 818c894e.

What the reviews did and did not cover

The R1–R3 chain lives at
/Users/maxghenis/spm-rebuild-20260908/rollout/codex-continuation-20260912/.
The author-side records are the six WRAPPER-RELEASE-BUILD* files —
-PREPARATION.md and -SOURCE-INVENTORY.json (R1, staged patch
39c12b24…), -R2-RESPONSE.md and -SOURCE-INVENTORY-R2.json (patch
cb7c8ef0…), -R3-RESPONSE.md and -SOURCE-INVENTORY-R3.json (patch
f8856478…). The independent verdicts sit beside them in
wrapper-build-source-review/: R1 INDEPENDENT-REVIEW.md "not APPROVE",
thirteen findings; R2 INDEPENDENT-REVIEW-R2.md "APPROVE with limits,
conditional on Findings 1–3"; R3 INDEPENDENT-REVIEW-R3.md "APPROVE with
limits", closed by ROOT-REVIEW-R3.json as source_preparation_approved.

Every reviewer worked without a shell and ran nothing. Two consequences show up
as the defects this PR fixes.

  • tests/test_spm_bundle_bootstrap.py is named in none of the six files, none
    of the three frozen inventories, and none of the three reviews. Nobody checked
    whether its assertion still matched the workflow the patch rewrote.
  • tests/test_spm_household.py was carried by hash, not read.
    WRAPPER-RELEASE-BUILD-R2-RESPONSE.md:35: "tests/test_spm_household.py
    remains SHA256 061cbc1b…. It has been provided unchanged to root's separate
    household verification agent. This lane did not run household calculations
    against the unrelated installed 2.0.0 model." The three independent reviews
    contain zero occurrences of "household".

assert_source_origin came from R1 finding F9
(INDEPENDENT-REVIEW.md:103-110) and was reviewed as a fix, never exercised.

Commits

35981756 — restore the reviewed household test. Returns
tests/test_spm_household.py to its 1b6c001c content, byte-identical (sha256
fe6df0c7…). The 818c894e rewrite (sha256 061cbc1b…) drives the assisted
case through spm_unit_allocated_housing_subsidy and
spm_unit_allocated_tenant_payment. Neither exists in the pinned
policyengine-us 2.0.0 — zero class <name>(Variable) definitions across the
installed model — so dispatch_extra_variables raises ValueError before any
assertion runs. Both are added by the unified country candidate
PolicyEngine/policyengine-us#9467, verified from its file list:
policyengine_us/variables/household/income/spm_unit/spm_unit_allocated_housing_subsidy.py
and …/spm_unit_allocated_tenant_payment.py. The other three names the rewrite
introduces (pre_subsidy_rent, pha_payment_standard,
receives_housing_assistance) do exist in 2.0.0.

The rewrite returns with the country repin — once #9467 publishes and the
pin in pyproject.toml moves, not before. 1b6c001c is the reviewed
post-Ruling-A content, so this revert leaves Ruling A intact.

14e5db47 — follow the publication gate through its new indirection.
818c894e replaced the Publish job's inline
bundle.py check --published-spm --include-tros --strict-tros with
release_build.py publish-check. The test still searched the Publish steps for
the inline command, so its generator matched nothing and next raised
StopIteration instead of asserting. It now finds the step that carries the
gate (push.yaml:164, index 2) and still requires it before
pypa/gh-action-pypi-publish (index 9). publish_check runs that same check
itself (scripts/release_build.py:1381), pinned by
test_publication_checks_existing_strict_gates_before_member_comparison.
NotifyConsumers is untouched and keeps the dependency-light
python -S scripts/bundle.py check --published-spm.

abf75454 — place a namespace package by its portions.
assert_source_origin refused every policyengine* module whose __file__ was
None. policyengine.tax_benefit_models has no __init__.py, so its __file__
is None while its __path__ points inside the checkout, and any interpreter
that has imported a country model carries it. An instrumented probe over three
import phases located it rather than guessing: bare import policyengine gives
0 offenders; after tests/conftest.py, exactly one — that namespace package;
after tests/test_graph/test_extractor.py, two more. That is why
test_source_origin_rejects_previously_imported_other_checkout failed even when
run alone.

module_origin now places a module by __file__ when it has one, otherwise by
every __path__ portion, each of which must resolve inside the prepared source.
A module with neither, an empty __path__, or any portion outside is still
refused — the shape a synthesized stand-in takes.
tests/test_graph/test_extractor.py was the session's second poisoner: it
installs bare stand-ins for policyengine and policyengine.graph at import
time, i.e. during collection, and never removed them. It now restores
sys.modules in a finally and leaves nothing behind at all.

Three new cases cover the acceptance and the three refusals, and both directions
are mutation-checked: reverting to the pre-fix logic fails the acceptance case,
and accepting any file-less module fails all three refusals.

44604506, b8cf1449, d841dae3, 82721388 and e98d280a are PROGRESS.md. The first overwrote the
prior lane's 178-line record; d841dae3 restores it verbatim beneath this
lane's section.

e0b1f4f2 — hold the strict property where session state cannot reach it.
An adversarial pass over the three fixes closed four gaps. A root policyengine
stand-in used to be refused by the file rule before anything else ran; accepting
a __path__ let it reach files("policyengine"), which raises AttributeError
— a class main() does not catch, so a hold would have ended in a traceback.
The root package's own __init__.py is now settled first. The docstring says
what the check does and does not establish: neither __file__ nor __path__ is
proof, since an in-process caller that can write sys.modules can assign either.
A test pins the shape the allowance newly admits instead of leaving it to be
discovered. The subprocess control from the approved harness patch is adopted as
well, and publish-check is now required before the git tag, not only before the
upload.

Conflicts with prior rulings

Two places where this brief and a prior approved ruling disagree. Reported, not
resolved here.

  1. The guard change runs against WRAPPER-R3-CI-DIAGNOSIS-20260913.md §1:
    "Keep the production guard strict. Do not delete arbitrary cached modules or
    accept missing origins merely to pass the full suite."
    A root-approved patch
    — wrapper-ci-harness-repair-20260913/ROOT-REVIEW.json, approved: true,
    parent 818c894e, patch sha256 9ad1e023… — repairs the same failure in the
    harness with a fresh subprocess and leaves assert_source_origin untouched.
    e0b1f4f2 adopts that subprocess control as an additional test, so the strict
    cross-checkout property is pinned either way and switching to the approved
    patch would cost only the namespace branch.
  2. §3 says to preserve the household rewrite, not revert it: "preserve this
    assertion and complete the already approved final-country pin transaction"
    .
    The brief instructed the revert, recorded as returning with the repin. The
    evidence justifying its restoration is
    household-integration-20260912/REPORT.md: 84 wrapper controls against the
    authenticated country 2.0.2rc1.

§1's line-208 reading also means the restore is two-part: the rewrite needs both
the two absent variables and the country's assisted ordinary-resource
independence. Re-adding only the variables would still fail.

A fourth blocker these fixes do not touch

gh pr checks 515 shows Nonpublishing wrapper candidate (release) failing in
12s at scripts/check_release_credentials.py — "Release verification requires
an authenticated Hugging Face access token with role read"
— before any strict
gate. This PR addresses the four Test (3.x) failures. #515 stays red until the
credential is settled.

Verification

Locked environment, uv sync --frozen, Python 3.14.4.

Check Result
pytest tests/test_release_build.py tests/test_spm_bundle_bootstrap.py tests/test_graph -q, before 2 failed, 98 passed (exit 1)
same, after 106 passed (exit 0)
pytest tests -q 1205 passed, 9 skipped, 358s (exit 0)
ruff format --check . exit 0
ruff check . 11 UP038 (exit 1)

The 11 ruff check findings are all present at 818c894e, all UP038, and none
in a file this PR touches; the touched files pass clean. The local lock resolves
ruff 0.12.11, which still has UP038; CI installs 0.16.7, which does not, and
CI's Lint job passes on this tree.

Not verified here

The commit message on abf75454 names tests/conftest.py →
tests/fixtures/us_reform_fixtures.py:12 as the import path to the namespace
package. That is a real path but not the first one: tests/conftest.py:6 →
tests/fixtures/filtering_fixtures.py:7 reaches it earlier in every session.
Both are unrestored module-level imports; the fix covers either.

🤖 Generated with Claude Code

MaxGhenis and others added 10 commits September 12, 2026 14:50
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The release-candidate commit rewrote
tests/test_spm_household.py::test_state_only_graphs_require_geography_only_where_measurement_is_used
to drive the assisted case through computed HUD semantics — `pha_payment_standard`,
`receives_housing_assistance`, `spm_unit_allocated_housing_subsidy` and
`spm_unit_allocated_tenant_payment`. Those are country behaviours the unified
candidate policyengine-us#9467 carries, not the pinned model, and the rewrite
went through the R1–R3 reviews unverified.

Return the file to the reviewed content at 1b6c001. The rewrite comes back with
the country repin, once #9467 publishes and the pin in pyproject.toml moves.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The release-candidate commit replaced the Publish job's inline
`bundle.py check --published-spm --include-tros --strict-tros` with
`release_build.py publish-check`, which runs that same check itself
(scripts/release_build.py:1381). The bootstrap test still searched the Publish
steps for the inline command, so its generator expression matched nothing and
`next` raised StopIteration instead of asserting anything.

Search for the step that now carries the gate. NotifyConsumers is unchanged: it
runs after PyPI visibility without the package installed, so it keeps the
dependency-light `python -S scripts/bundle.py check --published-spm`. That
publish-check still performs the published-spm check is pinned separately by
test_publication_checks_existing_strict_gates_before_member_comparison, which
asserts its exact call sequence; the comment now points there.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`assert_source_origin` refused every `policyengine*` module whose `__file__`
was None. `policyengine.tax_benefit_models` has no `__init__.py`, so its
`__file__` is None while its `__path__` points inside the checkout, and any
interpreter that has imported a country model carries it. The test session
does, through tests/conftest.py -> tests/fixtures/us_reform_fixtures.py:12, so
test_source_origin_rejects_previously_imported_other_checkout failed even when
run alone — an instrumented probe over the three import phases found that one
module and no other.

Locate each module through `module_origin`: by `__file__` when it has one,
otherwise by every `__path__` portion, each of which must resolve inside the
prepared source. A module with neither is unattributable and still refused,
as is an empty `__path__` and any portion outside the checkout — the shape a
synthesized stand-in takes. Namespace packages are recorded in the receipt as
the list of their portions.

tests/test_graph/test_extractor.py was the session's second poisoner: it
installs bare stand-ins for `policyengine` and `policyengine.graph` at import
time, i.e. during collection, and never took them out. It now restores
sys.modules in a `finally`, which leaves nothing behind at all. The loaded
module objects stay alive through the references it returns, and extractor.py
resolves its own `from policyengine.graph.graph import VariableGraph` while
the entries are still installed.

Three new cases cover the namespace acceptance and the three refusals. Both
directions are pinned: reverting the fix fails the acceptance test, and
accepting any file-less module fails all three refusals.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Commit 4460450 wrote this lane's progress over the file rather than adding to
it, dropping the prior lane's 178-line record: rulings A-E, the verified 2.0.1
prerequisites, the data-certification blocker, and the country archaeology
behind the rebaselined snapshots. None of that is recoverable from anywhere
else in the tree.

Restore it verbatim and keep this lane's section above it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Two claims made earlier in this lane needed tightening. The namespace package
is first imported through tests/conftest.py:6 -> filtering_fixtures.py:7, not
through us_reform_fixtures.py:12 as abf7545's message says; both are
unrestored module-level imports and the fix covers either. And the rewrite's
dependency on policyengine-us#9467 was asserted from the brief rather than
checked: #9467's file list does add exactly the two absent variables, so the
attribution now rests on the PR itself.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
An adversarial pass over the three fixes found four things worth closing, all
in the direction of keeping what the relaxed guard gave up.

A root `policyengine` stand-in used to be refused by the file rule before
anything else ran. Accepting a `__path__` let it through the walk and into
`files("policyengine")`, which raises AttributeError on it — a class main()
does not catch, so a hold would have ended in a traceback. Settle the root
package's own `__init__.py` first, and short-circuit before `files()`.

The relaxed rule newly admits a fileless module whose `__path__` points inside
the checkout. Neither `__file__` nor `__path__` is proof — an in-process caller
that can write sys.modules can assign either — so the docstring now says what
the check does and does not establish, and a test pins the admitted shape
instead of leaving it to be discovered.

WRAPPER-R3-CI-DIAGNOSIS-20260913.md §1 asks for the origin control to run in a
fresh interpreter matching the production `source_command` boundary, and a
root-approved patch (wrapper-ci-harness-repair-20260913/ROOT-REVIEW.json,
parent 818c894) does exactly that instead of touching the guard. Take the
subprocess control as well: it proves the cross-checkout refusal where no
collector has been, so the property survives independently of the allowance.

§2 asks that publish-check precede the tag as well as the upload. The git tag
is a public side effect between them, so assert the full order.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@MaxGhenis
MaxGhenis force-pushed the max/spm-canonical-wrapper-release-20260910 branch from 818c894 to 1b6c001 Compare September 15, 2026 04:03

This branch has not been deployed

No deployments
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