Skip to content

fix(package): partial-matrix handling, and close a gate that could pass having verified nothing - #98

Merged
JC-000 merged 1 commit into
masterfrom
fix/package-partial-failure
Aug 14, 2026
Merged

fix(package): partial-matrix handling, and close a gate that could pass having verified nothing#98
JC-000 merged 1 commit into
masterfrom
fix/package-partial-failure

Conversation

@JC-000

@JC-000JC-000 commented Aug 14, 2026

Copy link
Copy Markdown
Owner

Follow-up to #96, prompted by LaneCerts hitting this while running make package for their acceptance.

Two bugs, the second much worse than the first.

Problem 1 — one broken variant took the whole pipeline down

At the nistcurves v0.9.1 pin now on master, both onchip variants fail to link.
That took the entire packaging pipeline down: make package aborted inside
build_prgs.sh, so no disk images, no listener and no manifest were produced,
and make package-verify had nothing coherent to report. One broken variant
left the operator with no artifacts and no written record of what broke.

That is the wrong failure mode for precisely the situation it appears in. A
library bump breaking one profile is a normal event during release prep, and
the useful outcome is "here are the three that work, here is the error for the
fourth" — the release still cannot be cut, but the blocker is legible and the
good artifacts are testable.

The change

  • build_prgs.sh is three-valued: 0 all built, 2 partial, 1 nothing
    built. It also greps the first ld65/ca65 diagnostic out of each failed
    variant's log into dist/build-info.txt, so everything downstream can state
    why a variant is missing without anyone opening a log.
  • build_d64.sh skips absent PRGs and creates no image when none of its inputs
    exist, instead of erroring on the first gap.
  • write_manifest.sh still runs, and opens with an !! INCOMPLETE RELEASE !!
    block naming each missing variant, its make line and its exact error. Missing
    variants are marked in the guidance list and show FAILED in the size table.
    A partial release announces itself at the top rather than being a checksum
    list that merely has fewer lines than it should.
  • make package runs the pipeline to completion on a partial matrix, then
    prints the blocker and exits 1.
  • verify_release.py leads with a BLOCKER section, reports failed variants as
    [n/a ] in the reproducibility check rather than as fabricated failures, and
    ends RELEASE INCOMPLETE with exit 1.

Problem 2 — the gate could pass having verified nothing

Chasing problem 1, LaneCerts found the sharper bug: at master,
SKIP_REBUILD=1 SKIP_VICE=1 make package-verify printed

2/2 checks passed
RELEASE ARTIFACTS VERIFIED

on a dist/ missing half its PRGs and with no disk images. Reproduced, and it
degrades further — with the build record present, dist/ emptied and
SKIP_LISTENER=1 as well, master's verifier prints:

=== 2a. D64 contents (c1541 read-back, byte-compare) ===
============================================================
0/0 checks passed
RELEASE ARTIFACTS VERIFIED exit 0

A gate that verified literally nothing reporting success.

Being precise about the reproducer, because the exact conditions matter.
On a genuinely clean tree, master's gate does not go green: without the
listener bundle it prints 0/1 checks passed, exit 1, because check_listener
happens to assert the bundle's presence explicitly instead of iterating. So
whether a vacuous run went green depended entirely on whether some unrelated
check happened to be written defensively:

dist stateinvocationmaster's verdict
no disks, no bundleSKIP_REBUILD SKIP_VICE0/1 exit 1 (red, by luck)
no disks, bundle presentSKIP_REBUILD SKIP_VICE2/2VERIFIED exit 0
no disks, bundle present+ SKIP_LISTENER0/0VERIFIED exit 0

That inconsistency is the actual defect. The disk checks were globs, the
listener check was an assertion, and the gate's correctness rode on which style
each check happened to use. My first commit on this branch did not close
it: it caught variants that failed to build, and said nothing about artifacts
that were never produced — a distinction that is easy to re-collapse.

The cause is that both disk checks iterated DIST.glob("*.d64"). No disks
meant no iterations, no records, and nothing failed — absence of evidence read
as evidence of correctness, which is the exact failure mode this repo's own
guidance warns about.

  • expected_d64_images() derives the images that MUST exist from the build
    record (one per built variant, one per backend). Both disk checks iterate
    that, and record a FAIL for any expected image that is absent, plus one
    for stray images no variant accounts for. build_prgs.sh now records
    backend= per variant so this needs no second copy of the matrix.
  • A run that records zero checks fails outright.
  • RELEASE ARTIFACTS VERIFIED is now reserved for a run where every section
    executed. Any SKIP_* yields PARTIAL VERIFICATION — … Not a release gate,
    still exit 0 so SKIP_* stays usable for narrowing.

