← board

Why this exists and what it replaces

Track P closed roughly twenty bugs tonight, ranked by ready --track P. That ranking is a backlog order, not a usage order. This umbrella is the instrument that produces a usage order: point pxx at FPC's source, and the first failure is by definition the thing most in the way.

It does not replace the P backlog — it re-ranks it. Tickets the attempt hits inherit this umbrella's prio through effective_prio; tickets it never reaches were, by measurement, not blocking real-world Pascal.

Notation — ONE spelling for the corpus totals, and it is a guard

Corpus totals are written BOTH-OK / ORACLE-NO / PXX-FAIL as N / N / N, everywhere in this file, including in prose and in the summary: field. Two earlier spellings (21 BOTH-OK, 10 ORACLE-NO, 176 PXX-FAIL and the same with · separators) were normalised to it on 2026-09-16 without changing a number.

This is not tidiness; it is the only guard that would have caught the wall-13/14 error. From b1b61eeff (2026-09-16) this document stated the totals both ways at once — the summary: field said 21 BOTH-OK, 10 ORACLE-NO, 176 PXX-FAIL and the wall-13 table said 22 / 10 / 175 — and the two notations meant no grep could ever put them side by side. git log -S says it outright: -S'21 BOTH-OK' reaches back to 7f8d00b0d, while -S'21 / 10 / 176' returns only the 2026-09-16 correction as the oldest commit introducing it. Read literally, the second says the right answer had never been written down and was invented that day; the first says it had been there all along. Both are true about their own spelling and neither is true about the quantity. The record was not stale and was not silent — it held the right answer and the wrong one simultaneously, and every reader who looked at one region saw something internally coherent.

That is this repo's wrong-population rule arriving in a document rather than in an instrument: a search for a NAME enumerates one spelling, and a quantity with two spellings cannot be checked against itself. So: one notation, and when you record the totals, record all three numbers and read them off the probe's own SUMMARY line. Never derive one from an adjacent row — 207 − 22 − 10 = 175 is exactly how the wrong pair got in, and the arithmetic checking out is what kept anyone from re-deriving it.

(The one place the old spelling still appears in this file is the quotation two paragraphs up, so a grep for it counts this note — the observer inside the namespace it scans, one level up from the case the note is about. Expect one hit; a second is a real one.)

The check, with its baseline, so a later reader knows what clean looks like: grep -o '[0-9]* / [0-9]* / [0-9]*' <this file> | sort | uniq -c. That command could not be written at all while the totals had two spellings, which is the entire point.

AND ITS BASELINE HAD ROTTED BY THE NEXT DAY — re-measured 2026-09-17 (frankB) and recorded as ERAS, not as a corrected/superseded PAIR. The original baseline here named two counts, thirteen and five, and read them as the right triple against the wrong one. Walls sixteen and seventeen then moved the totals legitimately, and a third era arrived, so the pair framing no longer describes the file: today the command answers eighteen, twelve and one across three distinct triples, and none of that is an error. A reader obeying the old sentence would hunt a live mistake that does not exist — the born-red failure the note one paragraph up warns about, in the note's own baseline, one day later. The invariant that survives is the NOTATION, not any count: every triple in this file is N / N / N, each is true of the tree it was measured at, and the era is named beside it. Counts are spelled in words here so this sentence cannot match the pattern it describes.

Those two counts are spelled as WORDS deliberately, and the reason is that the first version of this sentence spelled them as digits and was BORN RED. It quoted one instance of each triple, so it was a member of the population it was counting: the documented command answered fourteen and six while the sentence claimed thirteen and five, and it could never have passed for any reader after the commit that wrote it. A later reader would have run it, got the extra hit, applied the criterion one line above, and gone hunting for a live wrong claim that does not exist -- and a guard that cries wolf on its first outside run teaches the reader to ignore it, which is worse than having no guard. Caught by frankuser running the check instead of reading it, 2026-09-16. Note the depth: the paragraph above already applies the observer-in-the-namespace rule to the QUOTATION, and missed it on the BASELINE NUMBERS, which were measured before the sentence stating them existed. Words take the sentence out of the set, so the command answers as documented for every later reader and keeps answering that way if someone re-words this paragraph -- the same move as putting the convention at the top of the file rather than beside the tables: change the structure so the failure cannot recur, instead of correcting the instance.

The target

FPC's compiler source, as shipped. Do not vendor it, do not reduce it, do not fix it — this is a corpus, and the cheat licence that applies to lekkerzeilen explicitly does not apply here: FPC is not ours, and bending it would destroy what the measurement is for.

How to grow this (the only supported method)

  1. Point compiler/pascal26 at a translation unit of FPC's source.
  2. Record the FIRST failure. Reduce it to a minimal repro before filing — a first-error reading is a LOWER BOUND, not a work estimate, and tonight produced three cases where reducing changed what the bug WAS.
  3. File it in backlog-pascal, wire it here with blocked-by, move to the next unit.
  4. Say in each ticket which FPC unit and line produced it.

Do not triage the existing P backlog into this umbrella. If an existing ticket turns out to be what the attempt hit, wire that one — membership is an edge, not a folder, and one ticket can sit under several umbrellas.

Two things measured 2026-09-09 that set expectations

Sibling targets, same mode

umbrella-compile-and-run-dosbox (prio 50, zero blockers — nobody has attempted it) and the busybox family, which has eleven open tickets across five folders and no umbrella of its own. Both are the same instruction: attempt the target, let the failures rank themselves.

Attempts

2026-09-09, frankH — attempt 1: uses cutils

First failure: constexp.pas:164, undefined variable (internalerrorproc). Reduced to 8+9+4 lines and filed as [[bug-p-a-unit-cycle-closed-through-an-implementation-uses-cannot-see-the-other-interface]] (p60). A unit cycle closed through an implementation uses clause cannot see the other unit's interface. 25 of 162 units in fpc-trunk/compiler close such a cycle, so this is structural in the corpus, not incidental — and nothing behind it can be measured until it moves.

Pre-existing, not a regression: pin v399 gives the identical error.

2026-09-09 — the owner's framing, which bounds this umbrella

"the challenge is just to compile FPC as a proof of pudding. we don't target any advanced compatibility. i can see what FPC is doing, sortof. we don't care. FPC is a great compiler and we have other goals, the common thing is pascal and that we sayd we target FPC's dialect as de-facto standard."

This is a PROOF, not a compatibility programme, and the distinction is the one most likely to be lost by whoever attempts it. Pointing pxx at 400k lines of another compiler's source will surface a great many differences. Almost none of them are ours.

A finding belongs under this umbrella only if it stops correct Pascal compiling or running. Not because FPC does it differently, not because a diagnostic differs, not because an intermediate has a different type. If the source compiles and the program behaves, there is nothing to file — and the temptation to file it anyway is exactly what turned 5035 tickets into 467 open ones.

Binary interop with FPC is NOT a goal — settled the same day in decide-how-a-hand-built-com-interface-becomes-callable, now in done/. Do not rank anything here on exchanging objects with FPC-compiled code, sharing its representations, or linking against its output.

2026-09-09, frankH — attempt 2: THE WHOLE CORPUS, and the invocation was wrong

READ THIS BEFORE ANY NUMBER IN ATTEMPT 1. Attempt 1 was invoked as pascal26 -Mobjfpc -Fu<compiler> -Fi<compiler>, and that is not how FPC's compiler is built. Under it, globtype.pas:115 unknown type: PInt looked like a frontend bug and was on its way to being filed. It is not one: PInt is declared only inside {$ifdef cpu64bitaddr} arms, fpcdefs.inc derives that from the CPU define, and FPC's own Makefile.fpc:381 passes -d$(CPC_TARGET). pxx already has the flag for exactly this--mimic-fpc-compiler (feature-mimic-fpc-compiler-define-profile), which supplies the FPC identity defines plus the one CPU define fpcdefs.inc derives the other forty from.

The corpus-attempt rule this produces, and it is the transferable half: compile the corpus the way its own BUILD compiles it, and prove it by running that build's compiler as an ORACLE on the same invocation. Four of the ten first failures in attempt 1 were mine, not pxx's, and one of them (ccharset) fpc itself refuses under that invocation — a unit the oracle cannot compile can never be evidence about us. Every row below is therefore fpc first, pxx second, same flags:

fpc      -Mobjfpc -dx86_64 -Fu{C} -Fu{C}/x86_64 -Fu{C}/systems -Fu{C}/x86 \
                            -Fi{C} -Fi{C}/x86_64 -Fi{C}/x86
pascal26 -Mobjfpc --mimic-fpc-compiler   <the same -Fu/-Fi set>

Driver: one program d; uses <unit>; begin end. per unit, all 207 .pas in fpc-trunk/compiler. Script committed as tools/fpc_compiler_corpus_probe.sh, so the method is runnable rather than described; it refuses to run at all when fpc is absent, because without the oracle a PXX-FAIL row is not evidence.

The distribution, measured at 24dbb0b37

units 207
compile under both 9 — compinnr dbgdwarfconst dwarfbase fpchash globtype macho symconst version wasmbase
oracle refuses (not evidence about us) 10
pxx stops 188

First failures, by count. These are first-failure counts and therefore LOWER BOUNDS on nothing and UPPER BOUNDS on nothing — a unit that stops on the cycle may stop on the next cause after it moves, and 144 units clearing does not mean 144 units compiling. What the count IS good for is ORDER.

n first failure ticket
144 undefined variable (internalerrorproc) [[bug-p-a-unit-cycle-closed-through-an-implementation-uses-cannot-see-the-other-interface]]
19 {$if} over a source const or a source type alias [[bug-p-a-conditional-directive-cannot-read-a-constant-or-a-type-the-source-declares]]
8 an object type cannot have a constructor [[feature-p-legacy-value-object-types]]
3 undefined variable (bitsizeof) none — see below
2 expected field name in record constant none yet
3 uses: unit source not found (unixcp, heaptrc, charset) RTL units, not frontend
2 conditional directive: malformed expression / expected operator none yet
1 each undefined variable (align), undefined variable (IsATTY) none yet

What moved this session, attributed rather than assumed

Two fixes landed. 588f18717 folds FPC's Sar* intrinsics; 24dbb0b37 lets a subrange bound be a folded call (low(TCGLoc)..pred(LOC_CREFERENCE), cgbase.pas:63). Re-running the whole probe before and after the second one, the diff is exactly five rows and no others: aasmcfi, cgbase, nbas, ncgmem and ncgnstmm move from cgbase.pas:63 unknown type: low to cgbase.pas:381 an object type cannot have a constructor, 318 lines further into the same file. No unit newly compiles, and saying that is the point: a corpus delta is easy to quote as progress and the honest unit of progress here is a wall, not a line number.

bitsizeof, banked rather than filed, with the whole answer

bitsizeof(x) is an FPC intrinsic like Sar*compinnr.pas:85, in_bitsizeof_x — and it stops constexp, cgobj and ppu. It is exactly SizeOf(x) * 8 for every type pxx can express, and that is measured, not assumed: fpc answers 32/64/32/8 for bitsizeof of a LongInt, an Int64, the LongInt type name and Byte, and the one place the two would differ is a bitpacked field (fpc says 2 for a: 0..3), which pxx has no bitpacked at all to reach — type T = bitpacked record is unknown type: bitpacked. So the desugar has no wrong answer available today and WILL when bitpacked lands; whoever writes it should say so in the comment.

It is not a one-line fix like Sar* for one reason worth recording: SizeOf's arm in pasparser_expr.inc runs ~700 lines with six separate AllocNode(AN_INT_LIT) exits, so * 8 is six edits, which is the shape this repo keeps finding a missed copy in. The honest version unifies those exits first.

What the attempt says about the two big walls

The cycle is not merely the largest, it is 77% of the corpus, and it is structural: FPC splits uses into two clauses precisely so units can be mutually recursive. Nothing behind it can be measured. The conditional-directive family is second and is the one that is shaped like work already done here — two forwarded questions of exactly its kind already exist beside it.

2026-09-09, frankH — attempt 3: the conditional-directive family, and what it uncovered

Two commits, abc681636 (read a source const, follow a type ALIAS through sizeof) and fcbe280b7 (the two probe defects underneath it). The whole probe was re-run before and after each, so both deltas below are per-unit diffs and not category arithmetic.

24dbb0b37 abc681636 fcbe280b7
compile under both 9 9 9
oracle refuses 10 10 10
pxx stops 188 188 188
— of those, the conditional-directive family 26 3 3
— of those, the unit cycle 144 155 158

NO UNIT NEWLY COMPILES, across both commits. That is the third time this umbrella has had to say it and it stays the honest unit of progress: a corpus delta is a wall moving, not a program building, and everything behind the 26 rows was standing in the unit cycle's queue.

