Skip to content

Put every file that decides a gate verdict on the gate surface - #308

Open
MaxGhenis wants to merge 1 commit into
codex/thesis-ledger-factsfrom
fix/gate-surface-trust-pins
Open

MaxGhenis wants to merge 1 commit into
codex/thesis-ledger-factsfrom
fix/gate-surface-trust-pins

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Summary

The append gate's gate_surface no longer matched the files that decide its verdict. Since the receipt shim refactor (4a23e31, #116, 2026-07-25), the trust pins live in scripts/receipt_pins.py. The surface still listed only the scripts that held them before, and never listed pyproject.toml or uv.lock, which pick the receipt version the judge runs.

receipt 0.6.2 treats a proposal as gate-changing only when it touches gate_surface (append_gate.py _classify_surfaces / check_surface_separation). A mixed data/gate proposal is refused, and unclassified changes are dropped on the data path. So an append that also edited the pins passed as plain data and was never named. The independent review of #299 found this by reading the code (verdict.md, "Outside this PR's scope").

Invariant (trust boundary): every file in the judging (base) checkout that can decide a gate verdict is on gate_surface.

Reproduction (before the fix)

These runs used the base gate exactly as the "Trusted base append gate" job runs it: PYTHONPATH=$J/scripts PYTHONNOUSERSITE=1 uv run --locked --no-dev --project $J python $J/scripts/check_thesis_facts_append.py --root <clone> --commit <c> --base-ref <b>, from another cwd.