Four cases, measured:

caseresultexit
A — built PRGs, disks deleted0/4, four "expected but absent"1
B — make package at the broken pinpartial artifacts + blocker1
C — partial dist, disks present6/6 present verify, 2 missing, RELEASE INCOMPLETE1
D — all-OK record, SKIP_* set4/4, PARTIAL VERIFICATION0

Nothing changes on a healthy, fully-run matrix: all four build, every section
runs, exit 0, RELEASE ARTIFACTS VERIFIED.

A regression test for the verdict

The verdict logic is now a pure summarize() function with
tools/test_package_verify.py pinning it — no VICE, no builds, runs in
milliseconds. 31 assertions.

Case D is the one deliberately guarded hardest, on LaneCerts's suggestion: it
is the invocation people actually use while iterating, so it is where the
pressure to just say VERIFIED will come back. It must stay exit 0 to remain
usable while never producing the word VERIFIED.

The subtlest test is test_verdict_empty_run_not_rescued_by_skips: the skip
branch is the friendly one and exits 0, so if it were evaluated before the
empty-run branch, skipping every section on an empty dist/ would report a
cheerful PARTIAL — the original bug wearing a different hat. The ordering is
part of the contract and is now pinned as such.

31 passed, 0 failed

The tests run as a preflight to make package-verify, not just standalone
— a regression test nobody runs is not a guard, and "the gate said yes" is only
worth something if the gate's yes still means what it should. A broken verdict
function now stops the run instead of blessing a release.

They are deliberately not in tools/run_all_tests.py: that runner allocates a
VICE instance per suite and dispatches run_tests(transport, labels, seed), a
shape a pure-logic test does not fit.

$ make package-verify SKIP_REBUILD=1 SKIP_VICE=1
python3 tools/test_package_verify.py
=== release-gate verdict regression tests ===
…
31 passed, 0 failed
python3 tools/package/verify_release.py
…
6/6 checks passed
RELEASE INCOMPLETE

Measured against the real failure at 21f2ce5

make package (exit 1):

[package] ***** INCOMPLETE: the following variants did NOT build *****
[package] uci-onchip ld65: Error: src/lib_contract_asserts.s(166): libs/nistcurves shared-primitive ownership claim moved — …
[package] ip65-onchip ld65: Error: src/lib_contract_asserts.s(166): libs/nistcurves shared-primitive ownership claim moved — …
[package] dist/ holds the variants that DID build; see MANIFEST.txt.
[package] Do not tag a release from this.

make package-verify (exit 1):

==============================================================================
BLOCKER — 2 of 4 variants did not build
==============================================================================
c64-https-uci-onchip.prg (make BACKEND=uci USE_NISTCURVES_ONCHIP=1)
ld65: Error: src/lib_contract_asserts.s(166): …
c64-https-ip65-onchip.prg (make BACKEND=ip65 USE_NISTCURVES_ONCHIP=1)
ld65: Error: src/lib_contract_asserts.s(166): …
=== 1. PRG byte-reproducibility (second build from clean) ===
[PASS] uci-reu reproduces — 66e37037deb9b295… vs 66e37037deb9b295…
[n/a ] uci-onchip — did not build; see the blocker above
[PASS] ip65-reu reproduces — d522e68469b8fc09… vs d522e68469b8fc09…
[n/a ] ip65-onchip — did not build; see the blocker above
=== 2a. D64 contents (c1541 read-back, byte-compare) === 4/4 PASS
=== 2b. D64 boots to the banner in VICE === 4/4 PASS
=== 3. Single-file listener selftest === SELFTEST PASSED (4 checks)
12/12 checks passed
Everything present verifies, but 2 variant(s) are MISSING — see the blocker above.
RELEASE INCOMPLETE

Incidental but worth recording: the two surviving REU PRGs at the v0.9.1 pin
hash identically in my worktree and in LaneCerts's, on independent checkouts —

c64-https-uci-reu.prg 66e37037deb9b295d3594a50b2bacfe95d09bb7e62f3dfc401c51b03c2461fed
c64-https-ip65-reu.prg d522e68469b8fc09709f34e64fd5573a0a72465349374a26257194bd5ed96cca

The underlying link failure is NOT fixed here

It lives in src/lib_contract_asserts.s, which this lane does not own. Full
diagnosis for whoever picks it up, all measured with od65 on the built
archives:

archiveSHARED_PRIMITIVESSHARED_CONSUMES
nistcurves-p256.a (REU)$0007
nistcurves-p256-onchip.a$0005$0005

