The job map cannot be asked whether a given source was exercised
The incident, and it is the instrument's own user falling for it
Checking whether test/test_record_class_var_fail.pas had run in full tier
5b5fdb0b32d3:
>>> [k for k in jobs if 'record_class_var' in k]
[]
Zero. And the file existed in that tree (git ls-tree 5b5fdb0b32d3 test/
confirms it), so the honest readings were "it did not run" and "it ran under
another name", which are not the same statement.
It ran. It is the fourth compile line of
test-core#src:test/strict_fpc_case_fail.pas, a job group of seven-plus
sources named after the first one. The job is the tier's single red, so not
only was the source covered — it was the thing that failed.
The reader who hit this had, minutes earlier, reported reading a result "off
the job map rather than from the report's absence" as a point of discipline.
That reading was sound only because c_crtl_wait.c happens to own a job key.
The method was never checked for the property it depends on.
The measurement
At 5b5fdb0b32d3, seven, full tier:
| jobs in the map | 4255 |
...keyed by a source (#src:) |
4193 |
test/ sources in the tree |
3264 |
| ...with no job key of their own | 384 (~11.8%) |
For roughly one source in eight, absence from the job map carries no information about coverage. The query does not error and returns the same empty list it would return for a source that genuinely never ran.
Why this is not the ticket that is already closed
bug-t-a-job-named-after-its-first-source-file-cannot-name-its-failing-step
(done/) is about the job's inability to say WHICH of its lines died — the
producer side, and Job.script()'s step-marker file fixed it.
This is the consumer side: a reader with a source in hand cannot ask the published map whether it was exercised. The step marker does not help, because the reader does not have the job name to look the marker up by. Same root naming decision, opposite direction, and not covered.
Routes
- Publish the group membership. The report already renders
— test/a.pas test/b.pas +5, so testmgr knows the full list; the state'sjobsmap keeps only the key. A siblingjob_sources: {job: [srcs]}would make the question answerable with no change to job identity. - A helper, so nobody hand-greps.
twatch.py --covered <path>returning the job and its status, or an explicitNOT COVERED— the third state, which is the whole point. - Not renaming jobs. Job identity is load-bearing for regression ranges and
ticket citations, and
bug-t-a-job-named-after-its-first-source-file...already ruled that out for good reasons.
Route 1 is the cheap one and route 2 is what makes it usable. Both are strictly additive.
The same mechanism at three scales, on one night
This is not a tstate quirk. Measured 2026-09-05, three instances, three granularities, one mechanism — a name that aggregates hides the ARITY of what it names, so a second cause inside the first is not merely possible, it is ENCOURAGED:
- the job group: 7+ sources under
test-core#src:test/strict_fpc_case_fail.pas, one status.test_record_class_var_failwas a distinct defect inside it. - the eight-red batch:
unknown type: TMethodacross eight jobs read as one cause. frankH separatedtest_record_class_var_failfrom it by compiling under the v404 pin (2d6bfadd6a verified ancestor) rather than letting it be absorbed — the only reason the two are known to be different. - the unit count: 28-vs-24 earlier the same night, the same shape at a third granularity.
In all three the aggregate reported truthfully about ITSELF and silently about its members. Ask an aggregate how many things it is before treating its answer as one thing.
The rule
A map keyed by one member of a group answers about the KEY, never about membership. An empty result means "no job is NAMED that", which is a statement about naming, not about coverage — and it looks exactly like the answer you were hoping to rule out.
2026-09-05 (frankZ) — two live instances in one night's triage, with costs
Filed as a gap in what the map can ANSWER. Both of these are the map giving a confident WRONG answer during live tier triage, which is the stronger case.
Instance 1 — one key, six unrelated targets. COMPILER_SRCHASH is a make
variable at the head of many recipes, so extract_src picked
tools/compiler_srchash.sh as the source identity for six different targets:
sqlite-threads x4, test-uforth, test-emit-obj. The report's near: text for all
six was the self-host fixedpoint: verified line PRECEDING the failure rather
than the failure. Reading the tier as "six jobs share a cause" was the obvious
inference and it was wrong — six distinct targets, six distinct causes. Cost:
the tier's denominator was unknown until it was settled by hand.
Instance 2 — the SAME 13 jobs under two identities, and this one nearly bought
a six-week bisect. seven.json carries both:
test-uforth#src:compiler/[email protected] status=absent last_pass=cc411ceee30b
test-uforth#src:tools/[email protected] status=fail last_pass=b8e3b3010249
The recipe changed, extract_src's answer changed with it, and the job's history
split across two keys — the old ones stranded as absent, the new ones
carrying the record. Nothing errors. Both key families look like real jobs.
The cost was concrete: reading the stranded family made test-uforth look like
it had never run, which combined with a (separately wrong) inference to suggest
the regression could be anywhere back to 2026-07-21 — a ~6.5 week window. The
correct window is b8e3b3010249..5b5fdb0b32d3, 234 commits, established
instead by reading the report itself (tier: full, skips: 1, zero uforth
mentions → it ran and passed).
WHAT WOULD HAVE PREVENTED BOTH: a job key that is stable under recipe edits, and a map that can be asked "was source X exercised in run Y" rather than "what is the status of key K". Instance 1 is the key being too COARSE (six targets collapse onto one name); instance 2 is the key being too BRITTLE (one job's history splits when its recipe is touched). Same root: the key is derived from the recipe's text rather than from the job's subject, so it inherits every property of the text including its instability.
Raising prio: filed on a plausible gap, now carries two measured failures with attributable cost in one triage session.
2026-09-06 — the BRITTLE half, where @N is the only thing naming the ABI
Measured at 874e55d0b (compiler/pascal26 = 6cd631730cf470e8, srchash
426da166f0f1e3b3, matching the tree) while clearing
test-xtensa#src:test/test_cross_record.pas@3.
test/test_cross_record.pas appears six times inside the single test-xtensa
target, and the occurrences are not repetitions — they are three different
ABIs, each with its x64 control:
| occ | Makefile | what it actually is |
|---|---|---|
| 1 / 2 | 22954 / 22955 | default (Call0) ABI + control |
| 3 / 4 | 23385 / 23386 | --xtensa-abi=windowed + control |
| 5 / 6 | 23518 / 23520 | the movsp decode probe, windowed vs Call0 |
So for this job the occurrence index is the only thing that names the
subject. @3 does not mean "the third time we happened to mention this file";
it means windowed ABI, and nothing in the key says so.
The failure mode this adds is silent REPOINTING. The two costs already
recorded here are a key standing for six unrelated targets (too coarse) and a
history split across two keys when a recipe changed (too brittle in the
forward direction — the key moves and the job looks new). This is the
backward direction and it is worse, because nothing looks new at all: insert
or delete one earlier compile line for this source and @3 keeps resolving,
keeps naming a real row, and now names Call0 instead of windowed. The
verdict history stays attached to a key whose subject changed underneath it.
No error, no gap, no split — the archive simply starts describing a different
ABI under the old name.
@N over recipe text is an index into a list nobody promised to keep stable,
used as the identity of the thing at that index. The subject here is
(target, abi, source), and all three are recoverable from the recipe that
already exists.
Status of the row that produced this
Not reproducible at 874e55d0b. All six occurrences measured green, including
the movsp pair with its positive control asserted and branched on
(windowed 14 movsp, Call0 0, both -d in_asm logs non-empty at 3689 / 3987
lines). Value and rc slots both green on occurrences 1-4.
This is an exculpation, so it names its owner for the residual question: I
cannot say whether the row was ever red, because tstate/ is not in this
checkout (it lives on seven) and the key reached me through a report, not a
run of my own. Whoever holds the tier verdict owns "was it red, and what fixed
it" — from here the only defensible claim is that it is green now, at that
tree, with that binary.
2026-09-06, later — the mechanism above is WRONG, and the true one is worse
Kept rather than repaired, because a corrected claim with no history reads as a current-looking assertion nobody will check.
@N does not index occurrences of the source in the Makefile. It indexes
jobs whose first source is that file. Three test-xtensa jobs qualify:
test-xtensa#84 qemu 3 lines test/test_cross_record.pas tools/expect_same.sh +1
test-xtensa#138 qemu 6 lines test/test_cross_record.pas tools/run_target.sh +1
test-xtensa#147 selfhost 115 lines test/test_cross_record.pas tools/expect_same.sh +4
The report's +4 names #147. The section above reasoned from six Makefile
occurrences and concluded @3 meant --xtensa-abi=windowed; @3 is #147,
a 115-line selfhost job.
Why the true mechanism is worse than the one first filed here. A Makefile
occurrence is at least countable — wrong, but checkable by reading the file
the key appears to describe. The job list is produced by the harness's own
recipe grouping, is not visible anywhere in the Makefile, and cannot be
enumerated without running testmgr --list. So the key indexes a list that
does not exist in the artefact it names, and the only way to resolve it is to
ask the tool that generated it.
Measured cost, and it was this section's own author. I read @3 as
occurrence 3, measured #138 (green, and still green), and published a "not
reproducible" exculpation for #147 — which was genuinely red, and had been
since f49c0e11f at 2026-09-05 19:48. Fixed at 0a96caf54.
The discriminator was in the report I was reading. Its truncated log carried
code=491372B / 446316B / 122648B; every program I built came out 196460B.
Nothing errored and nothing was hidden. A discriminator being present is not
the same as a discriminator being consulted — and a session reading its own
job's report is the least likely reader to notice a size column, because the
size is not what it came for.
An exculpation is the class that never gets revisited. A green nobody re-checks and a "not reproducible" nobody re-runs are the same object: a verdict that stops work. This one was caught only because a peer asked an unrelated question about a different ticket and the answer required opening the archive.
2026-09-06 (frank-coordinator) — the LIVE blast radius is 5 open tickets, and one of them is this one
Asked after frankZ's b0d7cd10e whether @N being systematically misread affects every
ticket citing a job by index. Measured rather than assumed, across the ranked and backlog
folders only (done/ is history and tstate/ is the archive):
- 5 open tickets cite a job key of the form
<job>#src:<path>@N. - 21 distinct keys between them.
- One of the five is this ticket.
backlog/regression-cascade-6758c7ce7dbd.md
backlog/regression-cascade-b8e3b3010249.md
backlog-core/bug-a-emit-obj-retains-pxxassert-so-one-ansistring-in-it-imports-the-whole-esp-pal.md
backlog-core/feature-a-a-refusal-is-a-claim-with-a-date-on-it.md
backlog-tools/bug-t-the-job-map-cannot-be-asked-whether-a-given-source-was-exercised.md
So the defect is systematic and the live cost is bounded. 128 further citations sit in
done/ and are records of what a past session read, not instructions — CLAUDE.md's precedence
rule already says not to repair those, and repairing them would date a claim that should stay
dated.
The pathological shape is visible in the key list, and it is worse than a generic source
Thirteen of the twenty-one are one file:
test-uforth#src:tools/compiler_srchash.sh@1 .. @13
compiler_srchash.sh is the FIRST SOURCE of thirteen different jobs, so for that file
@N carries no information except position in a list the reader cannot enumerate — the
job grouping is the harness's recipe grouping, invisible in the Makefile and unobtainable
without testmgr --list. Compare test-xtensa#src:test/test_cross_record.pas@3, where three
jobs share the source and the three are at least distinguishable by ABI once you know they
exist.
A key whose disambiguating component is a position in an unenumerable list is not ambiguous — it is unresolvable by hand. That is the sharpened form frankZ arrived at, and
compiler_srchash.shis the instance that shows the ceiling: thirteen rows, one name, and nothing a reader can do with the number.
This does not change the recommended fix — subject is (target, abi, source) and all
three are already in the recipe. It bounds the migration: 5 open tickets to re-key, not a
board-wide sweep.
2026-09-06 (frank-coordinator) — THE PROPOSED SUBJECT (target, abi, source) IS INSUFFICIENT, measured against the job table
frankuser asked the right question before anyone started the migration: do the thirteen jobs
sharing tools/compiler_srchash.sh differ in target and abi, or do some collide on all three?
Some collide, and by a wide margin. Measured with PXX_ALLOW_FULL_SUITE=1 testmgr.py --tier full --list — a print-and-exit that runs nothing; the guardrail is a speed limit and this is
the one lookup that cannot be done any other way.
68 jobs have tools/compiler_srchash.sh as their FIRST source, across 31 prefixes:
13 test-uforth#corpus #core #coreplustest #doubletest #exceptiontest #facilitytest
#localstest #memorytest #searchordertest #stringtest #coreexttest #toolstest #filetest
6 test-c-conformance 6 -i386 6 -arm32 6 -aarch64 6 -riscv32
1 each of 26 others (test-core, test-smoke, test-asm, test-nilpy, test-zlib, …)
The thirteen
test-uforthjobs share target AND abi AND first source. They differ only by corpus suite — which is not target, not abi, and not a source.(target, abi, source)collapses all thirteen into one key, and eachtest-c-conformancegroup of six likewise. The scheme fails at exactly the worst case the ticket's own key list points at.
And the harness ALREADY assigns a unique key per job — the citation form throws it away
--list prints 4312 jobs, every one uniquely keyed, and not one of them uses the
#src:<path>@N form:
test-xtensa#84 qemu 3 lines test/test_cross_record.pas tools/expect_same.sh +1
test-xtensa#138 qemu 6 lines test/test_cross_record.pas tools/run_target.sh +1
test-xtensa#147 selfhost 136 lines test/test_cross_record.pas tools/expect_same.sh +4
So #src:…@N is a report-side rendering, not the harness's key, and it discards
information that already exists one command away. The three xtensa jobs are separated by
job TYPE and by their FULL source list — qemu/qemu/selfhost, expect_same/
run_target/expect_same +4 — neither of which the @N form carries, and neither of which
is abi.
The residual, which is why this is a specification and not yet a fix
4259 of the 4312 keys are NUMERIC (test-xtensa#147) and only 49 are NAMED
(test-uforth#stringtest). The named ones are stable and meaningful. The numeric ones are
brittle-FORWARD: insert a job earlier in the target and every later index shifts, carrying
its verdict history onto a different subject — the same failure as @N, one level up, in the
harness's own key rather than in the report's rendering.
So there is no tuple of (target, abi, source) that works, and there does not need to be: the job already HAS an identity. What it lacks is a STABLE one. The fix is to make the harness's key name the job (as
test-uforth#<suite>already does for 49 of them) and to cite that key, rather than to derive a new subject from the recipe's contents.
Bounds unchanged: 5 open tickets to re-cite. The done/ and tstate/ citations stay as
they are.
2026-09-06 — THE NUMERIC KEY IS POSITIONAL AND IT DRIFTS ~25 A DAY. Measured twice, and the first reading was an inference
frankuser refused the previous section's claim as stated — "insert a job earlier and every later index shifts" was read off the shape of a numeric key, not measured, and a numeric key can equally be a stable id assigned at registration. Different claims, and the second is how a hazard gets ranked without ever being real. Both readings now exist and they fail differently.
Reading 1 — the code. Cited by SYMBOL, not by line, for the reason below:
tools/testmgr.py,split_jobs(target, lines)— its last statement isfor i, g in enumerate(groups): jobs.append(Job(target, i, g)).tools/testmgr.py,class Job.__init__(self, target, index, lines)— setsself.name = "%s#%02d" % (target, index).
groups is the recipe's line-groups in recipe order: split_jobs runs a union-find over the
temp files each group produces and consumes, then rebuilds groups = [buckets[k] for k in sorted(buckets)], sorted by the representative — the lowest line index in each merged group.
So the index is a POSITION in the Makefile, not a registered id and not derived from
content.
split_jobs contains three for i, g in enumerate(groups) loops. Only the last builds
Jobs; the other two are the union-find owner map and the bucket merge, and they are what
determine the final ORDER — so they are the same mechanism rather than siblings that a fix
must also touch.
THE LINE NUMBERS IN THE FIRST VERSION OF THIS SECTION WERE CORRECT AND WERE ALSO CORRECTED, AND BOTH READINGS WERE RIGHT
This section originally cited 2913 / 2914 / 1960. A reviewer corrected them to
2634 / 1673 / 1680 and noted, accurately, that 2913 lands mid-docstring — "the same
failure this repo already booked when three Makefile:<n> citations drifted and one landed on
fi; \."
Both sets are right, at different trees. 324304f1e (2026-09-05 22:04) added 281 lines
to testmgr.py; at 324304f1e^ the name line is at 1680, and at HEAD it is at 1960.
The reviewer's checkout was one commit behind.
A line citation drifted by 281 inside twenty-four hours, and the correction to it drifted by the same 281 in the other direction. The rule this repo already carries — cite the RECIPE or the SYMBOL, never
file:line— was demonstrated by the act of enforcing it. Neither reader was careless; a line number is simply not a fact about a file, it is a fact about a file at a tree, and nothing in the citation says which.
Reading 2 — the archive, which fails differently and gives a RATE. The rdrand skip is one
job with an unmistakable reason string, present in every full report from seven. Following
that one job:
20260829 #895..#915 (52 reports) 20260902 #989..#1018 (113)
20260830 #915..#948 (183) 20260903 #1018..#1035 (48)
20260831 #948..#963 (149) 20260904 #1035..#1063 (68)
20260901 #963..#989 (162) 20260905 #1090..#1113 (35)
20260906 #1113..#1132 (18)
One job. 131 distinct keys in nine days. It moved twenty places inside 2026-08-29 alone. A
test-core#Ncited in the morning names a different job by the evening.
And the mis-attribution is in hand, not hypothesised. test-core#895 was the rdrand job
on 2026-08-29. Today --list says:
test-core#895 unit 2 lines test/cvararg_double_b83.c tools/expect_same.sh
A C vararg test. Same key, different subject, eight days apart, no error, no gap, no
split — the same silent repointing this ticket already records for @N, in the harness's own
key, across 4259 of 4312 rows rather than 21.
AND THE TWO HALVES INTERACT — do not "fix" @N first
frankuser's second point, and it inverts the obvious repair order:
The
#src:<path>@Nrendering carries the SOURCE PATH, and that is the only thing that lets a historical verdict be re-identified independently of the index. If@Nis retired in favour of the numeric key, the archive loses its cross-check — and the numeric key is the unstable one.
So the rendering that looks like the defect is currently the only stable handle in a report.
Whoever does the naming work must read both halves together: name the jobs first (as
test-uforth#<suite> already does for 49 of them), and only then consider what the report
should render. Retiring @N before the keys are stable would make 1091 archived reports
unresolvable.
This is measured on test-core, one target, one job. The drift rate on other targets is
not measured and is not extrapolated here.