Judge Candidate Verdict
3dd95a0 (pre-#299 base) 55bbf3d (real release-20 resolver append) + a receipt_pins.py edit, vs d77afe2 OK: 221 rows, immutable prefix 128, +5 appended vs base, release 20
3dd95a0 #297's 7 rows + #299's receipt_pins.py, vs 3dd95a0 refused, but on the DigiCert signer pin; the mixed check never fired
75b74b1 (#299) #297's rows + a pins edit, vs a simulated #299 merge OK: 228 rows, immutable prefix 128, +7 appended vs base, release 21
75b74b1 #297's rows alone (control) the same OK ... +7 appended vs base, release 21 line

The mixed proposal and the pure one got the same verdict line.

What else decides the verdict (each observed on a scratch uv project)

  • scripts/**. The judge runs scripts/check_thesis_facts_append.py, so scripts/ is sys.path[0]. Both PR jobs also put it on PYTHONPATH.
    • A new scripts/sitecustomize.py ran at interpreter startup.
    • A new scripts/tempfile.py shadowed the standard library, even with no PYTHONPATH (the push job's spelling).
    • So the whole directory is on the surface, not just today's imports.
  • uv.toml and .python-version. Run from another cwd, uv sync --locked --no-dev --project <dir> failed on a project-dir uv.toml with required-version = "==0.0.1". A project-dir .python-version of 3.12 switched uv run to Python 3.12.14.
  • .venv/**. A committed .venv/.../site-packages/*.pth survived uv sync --locked and executed on uv run. This ran on one machine; on CI the committed venv's interpreter path would also have to exist on the runner.

The four explicit scripts/... entries are subsumed by scripts/**. receipt_pins.py now documents why each entry is there.

After the fix

The same candidates, judged by this branch's gate:

  • Both mixed proposals: thesis-facts append check failed: mixed data/gate proposal is forbidden: DATA_SURFACE changes=[...]; GATE_SURFACE changes=['scripts/receipt_pins.py']; split them into separate pull requests
  • 55bbf3d alone: OK: 221 rows ... +5 appended vs base, release 20
  • Record 7 first-print observation(s) via resolve_pending.py #297's rows alone: OK: 228 rows ... +7 appended vs base, release 21
  • A pins-only edit: OK: gate-only proposal; DATA_SURFACE unchanged; GATE_SURFACE changes=['scripts/receipt_pins.py']

Ordering and who is affected

  • This PR is judged by the current base gate, which does not yet see receipt_pins.py. It will pass as data (+0 appended) and will not name the change. The new surface applies to PRs whose base includes this merge.

  • Resolver proposals stay pure data. ThesisInstitute/thesis scripts/resolve_pending.py (propose_ledger_append → _prepare_release_files) commits only:

    • ledger/official_observations.jsonl
    • ledger/series_catalog.json
    • ledger/series_uuid_registry.jsonl (when minted)
    • the four releases/manifests/<release>.* files

    It reads scripts/build_series_catalog.py and scripts/receipt_pins.py from the base but never changes them.

  • Resolve catalog supersede links against every row's effective assertion id #306 touches pyproject.toml, uv.lock and scripts/build_series_catalog.py, with no ledger change. It becomes a named gate-only proposal, which still passes.

  • Future receipt bumps (like Pin receipt 0.6.2 and DigiCert's 2026 timestamp responder #299) become named gate-only proposals.

  • releases/README.md says a data PR "cannot change the verifier, cutter, canonicalizer, append workflow, or anchors". That held for the verifier code, but not for the pins or the receipt version, from Repin integrity shims: vidimus==0.1.2 → receipt==0.2.0 (upstream rename) #116 until this PR. The file is immutable after genesis, so the accurate statement goes in the root README.md.

  • Unchanged limits. Branch protection still requires only status checks, so a gate-only PR still needs no review. This PR adds the refusal and the naming, not a review requirement.

Tests

tests/test_thesis_append_shim_isolation.py (in the CI "Ledger observation invariants" list) covers seven deciders: scripts/receipt_pins.py, pyproject.toml, uv.lock, uv.toml, .python-version, scripts/sitecustomize.py and a .venv .pth.

  • A witnessed append plus a change to any one of them is refused as mixed, and the refusal names it.
  • Each one changed alone is a named gate-only proposal.
  • A pure append beside unchanged deciders passes with the exact accepting output. This is the resolver's shape.
  • Measured invariant: an audit-hook run of an accepting verdict records every file the gate opens or imports from its own checkout. All of them are on the surface: the pins, the scripts, the anchors, and the receipt wheel in .venv.
  • Structural invariant: the workflow's PYTHONPATH="$base_gate/scripts" and uv sync --project "$base_gate" spellings are pinned, and the surface covers scripts/** and the uv inputs.

tests/test_receipt_shim_transparency.py pins the shim's surface as a deliberate superset of the originals' (AST read of the hash-authenticated fixture). No existing differential case touches a decider, so byte-identity is unaffected. This is the only place the pair is meant to classify differently.

Mutation check: with the old gate_surface, 16 of the 17 new isolation tests fail. The pure-append control passes either way.

Verification run locally:

  • CI "Ledger observation invariants" list (six files): 174 passed, 5 xfailed (the xfails are the adversarial file's documented strict-xfail boundaries); the new transparency test separately: passed
  • scripts/check_thesis_facts_append.py --commit HEAD (push path): OK: 229 rows, immutable prefix 128, release 21
  • scripts/verify_release_chain.py --full: release chain OK: 22 releases, HEAD=0021-09d13cdddbedc992.json
  • ruff check clean on the changed files.
  • The full pytest tests was not run locally: the host is RAM-constrained. Instead I ran every test file that imports the pins or the gate; the change is one frozenset read only through APPEND_GATE_SPEC.

🤖 Generated with Claude Code

Since the receipt shim refactor (4a23e31, #116), the trust pins live in
scripts/receipt_pins.py, but the gate surface still listed only the files
that held them before. receipt's append gate calls a proposal gate-changing
only when it touches gate_surface, so a resolver-shaped append that also
edited the pins, or bumped receipt in pyproject.toml and uv.lock, was judged
as plain data: accepted, and never named.

Reproduced with the base gate invoked as the trusted CI job invokes it:
3dd95a0's gate accepted the release-20 append plus a receipt_pins.py edit
as "+5 appended vs base, release 20", and #299's gate accepted #297's seven
rows plus a pins edit as "+7 appended vs base, release 21".

The surface now covers:
- scripts/**: the judge runs a script there, so scripts/ is sys.path[0],
  and both pull request jobs also put it on PYTHONPATH. A new module there
  shadows imports (the standard library's included), and a sitecustomize.py
  runs at startup. Both were observed.
- pyproject.toml and uv.lock, which pick the receipt version the judge runs,
  and uv.toml, .python-version and .venv/**, which uv also reads from the
  project it provisions the judge from. Each was observed to change what
  `uv sync --locked --no-dev --project <dir>` and `uv run` do.

The same candidates are now refused as mixed, naming the decider, while
both pure appends still pass and a pins-only change is reported as
gate-only. The resolver's proposals change only ledger/** and
releases/manifests/**, so they stay data proposals.

Tests: every decider is refused as mixed beside a witnessed append and
named when changed alone; a pure append beside unchanged deciders passes;
an audit-hook run shows every file the gate opens or imports from its own
checkout is on the surface; and the shim's surface is pinned as a
deliberate superset of the originals'.

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

Copy link
Copy Markdown
Contributor Author

Arch checks is red for a reason that predates this PR: tests/test_build_series_catalog.py::test_committed_catalog_is_current_and_valid fails on its transcribed stripped-segments list (At index 32 diff: 'week_2026-08-29' != 'week_2026_06_13'). The base commit 598f394 fails the same way. #306 updates that list. Once #306 merges, this branch will be brought up to date (branch protection is strict), and the remaining Arch steps, including this PR's new tests, will run. An independent Opus review is queued.

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