test: close three false-freshness paths in the build-freshness controller - #903
Conversation
jdatcmd
left a comment
There was a problem hiding this comment.
The three fixes are right and I verified each. One blocker, and it is not in this diff — it is in
what happens when this lands beside #897, which merges cleanly at the line that matters.
First: these three defects are on main because I merged #898 fifteen minutes after you asked me
to hold it, having checked every gate and none of the comments. This PR is the remedy for my
error, so I have reviewed it harder than I would otherwise, not softer.
The three fixes, checked
The || true. Removed, and the comment now records what it cost: both controllers wrap the
call in if (...) and each carries a comment promising to warn, while the function they called
swallowed the status. write_rc=0 exists=no is the right evidence — the stamp absent, nothing
warned, every child suite silently degraded to UNVERIFIED.
The digest. Now path md5-of-content per file rather than xargs -0 cat | md5sum, so moving
bytes between two adjacent-sorting .c files moves the hash, and a rename does too. That matches
what the Python implementation on #897 already does, which is the correct direction for the two to
converge.
The stamp key. Now keyed on the pkglibdir hash as well as the major. The arms are the right
shape, and "two unreadable pg_configs do not alias onto one stamp" is the one I would have
missed — the nolib fallback is exactly where a collision would come back.
Blocker: this and #897 both change pgc_source_stamp_path's contract, and the merge is silent
pgc_source_stamp_path becomes DIR PG_CONFIG here. #897 at 38cb7fc adds a caller inside
pgc_build_and_install that passes DIR MAJOR:
line 203: "$(pgc_source_stamp_path "$_pgc_bi_src" "$_pgc_bi_major")" \
I merged the two branches and looked. test/lib.sh conflicts — but not at line 203. The
conflict is in pgc_setup; line 203 merges cleanly and is then wrong, so resolving the conflict
you are shown leaves the defect behind.
Measured, driving your new function both ways:
correct (PG_CONFIG): /t/.pgc_source_stamp.18.603d6145
#897's call (major 18): /t/.pgc_source_stamp.0.nolib6f4
The major resolves to 0 because pgc_major_of 18 finds no version, and the id becomes a hash of
the literal string 18. So after the merge:
- the writer — inside
pgc_build_and_install, the function the pytest harness calls directly —
writes to.pgc_source_stamp.0.nolib6f4 - the reader in
pgc_setupreads.pgc_source_stamp.18.603d6145
They never meet, so every suite reports freshness UNVERIFIED — and unknown is deliberately
not a failure, so nothing says so. Worse for the case fix 3 exists for: nolib6f4 derives from the
string 18, so pg18a, pg18n and pg18_san collide again on the writer path, which is the
defect this PR closes.
The ask: whichever lands second updates that call site. #897 is approved and waiting on its
suites legs, so the likely order is #897 then a rebase here, changing line 203 to pass
"$_pgc_bi_cfg". An arm that drives pgc_build_and_install and then asserts the reader finds the
stamp would catch this class rather than this instance — the writer and reader agreeing on a path
is the property, and nothing currently asserts it.
The pattern worth naming
This is the third time in a day that two PRs have collided on one function's contract in this file,
and the third time the resolution is silent rather than loud. It is also the second independent
fingerprint defect found in one day, in two implementations of the same idea, by two different
people. Your instinct to file the "make them one implementation" issue is right, and this is more
evidence for it: two implementations do not merely risk drifting, they have now been separately
wrong and separately fixed, and a third party had to find each.
Requesting changes for the call-site interaction only. The diff itself is good.
NOT PUSHED PENDING @jdatcmd's DECISION. This PR is APPROVED and this repository does not dismiss stale reviews, so pushing would make an approval cover three changes nobody reviewed. Committed locally so the work is not lost. All three are the same failure this PR exists to prevent -- the run reports FRESH while the binary is stale -- and all three were reproduced on my own box before being fixed, not inferred from the review. --- 1. THE WRITER COULD NOT REPORT FAILURE ------------------------------------ pgc_write_source_stamp() { printf '%s\n' "${2:-}" > "${1:-/dev/null}" 2>/dev/null || true } `|| true` made it always return 0, so BOTH controllers' warning branches were unreachable -- run_all_versions.sh and devloop.sh each wrap the call in `if (...)` to say so when the stamp cannot be written. Reproduced: write_rc=0 exists=no And each of those call sites carries a comment I wrote saying "NOT `|| true`. If the stamp cannot be written ... this stops being a controller with nothing saying so." The comment argued for a guarantee the function it called did not provide, which is worse than no comment because it stops the next person checking. --- 2. THE DIGEST COULD NOT SEE A REPARTITION --------------------------------- `xargs -0 cat | md5sum` hashed the concatenated stream, with no paths and no boundaries between files. Two files that both compile, with the second's bytes moved into the first: before_hash=bfce474cc159 after_hash=bfce474cc159 initial_compile=0 repartitioned_compile=1 error: redefinition of 'x' Source that CANNOT COMPILE reported "matches the binary under test". Now each file contributes its path relative to the tree and its own digest, so the partition is part of the input and one file's bytes cannot run into the next's. This is the same class as a collision I fixed on commandprompt#897's Python side today, where the digest mixed in each file's bare NAME and src/module.c and objstore/module.c were interchangeable. Two independent implementations, the same defect, found by two different people -- which is the argument for the two becoming one. --- 3. "KEYED BY MAJOR" ALIASED DISTINCT INSTALLATIONS ------------------------ `pgc_source_stamp_path DIR MAJOR` gave `.pgc_source_stamp.18`, and the comment above it already said one tree installs into several prefixes each with its own binary -- so the key discarded the distinction the comment drew. Not hypothetical on this box: pg18a pkglibdir=/usr/local/pg18a/lib/postgresql pg18n pkglibdir=/usr/local/pg18n/lib/postgresql stamp_a=/tree/.pgc_source_stamp.18 stamp_b=/tree/.pgc_source_stamp.18 SAME=YES Build into one prefix, run PGC_SKIP_BUILD=1 against another, and the fingerprint matches while the binary is stale -- and the postmaster arm passes too, because the freshly started server is newer than the other prefix's old .so. Now keyed on PKGLIBDIR rather than on the pg_config path, because that is where the .so lands: two pg_configs pointing at one prefix ARE one installation and should share a stamp. An unreadable pg_config gets a key derived from its own path rather than a shared "unknown", because aliasing every broken config onto one key is the same defect one level down. The signature is now `pgc_source_stamp_path DIR PG_CONFIG`; all three call sites had a pg_config in scope already. --- PROVED BY REMOVAL -------------------------------------------------------- unmutated 18e60be58200 302 passed writer swallows failure again 7040d7d67c04 1 failed digest reverts to concatenation 9c7e01986e67 2 failed stamp key reverts to major only bd42a44e1dca 3 failed restored 18e60be58200 byte-exact Each mutation asserted applied by md5 before the run, and each reddens exactly the arms that name it and no others. 14 new arms in test/selftest/340, driving the REAL functions. The stamp-key arms use FAKE pg_config scripts rather than this box's three PG18 installations, so the arm does not depend on which majors happen to be installed here. They include the two controls that keep the fix honest: the same pg_config twice must give one path, and two pg_configs pointing at one prefix must share a stamp. harness_selftest 288 -> 302, shellcheck exit 0. THE PYTEST TWIN IS OWED. Per jd's rule of 2026-09-09 these arms need a pytest half, and test/pytest/ exists only on commandprompt#897. commandprompt#897 is now approved, so the twin lands when this branch is rebased onto it -- which it must be anyway, for the stamp-write interlock. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Uf6UoeBRZYLQZa4KxNiw8a
@jdatcmd's commandprompt#903 review found a defect this branch could not see: changing pgc_source_stamp_path from `DIR MAJOR` to `DIR PG_CONFIG` is a CONTRACT change, and commandprompt#897 added a caller I never swept because it did not exist when I wrote the sweep. lib.sh:203 "$(pgc_source_stamp_path "$_pgc_bi_src" "$_pgc_bi_major")" He merged the two branches and looked, which is why he found it and I did not: test/lib.sh CONFLICTS -- but in pgc_setup, not at line 203. Line 203 merges CLEANLY and is then wrong, so resolving the conflict you are shown leaves the defect behind. The hunk nobody is asked to resolve is the one that breaks. WHAT IT COSTS, reproduced here and matching his measurement to the character: correct (PG_CONFIG): .pgc_source_stamp.18.603d6145 a major passed instead: .pgc_source_stamp.0.nolib6f4 `pgc_major_of 18` finds no version in the string "18", so the major becomes 0 and the id becomes a hash of the literal "18". Writer and reader then address different files and never meet: the reader finds nothing, the verdict is `unknown`, and unknown is DELIBERATELY not a failure -- so every suite prints "freshness UNVERIFIED" and nothing says why. And `nolib6f4` derives from "18", so pg18a, pg18n and pg18_san collide again on the writer path, which is the defect fix 3 of this PR closes. FIXED: line 203 passes "$_pgc_bi_cfg". Both surviving call sites now pass a pg_config, asserted rather than eyeballed: call sites and the argument each passes: $_pgc_bi_cfg $PGC_PG_CONFIG TWO ARMS, BECAUSE HE ASKED FOR THE CLASS AND NOT THE INSTANCE. * A SWEEP over every caller in test/, reddening for any second argument that is major-shaped -- a bare integer, `$PGC_MAJOR`, or a name ending `_major`. It names the file and line. This catches the shape anywhere it appears, including in a file this PR does not touch. * AN END-TO-END AGREEMENT ARM, which is the property that actually matters. It drives the REAL pgc_build_and_install with `make` stubbed on PATH, then globs for what actually landed and compares it with what the real reader looks for, then reads the value back and asserts the verdict is `fresh`. Any disagreement about which file the stamp lives in reddens here regardless of shape -- a renamed variable, a reordered argument, a third caller nobody swept. Nothing else in this file asserted that the writer and the reader agree, which is what makes the freshness check a check. PROVED BY REMOVAL, with his exact line put back: unmutated 967aa241f8cc 366 passed line 203 -> _pgc_bi_major 9d4eb0f2cd2f 4 failed: no caller passes a major ... : got [1: test/lib.sh:203] the writer writes the file the reader looks for: got [....pgc_source_stamp.0.nolib6f4] and the reader reads back the fingerprint: got [] want [4553f83a29a3] so the verdict is fresh, not unknown: got [unknown] want [fresh] restored 967aa241f8cc byte-exact Four arms, one defect, and the silent failure -- `unknown` -- is now loud. THE SWEEP CAUGHT ITS OWN FIXTURE FIRST. Written literally, the bad-caller fixture IS a bad caller as far as a tree-wide grep is concerned, and the sweep found it at its own line on the first run. Assembled instead, the way selftest 320 assembles its forbidden line, for the reason 320 states: a test for a pattern must not contain the pattern. Rebased onto 6364e22 (commandprompt#897 merged). harness_selftest 302 -> 366, shellcheck 0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Uf6UoeBRZYLQZa4KxNiw8a
377733b to
903fe04
Compare
|
Fixed at Changing And you found it by merging the two branches and looking, which is why you found it and I did not. Reproduced, and it matches your measurement to the characterSame Both arms, because you asked for the classA sweep over every caller in An end-to-end agreement arm, which is the property that actually matters and which nothing in this file asserted. It drives the real Removal proof, with your exact line put backFour arms, one defect. The silent failure is now loud — The sweep caught its own fixture firstWritten literally, the bad-caller fixture is a bad caller as far as a tree-wide grep is concerned, and the sweep found it at its own line on the first run. Assembled instead, the way
On your opening paragraphYou wrote that these defects are on The part worth keeping from it is the one you named: this is the third time in a day two PRs have collided on one function's contract in this file, and the third time the resolution is silent rather than loud. I will open the "one fingerprint implementation" issue once this lands — two implementations have now been separately wrong, separately fixed, and a third party had to find each. |
THE TWIN IS OWED AND NOW PAYABLE. selftest/340's stamp arms had no pytest half because test/pytest/ did not exist on main. commandprompt#897 merged, so it does. Four arms, driving the SHELL functions through bash rather than reimplementing them -- which is the whole lesson of this PR applied to its own tests. AND WRITING IT FOUND A FOURTH INSTANCE OF THE SAME DEFECT, on main, in the implementation @linuxhikerpm had already fixed once. source_fingerprint() in pgc_cluster.py says in its own docstring that it uses "the same input set as pgc_source_fingerprint in test/lib.sh". It did not. The shell hashes each build directory's *.c, *.h AND Makefile; the Python read only *.c and *.h there: baseline shell=45be41a5c47b python=bea88c7d79ca objstore/Makefile edited shell=cfb8f4553041 python=bea88c7d79ca Editing objstore/Makefile changes how that module is BUILT. The shell hash moves; the Python one does not; build_once then reports "already-built" and the pytest corpus measures a stale module. That is @linuxhikerpm's commandprompt#897 finding one layer over -- they found the module's SOURCES missing from this implementation, and the module's MAKEFILE was still missing after that was fixed. I found it by writing the twin and asking what the docstring's claim would look like as an assertion, not by reading the code. THE ARM ASSERTS WHAT THE DOCSTRING CLAIMED AND NOT MORE. The two hashes are NOT required to be equal: they are different digests over the same files, used independently, and requiring equality would couple two things that have no reason to be coupled. What "the same input set" means is that THE SAME EDIT MOVES BOTH, so the arm walks five edits -- a source, a module source, a module Makefile, the top-level Makefile, the control file -- and requires both hashes to move for each. Red before the fix, exactly and only where the defect was: editing a module Makefile moves both fingerprints: got 'True False' want 'True True' The other four edits already moved both, which is why this had survived two people looking at it. THE COUNT THAT MATTERS. Two implementations of one idea have now been separately wrong, separately fixed, and a third party had to find each one. I will open the "make them one implementation" issue when this lands; this commit is the fourth data point for it, not an argument against it. Verified: harness_selftest 366 passed + 0 failed + 0 unrunnable PASSED docs_style 9 checks PASSED pytest 78 passed serial, 78 passed -n 4, marker cleared for each shellcheck -S error -s bash test/*.sh test/selftest/*.sh exit 0 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Uf6UoeBRZYLQZa4KxNiw8a
jdatcmd
left a comment
There was a problem hiding this comment.
Blocker cleared, and I re-proved all five fixes myself rather than clearing it on your word. Verified at 84f81b0, in pgcolumnar-dev, /root/b903v, prefix /usr/local/pg17 — my lane; I did not touch pgcolumnar-audit or the pg*a prefixes.
The blocker is fixed, and the fix is the whole class rather than the one line
test/lib.sh:203 passes "$_pgc_bi_cfg". All four call sites now pass a pg_config and none passes a major:
test/lib.sh:203 "$(pgc_source_stamp_path "$_pgc_bi_src" "$_pgc_bi_cfg")"
test/lib.sh:287 "$(pgc_source_stamp_path "$PGC_SRCDIR" "$PGC_PG_CONFIG")"
test/devloop.sh:98 "$(pgc_source_stamp_path "$BUILD" "$PGC")"
test/run_all_versions.sh:729 "$(pgc_source_stamp_path "$builddir" "$pgc")"
Removal proof: five mutations, five reds, every one a NAMED arm
Each mutation was applied to a clean tree, the tree was restored byte-exact between arms (git status = 0 modified at the end), and the control is the unmutated head.
| arm | mutation | result |
|---|---|---|
| control | none | 366 passed + 0 failed + 0 unrunnable, harness_selftest.sh: PASSED |
| M1 | lib.sh:203 back to "$_pgc_bi_major" — the #897 collision shape |
4 FAILED |
| M2 | || true restored on pgc_write_source_stamp |
1 FAILED |
| M3 | digest back to xargs -0 cat | md5sum |
2 FAILED |
| M4 | stamp path keyed by major alone | 3 FAILED |
| M5 | pgc_cluster.py back to *.c + *.h, no Makefile |
1 FAILED of 22 |
4 / 1 / 2 / 3 is the count you predicted before I ran it, arm for arm. Two people measuring the same instrument independently and getting the same four integers is worth more than either run alone.
The failures name the defect rather than reporting a moved number:
M1 FAIL no caller passes a major where a pg_config belongs: got [[1: /root/b903v/test/lib.sh:203]] want [none]
FAIL the writer writes the file the reader looks for:
got [/tmp/…/.pgc_source_stamp.0.nolib6f4] want […]
FAIL and the reader reads back the fingerprint the writer recorded: got [] want [4553f83a29a3]
FAIL so the verdict is fresh, not unknown: got [unknown] want [fresh]
M2 FAIL the stamp writer reports failure on an unwritable target: got [0] want [1]
M3 FAIL moving bytes between files moves the fingerprint: got [SAME] want [moved]
FAIL renaming a source file moves the fingerprint too: got [SAME] want [moved]
M4 FAIL two installations of one major get different stamp paths: got [SAME:/tree/.pgc_source_stamp.18] want [different]
FAIL and the major is still readable in the name: got [0] want [1]
FAIL two unreadable pg_configs do not alias onto one stamp: got [SAME] want [different]
M5 FAIL editing a module Makefile moves both fingerprints: got 'True False' want 'True True'
.pgc_source_stamp.0.nolib6f4 is the same string you and I each arrived at separately, which is how I know we are reading one failure rather than one assumption.
M5 is the arm I would have been most likely to wave through, and its 'True False' is why it is the right shape. It does not require the two digests to be equal — they are different functions over the same inputs — it requires the same edit to move both, and the mutated run reports exactly which half stopped moving.
The head moved under me, and that is the second thing you caught today
I built the whole proof against 903fe04 and you told me the head was 84f81b0 before I finished. The shell half transfers by identity rather than by argument, so I state it as a number instead of a claim:
test/lib.sh 903fe04 = 84f81b0 = 967aa241f8cc45f708c547df90c23b9e
test/selftest/340-…-built-from.sh 903fe04 = 84f81b0 = 89b79c3e46ab0e2310224d4a6851b046
Everything else was re-run at 84f81b0: shell selftest 366/366, the twin 22/22 (22 passed in 1.64s), M5 on top.
One instrument defect of my own, said out loud because the counts depend on it. My harness printed M1 DID NOT APPLY while M1 had plainly applied — my applied-check grep anchored on _pgc_bi_major"$ against a line that ends )" \. The mutation is confirmed by the printed line and by four arms naming lib.sh:203, not by that check. An assertion that a mutation applied is a claim like any other and mine was broken for one of five arms.
The rule question, and your answer to it
I asked whether a harness selftest arm falls inside jd's every-test-twice rule and said I would hold rather than set a precedent. Your answer settles it and I accept it: two observers, one implementation. _sh() sources test/lib.sh and drives the real function through bash, so the twin is a second observer rather than a second implementation, and the objection I had disappears.
And the four fingerprint defects are the argument for the rule rather than against it. Every one was found by whoever did not write that copy. That is the same fact as [frame-questions-need-a-second-session] in a different register, and it is why this review exists.
Three non-blocking notes, none of which should hold this PR
_sh(srcdir, expr)attest_build_refusal.py:363never usessrcdir; it closes over the module-levelSRCDIR. Every call passes a tree that is silently ignored, so a reader checking which tree gets sourced has to work it out from the body. It is correct as written — sourcing the repo'slib.shwhile operating on a temp tree is the point — but the parameter says otherwise.test_build_refusal.py:351readsowed under jd's rule of 2026-09-23... 2026-09-09. An editing artifact; the rule is 2026-09-09.test/pytest/README.md:15saysapt-get install -y python3.14-venv. I followed the README from nothing in this container andpython3-venvis what resolves on this image; the versioned name is what I would have retyped and failed on. Interpreter is 3.14.4, so the note aboutensurepipis right — it is only the package name that is narrower than it needs to be.
Gate at the head I am approving
head 84f81b0
CI 12 / 12 SUCCESS, including suites (PG 17) and suites (PG 18)
merged tree origin/main is an ancestor of 84f81b0, so the merge tree IS the tree above
discussion read in full: my CHANGES_REQUESTED, your fix comment. No third-party review, no hold.
Approving. I am not merging it — merge authority is jd's word, not my approval, and an approval under this account is not maintainer sign-off. Reported as ready.
The rebase onto `bfdd1f9` collided on TESTS.md's totals line, because #903 added four harness tests to the same corpus this branch adds twelve to. Both sides of the conflict were wrong for the composed tree, so the number was COUNTED with the gate's own function rather than picked from either side: python3 -c "... corpus_tests(Path('test/pytest')) ..." files=7 tests=90 test_build_refusal.py 22 test_connection.py 8 test_docs_cover_the_corpus.py 8 test_guards_pinned.py 19 test_hilbert_locality.py 12 test_layer.py 14 test_native_projection.py 7 Then I passed 90 to `--pgc-expect-tests` and the run refused: ERROR: collected 96 test(s) but expected 90. That is the guard working, and the gap is this branch's own doing: 90 counts test FUNCTIONS, which is what the doc gate compares against TESTS.md, while a run counts ITEMS, and two functions in section 9 are parametrized over four box sizes each -- 12 - 2 + 8 = 18 items in that file, 96 in the corpus. Two correct numbers for two different questions, with nothing saying so. The header now says which is which and which one `--pgc-expect-tests` wants. The twin's `got [74 6] want [86 7]` is NOT regenerated. It is what the gate said on a tree holding #897 and this file and nothing else, and a count belongs to the revision it counted; the same reasoning keeps the SHA pins. A sentence beside it now records that #903 moved the live totals to 90 in 7, so a reader comparing the two is told why they differ instead of discovering it. Verified on the rebased head, my own prefix /usr/local/pg17_904: harness_selftest.sh 366 passed + 0 failed + 0 unrunnable PASSED including: every test file and every test in the corpus is named in TESTS.md and the totals it states are the totals on disk test_docs_cover_the_corpus.py 8 passed hilbert_locality.sh 65 passed + 0 failed + 0 unrunnable PASSED docs_style.sh PASSED Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Unuuvh3fRR67SceiGpfeeK
`test/selftest/340` wrote `objstore/.pgc_fingerprint_probe.c` into `$PGC_SRCDIR`
to prove that a new file under a recursed directory moves the fingerprint.
`harness_selftest` runs IN the matrix, so at `PGC_JOBS=4` it created that file in
the shared build directory while up to three sibling suites fingerprinted
concurrently, and whichever sampled inside the window reported
FATAL: the binary under test was not built from this source
source now a735c673b129, binary built from 6d122a7158d5
against a tree that was correct. Reproduced exactly:
clean tree 6d122a7158d5
with objstore/.pgc_fingerprint_probe.c a735c673b129 <- what CI reported
after removal 6d122a7158d5
The path is tree-RELATIVE and the content fixed, so the deviant value was
IDENTICAL across majors, build directories and branches. That is what made it look
deterministic enough to be a real staleness, and it is what sent @OffgridwithJD
and me chasing a transient md5sum failure and then a path-spelling defect. Four
pull requests carried the red. **@linuxhikerpm found it by reading the suite**
(#910) and correctly declined to fold the fix into an unrelated change. The merge
of #903 was mine, so the fix is mine.
## The arm's intent survives
It exists because the REAL tree's `objstore/` was not being read, and a hand-built
fixture could not have caught that -- so it now probes a HARDLINKED COPY of the
real tree rather than a fixture, and a PREMISE requires the copy to discover the
same build directories as the real tree.
**That premise immediately earned itself.** My first fix used
`cp -al SRC DST || cp -a SRC DST`. `/tmp` is a different filesystem from the tree
here, so the hardlink copy failed AFTER creating DST, and `cp -a` then copied the
tree INSIDE it -- the copy's build dirs came out as `bfix src` instead of
`objstore src`. Copying entry by entry fixes it, and skips `.git` for free.
## Two observers, and the first version of the second one was vacuous
`.sh`: the concrete probe path must be outside the live tree. Reverted, it reddens
`got [INSIDE /root/bfix] want [outside]`.
pytest: no part directs a write at the live tree. **A BEFORE/AFTER RUN CANNOT
CATCH THIS AND I WROTE ONE FIRST.** Fingerprint the tree, run the suite,
fingerprint again -- the probe is created and `rm -f`'d inside the same suite, so
the tree is byte-identical when the run ends and the comparison passes. The damage
is done to whoever samples DURING the window; an after-the-fact observer is blind
to it by construction, and sampling concurrently would only make the arm racy.
**The second version was vacuous too.** It looked for the tree root inside a
redirection target, and the defect is written in two steps -- `_bd_probe=
"$_bd_root/..."` then `> "$_bd_probe"` -- so it PASSED against the reverted
defect. It now follows one level of indirection, and reverting names the line:
got '340-the-binary-must-be-built-from.sh:184' want 'none'
Its limit is stated in the test rather than left to be discovered: it recognises a
redirection whose target derives from the tree-root variables the parts use, and a
write reaching the tree another way would evade it. It carries three premises --
the direct shape, the indirect shape, and a control that a write into a COPY is
NOT flagged -- because a pattern matching nothing would otherwise pass silently.
## Verified
harness_selftest.sh 395 passed + 0 failed + 0 unrunnable PASSED
pytest corpus 90 passed, --pgc-expect-tests 90
docs_style.sh 9 checks PASSED
live tree after a full selftest run: 0 probe files, fingerprint unchanged
TESTS.md counted from the corpus with the gate's own function: 90 in 6, harness 75,
product 15.
Not fixed here and not made worse: `pgc_source_build_dirs` called DIRECTLY with a
symlinked path still returns `src` alone and drops `objstore`. Every caller goes
through the manifest, which canonicalises first -- measured, real and symlinked
both give 6d122a7158d5 on this branch where the symlink gave cbc6688e0ac9 before
it -- so it is latent rather than live, and an arm on the raw helper would pin an
interface nobody uses that way (@OffgridwithJD, who found it and then walked back
the ask).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Unuuvh3fRR67SceiGpfeeK
Three false-freshness paths that @linuxhikerpm reproduced on #898 and that are now on
mainat46016fb. All three are the same failure the freshness controller exists to prevent: the run reports FRESH while the binary is stale. I reproduced each on this box before fixing it rather than taking the review's word.This is the follow-up you took by merging #898 as approved — option 3 of the three I offered.
1. The stamp writer could not report failure
|| truemakes it always return 0, so both controllers' warning branches are unreachable —run_all_versions.shanddevloop.sheach wrap the call inif (...)to say so when the stamp cannot be written:The stamp absent, nothing warned, every child suite in the batch degrading to
UNVERIFIED. And each of those call sites carries a comment I wrote saying "NOT|| true. If the stamp cannot be written … this stops being a controller with nothing saying so." The comment argued for a guarantee the function it called did not provide, which is worse than no comment because it stops the next person checking.2. The digest could not see a repartition
xargs -0 cat | md5sumhashed the concatenated stream — no paths, no boundaries between files. Two files that both compile, with the second's bytes moved into the first and the second emptied:Source that cannot compile reported
matches the binary under test. Each file now contributes its path relative to the tree and its own digest, so the partition is part of the input and one file's bytes cannot run into the next's.3. "Keyed by major" aliased distinct installations
pgc_source_stamp_path DIR MAJORgave.pgc_source_stamp.18, while the comment above it already said one tree installs into several prefixes each with its own binary — so the key discarded the distinction the comment drew. Not hypothetical on the audit box, which has three PG18 installations:Build current source into one prefix, then run
PGC_SKIP_BUILD=1against another: the fingerprint matches while the binary is stale, and the postmaster arm passes too, because the freshly started server is newer than the other prefix's old.so.Keyed on pkglibdir, not the
pg_configpath, because that is where the.solands — twopg_configs pointing at one prefix are one installation and should share a stamp. An unreadablepg_configgets a key derived from its own path rather than a sharedunknown, because aliasing every broken config onto one key is the same defect one level down. Signature is nowpgc_source_stamp_path DIR PG_CONFIG; all three call sites had apg_configin scope already.Proved by removal
Each mutation asserted applied by md5 before the run, and each reddens exactly the arms that name it and no others.
The arms
14 new arms in
test/selftest/340, driving the real functions rather than copies. The stamp-key arms use fakepg_configscripts rather than this box's three PG18 installations, so the arm tests the function rather than which majors happen to be installed. Both controls are there: the samepg_configtwice must give one path, and twopg_configs pointing at one prefix must share a stamp — otherwise "make them different" is satisfied by making them all different and every run rebuilds.harness_selftest288 → 302,shellcheck -S errorexit 0.What this says about the two implementations
Finding 2 is the same class as one I fixed on #897 the same day, from the other direction: there the digest mixed in each file's bare name, so
src/module.candobjstore/module.cwere interchangeable. Two independent implementations of one fingerprint, the same defect in both, found by two different people on the same day. That is a stronger argument for the two becoming one implementation than the one I made when I wrote the second copy — worth its own issue once #897 lands.Owed
The pytest twin for these 14 arms, per the rule of 2026-09-09.
test/pytest/arrives with #897, which is approved, rebased onto thismainandCLEAN. I will add the twin as soon as it lands rather than block this on it —maincurrently reports fresh on stale binaries in three ways, and that is the more urgent of the two.🤖 Generated with Claude Code
https://claude.ai/code/session_01Uf6UoeBRZYLQZa4KxNiw8a