abc681636 — 25 units changed and every one of them was a conditional-directive first failure. No row moved that was not in that family, which is what an attributed delta looks like. 11 went to the unit cycle, 5 to ALU not defined (a wall further into the same files), 5 to Unsupported tcompilerwidechar size, 2 to bitsizeof, 1 to unterminated conditional directive and 1 (ncnv) from one conditional row to another.

fcbe280b7 — 12 units changed, all forward. ALU not defined (5 units) and unterminated conditional directive (1) cleared entirely.

The two probe defects, because they are the transferable half

Neither was new with the const/alias questions; both are older and {$if declared(X)} had the first one first.

Unsupported tcompilerwidechar size is the charset row, not a frontend bug

7 units stop there and they are ONE cause, measured rather than inferred: ncon.pas:968 is FPC's own {$error} in the {$else} arm of {$if sizeof(tcompilerwidechar) = 2}; tcompilerwidechar = word lives in widestr.pas:35; widestr uses charset, which is an FPC RTL unit and is not on the probe's unit path. Two controls:

So these 7 belong on the same row as uses: unit source not found: charset, and the next measurable step for them is not the frontend: it is whether pxx compiles FPC's RTL at all. That is a different corpus and it should be decided as one before anybody starts adding System constants one error at a time.

What is left of the conditional-directive family: 3 units, 2 shapes

Filed as [[bug-p-a-conditional-directive-cannot-read-a-const-whose-value-is-not-an-integer-literal]] — set membership over a set-valued const (nld.pas:700, and ncnv reaches the same directive through uses nld) and a const whose value is a folded call (RS_INVALID = high(tsuperregister), cgbase.pas:400, asked by rgobj.pas:1728). Both are behind the unit cycle anyway.

make test-fpc IS NOT THIS TARGET, and it has never produced a verdict row

Two separate traps, and the second is the one that will catch a later attempt. test-fpc runs FPC's TEST SUITE; this umbrella compiles FPC's COMPILER. They share a name and nothing else — the suite is thousands of small conformance programs, the compiler is 207 mutually-recursive units, and a green on one says nothing about the other. And test-fpc is in NO TIER (frankuser, 2026-09-09, filed as bug-t-six-real-program-jobs-are-in-no-tier-so-they-never-run, T p65, with duktape, quickjs, chess-perft, sqlite-parity and wasm32): it is not skipped and not failing, no tier invokes it, so it has produced zero rows ever. A green from running it by hand is a claim about that one run on that one tree.

So: do not reach for make test-fpc to measure this umbrella, and do not read its absence from the tstate archive as a pass.

2026-09-10, frankH — attempt 4: the two biggest walls, and neither bought a unit

Two fixes, each with the WHOLE probe re-run before and after it, so both deltas below are per-unit joins and not category arithmetic.

590e7c100 6e8a821db (cycle) 7e4f69a34 (bitsizeof)
units 207 207 207
compile under both 9 9 measured below
oracle refuses 10 10
pxx stops 188 188
first failure 590e7c100 after the cycle fix
undefined variable (internalerrorproc) — the unit cycle 158 0
undefined variable (bitsizeof) 5 163
an object type cannot have a constructor 8 8
Unsupported tcompilerwidechar size 7 7
expected field name in record constant 2 2
conditional directive: expected operator 2 2
uses: unit source not found (unixcp, heaptrc, charset) 3 3
undefined variable (align) / (IsATTY) 1 each 1 each
RS_INVALID has no integer value 1 1

Exactly 158 units moved, every one of them from the cycle to bitsizeof, and nothing else moved at all. A join on the unit name says so.

The number to distrust here is the one that did NOT move

BOTH-OK stayed at 9 across the largest single fix this umbrella has had. 76% of the corpus was standing behind the unit cycle and the next wall was immediately behind it, so a 158-row delta bought no unit. That is the shape to expect from a corpus attempt and it is why this file reports walls rather than line numbers: a per-unit diff of 158 rows is easy to quote as progress, and the honest unit of progress is a wall.

bitsizeof was banked here with its whole answer and it held up

The banked note said SizeOf(x) * 8, no wrong answer reachable because pxx has no bitpacked, and not a one-liner only because SizeOf's arm has six AllocNode(AN_INT_LIT) exits — "the honest version unifies those exits first". All three were right. The exits are now one (EmitSizeOfResult) and the multiply exists once.

