Skip to content

test: the merge summary shows the majors distribution, not their union (#1048) - #1054

Merged
jdatcmd merged 7 commits into
mainfrom
test/1048-merge-summary-shows-the-distribution
Sep 14, 2026
Merged

jdatcmd merged 7 commits into
mainfrom
test/1048-merge-summary-shows-the-distribution

Conversation

@jdatcmd

@jdatcmd jdatcmd commented Sep 14, 2026

Copy link
Copy Markdown
Collaborator

Closes #1048.

pgc_ledger.py merge printed a union over rows, and a union cannot represent a minority set. Merge rows carrying {18} into a ledger whose rows carry {15,16,17,18,19} and the union does not move, so the line was byte-identical on a correct merge and an incorrect one.

It was the one statistic that could not see the only defect this summary has ever had, and it was the only one the merge emitted.

The defect it could not show, twice in three hours

rows written against caught by
#1041 12 at 18 934 at 15;16;17;18;19 CI's suites (PG 17) leg
#1042 8 at 18 1209 at 15;16;17;18;19 a manual uniq -c, locally

Both times the merge printed majors ... 15, 16, 17, 18, 19. The operator was not ignoring the output; the output agreed with them. A roll-up that cannot represent the failure is worse than no summary, because it actively confirms the wrong answer.

Before and after, same fixture

CORRECT merge (five logs, one per major)
    majors: uniform, all 5 rows carry 15;16;17;18;19
      rows 5 = sum of buckets 5

DEFECT merge (one PG18 log)
    majors: NOT UNIFORM -- 2 distinct sets over 5 rows
           3 rows  15;16;17;18;19
           2 rows  18
      rows 5 = sum of buckets 5

Under the union both printed the identical line. The reconciliation is there because this repo requires a list-derived claim to carry inputs == sum(buckets) beside it.

The arm holds the discrimination, not the wording

It merges the same two checks two ways and requires the two summaries to differ. Asserting on one output alone would pass against the union for any string containing 15, 16, 17, 18, 19.

The correct arm uses five logs, not one log naming five majors: the same name twice in one log is a duplicate sharing a row, which _by_run treats differently, and using it would have made the control unfaithful. My first version did exactly that and I replaced it.

Two premise assertions come first, so the arm cannot pass while proving nothing about the summary:

premise: the correct merge wrote the full major set     15;16;17;18;19
premise: the defect merge wrote the minority set        18

Removal proof

The mutation restores union semantics while keeping the new output shape, so what goes red is the distribution and not the rewording.

mutation APPLIED   9b90bae1d34ca3ab49f9d974fc20cf51 -> dfd2c05b0039efb25e3b91ae8cb54f8b
mutant PARSES
accounting: 2 pass + 1 fail + 0 unrun = 3
FAILED test_the_merge_summary_distinguishes_a_minority_major_set_from_a_uniform_one
RESTORED to 9b90bae1d34ca3ab49f9d974fc20cf51

A caution learned the hard way while reviewing #1052, and it applies to every mutation proof in this repo. Asserting the source is restored by md5 does not prove the runtime is. A same-size edit applied and restored inside one second leaves a .pyc that Python still considers valid, because it validates on (source mtime in whole seconds, source size) — so the import path silently keeps running the mutant while md5 says clean. The run above cleared __pycache__ before every arm. Worth adding to the protocol.

Also fixed

test/pytest/TESTS.md described the ledger as five tab-separated columns and omitted majors from the list — stale from the day that column landed (#1010) until now. The file has six.

Not in scope, deliberately

Whether merge should refuse a non-uniform result is a live design question. #1048 argues both sides and explicitly asks that it not be settled by the reporting change; the gate already refuses the same fact later and more expensively, so this is the earlier and cheaper observation of it, not a replacement.

Verification

guard leg, rebased on 9140b2f:  322 passed / 808 checks / 0 fail / 0 unrun
test_mutation_ledger.py:         29 passed / 158 checks

guard_tests moved 317 -> 318 on this branch against bf31e2f, while #1052 derived 321 against the same commit. Both are right for a tree that is not this one. Re-derived by collection on the merged tree: 322 tests collected.

No shell file touched. test_mutation_ledger.py has no .sh twin and never has — it drives the real Python tool, on that file's own stated reasoning that "a Python twin of a Python tool would agree with itself". Flagging it explicitly in case the both-harnesses rule is meant to reach tool tests too.

🤖 Generated with Claude Code

https://claude.ai/code/session_01NhwXKAgSmYDUjteWkfajHK

jdatcmd and others added 2 commits September 13, 2026 19:56
#1048)

`pgc_ledger.py merge` printed a UNION over rows, and a union cannot represent a
minority set. Merge rows carrying {18} into a ledger whose rows carry
{15,16,17,18,19} and the union does not move, so the line was byte-identical on a
correct merge and an incorrect one. It was the one statistic that could not see
the only defect this summary has ever had, and it was the only one emitted.

The defect occurred twice in three hours, to the same person, with a written note
about it in between: #1041 wrote 12 rows at `18` against 934 uniform ones, caught
only by CI's `suites (PG 17)` leg; #1042 wrote 8 against 1209, caught by a manual
`uniq -c`. Both times the merge printed `majors ... 15, 16, 17, 18, 19`. The
operator was not ignoring the output. The output agreed with them.

It now prints the distribution, says NOT UNIFORM when there is more than one set,
and prints `rows N = sum of buckets N` beside it, per the house rule that a
list-derived claim carries its reconciliation.

The arm holds the DISCRIMINATION rather than the wording: it merges the same two
checks two ways and requires the summaries to differ. Asserting on one output
alone would pass against the union for any string containing the five majors. The
correct arm uses five LOGS, not one log naming five majors, because the same name
twice in one log is a duplicate sharing a row and would make the control unfaithful.

Removal proof: restoring union semantics while KEEPING the new output shape
reddens the discrimination assertion, so what is load-bearing is the distribution
and not the rewording. Mutation asserted applied by md5, mutant asserted to parse,
restore asserted by md5.

Reporting only. Whether merge should REFUSE a non-uniform result is a live design
question and is deliberately not settled here.

Also: TESTS.md described the ledger as FIVE tab-separated columns and omitted
`majors` from the list, from the day that column landed (#1010) until now.

guard_tests 317 -> 318, re-derived by collection against bf31e2f, never by
arithmetic.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NhwXKAgSmYDUjteWkfajHK
Both branches touch expected_tests.txt and CHANGELOG.md. Kept both rationales in
each; no content dropped from either side.

guard_tests: 318 was derived on this branch against bf31e2f and 321 on #1052
against the same commit. Both are right for a tree that is not this one. Derived
by collection on the merged tree: 322 tests collected, 322 passed, 808 checks,
0 fail, 0 unrun.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NhwXKAgSmYDUjteWkfajHK

@OffgridwithJD OffgridwithJD left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving. CI 14/14, 0 pending, read from daba39a. The core change is right and I verified it live rather than from the diff — one finding, on a defensive line rather than on the fix.

Verified

Drove the real merge on the exact shape of #1041/#1042: 20 rows carrying all five majors, a log observed on 18 only introducing two new checks.

  ledger: rows=22 | runs=1, distinct checks this merge=2, observed red ever=0, never=22
    majors: NOT UNIFORM -- 2 distinct sets over 22 rows
          20 rows  15;16;17;18;19
           2 rows  18
      rows 22 = sum of buckets 22

The minority set is visible. The union could not have shown it — that is the whole defect, and this closes it. Cross-checked against the ledger file the merge actually wrote: same two buckets, same counts.

Finding: the reconciliation guards the comprehension, not the display

dist = collections.Counter(MAJOR_SEP.join(sorted(v[0])) for v in rows.values())
...
print(f"      rows {len(rows)} = sum of buckets {sum(dist.values())}")

The comment says it is there "so a bucket lost to a sort or a filter is visible rather than inferred". It catches a filter. It cannot catch a bucket lost in the display, because dist is built by consuming rows.values() exactly once — every row lands in exactly one bucket, so sum(dist.values()) == len(rows) holds by construction regardless of what the print loop does.

Mutation on your branch, truncating the display loop to the top bucket ([:1]), md5 applied and restored against 9b90bae1:

=== MUTANT: the display shows only the top bucket ===
    majors: NOT UNIFORM -- 2 distinct sets over 22 rows
          20 rows  15;16;17;18;19
      rows 22 = sum of buckets 22

Two rows went unprinted and the reconciliation balanced. The dropped bucket is the minority one — which is precisely the failure this summary exists to catch, and precisely what #1041 and #1042 were.

So the line is not vacuous, but it guards the step that cannot go wrong and not the step that can. A version with teeth accumulates during the print loop and reconciles against what was actually emitted:

shown = 0
for maj, n in sorted(dist.items(), key=lambda kv: (-kv[1], kv[0])):
    print(f"      {n:>6} rows  {maj}")
    shown += n
print(f"      rows {len(rows)} = sum of buckets printed {shown}")

Under the same mutation that reads rows 22 = sum of buckets printed 20, which is the diagnostic the reader needs.

Not blocking: the shipped behaviour is correct and the line is defensive. But by this repo's own rule that a wrong comment is an input to the next bug, the comment currently claims a guarantee the code does not provide, and the next person to shorten that loop will be told everything balances.

On not merging your own work on your own approval

Agreed, and it is worth recording why it bit: I approved #1052 on a verification that confirmed _at(2) was restored and that 70 of 70 pgc_skip sites read correctly, and never asked what an EMPTY group does. My own six mutations did not ask either. The fabrication bug was inside the proof I wrote and the review you ran, and neither instrument was pointed at it.

jdatcmd and others added 2 commits September 13, 2026 20:33
…nted (#1048)

Review found the reconciliation guarded the one step that cannot go wrong.

`sum(dist.values())` equals `len(rows)` BY CONSTRUCTION: `dist` is a Counter built
by consuming `rows.values()` exactly once, so the two agree whatever the display
does. It could only ever catch a filter on the comprehension, and never a bucket
lost in the print loop -- while the comment claimed it made "a bucket lost to a
sort or a filter visible rather than inferred", which is a guarantee the code did
not give.

Measured by @OffgridwithJD on this branch: truncating the display loop to `[:1]`
drops a bucket and the reconciliation still balanced at 5 = 5. The bucket it drops
is the MINORITY one, which is the exact thing this whole summary exists to show.

`emitted` now accumulates inside the branch that prints, and the line reads
`rows N = sum of buckets printed N`.

Two arms hold it. The first re-adds the bucket counts parsed out of the output and
requires them to equal the stated total; the second requires that total to still
account for every row. Parsed PER LINE rather than from the whitespace-joined
block: joined, a bucket's major set runs into the next line's first word, so
"... 2 rows 18" plus "rows 4 = ..." yields a phantom "18 rows" and the first
version of this arm summed 26 against 4. It failed for that reason and the parse
was fixed rather than the expectation.

Removal proof, isolating: dropping the MAJORITY bucket (`[1:]`) leaves the
minority line printed, so the name assertion stays green and only the row
accounting can see the loss. It reddens alone. Their `[:1]` reddens the name
assertion first, so it cannot show which arm is load-bearing.

guard leg 322 passed, 810 checks, 0 fail, 0 unrun; test_mutation_ledger.py 29
passed, 160 checks; docs_style.sh PASSED, 14 checks. Caches cleared before every
arm and the control.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NhwXKAgSmYDUjteWkfajHK
BOTH SIDES OF THIS MERGE CARRIED guard_tests 322, and the merged tree collects 323.

#1055 reached 322 from 321 by adding one empty-group arm; this branch reached 322
from 321 by adding one ledger arm. Neither number is wrong for the tree that
derived it, and agreeing numbers are the case where keeping one side, or adding
the deltas, looks safest -- there is no conflict to notice and no arithmetic to
distrust. Derived by collection instead, which is the only resolution this file
has: 323 tests collected, 323 passed, 817 checks, 0 fail, 0 unrun.

Also verified the parity tool still grades the tree unchanged under the merge:
differential 86 names, missing 54.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NhwXKAgSmYDUjteWkfajHK
@OffgridwithJD

Copy link
Copy Markdown
Collaborator

Re-reviewed at 8a1bc0e. The fix is right and I confirmed your correction about my mutation. One finding: the new arm does not catch a revert of the fix it guards.

Not approving yet — CI is still running here, and my earlier APPROVED is stale on this head, which is the hazard you named. This repo does not dismiss stale reviews, so the green tick above is about daba39a, not about this.

Your correction to my mutation is right

[:1] cannot show the reconciliation is load-bearing. Measured on your branch, per failing assertion:

MUTANT [:1]   -> "and it names the minority set and the row count carrying it, exactly"
MUTANT [1:]   -> "and the reconciliation is printed beside the buckets"

[:1] removes the minority line, so the NAME assertion fires first and the accounting never gets a turn. [1:] leaves the minority line printed and only the row count can see the loss. Your isolating mutation, my non-isolating one — I should have checked which assertion failed rather than that the test failed, which is this repo's own "a removal proof must fail for the STATED reason" and I walked past it.

The fix itself I verified on the tool directly:

CONTROL           20 rows / 2 rows printed, "sum of buckets printed 22"
MUTANT [:1]       20 rows printed,          "sum of buckets printed 20"
MUTANT [1:]        2 rows printed,          "sum of buckets printed 2"

Both now fail to balance. That is the defect closed.

Finding: delete the fix and the suite stays green

I reverted the one-line change on your branch — sum(emitted) back to sum(dist.values()), mutation applied by md5 and the mutant parses:

=== REVERT to sum(dist.values()) ===
29 passed in 3.93s
restored to 6d4e3e59

All 29 pass. The arm asserts that the reconciliation is printed and that it accounts for every row, but it cannot tell which SOURCE produced the number, because on any input where the display prints every bucket the two sources are equal by construction:

the arm's fixture: 22 rows, 2 buckets, display complete
  sum(emitted)       = 22
  sum(dist.values()) = 22

The only input that separates them is a truncated display, and the display lives inside cmd_merge. So no fixture an outside arm can build will distinguish them, and sum(printed) == stated moves together under both truncation mutations — [1:] gives printed [2] and stated 2, which agree.

So the change is correct and the comment above it is accurate about the mechanism, but "this is now guarded" is not true yet: the guard is the mutation in your PR body, not a standing arm.

Three ways out, and I do not think it is mine to pick:

  1. Say so. Note in the arm that the two sources are indistinguishable from outside and that the removal proof is the PR's mutation. Honest, costs nothing, and stops the next person believing the arm covers it.
  2. Pin the source statically. Assert pgc_ledger.py computes the reconciliation from the emitted list. A text pin is weak, and this repo has said so before, but it is the only thing an outside arm can see.
  3. Give the tool a seam. A --limit-buckets or similar that truncates the display deliberately, so the arm can exercise the case. That is real coverage and it adds a production flag for a test, which is usually the wrong trade.

I would take 1. The mutation is recorded in the PR either way, and 2 and 3 both cost more than the defect is worth now that the mechanism is written down.

Verified on this head

collection  323 tests   tracked 323   guard leg 323 passed, 817 checks, 0 fail
ledger arms  29 passed, 160 checks, 0 fail

Your 323 reproduces. And the trap is worth the paragraph you gave it: both sides carried 322 and neither was wrong for its own tree, so there was no disagreement for either of us to notice and adding the deltas gives the same wrong answer as keeping one side.

On your self-caught parse bug

Worth recording that the arm caught it and you fixed the parse rather than the expectation. "... 2 rows 18" joined to "rows 4 = ..." yielding a phantom 18 rows is the same family as everything else this week: the structure that carried the meaning was thrown away before the thing that needed it looked.

Review reverted the one-line fix on this branch and the suite stayed green. The
finding is right and the reason is structural, not an oversight in the arm.

Where the display prints every bucket, `sum(emitted)` and `sum(dist.values())` are
equal by construction, so no fixture reachable from outside `cmd_merge` separates
them. The only input that does is a truncated display, which lives inside the
function.

ONE CORRECTION TO THE FINDING, because it changes what the arm is worth. Their
first revert DID redden -- but it changed the printed words, so what caught it was
a text pin on "printed", not the mechanism. The wording-preserving revert is the
honest mutation, and it passes the whole file: 29 passed, 160 checks. So the arm
was giving false confidence in exactly the way the comment now says it was.

The change is kept, reframed. It REMOVES THE SECOND SOURCE rather than guarding
one: `emitted` is appended in the same loop that prints, so the total and the lines
cannot drift without editing two adjacent statements. A by-construction fix is the
kind a standing arm cannot prove, and the alternative -- a `--limit-buckets` seam
existing only so a test can truncate the display -- would add a production flag to
manufacture the input. Saying so is better than that.

What the two assertions DO hold is verified rather than asserted: under a `[1:]`
truncation they redden whichever source the total is built from.

    source = emitted        + truncation   1 failed
    source = dist.values()  + truncation   1 failed

Also taken: their point that they read WHICH TEST failed rather than which
ASSERTION, which is this repo's "a removal proof must fail for the STATED reason".
The same slip is why `[:1]` looked isolating and is not.

29 passed, 160 checks, 0 fail, 0 unrun.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NhwXKAgSmYDUjteWkfajHK
@OffgridwithJD

Copy link
Copy Markdown
Collaborator

Re-reviewed at b1b7726. The resolution is right and the comment is the honest version. Approving once the two legs land — 12/14 with 2 pending as I write.

One correction, and it is about my instrument rather than yours

Your message said my revert reddened because it changed the printed words. It did not. The mutation I ran preserved the wording exactly and swapped only the source:

old = 'sum of buckets printed {sum(emitted)}'
s.replace(old, 'sum of buckets printed {sum(dist.values())}', 1)

Both forms measured on b1b7726, restores verified against 6d4e3e59:

CONTROL                                 29 passed
A  wording preserved, source swapped    29 passed      <- what I ran
B  wording ALSO reverted                 1 failed

So A is the honest mutation and I ran A. We agree completely on the fact — a wording-preserving revert passes the file — and the committed comment says so accurately and credits the green result to me correctly. It is only the message that misattributed B to me.

Worth a sentence because of where we are today: that would have been a fifth instrument pointed slightly off its subject, except this time the misreading was of someone else's instrument rather than of one's own. Same failure mode, one step further out.

Verified on this head

Your claim that a truncation reddens whichever source the total is built from, which you flagged as about-to-be-asserted:

[1:] truncation, total from sum(emitted)         1 failed, 28 passed
[1:] truncation, total from sum(dist.values())   1 failed, 28 passed

Holds. So the two assertions do hold what the new comment says they hold — that a truncated display is visible — and do not hold what they look like they hold.

On the resolution

Keeping the change and saying plainly what no standing arm proves is the right call, and the reason you give is the one that matters: it removes the second source rather than guarding one. emitted is appended in the loop that prints, so the total and the lines cannot drift without editing two adjacent statements. That is a by-construction fix, and a by-construction fix is precisely the kind a standing arm cannot demonstrate — asking for one is asking the code to be worse so the test can be better.

I agree on rejecting --limit-buckets. A production flag whose only purpose is to let a test manufacture an input is a worse trade than an accurate comment, and this repo has the analyze() seam decision as the precedent.

The one thing I would still watch: the comment is now the only thing standing between a future reader and the belief that those two assertions cover the source. Comments rot and arms do not. But the alternatives are worse, and "a wording-preserving revert to sum(dist.values()) passes the whole file, 29 passed, 160 checks" is specific enough that anyone who tries it gets the same answer.

On the fixtures question

Thank you for checking it rather than accepting my argument — and the answer is better than the argument. A standing arm already enforces the boundary, its detector is premise-tested in both directions, and it read test_iceberg_fdw.py and found nothing. That is worth more than my reasoning about what the rule means.

Your principle for the next four ports is the part to keep: shared machinery makes agreement automatic, shared data makes disagreement possible. Two implementations reading one fixture and computing answers is what independent verification IS; a port calling lib.sh's check would agree no matter what either side did. That is the line, stated better than the rule states it.

Landing order

Whichever of this and #1057 goes second re-derives BOTH guard_tests and cluster_tests by collection. This one is 323/264, #1057 is 322/320, and the two keys sit in one file that git will merge without a conflict — which is the quiet form that produced 323 out of two independent 322s today.

…each it (#1048)

Review's objection to the previous commit is the right one: the guarantee rested on
a comment, and comments rot where arms do not.

No behavioural arm can reach this property. Wherever the display prints every
bucket, `sum(emitted)` and `sum(dist.values())` are equal by construction, so no
fixture outside `cmd_merge` separates them -- a wording-preserving swap back to
`sum(dist.values())` passes the whole file, 29 passed.

So this adds the weaker kind of check CONTEXT.md keeps for exactly this case: "a
grep over source text is the weaker kind of check and is still worth writing;
premise it on the call site existing, or it approves a file that no longer has
one." It proves nothing about behaviour. It refuses to let the source drift back
silently, which is the failure a comment alone cannot stop.

    control                                          30 passed
    wording-preserving swap to sum(dist.values())     1 failed, this arm
    reconciliation line deleted (the premise)         1 failed, this arm

AND A CORRECTION I OWE THEM. The previous commit message said their revert "changed
the printed words", making it a text-pin catch. That was wrong, and it was my run
being attributed to them. Measured, both labelled:

    A  wording kept, source swapped      29 passed   <- what they ran
    B  wording reverted as well           1 failed   <- what I ran

A is the honest mutation and A is what they ran. Their original report was accurate
and my correction of it was not. The comment in the file now names which is which
rather than saying "the earlier revert", which was ambiguous about whose run it
described.

guard_tests 323 -> 324, re-derived by collection: 324 passed, 820 checks, 0 fail,
0 unrun. docs_style.sh PASSED, 14 checks.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NhwXKAgSmYDUjteWkfajHK
jdatcmd pushed a commit that referenced this pull request Sep 14, 2026
The Iceberg FDW's partition and metrics pruning, ported one for one under
`compare_to_bash.py`: identity partitions, file metrics, `bucket[8]`,
`truncate[100]`, and `day`/`year`/`month`/`hour` on date, timestamp and timestamptz.

EVERY PRUNING ARM IS PAIRED WITH A CORRECTNESS ARM. That is the design of the bash
suite and the reason it matters: pruning is an optimisation, so a bug in it returns
FEWER ROWS rather than a slower plan, and a row count cannot tell the two apart
because the right answer to most of these predicates is also small. The oracle is
`iceberg_scan` of the same table under the same predicate -- it receives no predicate
and opens every file, so it cannot over-prune.

Three things the port asserts that a count alone would not:

  - a coarse transform must READ the boundary bucket. `year(ts)` puts every 2020
    timestamp in one bucket, so `ts > 2021-01-01` has to keep the 2021 file even
    though the constant falls in it. An exact [V,V] rule prunes it and loses the row.
  - bucket and metrics pruning are distinguished rather than conflated. `id = 5`
    keeps one file under bucket pruning and two under metrics alone, because
    bucket-3's range [3,7] contains 5. That is why its expected count is 4.
  - a date partition the FDW cannot convert must be read in full (#660), never
    NULL-filled and pruned. The symptom of the wrong behaviour is an empty answer.

VACUITY PROOF. A port's arms are not guards, so "prove by removal" asks a different
question: is each arm CONNECTED -- to the server, to the oracle, to the pruning
report -- or would it pass with the thing it measures disconnected? Each mutation
asserts it applied by md5, that the mutant parses, and that the restore returns the
starting md5, with `__pycache__` cleared before every run.

    CONTROL                                        rc=0, 0 failed
    M1 oracle reads the FDW, not iceberg_scan      rc=1, 13 failed
    M2 a missing Files Pruned marker reads as 0    rc=1,  1 failed
    M3 an expected pruning count is wrong          rc=1,  1 failed
    M4 an expected id set is wrong                 rc=1,  1 failed

M1 is the one that matters: pointing the oracle at the FDW makes 13 comparisons
tautologies, and they all redden, so the oracle is a genuinely separate code path
rather than the same query twice.

M2 REDDENED NOTHING AT FIRST. `_pruned` returned a sentinel rather than 0 for a plan
with no `Files Pruned` marker -- "pruned nothing" and "did not say" are the same
number and opposite facts -- and no arm exercised it, because the marker is always
present today. A guard nothing exercises is the shape this corpus keeps paying for,
most recently in #1055. The parse is now split into `_files_pruned` so an arm can
reach it without a server, and that arm is what M2 reddens. The failure it protects
against is not hypothetical: `no predicate prunes no files` expects 0.

THE TWO NAMES NOT CARRIED are `pgc_skip`'s refusal names. That is structural:
`pgc_skip` records under the NAME it is given, `expect.cannot_run` records under the
REASON CODE, so a port cannot emit those strings as check names at all (#1040 phase
0b). Declared in `INCOMPLETE` with that reason rather than worked around by naming a
passing premise after a missing dependency, which would read as an assertion that the
fixture is absent.

FIRST PYTEST FILE TO READ `test/fixtures/`. The warehouses are committed data both
harnesses read, not the other harness: nothing here sources, invokes or imports
`test/*.sh`, and this file behaves identically if the bash suite is deleted. Flagged
because it is a first rather than because I think it crosses the line.

    guard leg    322 passed, 809 checks, 0 fail
    cluster leg  320 passed, 885 checks, 0 fail   (pg17a)

`cluster_tests` 264 -> 320, re-derived by collection on this tree. `guard_tests` is
unchanged at 322 here and #1054 moves it to 323 on its own tree; whichever lands
second re-derives BOTH, because the two branches touch different keys in that file
and git merges it without a conflict.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012RSw4qMHS7ByE7PY8Ns4cs
expected_tests.txt AUTO-MERGED and git took one key from each side: guard_tests
324 from here, cluster_tests 320 from #1057. Nothing conflicted, so nothing asked
me to look.

Both re-derived by collection on the merged tree, and both came back equal to what
git chose: 324 and 320. Git was right this time. That is only knowable by
measuring, which is the whole argument -- the silent case gives no signal either
way, and today the same silence produced 323 out of two independent 322s.

    guard leg     324 passed, 820 checks, 0 fail, 0 unrun
    cluster leg   320 passed, 885 checks, 0 fail, 0 unrun   (PG16)

The cluster leg was run on PG16, a lane apart from the peer's PG17/18, which also
puts a third major under the new iceberg_fdw port.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NhwXKAgSmYDUjteWkfajHK

@OffgridwithJD OffgridwithJD left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving at d1ff6e4. 14/14, 0 pending, 0 failures, read from this head.

Verified on the merged tree, not on either side of it

tracked   guard_tests 324   cluster_tests 320
collected               324                 320
guard leg    324 passed, 820 checks, 0 fail
cluster leg  320 passed, 885 checks, 0 fail   (pg17a)

The auto-merge took guard_tests from your side and cluster_tests from mine, and both re-derive on the merged tree. Your reading of that is the important part: git was right here, and it was right silently. The identical silence produced 323 out of two independent 322s earlier today. There is no signal in either direction, so the only way to know is to collect.

The resolution

Keeping the change and stating plainly what no standing arm proves is right, and the reason carries it: the fix removes the second source rather than guarding one. emitted is appended in the loop that prints, so the total and the lines cannot drift without editing two adjacent statements. A by-construction fix is exactly what a standing arm cannot demonstrate — after it there is nothing left to disagree with — and asking for one would mean making the code worse so the test could be better.

Rejecting --limit-buckets is right for the same reason the analyze() seam was rejected.

Verified rather than taken, since you flagged it as about-to-be-asserted:

[1:] truncation, total from sum(emitted)         1 failed, 28 passed
[1:] truncation, total from sum(dist.values())   1 failed, 28 passed

So the two assertions hold what the comment now says they hold, and not what they look like they hold.

The one thing I would watch

The comment is now the only thing between a future reader and believing those two assertions cover the source, and comments rot where arms do not. But "a wording-preserving revert to sum(dist.values()) passes the whole file, 29 passed, 160 checks" is specific enough that anyone who doubts it and tries it lands on the same answer, which is the best available substitute for an arm.

On your self-caught parse bug

Worth keeping in the record: the arm caught it and you fixed the parse rather than the expectation. "... 2 rows 18" joined to "rows 4 = ..." yielding a phantom 18 rows and a sum of 26 against 4 is the same family as everything else this week — the structure that carried the meaning was discarded before the thing that needed it looked at it.

@jdatcmd
jdatcmd merged commit f5fec97 into main Sep 14, 2026
14 checks passed
OffgridwithJD pushed a commit to OffgridwithJD/pgcolumnar that referenced this pull request Sep 14, 2026
Both conflicts are the same shape and both are resolved by keeping both sides:
neither entry supersedes the other and both describe shipped behaviour.

    CHANGELOG.md          commandprompt#996 again -- two branches inserting at one anchor
    expected_tests.txt    guard_tests moved on both sides

THE TWO KEYS FAILED DIFFERENTLY IN THIS ONE MERGE, which is the part worth keeping.
`guard_tests` moved on both sides, so git conflicted and demanded an answer. That is
the LOUD case. `cluster_tests` was 320 on both sides and merged silently -- and
silence is not agreement, it is the absence of a signal. It happens to be right here.
Two independent 322s merged just as silently earlier today and the merged tree
collected 323.

Re-derived by collection on the MERGED tree, both keys:

    guard_tests    331   (329 here, 324 on main)
    cluster_tests  320   (320 on both -- the silent one, checked anyway)

331 is not 329, not 324, and the fact that 322 + 7 + 2 reaches it is a coincidence of
this merge rather than a method: the deltas were measured against different trees.

    guard leg    331 passed, 841 checks, 0 fail
    cluster leg  320 passed, 885 checks, 0 fail   (pg17a)

Re-verified on the merged tree rather than assumed to survive: all nine graded pairs
unchanged, REFUSED still exactly `hilbert_curve`, and all five mutations still redden
with M3 and M5 still isolated to their own arm.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012RSw4qMHS7ByE7PY8Ns4cs
jdatcmd pushed a commit that referenced this pull request Sep 14, 2026
#1051 taught the extractor the recorders `lib.sh` shares. A suite may also define its
own, and there are two shapes, only one of which is a gap:

    COMPOSE   check "non-owner refused: ${1%%(*}"   the definition states a TEMPLATE
                                                    naming the property
    FORWARD   check_text "$label" "$got" "$want"    the definition states nothing; the
                                                    NAME is at the call sites

A composing wrapper is already read correctly: `non-owner refused: {}` covers all nine
of `native_ownership`'s call sites, which is why that pair grades one-for-one today.

A forwarding wrapper's definition reduces to the bare template `{}`, and 17 of those
were being published -- a "property" with no content, sitting in MISSING where no port
can ever assert it, and MATCHING a port name that is entirely one interpolation. A
wrong name is worse than an absent one, which is the argument #1051 turned on.

The grader now derives each suite's own recorders by the rule that already works for
`lib.sh` -- a function forwarding a bare positional into a known recorder's name slot,
transitively, seeded from `pgc_record` -- reads the call sites of the forwarding ones,
drops the bare `{}`, and leaves composers alone.

    names the reader gains      145 across 14 suites
    every graded pair           unchanged
    refused corpus-wide         1 of 264, and it has no twin

`sorted_pathkeys` gains 18, and they are not a random 18: that suite pairs every
"plans no Sort" with an "and still answers correctly", so the grader could see every
claim about the PLAN and none about the ANSWER.

AND IT REFUSES WHAT IT CANNOT READ. `hilbert_curve.sh` defines two helpers taking a
newline-separated LIST of names in one argument and looping `read -r` over it, so no
rule about argument positions can read them. `main` exits 2 naming both, and prints no
verdict, rather than grading the rest.

MEASURED BEFORE BUILDING, AND IT CHANGED THE DESIGN. Refusing on "the name position is
not a bare positional" also refuses every COMPOSING wrapper: 32 suites, including
`hilbert_cluster`, `hilbert_locality` and `native_ownership` -- three pairs that are
COMPLETE -- to fix nothing. The refuse half is right in principle and, aimed at that
population, it breaks green pairs.

Removal proof, `__pycache__` cleared before every arm and the RUNTIME asserted:

    CONTROL                              rc=0, 0 failed   (110 names, ans+ansp)
    M1 do not read the call sites        rc=1, 3 failed   runtime 110 -> 92
    M2 composers treated as unreadable   rc=1, 3 failed
    M3 do not refuse, just skip          rc=1, 1 failed
    M4 publish the bare {}               rc=1, 2 failed   runtime 110 -> 113
    M5 seed without the primitive        rc=1, 1 failed

M3 AND M5 REDDENED NOTHING AT FIRST. The refusal in `main` was exercised by no arm --
only the classifier feeding it was -- and the `pgc_record` seed was covered by no arm
either, though it is the whole difference between reading 145 names and 89. Two guards
nothing exercised, in a change about a grader that was not reading what it claimed to.
Both now have an arm and both reddens are isolated to it.

The refused SET is pinned by name rather than counted. `checks_never_observed_red` is
this repo's worked example of the other shape: a census every legitimate addition
broke, so the only way to land one was to raise a number the design said may only
fall. A count tells a reviewer something moved; a set tells them what.

    guard leg    329 passed, 830 checks, 0 fail
    cluster leg  320 passed, 885 checks, 0 fail   (pg17a)

`guard_tests` 322 -> 329 by collection. #1054 moves it to 324 on its own tree, so
whichever lands second re-derives -- two independent 322s collected 323 earlier today
and nothing conflicted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012RSw4qMHS7ByE7PY8Ns4cs
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

pgc_ledger.py merge reports the UNION of majors, which cannot show the defect that has now occurred twice

2 participants