The duplicate-expectation ratchet is .npy-only, and the first escape was a .pas test
What happened
bug-t-89-nilpy-expectations-are-duplicated-across-two-targets-with-nothing-keeping-them-in-sync
(done, 2026-08-30) built exactly the right guard for exactly the wrong population.
tools/npy_cross_target_expectation_devtest.py ends its scan with
MULTI = {s: t for s, t in BY_SOURCE.items() if s.endswith(".npy") and len(t) > 1}
while the scanner feeding it already matches all three source kinds:
COMPILE_RE = re.compile(r"\$\(COMPILER\)\s.*?(test/[A-Za-z0-9_./-]+\.(?:npy|pas|c))\s+...")
The data was in the instrument and one predicate threw it away.
Same day, same Makefile block, one file extension over. 10c869750 added
reord/reord2 rows to test/test_opt_store_reload.pas and updated the
test-nilpy copy of its expectation (labels .1/.2, Makefile ~line 851). The
test-core copy (labels .3/.4, ~line 11215) kept the short golden, and
test-core went red on plexus and seven, stayed red from 06:57Z to 08:45Z, was
auto-filed as a p70 regression, mistracked to P, retracked to A, and dispatched
to a session — for two missing lines of printf. That is the exact cost curve
the original ticket predicted, arriving before the ink dried.
The escape is worth naming precisely, because it is not "we forgot .pas":
The ratchet was scoped to the population that had been measured (111
.npysources, 0 drift) rather than to the mechanism (a Makefile with two copies of one expectation thousands of lines apart). The mechanism does not read file extensions.
Why the naive widening is wrong, with numbers
Measured at 780ec9f7c + this fix. Dropping the .npy filter and keeping the
guard's own source-keying:
| population | multi-target sources | divergent |
|---|---|---|
.npy |
111 | 0 |
.pas |
176 | 144 |
.c |
7 | 6 |
144 findings on day one is the report that teaches everyone to scroll past it — the failure mode the original ticket explicitly refused. Almost all of it is legitimate: a cross-target row compares a qemu run against the x64 binary, so the two targets' payloads must differ.
The sub-population that is actually ratchetable
Key on (source, compile invocation with the $(TESTTMP) output name normalised
out), and keep only blocks where neither the compile line nor any payload names
a cross target (--target, run_target.sh, qemu, wine):
| count | |
|---|---|
| native, >1 target, identical compile line | 137 (111 .npy, 25 .pas, 1 .c) |
| divergent expectations | 15 |
And all 15 are deliberate asymmetry, not drift — one target asserts a weaker form of the same thing:
test/test_pyeval_m1.pas [test-core test-nilpy]
test-core "$$(tail -1 .../OUT.log) $$(grep -c '^ok ' .../OUT.log)" "ALL PASS 23"
test-nilpy "$$(.../OUT | tail -1)" "ALL PASS"
test/test_tthread_sync.pas [test-quick test-threads]
test-quick "$$(.../OUT | tail -1)" "TTHREAD SYNC OK"
test-threads "$$(.../OUT)" "$$(printf 'sync=200 ...\nTTHREAD SYNC OK')"
(12 test_pyeval_*.pas of one shape, test_mutex/test_tthread_sync of another,
csystem_libs_granular_math_b112.c of a third.) So the shape is the one the
existing devtest already uses for KNOWN_NAME_COLLISIONS: ratchet 122, freeze
15 with the reason written down. test_opt_store_reload.pas was in the 137 and
divergent before this fix; it is not after it, which is the check that the key
selects the right thing.
Suggested work
- Replace the
.npyfilter with the native/identical-invocation key above. - Freeze the 15 in a
KNOWN_ASYMMETRIC_EXPECTATIONSdict — value = the reason, not just the name, so the sixteenth cannot be added without someone reading why the fifteen are there. - Keep
t_the_population_is_still_there()'s instrument-guard and raise its floor to the new population, for the same reason it exists: a scan that silently stops matching reports zero drift forever.
Numbers above are reproducible from the Makefile alone (no build), so this is cheap to re-measure before starting — do that rather than trusting the table, the population moves with every added row.
Not proposed: de-duplicating the expectations into shared variables. That is the
same decide-* the original ticket declined to fold in, and it is still a design
call about whether test-core should re-run these sets at all.
Gate
Track T's own full-tier sweep, green. The devtest itself runs in milliseconds against the Makefile and needs no compiler.
Aside, free with this ticket
.claude/hooks/no-full-suite.sh matches on command TEXT, so writing this ticket
via a bash heredoc was refused for quoting T's own gate command inside prose. The
hook cannot distinguish running a command from writing about one. Harmless here
(the file went in through the Write tool) but it will keep firing on anyone
documenting the gate, which is exactly the population you want writing it down.
Deprioritised 2026-09-02 — the Track T tooling backlog was cut as a pile
This ticket is not being called wrong. It was moved as part of a pile, not judged individually, and nothing here disputes its finding.
Owner decision. 73 of the 74 open track: T tickets were filed between
2026-08-31 and 2026-09-02, 58 on one day. The pile was too large to work through
and returned almost nothing, and a ticket nobody will fix does not sit neutrally
— it stays in the ranker forever at zero value, which is the argument CLAUDE.md
already makes for a terminal folder over a low prio.
Four were kept in the ranker on a purely structural test — an active umbrella or
a hard blocked-by: edge from live work:
umbrella-one-full-tier-run-with-no-red-tier,
feature-t-freebsd-image-and-runner, and the two regression-test-core-* reds
that block the umbrella.
Kept, not deleted, for two reasons: so the finding is not rediscovered and refiled from scratch by the next agent who trips over it, and so it can be pulled back if what it touches becomes load-bearing.
To revive it: move it to the owning lane's backlog, set status: backlog,
and say in the ticket WHAT CHANGED to make it matter now. Restoring it because it
reads well is how the pile comes back.