test/pytest: call the fingerprint module where it lives, not through lib.sh (#432) - #944
Conversation
linuxhikerpm
left a comment
There was a problem hiding this comment.
Reviewed exact head 2776a7ee105f. Not approving yet: PG17/PG18 suites were still in progress at review time, and this SHA still contains _sh(srcdir, expr) so it conflicts with #942.
The direction is right for CONTEXT.md's independence rule. Fingerprint/manifest arms now call test/pgc_fingerprint.py (in-process, or the module CLI for the unprivileged/locale cases) instead of python → bash → lib.sh → python3 → the module. _fp_of / _mf_of preserve the module contracts ("" vs None), and the trailing newline in the fingerprint-from-manifest arm matches fingerprint() itself.
SHELL_REFERENCES as a two-way set equality is the inventory that should have caught #923's coupled file. Assembling _LIB from fragments so the detector does not flag this file is load-bearing; the docstring/f-string/once-per-JoinedStr premises belong there.
Please rebase onto #942 (or land #942 first) so _sh is not still taking a tree it ignores, wait for the suite jobs, and I will approve a green non-conflicting SHA.
Remaining debt you already named is fine for a follow-up: stamp/freshness helpers are still pure shell, and test_the_fix_does_not_rebaseline_stamps_already_on_disk still calls pgc_source_build_dirs out of real lib.sh.
Stage 2: the shell→python direction, first filePushed
Why it had to move rather than be rewritten. 370 pinned the position of Its other three properties were already covered. Verified by running the arms, What the new arm is worth, measured both ways.
So 370's stated reason — that the absent arm returns a pass first — describes a CONTEXT.md's count was wrong: seven shell files, not threeThe entry said seven shell files reach across the boundary. Measured: three.
Both of those names are shell functions defined in Gatepg18a, tree with 370 removed: Remaining in this branch's scope: |
…lib.sh (commandprompt#432) CONTEXT.md's independence rule: the two harnesses are parallel in functionality and independent in implementation. A pytest test that drives `test/lib.sh` is the first measurement wearing a Python wrapper -- it agrees with the shell by construction and can never report it wrong -- so the coupling turns a twin into a mirror, which is what the twin rule exists to avoid. `test_build_refusal.py` was the largest item on that inventory: 36 cross-harness calls behind three helpers. TWENTY-TWO ARE GONE. THEIR SUBJECT WAS NEVER lib.sh. `test/pgc_fingerprint.py` has been the one implementation since commandprompt#907, and `pgc_source_fingerprint` and `pgc_source_manifest` are thin wrappers that shell out to exactly that module. The old path was python -> bash -> lib.sh -> python3 -> the module, so removing the middle two changes no subject -- it removes a wrapper, not a measurement. MEASURED BEFORE CONVERTING A SINGLE ARM, because "it should be the same value" is what this directory refuses to accept: the fingerprint is byte-identical through both paths, the manifest is identical line for line, and both agree across LC_ALL=C, C.UTF-8 and en_US.UTF-8. The hash-of-manifest relation was checked too -- md5 of the manifest text plus the newline `md5sum` reads from the pipe, first 12 hex, equal to the fingerprint and to the shell pipeline on the same tree -- before that arm was converted to compute it in Python. TWO ARMS STILL NEED A SEPARATE PROCESS, and they use the MODULE'S OWN CLI rather than lib.sh: one reads as an unprivileged user, because root ignores `chmod 000` and an in-process call cannot see a denied read, and one varies the locale. That CLI is the entry point lib.sh itself invokes, with lib.sh taken out of the path. PROVEN LIVE rather than assumed green. Both of those arms have `cannot_run` guards, so passing does not mean the body ran: breaking `_fp_cli` to return a constant reddens three arms, and the preconditions are real on this box -- `postgres` as the unprivileged user, two locales installed. THIRTEEN CALLS REMAIN AND ONLY SEVEN ARE DEBT. They drive `pgc_write_source_stamp`, `pgc_source_stamp_path`, `pgc_freshness_report` and `pgc_freshness_verdict`, which are PURE SHELL rather than wrappers over shared code -- so an arm here is a second harness testing the first, and those properties belong to the shell harness. That is the next step, not this one. Two are `test_the_two_fingerprint_implementations_cover_the_same_inputs`, which reaches across ON PURPOSE and should. Its subject is that neither side carries a private copy, which cannot be expressed from one side; the rule's own escape clause says "say which, and say why", and the module docstring and CONTEXT.md now say it. It caught four defects in one day (commandprompt#907) and is what reddens on the FIRST edit if a private implementation returns rather than on the first edit that happens to diverge. Two are a historical-parity arm that embeds the previous shell algorithm as its own fixture -- rule 2, permitted -- bar one call for a directory list. Feeding it the module's `build_dirs` would make it self-contained; that is a change to a historical arm and not in this one. THE INVENTORY IS NOW A MECHANISM. It was prose: falsifiable by hand, but nothing reddened when a new reference appeared, and commandprompt#923 nearly landed a fourth coupled file with a person reading being what caught it. `SHELL_REFERENCES` declares the three files and the mechanism each uses, asserted in BOTH directions -- a new file that reaches in reddens, and a file that stops reaching and stays listed reddens too, which is what stops a record of debt becoming a permanent exemption. A FILE-LEVEL GUARD, which is what CONTEXT.md asks for ("count files, not lines") and also the most it can honestly be: within a flagged file it cannot tell a path joined onto the real tree from the same name joined onto a `tmp_path`, because both are the string `lib.sh` and only the dataflow says which. The limit is stated beside the arm. THREE OF MY OWN MISTAKES, all found by running rather than reading: * I wrote `source_manifest` as a join over (relpath, digest) pairs. `manifest()` returns the joined TEXT, and my verification probe had an `isinstance` fallback that quietly stringified it and then reported the two "identical" -- so the wrong assumption read as VERIFIED until the real call raised. The delegate also returns None unchanged now, because None is the module's "a digest failed" and an `or ""` here would re-create in this file the defect the module exists to refuse. * The detector counted an f-string as TWO references, because the pieces of one are `Constant` nodes of their own. * The detector flagged THIS declaration, because the entries' descriptions named the shell files -- four files where the tree has three. A scan flagging its own test data is the third time that shape cost me a measurement in one session, so the fixtures assemble the name from fragments and the descriptions name the mechanism instead. Prove by removal: planting a cross-harness reference in `pgc_vacuity.py`, a file with none, reddens the inventory arm. Gate: the converted file 36 passed driver-free job 10 files, 183 passed, psycopg absent from the venv full corpus 264 passed with a cluster on pg18a harness_selftest 588 checks, 588 passed + 0 failed + 0 unrunnable, PASSED Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Uf6UoeBRZYLQZa4KxNiw8a
The shell part `test/selftest/370` pinned the position of `plan_marker`'s empty-plan refusal by reading the Python source as text. A shell part can pin text; it cannot run the function, so it could not tell a refusal that still fires from one that has been stranded. That is the cross-harness dependency the two-harness rule removes. What changed: - `test_guards_pinned.py` gains `test_the_empty_plan_refusal_precedes_the_arms_it_protects`. It takes the class off the `expect` fixture, so the file still imports nothing from the harness. - `test/selftest/370-the-plan-marker-guard-must.sh` is deleted. Its other three properties were already covered by arms in `test_guards_pinned.py`; this was confirmed by running them, not by reading them. - `TESTS.md` names the new arm and records what it is worth. - `CONTEXT.md`'s count of shell files that reach across the boundary goes from seven to three. The other four were rule-3 lookalikes: `lib.sh`, `selftest/030` and `selftest/040` matched only the shell functions `pgc_cluster_datadir` and `pgc_cluster_is_ours`, both defined in `lib.sh`. What the new arm is and is not: It discriminates — move the refusal to the end of `plan_marker` and it fails; leave it where it is and it passes. It is not load-bearing today: with the refusal at the end, `plan_marker([], absent=True)` still refuses, because `plan_marker` has no early return. So 370's stated reason, that the absent arm returns a pass first, describes a shape the function does not have. The arm is prospective insurance against a refactor that adds an early return, and it is labelled that way rather than sold as a live hole. Verified on pg18a: selftest 608 checks, 0 FAIL; the driver-free subset 184 passed; the full corpus 269 passed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Uf6UoeBRZYLQZa4KxNiw8a
…th file arrived Rebased onto main (d05e3c3). Deleting `test/selftest/370` turned out to touch three artifacts beyond the part itself, and the rebase surfaced a new item of debt that the inventory arm was built to catch. Artifacts the deletion reaches: - `test/selftest/parts.manifest` still named 370. commandprompt#925's arm "every name in the manifest is a part on disk, so a deletion reddens" caught it, which is the arm doing precisely its job on the first deletion after it landed. - `test/check_ledger.tsv` held 12 rows for 370, all `never`. Removed. - `test/check_ledger_budget.txt`'s census `checks_never_observed_red` goes 826 -> 814. DERIVED from the ledger after the removal rather than typed, because that file says of itself: "it is not a ceiling; it is a measurement that must be true". `suites_not_covered` stays at 250 — deleting a part removes no suite. The 12 ledger rows are also the evidence for the claim made when 370 was deleted. They include "a neutered present arm is caught" and "premise: plan_marker's body was actually cut out of the file": 370 cut the Python body out and re-grepped its own detector. Every arm was a text pin, which is what commandprompt#927's precedent is about. A fourth coupled file arrived with commandprompt#925: `test_mutation_ledger.py` runs `run_all_versions.sh --list-suites`. The set-equality assertion in `test_harness_deps.py` reddened on the rebase naming a file the declaration did not have — the direction the inventory was made a mechanism for, working on something nobody wrote it for. It is now declared, with its mechanism, and CONTEXT.md records both the file and the fact that the arm is what found it. Same mechanism as `test_suite_accounting.py`, so the two are one item of debt twice and should move together. Verified on pg18a: selftest 815 checks, 0 FAIL; driver-free subset 196 passed; the full corpus green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Uf6UoeBRZYLQZa4KxNiw8a
8cdf389 to
98f16c6
Compare
linuxhikerpm
left a comment
There was a problem hiding this comment.
Approving exact head 98f16c6666a5. MERGEABLE. Pytest harness guards are green on this SHA; remaining build jobs were still landing at review time.
Deleting selftest/370 is the right independence move: a shell part that greps Python source cannot tell a live refusal from a stranded one. The replacement test_the_empty_plan_refusal_precedes_the_arms_it_protects reads plan_marker off the expect fixture, so this file still imports nothing, and it says it is prospective rather than load-bearing.
The follow-up commit is what #925's gate is for. Removing 370 without parts.manifest, check_ledger.tsv, and the census would have failed after #925 landed. Declaring test_mutation_ledger.py in SHELL_REFERENCES is the inventory catching a file that arrived after it was written.
_sh(srcdir, expr) is still unread; that is #942, not this PR.
Non-blocking: "if not nodes:" in l will also match a comment that contains those words. Cheap, and the premise arm requires all three hits, so a missing line still fails.
…more of them `test/selftest/360` pinned the SHAPE of `test/pytest/pgc_vacuity.py` with eleven greps: the unrunnable field is written, something reads it, the read reaches `session.exitstatus`, the override is conditional. A shell arm asserting a grep matches cannot prove a python arm is caught -- commandprompt#927's precedent -- and the behaviour is pinned where it can be observed, by four arms in `test_layer.py` that run pytest inside pytest and assert on the inner run's exit status. MEASURED BEFORE DELETING ANYTHING. With `session.exitstatus = EXIT_INCOMPLETE` made unreachable in `pgc_vacuity.py`, the defect exactly as it shipped, those four arms go from `4 passed` to `2 failed, 2 passed`. Two rather than four is correct, not partial: the other two assert exit 1 for a run with a real failure and exit 0 for a run with nothing unrunnable, and neither outcome moves. Restored and compared byte-for-byte afterwards. THE PART'S OWN JUSTIFICATION WAS STALE. It said those arms "need pytest, psycopg and a virtualenv; CI installs none of them". CI has a `pytest-guards` job that installs pytest pinned from `requirements-test.txt`, asserts psycopg is absent, and runs the database-free file list, which contains `test_layer.py`. WHAT IS LEFT IS THE PERMITTED CROSS-REFERENCE, and there are three of them where the part checked one. The INCOMPLETE exit code was checked; the closed list of unrunnable reasons and the one-line shape an unrunnable check prints were not, though both are written down twice in two languages exactly as the exit code is. All three are now parsed out of both files -- never restated here, which would test this file against itself -- and each has a drifted fixture so the comparison can fail. Two instrument defects of mine, both caught by the new arms' own premises: - The shape parse took the first line matching `UNRUN `, which in `pgc_vacuity.py` is the docstring that spells the shape out for a reader. Requiring a quote before the marker selects the quoted string in both languages and excludes the prose, which opens with a backtick. - The drifted-shape fixture wrote ONE space where the real shape has two, so the parse found nothing and the comparison was empty-against-real. That "differs", for the wrong reason. Ledger: 360's rows regenerated with `pgc_ledger.py merge` from a real pg18a run rather than hand-edited, 17 rows out and 16 in, and `checks_never_observed_red` re-derived to 813. One check name was duplicated inside the part and is renamed, so one ledger row no longer covers two checks. `suites_not_covered` stays at 250: rewriting a part's checks removes no suite. Verified on pg18a: selftest 814 checks, 0 FAIL. `pgc_ledger.py gate` with the registered list and `--against auto`: rc=0, `new this run=0`, ceiling 250. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Uf6UoeBRZYLQZa4KxNiw8a
…ytest `test/selftest/380` covers two of @linuxhikerpm's commandprompt#897 findings, and they have different subjects. Finding 1 is `test/pgc_fingerprint.py`, which `lib.sh` runs with the system interpreter and which is not part of the pytest harness: checking it is this part's own business and it stays. Finding 2 is `test/pytest/pgc_cluster.py`, and the nine arms that read it as text are gone. SEVEN of the twenty-one checks had a cross-harness subject, not twenty-one. The other fourteen read `pgc_fingerprint.py`, `lib.sh`, or fixtures the part writes itself. Counting the file rather than the arms would have deleted coverage that was never debt. `make_cluster`'s cleanup shape moved to `test_build_refusal.py` as `test_the_cleanup_guard_has_the_shape_the_leak_needs`, beside the behavioural arm that provokes a real failed setup and asserts no directory is left. It is a SOURCE check and it is labelled as one: `make_cluster` fails exactly one way in that arm, a missing `pg_config`, while three more properties decide whether the guard works -- surviving a KeyboardInterrupt, stopping a postmaster it already started, and re-raising rather than returning None -- and two of them cannot be provoked from a test at all. `test_this_module_keeps_no_private_fingerprint` moved for the same reason. FIVE MUTATIONS SAY THE MOVED ARMS DISCRIMINATE, each asserted to have applied, each leaving the module well-formed, restored byte-for-byte afterwards: BaseException narrowed to Exception source arm cluster.stop() removed source arm bare re-raise turned into pass source arm AND the behavioural one a private hashlib.md5 added the no-private-digest arm shutil.rmtree(root, ...) removed source arm AND the behavioural one The two that redden both are the two whose effect reaches the filesystem. The three that redden only the source arm are exactly the properties the behavioural arm cannot see, which is why they are written down separately. One mutation did not apply first time: the `cluster.stop()` pattern assumed twelve spaces of indentation and the call sits at sixteen, inside a nested `try`. The harness refused to report a result rather than printing a green. AND ONE THING WENT WRONG THAT IS WORTH MORE THAN THE CHANGE. I ran the selftest before naming the two new arms in TESTS.md, so the run was red on the doc-coverage check, and then merged that log into `check_ledger.tsv` -- whose entire subject is which checks have ever been red. It recorded a red for part 350's "every test file and every test in the corpus is named in TESTS.md", a check that failed only because my own change was half-finished. Reverted with `git checkout HEAD --`; there is no second copy to repair from. The merge step now refuses a log that is not DEFINITELY green: a numeric `checks run:` line, a floor on it so an aborted run cannot pass, zero FAIL lines, and rc=0. The absence of a FAIL line is not enough, because an aborted run has none either. Verified on pg18a: selftest 805 checks, 0 FAIL; `pgc_ledger.py gate` rc=0 with `new this run=0` and the ceiling still 250; driver-free subset 198 passed; the full corpus 283 passed. 380's ledger rows regenerated from that green run, 21 out and 12 in, and `checks_never_observed_red` re-derived to 804. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Uf6UoeBRZYLQZa4KxNiw8a
Stage 3:
|
mutation of pgc_cluster.py |
what reddens |
|---|---|
except BaseException: → except Exception: |
the source arm |
cluster.stop() removed |
the source arm |
bare raise → pass |
the source arm and the behavioural one |
a private hashlib.md5 added |
the no-private-digest arm |
shutil.rmtree(root, …) removed |
the source arm and the behavioural one |
The two that redden both are the two whose effect reaches the filesystem. The three
that redden only the source arm are exactly the properties the behavioural arm cannot
see. One mutation did not apply first time — the cluster.stop() pattern assumed
twelve spaces and the call sits at sixteen, inside a nested try — and the harness
refused to report a result rather than printing a green.
A mistake worth more than either change
I ran the selftest before naming the two new pytest arms in TESTS.md, so the run
was red on the doc-coverage check — and then merged that log into
check_ledger.tsv, whose entire subject is which checks have ever been red. It
recorded a red for part 350's "every test file and every test in the corpus is named
in TESTS.md": a check that failed only because my own change was half-finished.
Reverted with git checkout HEAD --. There is no second copy to repair from.
The merge step now refuses a log that is not definitely green — a numeric
checks run: line, a floor on it so an aborted run cannot pass, zero FAIL lines,
and rc=0. Absence of a FAIL line is not enough: an aborted run has none either.
That is the same shape as a pending-count that cannot see a job which never started.
@jdatcmd — this is the second independent way that artifact can be poisoned in one
day. The first was a regeneration in a tree copied without .git, where 15 checks
failed on "premise: the source tree is a git checkout". Both are a red that the
check did not earn, and both were caught by a person looking rather than by
anything in the tooling. It may be worth pgc_ledger.py merge refusing a log that
does not reconcile to zero failures unless --mutation is given, since a mutation
run is the one case where reds are the point.
Gate, on f11b3386
harness_selftest.sh 805 checks run, 0 FAIL
pgc_ledger.py gate rc=0, new this run=0, ceiling 250
driver-free pytest subset 198 passed
full pytest corpus 283 passed
Ledger rows regenerated from that green run for both parts: 360 went 17 → 16, 380
went 21 → 12, and checks_never_observed_red was re-derived to 804 rather than
typed.
Still to do on this branch
selftest/350 is the last shell file, and it is the largest: 50 checks, and its
arms split three ways rather than two. Roughly eighteen have a pytest twin already;
four have a shell subject (ci.yml) and stay; and about sixteen are self-tests of
its own detectors for properties the pytest side implements but does not test —
the contents-list anchor rule, and the mode-counting rule's edges (an id of fewer
than three words, an id named twice, stopping at the next heading, reading a row's
value rather than a digit in its label). Those have to be ported, not deleted, and
the pytest counting rule needs a seam so a fixture can reach it.
Plus the 7 pure-shell calls still in test_build_refusal.py, and the two
run_all_versions.sh --list-suites callers.
…r than die
Forty-six of `test/selftest/350`'s fifty checks had their subject on the other
side of the harness boundary: they globbed `test/pytest/*.py` and parsed Python out
of it, or swept the markdown in that directory. The pytest corpus asserts all
three properties natively in `test_docs_cover_the_corpus.py`, where the subject
is, so the shell copy could only ever agree with it.
TWO RULES MOVED RATHER THAN BEING DELETED. Both were implemented on both sides and
SELF-TESTED only on the shell side -- the side that cannot run the corpus it
counts. Deleting those fixtures would have left the Python implementation with no
fixtures at all, so they went with them:
the mode-counting rule's edges an id of fewer than three words, an id named
twice, stopping at the next heading, section 3's
back-references, and the row reader taking the
value cell rather than a digit inside its label
the contents-list anchor rule GitHub's derivation, the broken link that
shipped, and a control beside it
`_named_modes_in(text)` and `_stated_row(text, label)` are new seams so a fixture
can reach the rules at all; `_named_modes()` delegates and the 19 existing arms are
unchanged. The row reader's fixture makes the label digit and the value DIFFER --
label "section 2", value 9 -- because 350's own fixture had both as 2 and so could
not see the bug it was written for.
WHAT STAYS IS THE CI WORKFLOW, which belongs to neither harness. A shell part may
read `ci.yml` for the same reason it may read the Makefile, and no pytest arm can
assert that the gate runs pytest without assuming the thing in question. One arm is
added while the part's subject is being settled: the job must ASSERT psycopg is
absent rather than assume it.
THE STALE JUSTIFICATION, IN TRIPLICATE. TESTS.md section 6 said "Nothing runs
pytest. Not run_all_versions.sh, not any workflow under .github/", and concluded
the .sh half was therefore the enforcement. CI has a `pytest-guards` job. That
claim was load-bearing in three places at once -- TESTS.md section 6,
selftest/360 and selftest/380 -- each asserting the behavioural half could not run
in CI. Nothing was wrong in any of them; the REASON was, three times, and no arm
reddens on a stale justification. All three corrected.
AND ONE RULE-3 INSTANCE OF MY OWN. Two arms reddened with
`needs-a-cluster:test_docs_cover_the_corpus.py` after the anchor fixtures moved in.
The cause was a literal `test_harness_deps.py` inside sample markdown:
`_mentioned_files` reads a corpus file name in code as "this file drives that one",
so the classifier called the file cluster-bound. Sample markdown about a heading,
read as a dependency. The names are assembled now, the same convention
`SHELL_REFERENCES` uses -- CONTEXT.md's rule 3 caught for the third time in this
PR, twice by me.
WHERE THE TWO HARNESSES NOW STAND. Shell parts holding a reference to anything
under `test/pytest/`: ONE, and it is deliberate. `selftest/360` reads
`pgc_vacuity.py` to compare the three values both harnesses write down. The
inventory claimed seven; the measured answer was three plus a deleted fourth; the
end state is one.
Verified on pg18a: selftest 757 checks, 0 FAIL; `pgc_ledger.py gate` rc=0 with
`new this run=0` and the ceiling still 250; driver-free subset 207 passed across 11
files; the full corpus 292 passed. 350's ledger rows regenerated from a green run,
53 out and 5 in, and `checks_never_observed_red` re-derived to 756.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uf6UoeBRZYLQZa4KxNiw8a
Stage 4:
|
jdatcmd
left a comment
There was a problem hiding this comment.
Approving at 70a90498, 13/13 green. Your two judgement calls below, both answered from the code rather than from the principle — and the first has a sharper answer than the framing you offered.
The headline verified, with a control that moves
I measured your "exactly one shell part references anything under test/pytest/" myself, non-comment lines only:
#944 360-an-unrunnable-pytest-test-must.sh 1 -> 1 file
main 350: 3 360: 1 370: 1 380: 1 -> 4 files
I nearly reported this as wrong. My first sweep matched pgc_fingerprint.py and found three files, not one. That file lives in test/, not test/pytest/ — it is the shared module, which is the entire point of this PR. The claim is about the pytest corpus, and anchoring on pytest/ is what makes the measurement match the sentence.
1. Keep 350's ci.yml arms — and one of them is mandatory, not merely permitted
Your reasoning was that ci.yml is not a test suite, so a shell part may read it as it may read the Makefile. Agreed, but there is a stronger argument sitting in the part already, and it changes one arm from allowed to required.
Of the five arms, four duplicate test_harness_deps.py, which already reads ci.yml:
selftest 350 test_harness_deps.py
ci.yml exists yes yes
derives the file list from NO_CLUSTER yes yes
derives the pins from requirements-test yes yes
the job installs no driver yes yes
`pytest-guards:` job exists yes NO
Those four are not redundancy — they are the standing rule that a property ships in both harnesses, independently implemented. Two arms reading one file for one property is exactly the pair the rule asks for.
The fifth cannot live on the pytest side at all. the gate runs the harness guards asserts the existence of the job that would run the assertion. Delete pytest-guards: from ci.yml and a pytest-side version of that arm does not fail — it never executes. That is the self-referential hole, and 350 is the only place it can be closed.
So the boundary I would write down is not "a shell part may not name pytest" but "a shell part may not read or execute the pytest corpus". ci.yml is configuration, and an arm about whether CI runs a suite has to live outside the suite.
2. Moving the two rules in was right, and the objection answers itself
Your alternative reading — a rule implemented on both sides should be self-tested on neither — fails on the standing rule. If a property ships in both harnesses, its self-test ships in both too; the question is only where the fixtures live, and fixtures belong on the side that can exercise the rule against the real corpus. The shell side cannot run the corpus it counts, which is the fact that decided it.
"Nine arms added in a PR about removing coupling" is not the contradiction it looks like. The PR's subject is coupling, not arm count. Moving a self-test from a harness that cannot exercise it to one that can reduces coupling and increases evidence at the same time; those pull the same way. A rule whose fixtures were deleted would pass because the document happens to agree with it today, which is the vacuity this directory spends its time refusing.
_named_modes_in and _stated_row are a fair price. They are seams inside one harness, not across two.
On your answer to the remainder
Go ahead on the python→shell remainder now. Both answers above are stable and neither depends on anything still moving: keep ci.yml arms in a shell part, and put a rule's fixtures where the rule can be run. The seven pure-shell calls in test_build_refusal.py are the interesting half, since pgc_write_source_stamp and friends have no python implementation to call — that is a decision per property, as you said, rather than a port.
Recorded
Your #943 finding is the one I would want on the record most: a rebase changed the accounting line from four terms to five, your fixture kept writing four, and both readers rejected it and agreed about it — so the arm whose entire subject is that they disagree on exactly one shape would have passed measuring nothing. A rebase is the most ordinary way for a fixture to stop expressing its question, and there is no sweep for it. The premise arm is what caught it, which is the argument for premise arms in one sentence.
…946) #944 landing took the census 826 -> 756 while this branch takes it to 833, so neither number survives the compose. Arithmetic says 763. The rows are the source and the number is derived, so it came from a run rather than from addition. THE LEDGER AUTO-MERGED, WHICH IS THE DANGEROUS CASE, so I checked it against the run rather than against its own count. It took main's deletions and this branch's additions correctly: part 350 main 5 mine 53 -> 5 main's deletion kept part 370 main 0 mine 12 -> 0 main's deletion kept part 380 main 12 mine 21 -> 12 main's deletion kept part 410 main 111 mine 118 -> 118 this branch's addition kept Verified by set comparison against a real run on the composed tree, not by count: checks in the run absent from the ledger, 0. AND ONE STALE ROW THAT WAS MINE TO CLEAN. Three ledger rows did not appear in the run. Two are pre-existing conditional checks in part 330 that also sit in main's ledger. The third is the check THIS BRANCH RENAMED: main's ledger carries it under the old name, this branch adds it under the new one, and the merge kept both. reports vanished=3 and cannot pair them, because the new name was already present. Dropped after checking it carried nothing: verdict , no mutation, so no history was lost. A row with a date or a mutation would have had to travel with the rename instead. selftest on the composed tree 763 checks, 762 passed + 1 failed the one failure was the census arm naming 763, which is the arm doing its job ledger 763 -> 762 rows after the stale row census derived from the ledger, not typed ceiling 250, untouched driver-free pytest 207 passed Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EbyGSaU93XYQr8aH4NrUiw
…946) #944 landing took the census 826 -> 756 while this branch takes it to 833, so neither number survives the compose. Arithmetic says 763. The rows are the source and the number is derived, so it came from a run rather than from addition. THE LEDGER AUTO-MERGED, WHICH IS THE DANGEROUS CASE, so I checked it against the run rather than against its own count. It took main's deletions and this branch's additions correctly: part 350 main 5 mine 53 -> 5 main's deletion kept part 370 main 0 mine 12 -> 0 main's deletion kept part 380 main 12 mine 21 -> 12 main's deletion kept part 410 main 111 mine 118 -> 118 this branch's addition kept Verified by set comparison against a real run on the composed tree, not by count: checks in the run absent from the ledger, 0. AND ONE STALE ROW THAT WAS MINE TO CLEAN. Three ledger rows did not appear in the run. Two are pre-existing conditional checks in part 330 that also sit in main's ledger. The third is the check THIS BRANCH RENAMED: main's ledger carries it under the old name, this branch adds it under the new one, and the merge kept both. The rename-scan subcommand reports vanished=3 and cannot pair them, because the new name was already present. Dropped after checking it carried nothing: verdict "never", no mutation, so no history was lost. A row with a date or a mutation would have had to travel with the rename instead. selftest on the composed tree 763 checks, 762 passed + 1 failed the one failure was the census arm naming 763, which is the arm doing its job ledger 763 -> 762 rows after the stale row census derived from the ledger, not typed ceiling 250, untouched driver-free pytest 208 passed AND A DEFECT IN THIS COMMIT'S OWN MESSAGE, fixed by amend: I wrote the first version through an unquoted heredoc, so the backticked words rename-scan and never were run as commands and left holes in the text. The message is written from a file now. A commit message is the durable record and a hole in it is a hole in the record. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EbyGSaU93XYQr8aH4NrUiw
…mpt#946) `merge` already refused a log that does not RECONCILE, and reconciliation is not the property that matters. Both logs that poisoned this ledger on the day it landed reconciled: - mine, on commandprompt#925: 827 records against `checks run: 827`, fifteen checks red because I had copied the tree without `.git`. Flawless self-consistency, fifteen environment reds, and I caught it by noticing the failures rather than by the tool refusing them. - @OffgridwithJD's, on commandprompt#944: one FAIL from an unfinished change, recording a red for part 350's doc-coverage check that the check never earned. Two independent routes on day one, from the two people who knew the tool best. A third costs no imagination: a run against a stale `.so`. THE RATE IS THE ARGUMENT, not either instance. AN ENVIRONMENT RED AND A REAL REGRESSION ARE IDENTICAL IN THE LOG. Nothing in a RESULT record says which, so the tool cannot infer it and now makes the caller assert it -- the same move `check_ledger_budget.txt` already makes when it names a census apart from a ceiling. `merge` refuses a log carrying any FAIL unless the caller says which kind of red it is: `--mutation NAME` (exists, a deliberate break) or `--reds-are-real` (new, a genuine observation of the code under test). Refused BEFORE any row is built, so a declined merge is never half-applied. NOT "refuse FAILs unless --mutation", which @OffgridwithJD proposed and we rejected together. A genuine CI red is the most valuable row this ledger can hold and has no mutation to name, so that rule would refuse precisely the entry the ledger exists for -- the deadlock the budget file already argues against for `checks_never_observed_red`. A DEFAULT REFUSAL IS AFFORDABLE BECAUSE NOTHING AUTOMATED CALLS `merge`. `grep -rn 'pgc_ledger.py merge' .github/ test/` finds one hit and it is the gate's own advice string. `gate` is the automated path. That was the cost I expected to have to weigh and it does not exist. Both harnesses, independently implemented. The shell twin builds its own fixtures and names no pytest file; the pytest twin drives the tool directly. EIGHT EXISTING CALL SITES UPDATED, AND I CHECKED EACH RATHER THAN CHASING GREEN. Four arms broke, all of them using a red log as a VEHICLE for a different subject -- duplicate detection, rename grouping, date monotonicity, mutation attribution. None asserts that a red merges without a reason, so passing the new flag keeps each testing what it tests. One needed more than that: the control in `test_a_mutation_names_one_check_not_every_casualty` was named "the same log merges without --mutation", which stops being what it demonstrates. It now says the refusal above is --mutation-across-two-checks and not the log. Proved by removal, and by a control that must not move: guard neutered (`if False`) the new arm FAILS, alone control an all-PASS log merges with no flag, rc=0 driver-free pytest 196 passed Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EbyGSaU93XYQr8aH4NrUiw
First cut at CONTEXT.md's harness-independence debt, on jd's instruction.
The rule: the two harnesses are parallel in functionality and independent in implementation. A pytest test that drives
test/lib.shis the first measurement wearing a Python wrapper — it agrees with the shell by construction and can never report it wrong, so the coupling turns a twin into a mirror.The inventory is smaller than its own headline
CONTEXT.md said "36 calls" for
test_build_refusal.py. Counted by mechanism rather than by line — which is the rule that inventory states — the real executable coupling across the whole corpus is six sites in three files:pgc_cluster.pyonce,test_build_refusal.pythree times,test_suite_accounting.pytwice. The twolib.shwrites intest_build_refusal.pyare fake trees undertmp_path— rule 2, not references, exactly as the inventory says.22 of the 36 calls are gone, and their subject was never
lib.shtest/pgc_fingerprint.pyhas been the one implementation since #907, andpgc_source_fingerprint/pgc_source_manifestare thin wrappers that shell out to exactly that module. The old path was python → bash → lib.sh → python3 → the module. Removing the middle two removes a wrapper, not a measurement.Measured before converting a single arm, because "it should be the same value" is what this directory refuses to accept:
Two arms need a separate process and use the module's own CLI — the entry point
lib.shitself invokes, withlib.shout of the path: one reads as an unprivileged user (root ignoreschmod 000), one varies the locale.Proven live rather than assumed green. Both have
cannot_runguards, so passing does not mean the body ran: breaking_fp_clireddens three arms, and the preconditions are real here —postgresas the unprivileged user, two locales installed.13 calls remain and only 7 are debt
pgc_write_source_stamp,pgc_source_stamp_path,pgc_freshness_report,pgc_freshness_verdict— pure shell, not wrappers over shared code, so an arm here is a second harness testing the first. Those properties belong to the shell harness. Next step, not this one.test_the_two_fingerprint_implementations_cover_the_same_inputsasserts neither side carries a private copy — a property that is the relationship, so it cannot be expressed from one side. The rule's own escape clause says "say which, and say why"; the module docstring and CONTEXT.md now say it. It caught four defects in one day (Two implementations of the source fingerprint produced four defects in one day #907) and reddens on the first edit if a private implementation returns, not the first edit that happens to diverge.The inventory is now a mechanism
It was prose — falsifiable by hand, but nothing reddened when a new reference appeared, and #923 nearly landed a fourth coupled file with a person reading being what caught it.
SHELL_REFERENCESdeclares the three files and the mechanism each uses, asserted in both directions: a new file that reaches in reddens, and a file that stops reaching and stays listed reddens too — which is what stops a record of debt becoming a permanent exemption, the way every hand-maintained exempt list here has gone wrong.A file-level guard, which is what the rule asks for ("count files, not lines") and the most it can honestly be: within a flagged file it cannot tell a path joined onto the real tree from the same name joined onto a
tmp_path, because both are the stringlib.shand only the dataflow says which. The limit is stated beside the arm.Three of my own mistakes, all found by running
source_manifestas a join over(relpath, digest)pairs.manifest()returns the joined text — and my verification probe had anisinstancefallback that quietly stringified it and then reported the two "identical", so the wrong assumption read as verified until the real call raised. The delegate now returnsNoneunchanged, becauseNoneis the module's "a digest failed" and anor ""would re-create here the defect the module exists to refuse.Constantnodes of their own.Gate
Merged with main
d24e1003. Touchestest/pytest/andCONTEXT.mdonly — no shell file changes, so it cannot move any shell-side count.Overlap to know about: #942 also edits
test_build_refusal.py(it removes_sh's unusedsrcdirparameter). Whichever lands second will want a look at_sh's signature; the two changes are independent in substance.🤖 Generated with Claude Code
https://claude.ai/code/session_01Uf6UoeBRZYLQZa4KxNiw8a