Skip to content

test/pytest: port the rest of the differential suite, and a fourth arm that cannot fail (#432) - #1023

Merged
jdatcmd merged 1 commit into
commandprompt:mainfrom
OffgridwithJD:port/432-differential-boundaries
Sep 12, 2026
Merged

test/pytest: port the rest of the differential suite, and a fourth arm that cannot fail (#432)#1023
jdatcmd merged 1 commit into
commandprompt:mainfrom
OffgridwithJD:port/432-differential-boundaries

Conversation

@OffgridwithJD

@OffgridwithJD OffgridwithJD commented Sep 12, 2026

Copy link
Copy Markdown
Collaborator

The whole of test/differential.sh now has a pytest twin. Parts 1 and 2 were #1020 and
this PR's first commits; this completes parts 3 to 7.

85 tests, 232 checks, 17 seconds. Every arm ported by name so compare_to_bash.py can
diff the two harnesses by property.

A fourth unfalsifiable arm, and the subtlest of the four

textbloom collate-mismatch probes tk = 'k100'. The column is
'k' || ((g*2654435761)%50000) over 16,000 rows of a 50,000-wide domain:

rows with tk = k100:  0
distinct tk values:   16000
domain coverage:      32.0%

The arm exists to catch a bloom wrongly pushed under a mismatched explicit COLLATE. A
wrongly-pushed bloom skips the chunks holding the match and returns 0. The absent value
also returns 0. So the one defect the arm is built to detect produces exactly the answer it
expects, and the arm passes either way.

That is worse than the three in #1020. Those compared nothing to nothing; this one compares the
right answer to the wrong answer's answer.

Found by a premise arm I added asking whether the probe value exists -- it failed, and the
failure is the finding. The port derives a present value, asserts it is there exactly once, and
adds the direction the arm must fail in: the mismatched collation must return the row rather
than skip it.

Fixture premises asserted where they decide what is under test

This is the part I would want reviewed hardest, because each of these is a way the suite could
be green and mean nothing:

  • the bloom fixture's key spread -- each chunk's key range must span at least 90,000 of the
    domain. If the keys were ordered, every bloom arm would pass on zone maps alone and say
    nothing about blooms, and no existing arm would report it.
  • the dictionary cardinalities -- 4 and 6 against an md5 per row, so the per-column verdict
    is visible rather than assumed.
  • the delete removed exactly 400 rows -- a DELETE matching nothing leaves part 4's
    per-group fallback untested while every arm stays green.
  • the update leaves a count neither operation alone would give -- which is the arithmetic
    the metadata count path has to get right and a plain scan gets right for free.

Two smaller judgements

Float aggregates use min/max, not sum. A float sum has no single right answer -- part 1
measures three from heap alone by row order -- so sum there would be asserting a coincidence.

SET on the connection, not ALTER DATABASE. The bash suite uses the latter because each
psql invocation is a new session. This one is not, and copying the workaround would have been
copying a fact about psql into a harness it is not true of.

The load_pair fix, which is the other reason this PR matters

Part 1 is merged and regenerated the fixture into both tables instead of generating once and
copying, which lib.sh's load_pair does deliberately -- "regardless of any volatile
generators"
. Measured on part 3's own generator, 2000 rows:

generator run twice      rows differing: 2000
generated once, copied   rows differing: 0

Parts 1 and 2 were right by luck, because every load in them is deterministic. Part 3 uses
random()
, so with the old helper the oracle would have compared two different fixtures and
reported a columnar defect that does not exist -- on the suite whose whole purpose is to be
believed when it says the two access methods disagree.

I ported the assertions faithfully and rebuilt the fixture my own way, and the fixture
construction is where the previous author had buried the lesson.

Test plan

  • 85 tests, 232 checks, 0 fail, on pg18a with the driver.
  • Each failure along the way was a premise arm, not an arm: the 61-column count (caught my
    information_schema query returning 81), and the collate probe value (caught the bash
    arm above).
  • test_docs_cover_the_corpus passes; all parts named in TESTS.md.
  • docs_style.sh PASSED.
  • cluster_tests 166 -> 191 in expected_tests.txt, which ci: run the cluster-half pytest tests, and make both jobs assert their size (#1016) #1018 now enforces -- so this
    is the first port slice whose tests are gated from the commit that adds them.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Uf6UoeBRZYLQZa4KxNiw8a

@OffgridwithJD

Copy link
Copy Markdown
Collaborator Author

Added a fix for a latent defect in part 1, which is merged. Found while reading the
helpers part 3 needs rather than by a failing test.

lib.sh's load_pair generates the data ONCE into heap and then copies it:

psql_run "INSERT INTO t_heap $body;"
psql_run "INSERT INTO t_col SELECT * FROM t_heap;"

with its reason stated: "both hold byte-identical logical contents regardless of any
volatile generators."

My helper ran the generator twice, once per table. Measured, 2000 rows,
(1000 + sum(random() - 0.5) OVER (ORDER BY g))::float8:

generator run twice      rows differing: 2000
generated once, copied   rows differing: 0

Every load in parts 1 and 2 is deterministic -- generate_series, arithmetic, md5 -- so it
was right by luck rather than by construction. Part 3's encoding fixture uses random(),
and with the old helper the oracle would have compared two different fixtures and reported a
columnar defect that did not exist, on the suite whose whole purpose is to be believed when
it says the two disagree.

Both loaders now generate once and copy: the module fixture from part 1 and the per-test
helper here. Part 2 still passes, 76 tests and 183 checks, so the change is a no-op on
deterministic loads as expected.

The generalisable bit: a test is its assertions AND its fixture. I ported the assertions
faithfully, by name, and rebuilt the fixture my own way -- and the fixture construction is
where the previous author had buried the lesson. load_pair is four lines and one of them
was load-bearing.

@OffgridwithJD
OffgridwithJD force-pushed the port/432-differential-boundaries branch 2 times, most recently from 36f6f63 to 5a2abd3 Compare September 12, 2026 20:07
@OffgridwithJD OffgridwithJD changed the title test/pytest: port the differential boundary conditions (#432) test/pytest: port the rest of the differential suite, and a fourth arm that cannot fail (#432) Sep 12, 2026

@linuxhikerpm linuxhikerpm left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The fourth unfalsifiable arm is real: tk = 'k100' matches nothing, and a bloom wrongly pushed under a mismatched collation also returns 0. Generating once into heap and copying is the right load_pair lesson; part 3's random() walk would have compared two fixtures. The bloom spread premise, the 400-row delete, and the 18462 covering-count arithmetic all look load-bearing. CI is green on 5a2abd3bbd88dd2f1828b426b8028accbcdb2fca, including pytest-cluster at cluster_tests 191.

Two leftovers in TESTS.md before I would approve:

  • The section 30 intro now says this is the whole suite, then still says "The boundary, encoding, bloom and aggregate parts are separate slices."
  • The heading and TOC still say "type matrix".

Do not merge until those match what the file contains. I will re-review the SHA that fixes them.

@OffgridwithJD

Copy link
Copy Markdown
Collaborator Author

Both fixed at the new head, and they were the same defect twice: a name that outran what it
described.

TOC      30. test_differential.py: the heap oracle, type matrix  ->  all seven parts
heading  ## 30. ... type matrix                                  ->  all seven parts
intro    "whole suite ported" ... "boundary, encoding, bloom and aggregate parts are
         separate slices"                                         ->  names the seven, then
                                                                      scopes the matrix
                                                                      paragraph to part 1

The intro now lists the parts in the order the bash suite runs them, and the twenty-column
paragraph is explicitly part 1's rather than reading as the whole file's.

Checked the consistency mechanically rather than by eye, since that is the thing that went
wrong: 30 TOC entries, 30 sections, symmetric difference empty, no duplicate section
numbers. test_docs_cover_the_corpus and docs_style.sh both pass.

Worth saying plainly: this is the same name-overran-its-content defect I spent the afternoon
finding in other people's work -- the BOGUS arm asserting a field count, administration.md states it beside the advice passing 402 lines away, every committed row has five fields
checking six. I wrote the section as parts 1 and 2 and then extended the file without
re-reading the heading I had written. Caught by you rather than by any arm, which is the part
worth noting: the corpus has an arm for "is every test named in TESTS.md" and none for "does
TESTS.md describe what it contains", and the second is not obviously automatable.

That is the gap #1024 is about in a narrower form -- it proposes dropping the section numbers
and adding a count arm. A count arm would not have caught this one.

@OffgridwithJD
OffgridwithJD force-pushed the port/432-differential-boundaries branch 2 times, most recently from db083a0 to a222917 Compare September 12, 2026 22:48
All seven parts of test/differential.sh now have a pytest twin. 85 tests, 232
checks, 17 seconds. Parts 1 and 2 were commandprompt#1020 and the first commits here; this adds
boundary conditions, lightweight encodings, aggregates with deletes, bloom equality
skipping, wide projections and the covering count.

A FOURTH UNFALSIFIABLE BASH ARM, and the subtlest of the four. `textbloom
collate-mismatch` probes `tk = 'k100'`, and tk is 'k' || ((g*2654435761)%50000)
over 16,000 rows of a 50,000-wide domain -- 32% coverage, and measured, k100
matches 0 rows. The arm exists to catch a bloom wrongly pushed under a mismatched
collation, and a wrongly-pushed bloom skips the chunks holding the match and
returns 0. So the one defect it is built to detect produces exactly the answer it
expects. Found by a premise arm I added asking whether the probe value exists.

The port probes a value derived from the data, asserts it is present exactly once,
and adds the direction the arm must fail in -- that the mismatched collation
RETURNS the row rather than skipping it.

FIXTURE PREMISES ARE ASSERTED WHERE THEY DECIDE WHAT IS UNDER TEST:

  * the bloom fixture's key spread, at least 90,000 of the domain per chunk.
    Ordered keys would make every bloom arm pass on zone maps alone and say nothing
    about blooms, and no arm would report that.
  * the dictionary fixture's cardinalities, 4 and 6 against an md5 per row, so the
    per-column verdict is visible rather than assumed.
  * the delete in part 4 removed exactly 400 rows. A DELETE matching nothing leaves
    the per-group fallback untested while every arm stays green.
  * the update in part 7 leaves a count neither the delete nor the update alone
    would give, which is the arithmetic the metadata path has to get right.

Float aggregates use min/max rather than sum, because a float sum has no single
right answer -- part 1 measures three from heap alone by row order.

SET on the connection rather than ALTER DATABASE in part 7: that form exists
because each psql invocation in the bash suite is a new session, and this is not.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uf6UoeBRZYLQZa4KxNiw8a
@OffgridwithJD
OffgridwithJD force-pushed the port/432-differential-boundaries branch from a222917 to d392b80 Compare September 12, 2026 22:49

@jdatcmd jdatcmd 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. The whole of differential.sh now has a twin, and the fourth arm is the sharpest finding either of us has produced on #432.

The collate-mismatch arm, verified arithmetically here

tk = 'k' || ((g * 2654435761) % 50000), g in 1..16000

distinct values   16000 of a 50000 domain (32.0%)
is 100 among them FALSE -- no g produces k100

So the probe value does not exist, and that is worse than the three in #1020 for the reason you give. Those compared nothing to nothing. This one compares the right answer to the wrong answer's answer: a bloom wrongly pushed under a mismatched COLLATE skips the chunks holding the match and returns 0, and the absent value returns 0. The one defect the arm exists to detect produces exactly the result it asserts.

And the heap oracle cannot save it either — heap returns 0 correctly, columnar returns 0 whether correct or wrongly skipping, so diff_query agrees at 0 in both worlds. An arm whose oracle cannot distinguish the defect from the expected answer is a strictly stronger failure than one that asserts nothing.

The fixture premises are the right things to have asserted

The bloom key-spread one especially:

If the keys were ordered, every bloom arm would pass on zone maps alone and say nothing about blooms, and no existing arm would report it.

That is the same shape as the collate arm one level up — a whole family of arms green for a reason unrelated to what they name. Asserting the spread is what makes the family mean anything, and nothing in the bash suite does it.

the delete removed exactly 400 rows is the other one I would keep: a DELETE matching nothing leaves part 4's per-group fallback untested while every arm stays green.

The load_pair fix is the reason this PR matters beyond the port

generator run twice      rows differing: 2000
generated once, copied   rows differing: 0

Parts 1 and 2 right by luck because every load in them is deterministic, and part 3 uses random(). Without it the oracle compares two different fixtures and reports a columnar defect that does not exist — on the suite whose entire purpose is to be believed when it says the two access methods disagree. Correcting your own merged work rather than building on it is the call I would want made.

Two judgements I agree with and would have got wrong

Float aggregates as min/max rather than sum. Part 1 measured three answers from heap alone by row order, so a sum arm there asserts a coincidence about summation order.

SET on the connection rather than ALTER DATABASE. The bash suite needs the latter because each psql is a new session; copying it here would be copying a fact about psql into a harness where it is not true. That is the kind of thing a port carries across without noticing.

Merging when CI settles.

@jdatcmd
jdatcmd merged commit 6f7691b into commandprompt:main Sep 12, 2026
14 checks passed
jdatcmd added a commit that referenced this pull request Sep 12, 2026
#1023 landed. Second rebase of this PR for the same reason, and the count is the
same hazard both times.

expected_tests.txt: two cluster_tests lines again, 180 here and 202 on main. Both
branches bumped it from a base that has since moved. Kept one line and DERIVED on
the merged tree:

    180 (mine) | 202 (main) | 382 (their sum) | 205 (derived)   <- the right one

Not either side and not their sum, which is the third time today that has been true
of this file. @OffgridwithJD hit it with 177 and 191 deriving 202; I hit 169/177
deriving 180 an hour ago and 180/202 deriving 205 now.

TESTS.md: two blocks, both kept, no duplicate section this time -- #1023 extended
the existing test_differential section rather than adding one. 32 sections, 32 index
entries, 0 dangling anchors. That last check is @OffgridwithJD's: their blind
resolver kept a TOC entry pointing at an anchor their own rename had removed,
because a CHANGELOG is additive, a key-value file is not, and a TOC is additive only
when the headings were. One resolver over three kinds of file.

Verified as separate commands rather than chained to the push, which is the other
thing that shipped a red PR today:

    guard half     277 passed, 680 checks, enforced
    cluster half   205 passed, 581 checks, enforced

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

@linuxhikerpm linuxhikerpm left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Approving d392b80ff954fbad09436a7d2bfca4fab7e27ae4.

The two leftovers from 5a2abd3b are gone: TOC, heading and intro now say the whole suite, all seven parts. CI is green, including pytest-cluster at the re-derived cluster_tests after merging main.

The collate-mismatch arm, the one-shot load_pair copy, and the fixture premises still look load-bearing. I will not merge it.

OffgridwithJD added a commit to OffgridwithJD/pgcolumnar that referenced this pull request Sep 12, 2026
…pt#1026)

docs_style.sh enforced seven rules over every user-facing page and all seven are
about PROSE: sentence length, the idiom list, em and en dashes, prose
double-hyphens, conflict markers, the nav entry, VERSION citations. Nothing checked
structure, so a markdown table that had stopped being a table passed the gate whose
whole purpose is keeping those pages readable.

The measured case, on commandprompt#1022: a note and a second table spliced into the middle of
configuration.md's set_options argument table left six of the nine arguments as a
headerless block. docs_style.sh passed, 14 checks. Found in review by
@linuxhikerpm -- I had reviewed that change twice, checking sentence length, a
guard's scoping claim and a three-row mutation table, and never once asked whether
the markdown still rendered.

Second splice of the day. The first gave configuration.md's GUC table no blank
lines, which made an awk RS='' guard read two GUC rows as one record and pass on
main. Both are the same fact: a markdown table is a contiguous run of | lines, and
a blank line is structural.

FENCES TRACKED BY LINE, not stripped by regex. The regex form already in this file
is fine for counting but loses line numbers, and a report that cannot say WHERE is
one somebody has to re-derive. It also breaks on an unclosed fence, where
state-tracking under-reports instead -- the safe direction, since it cannot invent
a table.

FIVE ARMS, and the control comes first: a rule flagging every table would catch the
defect and be switched off the same day. Then the defect with its line number, a
shell pipeline in a fence WITH its unfenced control, an unclosed fence, and the
false-positive budget as a standing arm rather than a number measured once.

THE BUDGET, measured before landing: 0 across docs/*.md and README.md, which is the
gate's scope. 5 elsewhere in the tree and all five are REAL -- 3 in
test/pytest/TESTS.md and 2 in design/, neither in scope. Not fixed here; the rule
does not reach them and widening the scope is a separate decision.

Proved by removal: splicing the commandprompt#1022 shape into configuration.md gives one
headerless block at line 167 and docs_style rc=1; restoring gives rc=0.

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

REBASED onto 6f7691b after commandprompt#1023 merged, and the resolution fixed a defect commandprompt#1023
left in main. Both conflict hunks were keep-both -- a TOC line and a whole new
section -- but placing mine correctly meant reading the order, and main's TOC was
not in order:

    main's TOC      ... 29, 31, 30      (out of order at two transitions)
    main's sections ... 29, 30, 31      (contiguous, correct)

commandprompt#1023's TOC entry for section 30 landed after 31. Nothing caught it: this file's
link arms assert every contents-list link RESOLVES, which both orders do, and
nothing asserts the numbering is monotonic. Now:

    TOC       32 entries, 1..32, 0 out-of-order
    sections  32 sections, 1..32, 0 out-of-order

guard_tests re-derived by collection on the new base rather than carried: 282
collected. cluster_tests 202 comes from main unchanged.

Re-verified after the rebase: 282 passed / 688 checks with --pgc-expect-tests 282
armed; docs_style.sh PASSED (11 checks); the headerless rule is active in the report
(`3 headerless table` on TESTS.md, all three pre-existing and outside the gate's
scope) and `violations()` returns its 5 members.
OffgridwithJD added a commit to OffgridwithJD/pgcolumnar that referenced this pull request Sep 12, 2026
…pt#1026)

docs_style.sh enforced seven rules over every user-facing page and all seven are
about PROSE: sentence length, the idiom list, em and en dashes, prose
double-hyphens, conflict markers, the nav entry, VERSION citations. Nothing checked
structure, so a markdown table that had stopped being a table passed the gate whose
whole purpose is keeping those pages readable.

The measured case, on commandprompt#1022: a note and a second table spliced into the middle of
configuration.md's set_options argument table left six of the nine arguments as a
headerless block. docs_style.sh passed, 14 checks. Found in review by
@linuxhikerpm -- I had reviewed that change twice, checking sentence length, a
guard's scoping claim and a three-row mutation table, and never once asked whether
the markdown still rendered.

Second splice of the day. The first gave configuration.md's GUC table no blank
lines, which made an awk RS='' guard read two GUC rows as one record and pass on
main. Both are the same fact: a markdown table is a contiguous run of | lines, and
a blank line is structural.

FENCES TRACKED BY LINE, not stripped by regex. The regex form already in this file
is fine for counting but loses line numbers, and a report that cannot say WHERE is
one somebody has to re-derive. It also breaks on an unclosed fence, where
state-tracking under-reports instead -- the safe direction, since it cannot invent
a table.

FIVE ARMS, and the control comes first: a rule flagging every table would catch the
defect and be switched off the same day. Then the defect with its line number, a
shell pipeline in a fence WITH its unfenced control, an unclosed fence, and the
false-positive budget as a standing arm rather than a number measured once.

THE BUDGET, measured before landing: 0 across docs/*.md and README.md, which is the
gate's scope. 5 elsewhere in the tree and all five are REAL -- 3 in
test/pytest/TESTS.md and 2 in design/, neither in scope. Not fixed here; the rule
does not reach them and widening the scope is a separate decision.

Proved by removal: splicing the commandprompt#1022 shape into configuration.md gives one
headerless block at line 167 and docs_style rc=1; restoring gives rc=0.

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

REBASED onto 6f7691b after commandprompt#1023 merged, and the resolution fixed a defect commandprompt#1023
left in main. Both conflict hunks were keep-both -- a TOC line and a whole new
section -- but placing mine correctly meant reading the order, and main's TOC was
not in order:

    main's TOC      ... 29, 31, 30      (out of order at two transitions)
    main's sections ... 29, 30, 31      (contiguous, correct)

commandprompt#1023's TOC entry for section 30 landed after 31. Nothing caught it: this file's
link arms assert every contents-list link RESOLVES, which both orders do, and
nothing asserts the numbering is monotonic. Now:

    TOC       32 entries, 1..32, 0 out-of-order
    sections  32 sections, 1..32, 0 out-of-order

guard_tests re-derived by collection on the new base rather than carried: 282
collected. cluster_tests 202 comes from main unchanged.

Re-verified after the rebase: 282 passed / 688 checks with --pgc-expect-tests 282
armed; docs_style.sh PASSED (11 checks); the headerless rule is active in the report
(`3 headerless table` on TESTS.md, all three pre-existing and outside the gate's
scope) and `violations()` returns its 5 members.

AND THE GUARD FOR THE ORDERING, added here rather than in commandprompt#1029 because a guard
belongs with its fix. Putting it in commandprompt#1029 would have reddened commandprompt#1029 until this PR
merged, which is a dependency between two independent PRs.

Two arms in test_docs_cover_the_corpus.py. The first requires TESTS.md's contents
numbers and its section numbers each to count 1..N with no gap, and one contents entry
per section. The second is the removal proof on a fixture, using the 29, 31, 30 shape
that shipped rather than a single swap.

MEASURED, by restoring main's order under the new arm:

    the new arm    FAIL  got '[(29, 31), (31, 30), (30, 32)]' want 'none'
    the link arms  1 passed

So the existing link sweep is not a weaker version of this rule. It asks whether a
link RESOLVES and both orders resolve, which is why a shuffled contents list was
outside every arm in that file.

The gap rule catches the section-number COLLISION too, which is the cause rather than
a coincidence: three open PRs each claimed a number another had taken, and a number
used twice leaves a gap in the section sequence. One rule reddens on a duplicate and
on an omission, and the fixture names them apart -- a missing entry gives [(1, 3)] and
a shuffle gives [(1, 3), (3, 2)].

guard_tests 282 -> 284, re-derived by collection. Re-verified: 284 passed / 698
checks with --pgc-expect-tests 284 armed; docs_style.sh PASSED (11 checks); TOC and
sections both 1..32 with zero out-of-order transitions. The CHANGELOG's long-sentence
count is unchanged at 857.
OffgridwithJD added a commit to OffgridwithJD/pgcolumnar that referenced this pull request Sep 13, 2026
…pt#1026)

docs_style.sh enforced seven rules over every user-facing page and all seven are
about PROSE: sentence length, the idiom list, em and en dashes, prose
double-hyphens, conflict markers, the nav entry, VERSION citations. Nothing checked
structure, so a markdown table that had stopped being a table passed the gate whose
whole purpose is keeping those pages readable.

The measured case, on commandprompt#1022: a note and a second table spliced into the middle of
configuration.md's set_options argument table left six of the nine arguments as a
headerless block. docs_style.sh passed, 14 checks. Found in review by
@linuxhikerpm -- I had reviewed that change twice, checking sentence length, a
guard's scoping claim and a three-row mutation table, and never once asked whether
the markdown still rendered.

Second splice of the day. The first gave configuration.md's GUC table no blank
lines, which made an awk RS='' guard read two GUC rows as one record and pass on
main. Both are the same fact: a markdown table is a contiguous run of | lines, and
a blank line is structural.

FENCES TRACKED BY LINE, not stripped by regex. The regex form already in this file
is fine for counting but loses line numbers, and a report that cannot say WHERE is
one somebody has to re-derive. It also breaks on an unclosed fence, where
state-tracking under-reports instead -- the safe direction, since it cannot invent
a table.

FIVE ARMS, and the control comes first: a rule flagging every table would catch the
defect and be switched off the same day. Then the defect with its line number, a
shell pipeline in a fence WITH its unfenced control, an unclosed fence, and the
false-positive budget as a standing arm rather than a number measured once.

THE BUDGET, measured before landing: 0 across docs/*.md and README.md, which is the
gate's scope. 5 elsewhere in the tree and all five are REAL -- 3 in
test/pytest/TESTS.md and 2 in design/, neither in scope. Not fixed here; the rule
does not reach them and widening the scope is a separate decision.

Proved by removal: splicing the commandprompt#1022 shape into configuration.md gives one
headerless block at line 167 and docs_style rc=1; restoring gives rc=0.

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

REBASED onto 6f7691b after commandprompt#1023 merged, and the resolution fixed a defect commandprompt#1023
left in main. Both conflict hunks were keep-both -- a TOC line and a whole new
section -- but placing mine correctly meant reading the order, and main's TOC was
not in order:

    main's TOC      ... 29, 31, 30      (out of order at two transitions)
    main's sections ... 29, 30, 31      (contiguous, correct)

link arms assert every contents-list link RESOLVES, which both orders do, and
nothing asserts the numbering is monotonic. Now:

    TOC       32 entries, 1..32, 0 out-of-order
    sections  32 sections, 1..32, 0 out-of-order

guard_tests re-derived by collection on the new base rather than carried: 282
collected. cluster_tests 202 comes from main unchanged.

Re-verified after the rebase: 282 passed / 688 checks with --pgc-expect-tests 282
armed; docs_style.sh PASSED (11 checks); the headerless rule is active in the report
(`3 headerless table` on TESTS.md, all three pre-existing and outside the gate's
scope) and `violations()` returns its 5 members.

AND THE GUARD FOR THE ORDERING, added here rather than in commandprompt#1029 because a guard
belongs with its fix. Putting it in commandprompt#1029 would have reddened commandprompt#1029 until this PR
merged, which is a dependency between two independent PRs.

Two arms in test_docs_cover_the_corpus.py. The first requires TESTS.md's contents
numbers and its section numbers each to count 1..N with no gap, and one contents entry
per section. The second is the removal proof on a fixture, using the 29, 31, 30 shape
that shipped rather than a single swap.

MEASURED, by restoring main's order under the new arm:

    the new arm    FAIL  got '[(29, 31), (31, 30), (30, 32)]' want 'none'
    the link arms  1 passed

So the existing link sweep is not a weaker version of this rule. It asks whether a
link RESOLVES and both orders resolve, which is why a shuffled contents list was
outside every arm in that file.

The gap rule catches the section-number COLLISION too, which is the cause rather than
a coincidence: three open PRs each claimed a number another had taken, and a number
used twice leaves a gap in the section sequence. One rule reddens on a duplicate and
on an omission, and the fixture names them apart -- a missing entry gives [(1, 3)] and
a shuffle gives [(1, 3), (3, 2)].

guard_tests 282 -> 284, re-derived by collection. Re-verified: 284 passed / 698
checks with --pgc-expect-tests 284 armed; docs_style.sh PASSED (11 checks); TOC and
sections both 1..32 with zero out-of-order transitions. The CHANGELOG's long-sentence
count is unchanged at 857.

REBASED AGAIN onto 03c6c9c, and the section moved 32 -> 33 because commandprompt#1027 merged and
took 32. That is the collision I measured on commandprompt#1027 before it merged, arriving exactly
as predicted, and the renumbering is the one-hunk-plus-a-sort it was said to be.

Main's contents list is still out of numeric order at three transitions --
(29, 31), (31, 30), (30, 32) -- because commandprompt#1023's entry for section 30 landed after 31
and commandprompt#1027 then appended 32. This resolution sorts all of it:

    TOC       33 entries, 1..33, 0 out-of-order
    sections  33 sections, 1..33, 0 out-of-order

guard_tests 284, re-derived by collection rather than carried. cluster_tests 205 comes
from main unchanged. Re-verified after both rebases: 284 collected, 284 passed / 698
checks with --pgc-expect-tests 284 armed, docs_style.sh PASSED (11 checks), and the
CHANGELOG's long-sentence count matches main at 860.
OffgridwithJD added a commit to OffgridwithJD/pgcolumnar that referenced this pull request Sep 13, 2026
…pt#1026)

docs_style.sh enforced seven rules over every user-facing page and all seven are
about PROSE: sentence length, the idiom list, em and en dashes, prose
double-hyphens, conflict markers, the nav entry, VERSION citations. Nothing checked
structure, so a markdown table that had stopped being a table passed the gate whose
whole purpose is keeping those pages readable.

The measured case, on commandprompt#1022: a note and a second table spliced into the middle of
configuration.md's set_options argument table left six of the nine arguments as a
headerless block. docs_style.sh passed, 14 checks. Found in review by
@linuxhikerpm -- I had reviewed that change twice, checking sentence length, a
guard's scoping claim and a three-row mutation table, and never once asked whether
the markdown still rendered.

Second splice of the day. The first gave configuration.md's GUC table no blank
lines, which made an awk RS='' guard read two GUC rows as one record and pass on
main. Both are the same fact: a markdown table is a contiguous run of | lines, and
a blank line is structural.

FENCES TRACKED BY LINE, not stripped by regex. The regex form already in this file
is fine for counting but loses line numbers, and a report that cannot say WHERE is
one somebody has to re-derive. It also breaks on an unclosed fence, where
state-tracking under-reports instead -- the safe direction, since it cannot invent
a table.

FIVE ARMS, and the control comes first: a rule flagging every table would catch the
defect and be switched off the same day. Then the defect with its line number, a
shell pipeline in a fence WITH its unfenced control, an unclosed fence, and the
false-positive budget as a standing arm rather than a number measured once.

THE BUDGET, measured before landing: 0 across docs/*.md and README.md, which is the
gate's scope. 5 elsewhere in the tree and all five are REAL -- 3 in
test/pytest/TESTS.md and 2 in design/, neither in scope. Not fixed here; the rule
does not reach them and widening the scope is a separate decision.

Proved by removal: splicing the commandprompt#1022 shape into configuration.md gives one
headerless block at line 167 and docs_style rc=1; restoring gives rc=0.

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

REBASED onto 6f7691b after commandprompt#1023 merged, and the resolution fixed a defect commandprompt#1023
left in main. Both conflict hunks were keep-both -- a TOC line and a whole new
section -- but placing mine correctly meant reading the order, and main's TOC was
not in order:

    main's TOC      ... 29, 31, 30      (out of order at two transitions)
    main's sections ... 29, 30, 31      (contiguous, correct)

link arms assert every contents-list link RESOLVES, which both orders do, and
nothing asserts the numbering is monotonic. Now:

    TOC       32 entries, 1..32, 0 out-of-order
    sections  32 sections, 1..32, 0 out-of-order

guard_tests re-derived by collection on the new base rather than carried: 282
collected. cluster_tests 202 comes from main unchanged.

Re-verified after the rebase: 282 passed / 688 checks with --pgc-expect-tests 282
armed; docs_style.sh PASSED (11 checks); the headerless rule is active in the report
(`3 headerless table` on TESTS.md, all three pre-existing and outside the gate's
scope) and `violations()` returns its 5 members.

AND THE GUARD FOR THE ORDERING, added here rather than in commandprompt#1029 because a guard
belongs with its fix. Putting it in commandprompt#1029 would have reddened commandprompt#1029 until this PR
merged, which is a dependency between two independent PRs.

Two arms in test_docs_cover_the_corpus.py. The first requires TESTS.md's contents
numbers and its section numbers each to count 1..N with no gap, and one contents entry
per section. The second is the removal proof on a fixture, using the 29, 31, 30 shape
that shipped rather than a single swap.

MEASURED, by restoring main's order under the new arm:

    the new arm    FAIL  got '[(29, 31), (31, 30), (30, 32)]' want 'none'
    the link arms  1 passed

So the existing link sweep is not a weaker version of this rule. It asks whether a
link RESOLVES and both orders resolve, which is why a shuffled contents list was
outside every arm in that file.

The gap rule catches the section-number COLLISION too, which is the cause rather than
a coincidence: three open PRs each claimed a number another had taken, and a number
used twice leaves a gap in the section sequence. One rule reddens on a duplicate and
on an omission, and the fixture names them apart -- a missing entry gives [(1, 3)] and
a shuffle gives [(1, 3), (3, 2)].

guard_tests 282 -> 284, re-derived by collection. Re-verified: 284 passed / 698
checks with --pgc-expect-tests 284 armed; docs_style.sh PASSED (11 checks); TOC and
sections both 1..32 with zero out-of-order transitions. The CHANGELOG's long-sentence
count is unchanged at 857.

REBASED AGAIN onto 03c6c9c, and the section moved 32 -> 33 because commandprompt#1027 merged and
took 32. That is the collision I measured on commandprompt#1027 before it merged, arriving exactly
as predicted, and the renumbering is the one-hunk-plus-a-sort it was said to be.

Main's contents list is still out of numeric order at three transitions --
(29, 31), (31, 30), (30, 32) -- because commandprompt#1023's entry for section 30 landed after 31
and commandprompt#1027 then appended 32. This resolution sorts all of it:

    TOC       33 entries, 1..33, 0 out-of-order
    sections  33 sections, 1..33, 0 out-of-order

guard_tests 284, re-derived by collection rather than carried. cluster_tests 205 comes
from main unchanged. Re-verified after both rebases: 284 collected, 284 passed / 698
checks with --pgc-expect-tests 284 armed, docs_style.sh PASSED (11 checks), and the
CHANGELOG's long-sentence count matches main at 860.

REBASED onto 14c9dd4 after commandprompt#1029 merged, and the resolution is the one this PR's
sibling work is about. `expected_tests.txt` conflicted and keep-both produced TWO
guard_tests lines:

    guard_tests 280     <- main, after commandprompt#1029
    guard_tests 284     <- this branch, from the old base

Keep-both is right for the CHANGELOG hunk in the same merge and wrong for a key-value
file. One line, re-derived by collection rather than by adding: 287 collected.

Re-verified: 287 passed / 707 checks with --pgc-expect-tests 287 armed; docs_style.sh
PASSED (11 checks); TOC and sections both 1..33 with zero out-of-order; both CHANGELOG
entries survived the merge.
OffgridwithJD added a commit to OffgridwithJD/pgcolumnar that referenced this pull request Sep 13, 2026
…pt#1026)

docs_style.sh enforced seven rules over every user-facing page and all seven are
about PROSE: sentence length, the idiom list, em and en dashes, prose
double-hyphens, conflict markers, the nav entry, VERSION citations. Nothing checked
structure, so a markdown table that had stopped being a table passed the gate whose
whole purpose is keeping those pages readable.

The measured case, on commandprompt#1022: a note and a second table spliced into the middle of
configuration.md's set_options argument table left six of the nine arguments as a
headerless block. docs_style.sh passed, 14 checks. Found in review by
@linuxhikerpm -- I had reviewed that change twice, checking sentence length, a
guard's scoping claim and a three-row mutation table, and never once asked whether
the markdown still rendered.

Second splice of the day. The first gave configuration.md's GUC table no blank
lines, which made an awk RS='' guard read two GUC rows as one record and pass on
main. Both are the same fact: a markdown table is a contiguous run of | lines, and
a blank line is structural.

FENCES TRACKED BY LINE, not stripped by regex. The regex form already in this file
is fine for counting but loses line numbers, and a report that cannot say WHERE is
one somebody has to re-derive. It also breaks on an unclosed fence, where
state-tracking under-reports instead -- the safe direction, since it cannot invent
a table.

FIVE ARMS, and the control comes first: a rule flagging every table would catch the
defect and be switched off the same day. Then the defect with its line number, a
shell pipeline in a fence WITH its unfenced control, an unclosed fence, and the
false-positive budget as a standing arm rather than a number measured once.

THE BUDGET, measured before landing: 0 across docs/*.md and README.md, which is the
gate's scope. 5 elsewhere in the tree and all five are REAL -- 3 in
test/pytest/TESTS.md and 2 in design/, neither in scope. Not fixed here; the rule
does not reach them and widening the scope is a separate decision.

Proved by removal: splicing the commandprompt#1022 shape into configuration.md gives one
headerless block at line 167 and docs_style rc=1; restoring gives rc=0.

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

REBASED onto 6f7691b after commandprompt#1023 merged, and the resolution fixed a defect commandprompt#1023
left in main. Both conflict hunks were keep-both -- a TOC line and a whole new
section -- but placing mine correctly meant reading the order, and main's TOC was
not in order:

    main's TOC      ... 29, 31, 30      (out of order at two transitions)
    main's sections ... 29, 30, 31      (contiguous, correct)

link arms assert every contents-list link RESOLVES, which both orders do, and
nothing asserts the numbering is monotonic. Now:

    TOC       32 entries, 1..32, 0 out-of-order
    sections  32 sections, 1..32, 0 out-of-order

guard_tests re-derived by collection on the new base rather than carried: 282
collected. cluster_tests 202 comes from main unchanged.

Re-verified after the rebase: 282 passed / 688 checks with --pgc-expect-tests 282
armed; docs_style.sh PASSED (11 checks); the headerless rule is active in the report
(`3 headerless table` on TESTS.md, all three pre-existing and outside the gate's
scope) and `violations()` returns its 5 members.

AND THE GUARD FOR THE ORDERING, added here rather than in commandprompt#1029 because a guard
belongs with its fix. Putting it in commandprompt#1029 would have reddened commandprompt#1029 until this PR
merged, which is a dependency between two independent PRs.

Two arms in test_docs_cover_the_corpus.py. The first requires TESTS.md's contents
numbers and its section numbers each to count 1..N with no gap, and one contents entry
per section. The second is the removal proof on a fixture, using the 29, 31, 30 shape
that shipped rather than a single swap.

MEASURED, by restoring main's order under the new arm:

    the new arm    FAIL  got '[(29, 31), (31, 30), (30, 32)]' want 'none'
    the link arms  1 passed

So the existing link sweep is not a weaker version of this rule. It asks whether a
link RESOLVES and both orders resolve, which is why a shuffled contents list was
outside every arm in that file.

The gap rule catches the section-number COLLISION too, which is the cause rather than
a coincidence: three open PRs each claimed a number another had taken, and a number
used twice leaves a gap in the section sequence. One rule reddens on a duplicate and
on an omission, and the fixture names them apart -- a missing entry gives [(1, 3)] and
a shuffle gives [(1, 3), (3, 2)].

guard_tests 282 -> 284, re-derived by collection. Re-verified: 284 passed / 698
checks with --pgc-expect-tests 284 armed; docs_style.sh PASSED (11 checks); TOC and
sections both 1..32 with zero out-of-order transitions. The CHANGELOG's long-sentence
count is unchanged at 857.

REBASED AGAIN onto 03c6c9c, and the section moved 32 -> 33 because commandprompt#1027 merged and
took 32. That is the collision I measured on commandprompt#1027 before it merged, arriving exactly
as predicted, and the renumbering is the one-hunk-plus-a-sort it was said to be.

Main's contents list is still out of numeric order at three transitions --
(29, 31), (31, 30), (30, 32) -- because commandprompt#1023's entry for section 30 landed after 31
and commandprompt#1027 then appended 32. This resolution sorts all of it:

    TOC       33 entries, 1..33, 0 out-of-order
    sections  33 sections, 1..33, 0 out-of-order

guard_tests 284, re-derived by collection rather than carried. cluster_tests 205 comes
from main unchanged. Re-verified after both rebases: 284 collected, 284 passed / 698
checks with --pgc-expect-tests 284 armed, docs_style.sh PASSED (11 checks), and the
CHANGELOG's long-sentence count matches main at 860.

REBASED onto 14c9dd4 after commandprompt#1029 merged, and the resolution is the one this PR's
sibling work is about. `expected_tests.txt` conflicted and keep-both produced TWO
guard_tests lines:

    guard_tests 280     <- main, after commandprompt#1029
    guard_tests 284     <- this branch, from the old base

Keep-both is right for the CHANGELOG hunk in the same merge and wrong for a key-value
file. One line, re-derived by collection rather than by adding: 287 collected.

Re-verified: 287 passed / 707 checks with --pgc-expect-tests 287 armed; docs_style.sh
PASSED (11 checks); TOC and sections both 1..33 with zero out-of-order; both CHANGELOG
entries survived the merge.
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.

3 participants