$0007 -> $0005 is the §8.2 reu_mul bit ($0002) dropping out. This is
deliberate and documented upstream — libs/nistcurves/CHANGELOG.md (contract
v0.5.0, lib-contract #44) carries the exact table:

Build configurationSHARED_PRIMITIVESSHARED_CONSUMES
default, standalone$0007$0007
default + -D SHARED_REU_MUL_INIT$0005$0007
-D FP_ONCHIP_MUL$0005$0005

with the gating rule "profile switches (FP_ONCHIP_MUL) drop a bit from
both masks; SHARED_* deferral switches drop it from the ownership mask
only." Our onchip archive measures $0005/$0005 — precisely the documented
FP_ONCHIP_MUL row. It neither owns nor consumes the REU multiply primitive,
which is correct, because it has no REU mul path.

So the assert is profile-blind: it hardcodes = APP_OWNED ($0007), a value
measured at the v0.6.0 pin when both profiles reported $0007. It needs a
profile-dependent expected value.

One warning for that fix: the assert's own error text says to "re-derive
the archive member drops in tools/integration/build_nistcurves_p256.sh". That
is the wrong remedy here and will cost someone an afternoon — nothing about the
archive surgery changed, the mask semantics were refined upstream. Worth
rewording the message while fixing the value.

🤖 Generated with Claude Code

…f aborting
Reported by LaneCerts and reproduced here on a fresh checkout of 21f2ce5:
at the nistcurves v0.9.1 pin both onchip variants fail to link, which
took the entire packaging pipeline down. `make package` aborted inside
build_prgs.sh, so no disk images, no listener and no manifest were ever
produced, and `make package-verify` had nothing coherent to report. One
broken variant left the operator with no artifacts and no written record
of what broke.
That is the wrong failure mode for exactly the situation it shows up in.
A library bump breaking one profile is common, and the useful outcome is
"here are the three that work, here is the error for the fourth" — the
release still cannot be cut, but the blocker is legible and the good
artifacts are testable.
- build_prgs.sh is now three-valued: 0 = all built, 2 = partial,
1 = nothing built. It also greps the first ld65/ca65 diagnostic out
of each failed variant's log into dist/build-info.txt, so downstream
can state WHY a variant is missing without anyone opening a log.
- build_d64.sh skips PRGs that are absent and creates no image when
none of its inputs exist, rather than erroring on the first gap.
- write_manifest.sh still runs, and opens with an "!! INCOMPLETE
RELEASE !!" block naming each missing variant, its make line and its
exact error. Missing variants are marked in the guidance list and
show FAILED in the size table. A partial release announces itself at
the top rather than being a checksum list with fewer lines than it
should have.
- `make package` runs the pipeline to completion on a partial matrix,
then prints the blocker and exits 1.
- verify_release.py leads with a BLOCKER section, reports failed
variants as [n/a ] in the reproducibility check instead of as
fabricated failures, and ends "RELEASE INCOMPLETE" with exit 1.
Measured against the real failure at 21f2ce5:
make package -> uci-reu + ip65-reu built, both onchip variants
recorded with their ld65 error, manifest written
with the INCOMPLETE banner, exit 1
make package-verify -> 12/12 present-artifact checks pass (both REU
PRGs reproduce byte-for-byte, 4 disk images boot
to the correct banner, listener selftest 4/4),
2 variants reported as blockers, exit 1
The underlying link failure is NOT addressed here — it lives in
src/lib_contract_asserts.s, which this lane does not own. Diagnosis for
whoever picks it up: the assert hardcodes LIB_NISTCURVES_SHARED_PRIMITIVES
= $0007, measured at v0.6.0 when both profiles reported that. Upstream
has since split ownership from consumption (contract v0.5.0,
lib-contract #44), and the FP_ONCHIP_MUL profile now correctly reports
$0005 for both masks — od65 on our archives confirms $0007 for the REU
build and $0005/$0005 for onchip, exactly matching the table in
libs/nistcurves/CHANGELOG.md. The expected value needs to become
profile-dependent. Note the assert's own message misdirects: it tells
you to re-derive the archive member drops in build_nistcurves_p256.sh,
and nothing about the archive surgery changed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@JC-000
JC-000 merged commit 660101a into masterAug 14, 2026
@JC-000JC-000 changed the title fix(package): produce a partial release + a legible blocker instead of abortingfix(package): partial-matrix handling, and close a gate that could pass having verified nothingAug 14, 2026
JC-000 added a commit that referenced this pull request Aug 14, 2026
fix(package): close the vacuous pass in the release gate (#98 landed only 1 of 4 commits)
Sign up for freeto 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

@JC-000