Banking the whole answer rather than filing a ticket was the right call for a five-line desugar behind a six-copy refactor, and it is worth saying why it worked: the note carried the MEASUREMENT (fpc's four answers), the BOUNDARY (bitpacked, and that it is unreachable) and the COST (six exits). A ticket saying "implement bitsizeof" would have carried none of them.

What the attempt says now

The corpus is no longer dominated by one structural wall. After 7e4f69a34 the distribution is measured in the row below this section; whatever it says, the next causes are all SMALL — the largest before this attempt was 8 — which means the next rung of this umbrella is a different kind of work from the last two: several unrelated causes rather than one that everything queues behind.

2026-09-10, frankH — attempt 5: the System routines, and a THIRD consecutive null row

Two more walls cleared and BOTH-OK did not move at all, for the third attempt running. That is the number this section is about.

What landed

sha wall units it was the first failure of
959468420 / 0dfa0b298 unknown type: PSizeUint 150
dbb96cdb6 undefined variable (IndexQWord) 96

Both are FPC System-unit surface, not dialect features — the same class as Prefetch, which is already in builtin.pas for exactly this reason. The Index/Compare family went in beside it: IndexByte, IndexWord, IndexDWord, IndexQWord, CompareWord, CompareDWord. TFPList.IndexOf (cclasses.pas:890-895) selects the DWord or QWord arm by pointer width under a {$if}, so both arms must resolve for either to compile — a family with holes in it is not half-working, it is not working.

The measurement

Whole probe re-run at each sha, fpc as oracle, per-unit join rather than category arithmetic.

probe #8 @ 0dfa0b298 probe #9 @ dbb96cdb6
BOTH-OK 15 15
ORACLE-NO 10 10
PXX-FAIL 182 182

Join: exactly 96 units changed their row, every one of them IndexQWord -> unaligned, and nothing else moved. The BOTH-OK set is byte-identical between the two runs, not merely the same size:

cdynset compinnr constexp cstreams cutils dbgdwarfconst dwarfbase fpchash
globtype macho optbase symconst systems version wasmbase

First failures now

n first failure
150 undefined variable (unaligned)cclasses.pas:1327
6 Unsupported tcompilerwidechar size
7 an object type cannot have a constructor (two line numbers)
4 expected field name in record constant
3 unknown type: d
1 each IsATTY, align, swapendian, unixcp, RS_INVALID

The number to distrust, again, is the one that did NOT move

Three attempts in a row have cleared the single largest wall and bought zero units: 158 units moved for the unit cycle, 96 for the Index family, and BOTH-OK went 9 -> 15 -> 15 -> 15. The 15 was bought by bitsizeof alone.

The reading is not "the fixes did nothing". It is that in this corpus the walls are STACKED, and the depth is what nobody has measured. Every unit that cleared IndexQWord landed on unaligned — the same file, four hundred lines further down. Clearing wall N reveals wall N+1 in the same dependency, so the first-failure histogram is a picture of ONE unit's contents (cclasses.pas, which nearly everything uses) far more than of the corpus's difficulty.

So stop ranking this umbrella's blockers by how many units name them. 150 units naming unaligned is one call site in one shared unit; it says almost nothing about how much work stands between here and a compiling corpus. The honest instrument for that question is a probe that reports EVERY failure in a unit rather than the first, and nobody has built one. Until then, the count is a queue position, not a size.

Filed from this attempt: [[feature-p-unaligned-is-a-transparent-lvalue-not-a-function]] — and note it is NOT a builtin.pas job like the last two looked: 275 call sites in the corpus and some are assignment targets, so a function-shaped fix clears all 150 units and still fails on ogomf, owomflib and entfile for the identical spelling.

2026-09-10, frankH — attempt 6: the prediction, tested the same hour it was written

b9c8fc160 landed unaligned. Probe #10, whole corpus re-run, per-unit join:

exactly 150 units changed their row, every one unaligned -> Finalize(x, n), nothing else moved, BOTH-OK byte-identical at 15 for the third run running.

That is the FOURTH consecutive null row, and it is the sharpest one, because the section above predicted it in this form: the walls are stacked inside cclasses.pas and the count measures queue position rather than size. Three consecutive walls, one file:

probe wall line in cclasses.pas units
#8 undefined variable (IndexQWord) 895 96
#9 undefined variable (unaligned) 1327 150
#10 Finalize(x, n) — the element-count form 1726 150

TFPHashList.Clear calls Finalize(FItems^, FCount). Each fix delivered its entire population intact to a wall a few hundred lines further down the same file.

This is now the umbrella's most reliable finding and it should shape how the next seat works it. Fixing the top of the histogram is nearly free of value per fix while cclasses is unfinished — the honest unit of work is "make cclasses.pas compile", not "clear the 150". The instrument that would say how far that is reports EVERY failure per unit rather than the first, and it still does not exist; building it is worth more than the next four walls.

Promoted to CLAUDE.md from here, on the recurrence test: frankB's lekkerzeilen umbrella produced the same shape independently (six import walls across two passes, modules-compiling moved by zero both times) — two corpora, no shared code, five null rows between them.

2026-09-10 — the ten ORACLE-NO units, named, because the category reads as a scandal

The owner asked, reasonably, whether this umbrella was claiming "fpc can't compile its own units". It is not, and the category name invites that reading. Measured by running only the oracle half of tools/fpc_compiler_corpus_probe.sh (207 fpc compiles, no pxx):

unit fpc's own message why
pp Syntax error, "UNIT" expected but "PROGRAM" found it is a PROGRAM — FPC's compiler main. uses pp is meaningless
cg64f32 Identifier not found "tcg64" 32-bit codegen; tcg64 exists only on a 32-bit target
ogomf RELOC_ABSOLUTE16 16-bit x86 object format
ogrel RELOC_ABSOLUTE_HI8 AVR
ogwasm TWasmBasicType wasm
oglx tobjectinput platform-conditional
browcol TCallbackFunBoolParam platform-conditional
impdef dirstr platform-conditional
cepiktimer Cannot open include file "../../epiktimer/epiktimer.pas" external package, not installed
ccharset fpcdefs.inc(1,2) Mode switch "OBJFPC" not allowed here the probe's own -Mobjfpc colliding with the include

Nothing here is fpc failing to build fpc. The probe fixes -dx86_64; build FPC for i386 and cg64f32 compiles, for AVR ogrel, for wasm ogwasm. The exclusion exists so a unit the ORACLE rejects under OUR flags can never be counted against us — which is the right design and is why attempt 1 had four false findings before it existed.

Two of the ten are OURS, not the corpus's

pp.pas is a program and should be excluded by name: counting it inflates the denominator by one and it can never pass, so it is a permanent can-never-be-green row sitting in the corpus total. ccharset is a flag collision the probe creates itself — -Mobjfpc on the command line against an include that sets its own mode switch — and may well compile once the probe stops forcing the mode it is already getting from fpcdefs.inc.

So the honest shape is 206 units: 8 genuinely out of scope for an x86-64 probe, 15 BOTH-OK, 182 PXX-FAIL. Worth fixing in the probe rather than re-explaining every time someone reads the category — a number that needs a paragraph of defence each time it is quoted is a number with the wrong denominator.

2026-09-11, frankS — attempt 7: cclasses.pas compiles, and the null row ends at four

Probe #11 at e013c4344, binary 79b76b2cc67f, whole corpus, fpc as oracle.

probe #10 @ b9c8fc160 probe #11 @ e013c4344
BOTH-OK 15 18
ORACLE-NO 10 10
PXX-FAIL 182 179

Join against attempt 6's BOTH-OK set, which was listed there by name: all fifteen are still present and the three added are cclasses, crefs, rabase. crefs is uses globtype, cclasses; rabase is uses cclasses, systems. So the delta is exactly the unit that was unblocked plus its two direct dependents, and nothing else moved — the same per-unit join the earlier attempts used, and the reason it is worth the extra minute is that a +3 with a different membership would have meant something else entirely.

What it took, and it was two bugs in one file

The instrument attempt 6 asked for — one that reports EVERY failure per unit rather than the first — turned out not to be a wrapper. Finalize(x, n) was raised through Error(), which HALTS, so every later wall in the same file was structurally invisible. Converting that one site to ErrorRecover took cclasses from ONE reported failure to THREE, and the second was real and pre-existing:

  1. [[bug-p-a-method-parameter-typed-through-a-forward-pointer-alias-never-matches-its-own-body]] (ee560d0ad) — a method parameter typed through a forward pointer alias never matched its own body, so the body died at codegen with unresolved forward naming a file the author never wrote.
  2. [[feature-p-the-element-count-form-of-initialize-and-finalize]] (d095cb08d) — the wall itself, which was a DELIBERATE refusal recorded in done/feature-a-implement-initialize-and-finalize-over-the-arc-helpers. That refusal is right about ignoring the form and is not an argument against implementing it.

ok: dpxx [code=536232B data=109348B bss=93392B procs=1487].

The four null rows were a queue and the queue emptied — but NOT for free

Attempt 6's finding stands and this attempt is its confirmation, not its refutation. The 150 did not become 150 BOTH-OK; they became 119 units stopped at the next wall, in a different file. Three of 150 is what "clearing the wall the 150 were on" was actually worth, and that is the number to quote against any future proposal to rank a blocker on its unit count.

First failures now

n first failure
119 unknown type: TExecuteFlagscfileutl.pas:136, in its INTERFACE
18 an object type cannot have a constructor
10 expected field name in record constanttokens.pas:378
10 Unsupported tcompilerwidechar size
6 uses: unit source not found: charset
4 unknown type: dsizeof(d) of a PARAMETER inside a local type decl
4 unknown type: TDoubleRecx86_64/cpuinfo.pas:36
2 conditional directive: expected operator
1 each unixcp, heaptrc, swapendian, align, IsATTY, RS_INVALID

TExecuteFlags is not a parser gap — it is sysutils, and it was already filed by frankH on 2026-09-09 as [[feature-b-sysutils-has-no-executeprocess-and-no-texecuteflags]], back when it was the frontier for a handful of units rather than for 119. Every line of that diagnosis still holds and none of it needed re-deriving; it has a dated note with the new count and nothing else. The hard half (ExecutePipeline / PalVforkAndExec) is already written in the same unit, and a type-only stub is worse than nothingcfileutl's implementation calls ExecuteProcess, so the type alone just moves the failure to link time. Do not rank it on 119: this umbrella's own most reliable finding says that number is a queue position, and attempt 7 just measured the conversion rate at three units per wall.

One row is already gone, and it is recorded here so the next join is honest

expected field name in record constant was reduced and fixed the same hour — [[bug-p-a-set-valued-record-field-cannot-be-written-in-a-record-constant]] (138604b5e): FPC's tokens.pas writes keyword:[m_none] in ~400 record constants and the set arm of TryParseInitValForm had been written out by hand in the const-ARRAY loop and given to no other caller. tokens.pas compiles. This table is probe #11's. Probe #12 is below and it is the current one.

Probe #12 @ 4c7c88d36, the same evening: 18 -> 20, and the ratio held a third time

#10 @ b9c8fc160 #11 @ e013c4344 #12 @ 4c7c88d36
BOTH-OK 15 18 20
ORACLE-NO 10 10 10
PXX-FAIL 182 179 177

Join again: all eighteen of #11's BOTH-OK units are still there and the two added are tokens and rescmn — the two units that carried a set-valued record field themselves. rescmn is uses Systems and opens with res_elf_info : tresinfo = (...). The other eight of the ten that stopped at expected field name in record constant moved to unknown type: TExecuteFlags, which is why that row went 119 -> 127 while nothing about TExecuteFlags changed.

Three walls, three joins, three small numbers: 3, then 2. A wall's unit count has now predicted its yield wrongly five times running, and the two measurements in this attempt are the first where the yield was positive at all — which makes them the strongest form of the same finding, not a counterexample to it. The useful reading: a wall's population tells you how many units are QUEUED behind it; the units that become BOTH-OK are the ones for which it was the LAST wall. Those are near-disjoint sets, and only the second is worth a number.

First failures now (probe #12)

n first failure
127 unknown type: TExecuteFlagscfileutl.pas:136, in its INTERFACE
18 an object type cannot have a constructor
10 Unsupported tcompilerwidechar size
6 uses: unit source not found: charset
4 unknown type: dsizeof(<a PARAMETER>) in a const expression
4 unknown type: TDoubleRec
2 conditional directive: expected operator
1 each unixcp, heaptrc, swapendian, align, IsATTY, RS_INVALID

unknown type: d is entfile.pas:371, array[0..sizeof(d)-1] where d is the enclosing function's PARAMETER. ConstEvalFactor's sizeof arm resolves a TYPE NAME only and says so in its own comment; the expression path handles a variable and SizeOf(d) in a statement answers 8 correctly. Reduced to ten lines and left unfixed here rather than folded in — the honest fix is narrow but it is in a 300-line intrinsic and deserves its own pass.

2026-09-11, frankH — two blockers closed, and the wall census gets a fourth null row

Both were taken from this umbrella's blocked-by, both are Track P, and the combined corpus movement is one unit advanced to the next wall and zero units compiling. That is the fourth independent time this umbrella has measured that shape, and it is worth one line rather than another paragraph: the finding is no longer news, it is the baseline expectation.

bug-p-sizeof-of-a-variable-is-not-folded-in-a-constant-expressionsizeof(<a variable>) in a const expression (FPC entfile.pas:371). Fixed; 4 units stopped there and none of them compiles now either.

bug-p-a-conditional-directive-cannot-read-a-const-whose-value-is-not-an-integer-literal — this one was two defects plus a fourth hop the ticket had not traced, and that is the part worth carrying forward:

THE SPLIT IS THE TRANSFERABLE PART. One ticket carried both shapes and their sizes differ by an order of magnitude: the half that landed reused walks that already existed and added no value kind, while the half split out needs a fourth value kind on the directive stack, set-union folding, enum-member resolution and an in operator the grammar does not have. A summary cannot be true about both, and this umbrella ranks by blocked-by edges — so a mixed ticket prices the cheap half at the expensive half's cost, in the one place where the price is what gets read.

The every-failure census, 2026-09-16 (binary e790ec9e027f at 9281da35b)

Run it as PXX_CORPUS_DETAIL=<dir> tools/fpc_compiler_corpus_probe.sh. The probe has had that flag and errs=N since 2026-09-11 — this umbrella's own headline still said nobody had built the instrument, and a seat nearly rebuilt it. Fixed above.

Totals: 21 / 10 / 176 (BOTH-OK / ORACLE-NO / PXX-FAIL). BOTH-OK unchanged at 21, as expected — nothing landed for P between probes.

First-failure histogram (still ranks by queue position — do not rank on it):

n first failure
132 unknown type: TDoubleRec
18 an object type cannot have a constructor
10 Unsupported tcompilerwidechar size
6 uses: unit source not found: charset
4 undefined variable (entryreal_bytes)
2 conditional directive: expected operator

The result the histogram cannot show, and the reason for the detail flag: of the 132 units whose FIRST failure is TDoubleRec, 0 have it as their ONLY failure and all 132 also hit too many array initializer elements — and nothing else. Error counts per unit: 38 units report 1, 132 report exactly 2, and only one unit anywhere reaches MAX_REPORTED_ERRORS. So the two-error finding is real and not a truncation artefact.

Both walls are x86_64/cpuinfo.pas, lines 36 and 281. That is the cclasses.pas pattern again — one file's contents wearing the shape of a population — and it is the third time this umbrella has recorded it.

Prediction for the next run, stated before it: clearing BOTH lands the 132 on a third wall in the same file, not on 132 compiling units. A complete reported failure set is not a guarantee of compilation — ErrorRecover carries past SEMANTIC failures, and a halting diagnostic later in the unit would never appear. That is the honest caveat and it should not be dropped when this is quoted.


2026-09-16 (frankS) — the seventh null row, and the third wall is REAL but NOT in the same file

bug-p-an-array-constant-with-a-set-element-type-cannot-be-initialised is fixed (14df2066b). Measured what that delivered, and measured the prediction that was put on record before the work, which is the part worth keeping.

Three arms, same probe, 207 units, pxx-only (no fpc oracle — read the DELTA between columns, never the absolute count):

arm compiler cpuinfo walls units OK total errors
base pre-fix both present 21 396
fix post-fix TDoubleRec only 21 396
stub pre-fix both stubbed out 21 256

UNITS COMPILING MOVED BY ZERO IN BOTH DIRECTIONS — including with BOTH walls removed. That is the seventh null row and it is consistent with every previous one. 138 units report exactly two errors, TDoubleRec first.

THE FIX WORKED AND THE COUNT CANNOT SEE IT. base and fix are identical unit-by-unit and identical in total error count, which reads as "nothing happened". It is not what happened. Checked one unit directly:

aasmbase, pre-fix : pascal26:36  unknown type: TDoubleRec
                    pascal26:281 too many array initializer elements
aasmbase, post-fix: pascal26:36  unknown type: TDoubleRec
                    pascal26:35  an object type cannot have a constructor

The array error is gone and a different error took its slot. All 138 units traded one wall for the next, so the count held at 2 and the first-error histogram — which is what the PXX_CORPUS_DETAIL summary reports — held at TDoubleRec. An error COUNT is not an error IDENTITY, and a wall census compared on counts will report a successful fix as a null result. Compare the message sets, not the totals.

The prediction, scored honestly. It was: "clearing both lands the 132 on a third wall in the same file." Half right, and the wrong half matters.

That distinction is worth keeping because it changes what the next fix should be. "Another wall in cpuinfo.pas" would mean keep grinding that file; "one shared wall one file further on" means the next lever is bug-p-object-value-types-standard-meaning, which now gates 156 of 207 units and is the largest single wall this corpus has.

And the ranking caveat still applies to that 156, unchanged. It counts units QUEUED, not work, and this umbrella has now converted seven walls at a yield of zero additional compiling units. Predict before the next re-run; a null row is only information to someone who said what they expected.

ErrorRecover caveat, carried forward. A complete reported failure set is a claim about what the diagnostic emitted, not about what compiles. A halting diagnostic later in a unit never reaches the detail file at all, so "two errors" and "two fixes from compiling" are different statements — and the zero-yield rows above are consistent with that being true of some units here.

SCORING A WALL BY THIS CENSUS WILL REPORT A WORKING FIX AS A NULL RESULT

Measured 2026-09-16 (franks-ee), and it lands on the section above. Three arms over all 207 units:

arm compiler cpuinfo walls units OK total errors
base pre-fix both present 21 396
fix post-fix TDoubleRec only 21 396
stub pre-fix both stubbed out 21 256

base and fix are identical unit-by-unit, identical by count, and identical in the first-error histogram — and the fix WORKED. All 138 units traded the array wall for a different error in the same slot, so the count held at 2 and the first error stayed TDoubleRec:

aasmbase pre-fix : :36 TDoubleRec  /  :281 too many array initializer elements
aasmbase post-fix: :36 TDoubleRec  /  :35  an object type cannot have a constructor

AN ERROR COUNT IS NOT AN ERROR IDENTITY. PXX_CORPUS_DETAIL's summary reports counts and first errors, so scoring a wall with them alone reports a landed fix as nothing. Diff the detail files unit-by-unit, on the error TEXT, not on errs=N and not on the histogram. Same family as CLAUDE.md's line-31 finding: the instrument is honest and is answering a narrower question than the reader supplies.

SEVENTH NULL ROW — and the third wall is NOT in cpuinfo.pas

Stubbing BOTH walls out of a copy of cpuinfo.pas moved units-compiling by ZERO (21 -> 21). The prediction that a third wall would swallow the population was right; the prediction that it would be in the same file was wrong — cpuinfo.pas compiles clean with both stubbed. The 138 land one file further on, at an object type cannot have a constructor, which goes 18 -> 156 of 207 and is now the largest single wall this corpus has recorded.

That distinction is the whole value of the measurement: "another wall in cpuinfo" means keep grinding one file; "one shared wall one file on" means the next lever is feature-p-legacy-value-object-types / bug-p-object-value-types-standard-meaning. Ranking caveat fully intact — 156 is units QUEUED, and this umbrella has now converted seven walls at a yield of zero.

STUB BEFORE YOU FIX. It answered the size question for almost nothing and BEFORE the work, and it is the only reason we know the next wall is elsewhere.


2026-09-16 (frankS, later) — the wall ORDER matters, and I got it wrong before measuring

fa397c761 fixes var s: string = <named const> (globals.pas:502). It changes nothing here today, and the reason is worth more than the fix.

I claimed this wall gated 138 units and was the cheaper lever than the object wall. It does not, and it is not in front of it. globals.pas:502 sits BEHIND the object-constructor wall: those units stop at the object constructor and never reach the var initialiser. Measured on the real tree, before and after:

units OK total errors first-error histogram unit-by-unit
before fa397c761 21 396 unchanged identical
after 21 396 unchanged identical

It only appeared to be in front because I found it with the object wall STUBBED OUT. The stub was there to reveal what was behind; I then read its output as an ordering over the unstubbed corpus. A stub tells you what is BEHIND a wall — it cannot tell you a wall is in FRONT of one, because in the stubbed world that wall does not exist. The stub answers "what is next", and I read it as "what is first".

That is the same family as the count-versus-identity finding above and it arrives from the other side: there, an instrument that could not see a real change; here, an instrument that showed a real change in a world I had built. Both are honest and both are about a narrower question than the reader supplies.

What the fix does deliver, measured where it is reachable (stubbed tree, cpuinfo ×2 + versioncmp's ctor):

Wall six is TSystemTime — an RTL type gap, same family as TDoubleRec, Track B. With that, the walls this umbrella has hit now alternate between Track P parser gaps and Track B RTL type coverage:

1  TDoubleRec                  RTL type      (B, ticketed)
2  array-of-set var init       parser        (P, FIXED 14df2066b)
3  object constructor          language      (DECIDED against; see decide-old-style-object-types)
4  var = named string const    parser        (P, FIXED fa397c761)
5  TSystemTime                 RTL type      (B, unticketed)

So the binding constraint is not one subsystem. Two of the five are RTL types we simply do not declare, and neither needs a compiler change or a decision — which makes them the cheapest remaining lever, and the first structural read this umbrella has had that is not "one more wall in one more file".

Ninth null row. Five walls cleared or stubbed across the day, units compiling 21 → 22. The standing caveat holds and has now been demonstrated twice in one session: a wall's population is a queue position, the arms are pxx-only (read deltas, not the absolute 21), and an error count is not an error identityfa397c761 is invisible in every summary quantity and is still a real fix.

A STUB ORDERS THE STUBBED WORLD ONLY — it says what is BEHIND a wall, never that a wall is in FRONT

Measured 2026-09-16 (franks-ee), correcting itself, and it corrects the advice in the section above. "Stub before you fix" is right and it is what found walls 4 and 5 at all. What it does NOT do was never stated: in the stubbed world the stubbed wall does not exist, so the ordering you read off it cannot tell you that wall sits in FRONT of what you found.

globals.pas:502 was found with the object wall stubbed and reported as gating 138 units and as the cheaper lever. It gates nothing on the real corpus — those 138 stop at the object constructor and never reach the var initialiser. Before and after on the unstubbed tree: 21 units OK, 396 total errors, identical unit-by-unit AND by error identity.

The discipline, with the missing line: stub to find the next wall, then RE-RUN UNSTUBBED before claiming an ordering. One command.

Pair this with the count-vs-identity section above — they are the same family in opposite directions, twelve hours apart. There the instrument could not see a real change; here it showed a real change in a world the measurer had built. Both honest, both answering something narrower than the reader supplied.

And fa397c761 is the count-vs-identity rule biting a SECOND time: a real fix, invisible in every summary quantity — same units, same counts, same first-error histogram. Two independent instances now, not one.

THE WALLS, and the first structural read that is not "one more wall in one more file"

# wall kind state
1 TDoubleRec RTL type Track B, ticketed
2 array-of-set var init parser FIXED 14df2066b
3 object constructor language DECIDED AGAINST (decide-old-style-object-types, option A)
4 var = named string const parser FIXED fa397c761
5 TSystemTime RTL type FIXED c52d5b31b (franks-ee)
6 sizeof(files[0]) on a pointer-indexed element (finput.pas:544) parser FIXED a931bef4d (franks-ee)
7 parameterless call spelled WITHOUT parens when the name is OVERLOADED (comphook.pas:386) parser FIXED 3926a4098 (franks-ee)
8 StdErr is an fd, not a Text (comphook.pas:397, :399) RTL type FIXED d66f128a1 (franks-ee) — live without a pin
9 SysUtils.FileAge (comphook.pas:474) RTL FIXED 42e127d6d (franks-ee) — live without a pin
10 TRawByteSearchRec + FindFirst (cfileutl.pas:282) RTL type FIXED 34e3a2fa8 (franks-ee) — live without a pin
11 GetDir + the sysunixh.inc const block (cfileutl.pas:518, :543, :696) RTL FIXED d7bf36ce0 (franks-ee) — live without a pin
12 MkDir/RmDir/ChDir + IOResult plumbing (cfileutl.pas:714) RTL OPEN — the head of the STUBBED arm, Track B

THE NUMBER IN COLUMN 1 IS A ROW POSITION, NOT AN IDENTITY — CITE THE file:line. This heading said FIVE while the table held SEVEN rows, and on 2026-09-16 two seats disagreed in writing about which wall comphook.pas:386 is: this table calls it 7, franks-ee calls it 8, and both counts are honest — the umbrella's prose above counts walls CLEARED historically, and this table numbers its own rows. Neither is wrong and neither is quotable. Nothing downstream may key on the number; the subjects are unambiguous and the two seats never disagreed about those — finput.pas:544 is FIXED at a931bef4d, comphook.pas:386 is OPEN. Name walls by file:line in tickets, commits and messages. Two of the five are RTL types we simply do not declare — no compiler change, no decision, nothing to reverse. That makes them the cheapest remaining lever by some distance, and it is the first time this umbrella's blockers have sorted into kinds rather than into files.

Wall 3 is the one to understand before ranking anything: it is decided AGAINST, so walls 4 and 5 sit behind a wall nobody is authorised to remove. Do not rank 1 or 5 on the 138 — that count comes from the stubbed world. Nine null rows in a row; the yield of walls 1 and 5 stays unknown until wall 3 moves, and moving wall 3 requires a decision recorded below the existing one.

WALL 5 IS FIXED AND IT IS THE FIRST ONE THAT IS LIVE WITHOUT A PIN — rank RTL walls above compiler walls for that reason alone

Landed 2026-09-16 by franks-ee, c52d5b31b: SysUtils.TSystemTime, GetLocalTime, DateTimeToSystemTime, SystemTimeToDateTime. Wall 5 of the five-wall table above is cleared. The new head is finput.pas:544ReallocMem(files,afiles*sizeof(files[0])), sizeof of an element reached by INDEXING A POINTER. That is a parse gap, not a type gap, so it is Track P and it does not join the two RTL rows.

The property that makes the kind-sort in the table ACTIONABLE, and it was not stated when the table was written. A wall's kind said what it would COST to fix. It also says when the fix becomes REAL, and the two RTL rows differ from every other row on that axis:

So "cheapest lever" understated it. An RTL wall costs no compiler change, no decision, AND no pin — it is the only class of umbrella blocker whose fix is worth something to every other seat on the same day. Wall 1 (TDoubleRec) is the remaining member and should be ranked accordingly.

What this does NOT do is move the yield question. Walls 4, 5 and 6 still sit behind wall 3, which is decided against, so nine null rows in a row remain nine. Live-without-a-pin is an argument about WHEN a fix pays, never about WHETHER this umbrella's count moves.

Two conventions franks-ee read out of FPC's source rather than guessing, recorded so nobody re-derives them: TSystemTime.DayOfWeek is 0-based where SysUtils.DayOfWeek is 1-based (a fixture must assert the DIFFERENCE, or it passes under either convention); and SystemTimeToDateTime composes with a sign-correct ComposeDateTime, not by adding — 1899-12-29 06:00 is -1.25 composed and -0.75 added. GetLocalTime returns UTC deliberately.

No ticket, and that is correct. CLAUDE.md: filing instead of fixing is the error. The table above is the bookkeeping.

WALL 6 FIXED, AND ITS CENSUS IS THE SIXTH NULL ROW — PREDICTED AS ZERO IN ADVANCE, FOR THE SIXTH TIME

a931bef4d (franks-ee): a FindSym MISS kept SizeOf on the name path, which cannot index. SizeOf(<field>[index]) is accepted; 33/33 on the extended fixture, with a control that can fail — pin v410 and a purpose-built pre-fix binary both refuse it with expected ')' before '[', and TR is 12 bytes in that fixture so a pointer-width answer cannot pass for a correct one.

Four-arm census, against an expectation written down BEFORE the arms ran:

arm result
stubbed 105 units first-failed at the sizeof wall; afterwards zero detail files name expected ')' before '[' anywhere. Cleared, not moved. Units-OK 22 → 22.
unstubbed not one row changed. 21 → 21. No unit lost in either pair, compared unit by unit.

The unstubbed null is the half that carries the ordering claim, and it is the discipline this umbrella wrote for itself working: finput sits BEHIND the cpuinfo and versioncmp walls on the real corpus, so the fix is worth nothing today and worth the whole 105 the moment those clear. Sixth null row, predicted as zero in advance for the sixth time — which is the only thing that makes a null row information.

The absence instrument is the right one here. "Zero detail files name the error string anywhere" cannot be produced by accident; a units-OK count can. Cleared rather than moved is a claim about error IDENTITY, not about a count, and this umbrella has been burned by the difference before.

WALL 7 — a one-cell defect, and this seat FAILED TO REPRODUCE IT (which refutes nothing)

comphook.pas:386, system.str(getrealtime-starttime:0:3,hs2). franks-ee read the site off the detail file's own in: line rather than inferring it — necessary, because the diagnostic prints no file name and the symbol leads a reader to globals.pas:386, which is a comment. (Verified here: it is { contains tpackageentry entries }.) That is CLAUDE.md's same-line-number rule paying for itself a second time in this umbrella.

Its reduction, eleven lines, with the two controls that make it one cell:

shape verdict
A overloaded, bare REFUSED undefined variable (grt)
B overloaded, empty parens COMPILES
C overloaded, bare, plain RHS REFUSED
D the parameterful overload COMPILES
E control — NOT overloaded, bare COMPILES
F control — NOT overloaded, parens COMPILES

So only the intersection fails: a parameterless call spelled without parentheses when the name is overloaded. Same shape as wall 6 — a door wired for one spelling of a reference and not the other, and the passing spelling is the one everybody writes in a test.

THIS SEAT COULD NOT REPRODUCE IT AND THAT IS A FACT ABOUT THIS SEAT'S FIXTURE. Six reconstructed rows plus four more in the real site's str(...:0:3, s) shape ALL COMPILE at a931bef4d. The reconstruction is worthless as a refutation, and the control says why: it compiles under PIN v410 too — a binary that predates both walls. A fixture that passes on every compiler ever built cannot distinguish a fixed defect from one that was never there, which is this file's own "if the machinery did nothing at all, would this row still pass?" answering yes. Recorded so nobody reads it as a contradiction of franks-ee's table; the difference is in something the reconstruction did not copy, and the fixture to trust is the one with the failing rows in it. Asked for its eleven lines rather than guessing further.

The method note franks-ee volunteered is the one to keep: its FIRST run of that table reported all six rows REFUSED, controls included, because the harness broke on spaces in a tag and it was reading "no error line printed" as success. The controls caught it — E and F are not decoration, they are what separates a finding from an instrument. It nearly shipped a table in which the instrument was the finding, and said so unprompted.

WALLS 6 AND 7 WERE THE SAME DEFECT CLASS IN TWO TABLES, AND WALL 8 IS ELEVEN LINES BELOW WALL 7

Measured 2026-09-16, unstubbed AND stubbed, expectation recorded before each run.

Both are a lookup answering about the wrong member of a set and the caller reading that as a fact about the whole set, once through a miss and once through a representative. Neither is a missing check; both fired a confident diagnostic about the wrong thing. That is a sharper grouping than "two parser walls" and it predicts where to look next: any door that resolves a name ONCE and then reasons about arity, kind or type from the single answer.

The yield is still zero and it was predicted as zero both times. Wall 6 cleared 105 units onto wall 7; wall 7 cleared the same 105 onto wall 8, which is comphook.pas:397eleven lines below wall 7 in the same file. That is the cclasses.pas shape this umbrella already recorded (895 → 1327 → 1726): a first-failure census walking one file's contents and reporting each step as a new population. Units-compiling: 22 → 22 → 22 stubbed, 21 throughout unstubbed, where not one row changed either time because everything still dies earlier at walls 1 and 3.

A caution for whoever reads the next histogram. comphook.pas:397 nearly did not appear in it: the compiler mints a per-instantiation suffix (WriteMsgTypeColored$151860), so grouping the raw message split one 105-unit wall into 105 singletons, each ranking below every genuine small wall, and the visible rows were IDENTICAL to the previous run. Normalise $[0-9]+ before grouping, and check the buckets sum to the population. Worked up in debugging-playbook.md.

Eleven null rows in a row now. Walls 6, 7 and 8 all sit behind wall 3, which is decided against, so the count of units compiling cannot move by clearing them and nobody should rank them on it. What they buy is that the queue behind wall 3 is being drained in advance, and each one is a real defect that real Pascal hits outside this corpus — the overload one in particular is reachable by any program with an overloaded parameterless routine, which is why it landed with fixtures and a must-refuse control rather than as corpus bookkeeping. Named by file:line throughout, per the caution in the table above — I called comphook.pas:386 "wall eight" in a commit message and in messages to a peer while this table calls it row 7, and both counts were honest. The subjects were never in dispute.

FOUR CONSECUTIVE WALLS IN ONE FILE, AND THE KINDS ALTERNATE — comphook.pas 386 → 397 → 399 → 474

Measured 2026-09-16. finput.pas:544 delivered its 105 units to comphook.pas:386, which delivered the same 105 to :397, which delivered the same 105 to :474. Three fixes, three null rows, one file walked from line 386 to line 474. This umbrella already recorded that shape for cclasses.pas (895 → 1327 → 1726); it is now the dominant pattern here and not an anecdote, and it is the reason the first-failure census cannot be read as a work estimate.

The kinds alternate, which is the part worth acting on:

wall kind inert until a pin?
finput.pas:544 parser (Track P) yes
comphook.pas:386 parser (Track P) yes
comphook.pas:397 / :399 RTL type (Track B) NO — live immediately
comphook.pas:474 RTL (Track B) NO
cfileutl.pas:282 RTL type (Track B) NO
cfileutl.pas:518 / :543 / :696 RTL (Track B) NO
cfileutl.pas:714 RTL (Track B) NO

So the head of this umbrella is currently an RTL wall, and RTL walls are worth strictly more per hour than compiler walls — the umbrella's own earlier finding, now with a third instance (TSystemTime, then StdErr, and FileAge next). A lib/** fix is verifiable against the pin in place and reaches every other seat the moment it is pushed; a compiler/** fix sits inert until somebody pins, and nobody may pin while the owner is away. Whoever picks this up next should take comphook.pas:474 (FileAge) rather than looking for a parser wall.

StdErr was misclassified as a parser wall for about an hour, by me, in the table above. It reads like one — the diagnostic is an overload-resolution failure, which is what the two parser walls before it also were. It is an RTL type gap: StdErr was an integer fd constant with a parser special case, where FPC's is a Text. The diagnostic names the door that refused, not the thing that is missing, and three walls in a row arriving through overload resolution is exactly the run that makes the fourth look like more of the same.

WALL TEN — SysUtils.FileAge, and the FIFTH consecutive wall is where the pattern BREAKS

Measured 2026-09-16, franks-ee. FileAge, FileDateToDateTime and DateTimeToFileDate implemented in lib/rtl/sysutils.pas. Library-only, so it is live under pin v410 and inert for nobody.

Expectation, recorded before the run (w10_expectation.txt, and it was written before the first attempt, not after): the 105 leave the wall; units-OK does NOT move from 22; the likeliest next head is comphook.pas:1012 or something further down the same file, because that file had produced four walls in a row and FileDateToDateTime was already visible at :1012 with 12 units on it.

The first half was right and the second half was wrong, which is the finding.

arm units-OK the wall the new head
stubbed 22 → 22 105 → 0 cfileutl.pas:282 TRawByteSearchRec, 120 units
unstubbed 21 → 21 105 → 0 x86_64/cpuinfo.pas:36 TDoubleRec, 132 units

CLEARED, NOT MOVED, in the strong form: zero detail files name FileAge anywhere — not merely as a first error — across all 207 units in both arms. The same is true of FileDateToDateTime, whose own 12-unit wall at :1012 went with it, because the one fix supplied both names. No unit compiled before and fails now, and none newly compiles, in either arm, compared unit by unit. The ORACLE-NO set is identical. Ninth null row, ninth time predicted as zero in advance.

comphook.pas is now out of the picture entirely — it holds zero first errors and appears in zero detail files as a location. Four consecutive walls in one file, and then the file cleared rather than yielding a fifth. So the "one file walks downward" shape is real but it TERMINATES, and the section above it should not be read as predicting where a head goes next: it predicted :1012, the fix took :1012 out along with :474, and the head left the file. The cclasses.pas precedent (895 → 1327 → 1726) says the same thing in hindsight — a file stops producing walls when you run out of its distinct RTL dependencies, not when you reach its end.

THE UNSTUBBED HEAD IS ROW 1 OF THE TABLE ABOVE. TDoubleRec has been wall #1 in this umbrella from the beginning and is still open, and on the REAL corpus it is what 132 of 207 units hit first. Everything the stubbed arm reports is downstream of somebody agreeing to look past it.

And the stub is TWO files, not oneversioncmp.pas and x86_64/cpuinfo.pas. This seat wrote "only one file differs" after diffing fpcsrc/*.pas, which is the corpus ROOT only; the probe also passes -Fu$F/x86_64 -Fu$F/systems -Fu$F/x86, and cpuinfo.pas — the file that carries the whole 132-unit difference between the two arms — lives in one of those. A glob scoped to one directory answering about a tree is the ordinary shape of this mistake, and the tell was free: the two arms reported different heads, which one stubbed file in a unit almost nothing imports cannot explain.

Instrument note, for whoever runs this next: two full background corpus runs were KILLED at ~150 of 207 units, both reported as system memory pressure while the box had 35GB free and 56GB available. The run was confirmed genuinely dead rather than detached-and-still-running by checking whether the output file was still GROWING — not by a process scan, which cannot tell a finished run from one sampled between two compiler invocations. The corpus now takes a PXX_CORPUS_LIST of unit paths, and three ~2.5-minute FOREGROUND chunks complete where one background run does not. The three lists are asserted to be an exact partition of the glob before the run, and the assembled output is checked at 207 rows / 207 distinct units after it.

WALL ELEVEN — FindFirst, and the walk-downward shape RECURS in a second file

Measured 2026-09-16, franks-ee. FindFirst / FindNext / FindClose, TSearchRec / TRawByteSearchRec, the nine fa* attribute constants and AllFilesMask, in lib/rtl/sysutils.pas. None of it existed — not the record type, the whole family. Library-only, live under pin v410.

Expectation, recorded before the run: the 120 units leave cfileutl.pas:282 and the whole procedure clears rather than the head advancing a few lines (:287 findfirst and :290 sat right behind it, and one fix supplies all three names); units-OK stays at 22, "and I am saying so for the tenth time in a row", with the one shape that could move it named in advance — cfileutl is a DEPENDENCY, not a leaf, so if it compiled, everything blocked only on it would compile too. Next head deliberately NOT guessed, on the strength of wall ten's finding.

before after
units-OK (stubbed) 22 22
the wall 120 0
new head cfileutl.pas:518 GetDir, 120 units

Both halves held. No unit lost, none gained. Tenth null row.

THE WALK-DOWNWARD SHAPE IS BACK, IN A SECOND FILE: 282 → 518, the same 120 units, cfileutl.pas both times. Wall ten's section says that shape terminates and does not predict the next head, and both statements survive this: it did terminate in comphook.pas, and it has now started afresh in cfileutl.pas 236 lines lower, in a different procedure. So the shape recurs without being predictive — which is why the expectation above declined to name a next head and was right to. A file stops producing walls when its distinct RTL dependencies run out, and cfileutl.pas has more of them.

The fixture is where the real risk was, and the first version of it passed while measuring nothing. Run against test/, faAnyFile and faDirectory return the IDENTICAL answer — there is no dotfile, no symlink, no unwritable file and no device node in a checked-in tree, so every attribute-filter row is vacuous and a filter that ignores its argument entirely scores full marks. The fixture therefore builds its own directory: a dotfile, an unwritable file, a symlink to a file, a symlink to a directory, and a dangling symlink.

The strongest control is differential, and it was checked for sensitivity. The same probe compiled under fpc and under pxx against one directory produces byte-identical output across ten rows — names, counts and the raw attribute integers. That is only worth something if it can go red, so the filter's polarity was deliberately inverted (any bit instead of all bits) in a scratch copy of the RTL: the diff reddens on six of ten rows. A guard that cannot fail prints PASS, and this one was made to fail on purpose before its green was quoted.

A claim written into the source comment was disproved by that control, in the same hour it was written. The comment said faAnyFile being $1FF rather than the widely-quoted $3F matters because $3F "silently drops faNormal and faSymLink". Measured: on unix the two select exactly the same entries, in every arrangement probed — faNormal is never set by the unix attribute mapping, and faSymLink only appears when the caller already requested it, which puts the bit in the filter either way. The reason to carry FPC's value is that a caller may COMPARE against it, not that the other one filters. The comment now says the measured thing.

Two behaviours that look like bugs and are FPC's, both asserted: . and .. are returned (fpc's own cfileutl.pas:287 filters them by hand, which is only necessary because they arrive), and Attr is a PERMISSIVE filter rather than a requirement — faDirectory still returns ordinary files, because every unix entry carries faArchive and the filter accepts an entry when every bit it HAS is covered. faArchive and faReadOnly are forced into the filter unconditionally, as FPC does, or the obvious call returns nothing at all.

Found on the way, NOT fixed here, and it is a silent wrong value: PChar(AnsiString('literal')) yields one garbage byte under this compiler where fpc yields the string. It was found because the fixture used that spelling to create a symlink and got a dangling link with a one-byte target — which then made four unrelated assertions fail and looked exactly like an RTL defect. PChar(literal) and PChar(variable) are both correct; only the double cast is wrong. Compiler-side and therefore inert until a pin, so it is going in as its own change rather than riding along with an RTL fix.

WALL TWELVE — GetDir, and the corpus catching a HALF-DONE GROUP mid-change

Measured 2026-09-16, franks-ee. GetDir plus fpc's sysunixh.inc:29-53 const block in lib/rtl/sysutils.pas. Library-only, live under pin v410.

before after
units-OK (stubbed) 22 22
the wall 120 0
new head cfileutl.pas:714 rmdir, 120 units

Eleventh null row, predicted as zero in advance, with the dependency caveat named again and again not firing.

THE FIRST VERSION OF THIS CHANGE WAS HALF A GROUP, AND THE CORPUS SAID SO IN ONE RUN. GetDir and AllowDirectorySeparators are the two names the wall at :518 and :543 sat on, so those are what got written. The re-run moved the head to cfileutl.pas:696DriveSeparator, a member of the same const block in the same fpc include file, 178 lines further down. The fix was one line.

That is normalise-dont-special-case's sibling rule arriving as a declaration group rather than as a code path: one const block in fpc's source is one group here, and taking the two names a diagnostic happened to name is the same mistake as fixing one arm of a double case. The whole block went in on the second pass — DriveSeparator, ExtensionSeparator, PathSeparator, LFNSupport, maxExitCode, MaxPathLen, UnusedHandle, and the two writable case flags. The cost of getting this wrong is a full corpus re-measurement for a one-line constant, which is what it cost here.

DriveSeparator is an EMPTY STRING on unix, not a character, and the row that matters is what it DOES: Pos(DriveSeparator, s) is real fpc code at cfileutl.pas:696 and must answer 0 — that is how a drive-letter scan finds nothing on a platform with no drives. Asserting Length = 0 alone would pass for a constant that Pos then choked on, so both are asserted.

PLACEMENT IS A STATED DIVERGENCE, NOT AN OVERSIGHT. fpc declares all of this in SYSTEM, visible with no uses. pxx's System is compiler/builtin/builtin.pas, whose own header says it contains no syscalls — and GetDir is getcwd. Putting it there would mean layering the PAL into the one unit documented as having none, and it would be inert until the next pin: measured, the pinned compiler resolves lib/rtl from the LIVE tree and builtin from its OWN snapshot (stable_linux_amd64/default/builtin/). So it lives in SysUtils, and the cost is written into the source: GetDir(0, s) without uses SysUtils compiles under fpc and does not compile here.

Behind this wall there are only TWO errors left in cfileutl.pas, and the second one is ours: rmdir at :714 (which needs MkDir/RmDir/ChDir and IOResult plumbing — fpc's caller wraps it in {$I-} and reads ioresult), and then internal parser bug: statement made no progress in block (would hang) at cfileutl.pas:1495 — a Track P defect, not an RTL gap. The detail file is what shows this: it records every failure per unit, not just the first, which is the instrument this umbrella spent five null rows wishing for.

Values checked against fpc, harness checked for sensitivity. The same probe under both compilers from one directory diffs byte-identical across eight rows, then ten more for the const block. The separator set was then deliberately narrowed to ['/'] in a scratch RTL copy and the diff reddens — and the FIRST attempt at that control was itself wrong: the sed did not match, nothing was built, and diff failed on missing files while the shell reported "CAUGHT IT". A guard reporting a verdict for rows where nothing was built is the exact failure this umbrella keeps recording; it was re-run with the build asserted and branched on before the green was believed.

CORRECTION, 2026-09-16, to the paragraph above as first written. It said the live and pinned builtin/ trees were "byte-identical right now" and offered that as the trap a reader should not fall into. The claim was false, and it was false in the shape it was warning about: it came from cmp on ONE file (builtin.pas) with the conclusion asserted about the whole DIRECTORY — one location sampled, all locations asserted. diff -rq answers three:

file last changed
builtinheap.pas 445ce3e25, 09-14
pyeval.pas dea6cf762, 09-16
pylib.pas 334680199, 09-16

All three after pin v410. So the divergence is LIVE in three files rather than hypothetical, and the placement caution the paragraph argues for is better founded than its own evidence was. Found by frankuser re-measuring it rather than reading it — which is the only thing that catches this class, since the sentence was wrong in a way that made the surrounding argument sound MORE careful.

And the ledger has THREE states, not two: lib/rtl/** reaches from the live tree and is live on push; compiler/** is inert until a pin; compiler/builtin/** is inert until a pin AND is currently three files behind it, so two nilpy fixes that landed today reach nothing running under the pin.

WALL THIRTEEN — cfileutl.pas:1495, and it is not a wall, it is a BLINDFOLD

internal parser bug: statement made no progress in block (would hang). Taken out of order, ahead of rmdir, on frankuser's argument that characterising is not subject to the inert-until-pin discount: a reduction and a ticket land live whatever the pin does, so the RTL-first rule simply does not apply to them. That argument was right, and it turned out to be right twice over, because the thing behind this error was not a wall at all.

What it is

fpc's own cfileutl.pas:142 declares

{ hide Sysutils.ExecuteProcess in units using this one after SysUtils}
const
  ExecuteProcess = 'Do not use' deprecated 'Use cfileutil.RequotedExecuteProcess instead, ...';

— a constant whose entire job is to SHADOW a function — and then calls the real one qualified, twice, in its own body (:1494, :1496). We consulted the unit qualifier in the proc table ((qUnit < 0) or (ProcUnitIdx[i] = qUnit)) and in neither constant table, so the constant took the call.

Three faces, and the parser message is the least of them:

shape what pxx did fpc
assignment, const type fits printed the constant's text, arguments discarded, no diagnostic the function's result
set-const shadow the baked mask's ADDRESS printed as a string — a memory dump the function's result
argument position expected ')' before '(' the function's result
inside an if arm statement made no progress in block (would hang) the function's result

The boundary was measured, not assumed: only an untyped string const of length ≠ 1 captures the reference. A char const ('S'), a typed const (Thing: AnsiString = '…'), an integer const and a float const all resolve correctly — because only the string and set constants live in separate name tables keyed without a unit. const T = '' is the nastiest row: it prints nothing at all, which is indistinguishable from a blank.

Fixed at dc3fedb0a (one body per table, the existing name a thin wrapper, so the scoping rules cannot drift between the qualified and unqualified forms), pinned by a test at a29892c8c. Both are compiler/**, so both are INERT UNTIL THE NEXT PIN — the test runs under ./$(COMPILER) and so is live for anyone who builds, but nothing on $(PXX_STABLE) sees the fix.

The expectation, and the half of it that was wrong

Recorded before the re-run, as required. Right about the verdicts:

before after
units-OK (stubbed) 22 22
BOTH-OK / ORACLE-NO / PXX-FAIL 21 / 10 / 176 21 / 10 / 176
units whose verdict moved 0

Corrected 2026-09-16 — this row read 22 / 10 / 175 when written, in this table and in wall 14's, and both were wrong on arrival. The corpus totals were never 22/175 at any tree this ticket has measured. 207 − 22 − 10 = 175 is the whole mechanism: the BOTH-OK count was derived from the units-OK (stubbed) number directly above it and the PXX-FAIL count from the subtraction, instead of being read off the probe's own SUMMARY line — a conclusion written as a caption, in the row a reader is least likely to re-derive because the arithmetic checks out. The two metrics are not the same quantity and must not be inferred from each other. Established by a before/after A/B on 2026-09-16 at 6bc01579f, three foreground chunks of an asserted partition each time (rows = distinct units = 207, both runs): BEFORE (lib/rtl/textfile.pas reverted to HEAD~2) 21 / 10 / 176, AFTER 21 / 10 / 176, 0 units whose row changed across all 207. Nothing between the wall-14 measurement and that A/B could have moved it — the only commits touching lib/ or compiler/ are the two being A/B'd, the probe change e0e2baac8 added counters with byte-identical verdict logic, and the corpus is unchanged since 09-01 — so 21 / 10 / 176 was the true value at walls 13 and 14 as well. The ticket's own summary: field said 21 / 10 / 176 all along and was right; these two tables were the outlier. The units-OK (stubbed) row is left as written: it is a different measurement, it was not re-run here, and nothing in this A/B says anything about it.

A twelfth consecutive null row, and this one null BY CONSTRUCTION rather than by surprise: :1495 was never a HEAD. It sits behind :714 rmdir in the same file, so no unit ever reported it first.

What I got wrong is the finding. I predicted the give-up appeared in ONE detail file, because I had only looked at the head. It was in 134 — one wall seen through 134 importers, which the in: line confirms is cfileutl.pas every single time.

THE PART THAT MATTERS: A PARSER GIVE-UP IS A SECOND BLINDNESS, INSIDE THE INSTRUMENT BUILT TO CURE THE FIRST

PXX_CORPUS_DETAIL exists because a first-error census ranks by queue position and hides everything behind the head. It reports EVERY failure per unit. That is true and it is not sufficient, because statement made no progress in block is the parser ABANDONING THE BLOCK, and everything after the abandoned block is invisible to a per-unit error list just as thoroughly as it is to a head.

Total error lines across the 207 detail files:

before after
error lines 419 776
detail files naming the give-up 134 0

357 error lines were behind that one give-up, including three error KINDS that had never been seen in this corpus at all (cannot override: no virtual method found in parent chain: freeinstance, no overload of tostr, undefined variable (ppureal_bytes)).

The control is what makes that attributable rather than alarming: of the 41 detail files that never carried the give-up, grew=0, shrank=0, same=41. Every one byte-for-byte unchanged. The growth is confined, without exception, to the files where the parser previously stopped reading — so this is the instrument getting its sight back, not a regression.

So the corpus number has a third caveat, alongside the two already recorded (a head is a queue position; a units-blocked count is not a work count): a unit's error LIST is truncated wherever the parser gave up, and nothing in the output says so. Any future census of "how much is left" that predates dc3fedb0a undercounts, and undercounts invisibly.

Behind the head, now

cfileutl.pas has exactly one error left before it: rmdir at :714. That is the next wall and it is RTL — MkDir/RmDir/ChDir plus IOResult plumbing, fpc's caller wrapping it in {$push}{$I-}RemoveDir:=(ioresult=0), with IOResult/LastIOResult living in lib/rtl/textfile.pas:131/:314, i.e. cross-unit state.

Banked separately, and NOT caused by this fix

n := F('x') for a string-returning F and an Integer n compiles with no diagnostic and prints a pointer as a number. It was the one row of a 17-row differential matrix that still diverged from fpc after the fix, which is precisely the shape that reads as a confession — so it was attributed to a RANGE before being attributed to the change, and the pinned pre-fix compiler reproduces it on a program containing neither a qualifier nor a shadowing const. It had been MASKED here by the very defect above. A pre-existing bug can sit behind a second bug, and fixing the front one looks exactly like causing the back one. bug-p-a-string-function-result-assigned-to-an-integer-compiles-silently.

WALL FOURTEEN — cfileutl.pas:714 rmdir, and cfileutl.pas is now CLEAR

System's MkDir / RmDir / ChDir, added to lib/rtl/textfile.pas rather than SysUtils because they report through IOResult and nothing else — they raise nothing and return nothing — and IOResult/LastIOResult were already there. fpc's caller is cfileutl.pas:709:

{$push}{$I-}
 rmdir(d);
{$pop}
RemoveDir:=(ioresult=0);

Landed 387e1d4cc. RTL only, so it is LIVE ON PUSH — built and verified under the pinned compiler, which resolves lib/rtl from the live tree.

The measurement that stopped a wrong reuse

Every IOResult code was produced under fpc 3.2.2 by making the condition happen and reading it back. fpc documents none of them, and two rows disagree with the table this unit's FILE routines already use:

condition errno RmDir MkDir ChDir
does not exist ENOENT 2 2 2 3
name too long ENAMETOOLONG 36 3 3 3
permission denied EACCES 13 5 5 5
already exists EEXIST 17 5
a plain file, not a directory ENOTDIR 20 5 5 5
not empty ENOTEMPTY 39 5

ENOENT is 2 for RmDir/MkDir and 3 for ChDir. fpc separates "file not found" from "path not found" by the operation, not by the errno — the same errno, two answers — which no single errno→code map can express. And ENAMETOOLONG is 3 here where the file table answers 2. So DirErrnoToIO is a second table on evidence, not a second path out of convenience, which is the distinction normalise-dont-special-case.md actually asks for.

The empty path is a no-op returning 0 in all three. That is the row that matters most: passing '' to the syscall reports ENOENT for a call meant to do nothing, and an implementation resolving '' to the current directory would delete or enter it. test/lib_dirio.pas asserts the no-op's absence of effect as well as its code.

FPC runs test/lib_dirio.pas itself and also answers 31 / 31, so the test is an oracle-agreeing spec rather than a spec of our own behaviour. Two positive controls, asserted and branched on: the pre-change textfile.pas refuses the probe, and a one-code perturbation (chdir ENOENT 3 → 2) drops it to 30 / 31 naming exactly that row.

The corpus, against the expectation recorded first

before after predicted?
units-OK (stubbed) 22 22 yes
BOTH-OK / ORACLE-NO / PXX-FAIL 21 / 10 / 176 21 / 10 / 176 yes
units whose verdict moved 0 yes
the :714 wall 120 0 yes
detail files naming rmdir 134 0 yes (strong form)
new head globals.pas:1095 Replace 12 132 yes, 12 + 120
detail files without the wall, unchanged 41 of 41 yes (control)

The BOTH-OK row is corrected from 22 / 10 / 175, 2026-09-16 — see the same correction under wall 13 for the mechanism and the A/B that settled it. The predicted? column still reads yes: the prediction was that the row would not MOVE, and it did not. What was wrong is the level, in both the before and the after cell, which is why a null row can be right and its own table wrong.

A thirteenth consecutive null row, and the fourth in a row where clearing a shared dependency hands its whole population to the next wall in the same import chain.

cfileutl.pas now names itself ZERO times across all 207 detail files. Four consecutive RTL walls (FileAge, the FindFirst family, GetDir, and these three) plus one compiler fix cleared the file entirely.

THE SHRINK, WHICH MY OWN EXPECTATION CALLED A REGRESSION TO EXPLAIN

Total error lines went 776 → 642, and the expectation said a shrink would have to be explained rather than accepted. It is exactly explained: 776 − 642 = 134, all 134 wall files lost exactly one line each, that line was the rmdir line, and every remaining error in every one of them is byte-identical to before. Nothing was hidden behind this wall.

And that BOUNDS the wall-thirteen finding instead of leaving it open. An undefined variable error does not truncate a unit's error list; a parser give-up does, because it abandons the block. So the detail instrument's blindness is specific to the give-up class, not to errors in general — which is a much smaller caveat than "any count may be low", and it is checkable: grep the detail files for statement made no progress.

The two, which was exact after all

The pre-dc3fedb0a reading that "only two errors remain behind that wall" was recorded as a lower bound once the truncation was found. Measured across the three runs, cfileutl.pas's own distinct error LINES were:

run cfileutl.pas's own error lines
before the const fix :714, :1495
after the const fix :714
after rmdir (none)

So the two was exact, not a lower bound. The give-up at :1495 was the last thing in the file, so nothing of cfileutl's own was hidden behind it; the 357 lines it concealed were all in OTHER units of the import chain. The caveat is real for the corpus-wide count and did not apply to that particular two — worth recording, because a correction that is itself imprecise costs the next reader the same measurement.

Wall fifteen — charset, and the strongest null row this ticket has recorded

uses: unit source not found: charset was the sixth-largest head of the 207 and the largest genuinely LIVE one, named directly by 8 units. FPC's charset.pp is its codepage mapping registry, so this is Track B surface, not a compiler gap: the unit simply did not exist here.

Landed 6bc01579f (lib/rtl/charset.pas, 814 lines — the full public surface: loadunicodemapping, three loadbinaryunicodemapping rows, registermapping, registerbinarymapping, both getmap, mappingavailable, getunicode and getascii pairs) and 69ba50571 (BlockRead/BlockWrite at every count-out width, which the binary loader needed and which turned out to be memory corruption in its own right — see bug-p-a-var-parameter-accepts-a-narrower-actual-and-writes-past-it). Byte- identical to fpc 3.2.2 across an 86-line differential probe, with three deliberate divergences, all measured, all in places where fpc corrupts or crashes: bounded hex scans, a >= growth check moved outside the hex branch, and a nil-guard in getascii's buffer form (fpc writes ABuffer^ unchecked, so getascii(c, p, nil, 0) is a runtime error 216 for any unrepresentable character). test/lib_charset.pas pins it, 98 rows, with a parity mode that halts before the two divergence rows so fpc can run the same file: fpc 95/95, pxx 95/95 in parity mode, pxx 98/98 full, and a control at 91/98.

Both are lib/rtl/**, which reaches from the LIVE tree — they are live on push, not inert until the next pin.

The corpus, A/B rather than inferred

before after predicted?
BOTH-OK / ORACLE-NO / PXX-FAIL 21 / 10 / 176 21 / 10 / 176 movement yes, LEVEL no
units whose row changed 0 of 207 yes
regressions none yes
gains none yes

A fourteenth consecutive null row, and in an unusually strong form: not "units-OK is unchanged" but every one of the 207 rows is byte-identical, verdict and first error alike. That is an A/B and not an inference — the BEFORE leg reverted lib/rtl/textfile.pas to HEAD~2 and re-ran the whole corpus, both legs in three foreground chunks of an asserted partition (rows = distinct units = 207 each time).

The prediction was right about the movement and wrong about the level, and the reason is this ticket's own tables. I predicted "units-OK stays at 22" — the movement was zero, as predicted, but the baseline was never 22; it was 21, and it was 21 before the change too. I had taken it from the wall-13 and wall-14 tables, which is where the 22 / 10 / 175 error above was sitting. A stale row in a record is not inert: it was consumed as a premise by the next measurement that cited it, and the only reason it surfaced is that this row was measured by A/B instead of by reading the previous number forward. Both tables are corrected above.

The corpus is a corpus: an fpc loader bug found and NOT fixed

Bisected exactly while writing the differential probe, in fpc's own loadunicodemapping. After a line like 0x8E<tab>#DBCS LEAD BYTE (verbatim from Microsoft's CP932.TXT), a shorter following line 0x8E41<tab>0x4E00 leaves AD in the ShortString buffer; fpc's hex scan is unbounded, eats it, parses $4E00AD and truncates to a word — so U+4E00 registers as 173. Well-formed input, no diagnostic. Recorded here, not filed and not repaired: per the probe's own header, FPC is the corpus and the question is only ever whether it compiles. Our bounded scan is one of the three deliberate divergences above.

2026-09-17, frankB — the 09-17 probe @ 17b8561f2: the globals.pas:502 prediction is measured, and it is the FIFTEENTH null row

Measured at 17b8561f2, binary 93200e4b45db, three foreground chunks of a partition ASSERTED to union back to the glob exactly (69 + 69 + 69, rows = distinct units = 207, no overlap, nothing dropped). The binary was rebuilt after the pull and before the run — converged after 1 round(s) — per PUSH → LET THE PULL SETTLE → REBUILD → MEASURE.

Why this run happened, and what it was testing

decided/decide-old-style-object-types.md prices the object feature at +1 unit and then names a cheaper lever: the 138 blocked units "do not scatter — they land as one group on a fifth wall, globals.pas:502", the var-spelling of a string initialiser, with the instruction "That is Track P, needs no decision, and gates the same 138 — so it is the cheaper lever by a wide margin, and it should be taken before anyone reopens this page."

That lever was taken on 2026-09-16 by frankS (fa397c761, "a named string constant is a string initialiser too"), and fa397c761 is not an ancestor of 9281da35b, the tree the last recorded corpus totals were measured on. So the prediction had never been re-measured and this run is a test of it rather than a re-confirmation. 25 commits touching compiler/** or lib/** sit between the two trees, including the lever and a run of Track B RTL additions (charset, FindFirst/FindNext, MkDir/RmDir/ChDir, GetDir, StdErr/StdOut as real Text files, the SysUtils date converters).

THE EXPECTATION WAS RECORDED BEFORE THE RUN, verbatim: "Given this umbrella's FOURTEEN consecutive null rows, my actual expectation is ANOTHER NULL — the 138 land together on a sixth wall. I am recording that so the result is information either way."

The result

wall 15 @ 9281da35b 09-17 @ 17b8561f2
BOTH-OK 21 21
ORACLE-NO 10 10
PXX-FAIL 176 176

A fifteenth consecutive null row, and the prediction it was testing is now measured stale in this umbrella's own recorded shape. globals.pas:502 is the first failure of zero of 207 units — the lever is taken, and both spellings of the declaration compile under fpc and pxx when probed directly. The 138 were QUEUED behind it, not gated by it. They landed on the next wall.

(Scope: "zero units" is a statement about FIRST failures, which is what the probe reports. It is not a claim that no unit reaches that line behind an earlier error.)

Where the population went, and it is one file again

first failure probe #12 09-17
unknown type: TDoubleRecx86_64/cpuinfo.pas:36 4 140
an object type cannot have a constructor 18 18
unknown type: PUnicodeChar 7
unknown type: TExecuteFlags 127 0
Unsupported tcompilerwidechar size 10 0
uses: unit source not found: charset 6 0
unknown type: d (sizeof(<a PARAMETER>)) 4 0
conditional directive: expected operator 2 0
conditional directive, three named operands 3

Five walls cleared between the two probes and units-OK moved by zero. The whole delivered population is now standing on x86_64/cpuinfo.pas:36, which is the cclasses.pas picture for the fourth time: a histogram of one file's contents. TDoubleRec has gone 4 → 132 → 140 across three measurements while buying nothing, which is the cleanest available statement of this umbrella's own finding — a wall's population counts units QUEUED behind it, never work.

Attributions for the cleared walls, so the delta is not read as one seat's: tcompilerwidechar is 0728155f2 (the pre-pass must not raise an {$error}), charset is 6bc01579f, sizeof(<a PARAMETER>) is a931bef4d, expected operator is eaf776dd8. Only the first and last are mine.

The join instrument is the PREVIOUS SWEEP'S ROWS — keep them

Recorded here because this section gets the join half right and the other half wrong, and the next reader should not repeat it. The by-name BOTH-OK set below lets a later run join the units that COMPILE. The first-failure table above is an aggregate, so it cannot see two units swapping inside one error class — equal totals are satisfiable by a regression and a gain that cancel. The instrument that closes that is the previous sweep's 207 per-unit rows, joined unit by unit, which reports zero verdict changes in either direction rather than an unchanged count.

That is strictly stronger and it is nearly free, but only if the rows still exist: the probe writes them to a session scratchpad under /tmp, which /etc/tmpfiles.d/tmp.conf cleans with D /tmp ... 6h.

CHECK, DO NOT ASSUME, IN EITHER DIRECTION — and this sentence is a correction of the one that stood here for four hours. I first wrote "reaped at six hours" flatly, from the configured age. Measured 2026-09-17 at 18:45 on plexus: my own three chunk files, written 08:29-08:35, were all still present with 207 rows intact at TEN hours, and the oldest surviving file anywhere under /tmp/claude-1000 was 24 hours old. systemd-tmpfiles requires atime, mtime and ctime all to be older than the age, so an active session's tree outlives the number. Six hours is the configured age, not an observed deadline, and reading it as one costs an unnecessary eight-minute sweep — which is the same error as trusting it the other way, just in the direction nobody notices because the work still gets done. One box, one tree, one reading: ls the rows before re-running, and ls them before relying on them. Note also that seven's /tmp is a tmpfs on a different regime, so none of this travels there.

So keep the rows when you finish a sweep — franks-ee did the per-unit join on the first attempt after the [55] fixes purely because a pre-fix sweep was still sitting in the same directory from an hour earlier, and said plainly that coming to it cold he would have compared three numbers like anyone else. Not judgement; kept rows. I ran this total three times on 2026-09-17 and joined per unit on none of them.

AND ALIVE IS NECESSARY, NOT SUFFICIENT — A SECOND CORRECTION TO THIS SAME SECTION, AND IT IS THE ONE THAT WOULD HAVE MISATTRIBUTED A PIN. Everything above establishes that the rows still EXIST; it says nothing about what tree produced them, and I wrote it while holding an assignment whose whole value depends on that. The rows above were produced at 17b8561f2 at 08:26. Measured 2026-09-17: nine commits touching compiler/ or lib/ have landed since, every one of them a Track P fix and three of them mine —

c86e8b29d 6eb1db8b4 efe06a903 170197a54 160684761
2de677672 5c1db8c6b 5ce561a11 ec8a4d88c

— and the corpus totals have moved 21 / 10 / 176 -> 22 / 10 / 175 across two later sweeps by another seat. So these rows are a valid partner for exactly one question and not for the other:

question partner valid against these rows
what did the fleet's HEAD fixes buy since 08:26? a HEAD run now yes, with the nine-commit range named
what does the FLEET'S FLOOR become when the pin moves? a PINNED-binary run no

The second is the assignment, and against these rows it would credit the PIN with nine commits of HEAD movement — the favourable direction, which CLAUDE.md names as the one nobody checks. The instrument for a pin question is a same-tree two-binary A/B, PXXBIN=stable_linux_amd64/.../stable_pinned against compiler/pascal26, both sweeps in one session at one tree, which is the shape wall fifteen and the 401c00f2b row already used. It costs a second eight-minute sweep and that is the price of the question; the alive rows do not buy it, and believing they did is what this correction is for.

The BOTH-OK set, BY NAME — recorded because it never has been at 21

Every previous attempt that moved reported the join by name and it was the valuable half; the 21 has only ever been a count. The next run now has something to join against:

cclasses  cdynset  compinnr  comptty  constexp  crefs  cstreams  cutils
dbgdwarfconst  dwarfbase  fpchash  globtype  macho  optbase  rabase  rescmn
symconst  systems  tokens  version  wasmbase

Three named walls this run produced in Track P's own conditional evaluator

eaf776dd8 replaced the blunt conditional directive: expected operator with refusals that name the operand, and the corpus turned that into three precise one-unit walls rather than two anonymous ones:

unit directive refusal
nld nld.pas:700 right operand of in is not a set constant this pass can read: supported_optimizerswitches
nadd nadd.pas:1352 sizeof cannot size this type here: bestrealrec
hlcgobj hlcgobj.pas:4156 sizeof cannot size this type here: aintmax

supported_optimizerswitches is x86_64/cpuinfo.pas:205, a set constant built by +-joining other named set constants across units — the next step of eaf776dd8's own feature, not a new subsystem. bestrealrec and aintmax are both x86_64/cpuinfo.pas again.

AND THE LINE NUMBERS IN THAT TABLE ARE A FIX, NOT A READING. The probe originally reported these at 1334, 1112 and 1821 — two pointing backwards, one forwards, all three at ordinary statements. Error prints the CURRENT TOKEN's line and the evaluator runs from the directive handler. Fixed in ec8a4d88c, after this run; the directives above were grepped out of FPC's sources independently, before anything was changed. Nothing about the verdicts moved — the fix changes only where a diagnostic points — so the totals in this section stand for 17b8561f2 as measured.


2026-09-17 (frankS) — WALL SIXTEEN, object CONSTRUCTORS, and the SIXTEENTH NULL ROW KILLS THE INSTRUMENT THIS FILE ASKED FOR

Fixed at efe06a903, binary 1e1d3ce55597. Totals 21 / 10 / 176 -> 22 / 10 / 175. One unit. Zero regressions.

That is an ordinary null row and it is not why this section is long. The finding is that the every-failure census — the instrument this umbrella spent six null rows asking for, built on 2026-09-16, and called "the first proposal with a complete failure set behind it instead of a queue position" — predicted this wall's yield NO BETTER THAN THE FIRST-FAILURE CENSUS IT REPLACED. It said 18. The answer was 1.

The prediction, recorded before the run, and it was wrong

Written out in full before the corpus was touched:

The 18 units with {object-constructor} as their COMPLETE set should convert nonzero — between 5 and 18 (BOTH-OK 26..39), the first non-null wall this umbrella has proposed; the 141 {object-constructor, TDoubleRec} units are a pair population and should deliver zero. If the yield is 0, the complete-failure-set instrument is no better than the first-failure census — a negative finding about the method itself.

The pair half was right and bought nothing, as expected. The half the whole census existed to answer was wrong by a factor of 18, and the caveat stated beside it ("a hard-erroring type declaration suppresses downstream analysis, so a complete set of one is necessary, not sufficient") turns out to have been the entire story rather than a hedge.

Why, exactly — and this is the transferable half

A complete-failure-set census is complete only for the SUBJECT unit. Every unit it IMPORTS is still truncated at that unit's own first failure, so the census inherits first-failure blindness ONCE PER IMPORT.

The object-constructor diagnostic is raised in three files and none of them is usually the subject: versioncmp.pas (144 of the 161 reports), cgbase.pas (14), cmsgs.pas (3). When an imported unit stops, everything after that point in that same file is never analysed — so for 17 of the 18 subjects, the "complete set" was complete about a truncated import, not about the program.

The join is exact and leaves nothing to interpretation:

before after
versioncmp — declares its object IN ITS OWN SOURCE PXX-FAIL {object-ctor} BOTH-OK
14 units importing cgbase PXX-FAIL {object-ctor} PXX-FAIL cgbase.pas:401 not a constant
3 units importing cmsgs PXX-FAIL {object-ctor} PXX-FAIL cmsgs.pas:124 undefined variable (fail)

18 = 1 + 14 + 3. The one unit that converted is the one where the wall was in its own source and there was nothing behind it. The other seventeen moved 25 lines further down one file (cgbase.pas 376 -> 401) and 77 lines further down another (cmsgs.pas 46 -> 123).

So this is the cclasses.pas 895 -> 1327 -> 1726 shape for the fifth time, arriving in two new files — and this time it defeated the instrument built specifically to detect it. The census does not see a second wall in an imported file; it sees a truncated import and reports one error with total confidence.

What would ACTUALLY answer the size question

Not a deeper census. Stub the wall and re-run — apply the proposed lowering by hand over a copy of the tree, which is what the 2026-09-16 stub3 arm in decided/decide-old-style-object-types.md did. That arm predicted 22 units for this change. The real fix delivered 22. The stub was right and the census was wrong, because a stub removes the wall for the importing units too and therefore lets their next failure become visible.

Read that as the method, not as a score: a stub answers "what is behind this wall for everyone", a census answers "what did the compiler happen to reach". Only the first is a prediction. Where a stub is cheap, it beats any amount of diagnostic recovery — and this umbrella now has one measurement where the two disagreed by 18x and the stub won.

New first-error distribution, all 175

141  unknown type: TDoubleRec                     (x86_64/cpuinfo.pas:36)
 14  not a constant                               (cgbase.pas:401)   <- NEW
  7  unknown type: PUnicodeChar                   (+ uses cp1251)
  4  undefined variable (entryreal_bytes)
  3  undefined variable (fail)                    (cmsgs.pas:124)    <- NEW
  1  uses: unit source not found: unixcp
  1  uses: unit source not found: heaptrc
  1  undefined variable (swapendian)
  1  undefined variable (align)
  1  conditional directive: the right operand of `in` ...
  1  conditional directive: sizeof cannot size this type here

The two new walls, both named with their in: line rather than guessed

Both first lines carry NO file name — CLAUDE.md's line-31 hazard — and the first reading of 401 here was wrong twice before the in: line settled it.

  1. cgbase.pas:401, 14 units. NR_INVALID = tregister($ffffffff). An enum typecast in a constant expression. Isolated to three lines, and it is NOT about the explicitly-ranged enum it was found on:
    type TE = (a, b, c);
    const K = TE(2);        { pascal26:3: error: not a constant; fpc: fine }
    
    Filed as bug-p-an-enum-typecast-is-not-a-constant-expression, wired here.
  2. cmsgs.pas:124, 3 units. fail; — the standard procedure that exists ONLY inside an old-style object's constructor, and therefore a wall this very fix created the reachability for. Two units in the whole corpus use it (cmsgs, symtable). Behind it in the same file sits SetCodePage at :459. Recorded in feature-p-legacy-value-object-types, which is already wired here.

The RTL half of the residual, which nobody has called out

Of the 17 units that are neither TDoubleRec nor the object wall, 10 are pure RTL gaps, not frontend walls: PUnicodeChar + cp1251 (7), unixcp (1), heaptrc (1), swapendian (1). All five names were checked against lib/rtl rather than assumed — none is present, and the three units are absent outright.

CORRECTED IN PLACE, SAME EVENING: this paragraph first said 12, and the arithmetic beside it summed to 14. It counted entryreal_bytes (entfile, pcp) as an RTL gap. It is not one — entfile.pas:371 declares it as a LOCAL type inside a function, array[0..sizeof(d)-1] of byte over the enclosing function's own parameter, and uses it as a typecast. That is a Track P gap, and it is a TYPECAST gap, which is the same family as the two fixed tonight. So those two units are RTL plus one Track P wall, not RTL. The rest of the 17: nadd and nld (Track P conditional evaluator), fpcp and ppu (mixed with the object wall), machoutils (its own). 10 + 2 + 2 + 2 + 1 = 17.

Per this file's own WALL 5 note, an RTL wall is LIVE without a pin and a compiler wall is not — so if anyone wants delivered units rather than cleared walls, that is where the cheap ones are.


2026-09-17 (frankS, later) — WALL SEVENTEEN, the enum typecast: the SEVENTEENTH null row, and this time it was PREDICTED CORRECTLY

6eb1db8b4 + c86e8b29d, binary bc884808fda5 at c86e8b29d. Totals 22 / 10 / 175 -> 22 / 10 / 175. Unchanged.

The prediction, recorded before the run and in the form the previous section's mistake taught: "another null row; cgbase.pas is a large shared file, so I expect the 14 to land on its next wall; at most cgbase itself converts (23 / 10 / 174), most likely unchanged."

All 14 moved to TDoubleRec. Not one converted, cgbase included. So the 14 that appeared behind the object wall six hours earlier were, once again, a queue — which is now recorded seventeen consecutive times.

The run before this one was CONTAMINATED and is not reported here

I rebuilt compiler/pascal26 while a corpus sweep was in flight, so chunk 0 was measured against one binary and chunks 1 and 2 against another. That run (bgc5b75su) completed with a plausible-looking tally and is discarded. The numbers above are from a clean sweep started after the tree settled, from a checkout equal to origin, with the binary sha and commit recorded before the first chunk. CLAUDE.md names this exact hazard and I walked into it by treating "just rebuild to test a small fix" as separate from "the instrument"; a rebuild IS the instrument here, because the probe invokes the binary per unit.

The state this leaves the corpus in, which is the actionable part

155  unknown type: TDoubleRec          (x86_64/cpuinfo.pas:36)   <- 89% of failures
  7  unknown type: PUnicodeChar        (+ uses cp1251)
  4  undefined variable (entryreal_bytes)
  3  undefined variable (fail)         (cmsgs.pas:124)
  1  uses: unit source not found: unixcp
  1  uses: unit source not found: heaptrc
  1  undefined variable (swapendian)
  1  undefined variable (align)
  1  conditional directive: the right operand of `in` ...
  1  conditional directive: sizeof cannot size this type here

TDoubleRec is now the first failure of 155 of 175 failing units. The argument for taking it next is NOT that count — this file has seventeen rows saying a count is a queue position. The argument is that the corpus is blind behind it: at 89% there is almost nothing left to learn from another sweep until it moves, and every other wall's population is now too small to be informative about anything but itself.

It is feature-b-rtl-has-no-tdoublerec, already in blocked-by, and it is Track B / lib/rtl, which reaches from the live tree — so it delivers without waiting for a pin, unlike both fixes landed tonight. That combination (the only wall wide enough to see past, and the only kind of wall that is live immediately) is the strongest case this umbrella has had for a specific next lever, and it does not rest on a unit count.

2026-09-17 (frankB) — THE PIN FLOOR, v410 -> v411, and the FIRST PER-UNIT JOIN this file has ever carried

Run after pin v411 landed, to answer a question the null rows cannot: not what HEAD can do, but what the fleet's FLOOR becomes when the pin moves. Every $(PXX_STABLE) consumer stands on the floor, not on HEAD.

The instrument, and the trap it walked into first

frankuser specified PXXBIN=stable_pinned against PXXBIN=compiler/pascal26. That A/B is a tautology and the harness refused it: v411 was minted from this tree, so both are bc884808fda5 and the two arms had collapsed into one before the instruction was written. The measurer's own earlier step — a PIN — was the contaminant. Recorded because it is the cheapest possible instance of a rule this file keeps paying for, and because the refusal came from a guard rather than from a reader: tools/fpc_corpus_ab.sh declines when the two legs have the same sha256, on the grounds that an A/B which cannot tell its legs apart cannot fail.

So the legs are the OLD floor against the NEW one, everything else held:

leg binary sha256 provenance
A pin v410 c599e8546121 git show 764ee2ed2:stable_linux_amd64/default/stable_pinned
B pin v411 = HEAD bc884808fda5 compiler/pascal26 at 8d9d69bdc

Both pin identities checked against their own commit subjects and sha256sum, never a --grep. One tree (8d9d69bdcbb8+e3b0c442), one lib/rtl, one corpus, one oracle, one flag set; tree identity re-taken after both legs and unmoved; truncated=0 on both, which is what makes an errs= count a count.

The result

The floor moved 21 / 10 / 176 -> 22 / 10 / 175. That is the canonical notation this file requires; the block below is the probe's own SUMMARY lines quoted verbatim, and its both-ok= spelling is a QUOTATION and not a live claim -- the first draft of this section stated the totals in that spelling and in no other, which would have made this row invisible to the very grep the notation rule exists to enable. Caught by running the file's own check.

A  SUMMARY  both-ok=21 oracle-no=10 pxx-fail=176      (v410)
B  SUMMARY  both-ok=22 oracle-no=10 pxx-fail=175      (v411)

JOIN  identical=43  wall-moved=29  errs-only=134 (up 134 / down 0)
      gained=1  regressed=0  other-verdict=0  only-in-one=0

ZERO REGRESSIONS ACROSS 207 UNITS, and that is the sentence only a join can produce. The three-number reading is +1 unit and is silent on whether anything broke; equal or near-equal totals are satisfiable by a gain and a regression that cancel, and until tonight nobody here had ever checked. The gain is versioncmp.

The headline understates it, and the reason is the interesting part

+1 unit is true and is the least of what moved. 29 units changed their first error — the wall transitions, most common first:

n from to
14 an object type cannot have a constructor unknown type: TDoubleRec
8 Unsupported tcompilerwidechar size unknown type: TDoubleRec
3 an object type cannot have a constructor undefined variable (fail)
1 Unsupported tcompilerwidechar size conditional directive: sizeof cannot size this type here: bestrealrec
1 conditional directive: expected operator the right operand of 'in' is not a set constant this pass can read

And 134 units sat at the identical first error with errs= risen, 134 up and 0 down. The obvious reading is that more is broken. It is the opposite.

A RISING errs= IS NEWLY VISIBLE GROUND — measured, on 5 of 5 sampled

Detail-dumped under both binaries, aasmbase plus four drawn from the errs-only set (aasmdef, comprsrc, nmem, pexpr):

unit v410 errors / files reached v411 errors / files reached too many array initializer elements
aasmbase 2 / 1 8 / 4 present in A, gone in B
aasmdef 2 / 1 8 / 4 present in A, gone in B
comprsrc 2 / 1 8 / 4 present in A, gone in B
nmem 2 / 1 8 / 4 present in A, gone in B
pexpr 2 / 1 5 / 4 present in A, gone in B

Under v410 every one of these units reported two errors, both inside x86_64/cpuinfo.pas, and saw exactly one file. v411 fixes the second of them (cpuinfo.pas:281), and the compiler then walks on into three or four files it had never reached. So the count rose because a wall fell.

THIS IS THE cclasses.pas SHAPE ARRIVING FROM THE OTHER SIDE. Every previous row here recorded a cleared wall delivering its population to the next wall a few hundred lines down the SAME file, converting nothing. This one clears a wall and the population walks into DIFFERENT FILES — which is why it shows up as errs= rather than as units, and why no first-failure census could have seen it. The corpus did not merely gain a unit tonight; it became more informative, and the new information is the stratum below.

THE QUANTIFIER IS NOT MEASURED AND IS NOT CLAIMED: 5 of 5 sampled, not 134 of 134. The mechanism is unanimous in the sample and the sample is small.

The newly visible stratum, BY NAME — none of it had a wall row before

From the sampled units' detail, in files the corpus could not previously reach:

Per this file's own rule these are NOT to be ranked on how many units name them. They are recorded because they are new ground, not because they are large.

The instrument over-reported itself, and that is corrected in the tree

The first join printed moved=163. Only 29 of those were a different first error; the other 134 were the identical wall with a changed errs=. The join keyed on the whole row, errs=N included, so it answered honestly about a question nobody asked — a 5.6x overstatement of wall movement, one hour after I wrote the thing. tools/fpc_corpus_ab.sh now splits WALL-MOVED from ERRS and reports them separately; re-joining the same two leg files with the corrected awk reproduces the split exactly, and an independent Python pass over the same rows agrees. Two implementations that fail differently, which is the only kind of agreement this file accepts.