Repository navigation
docs(changelog): generation covers 0.6.0.0 now, not "from the next release on" - #210
Conversation
…lease on" #113 moved the changelog from per-PR entries to release-time generation. The policy is right; the sentence describing it put 0.6.0.0 on the wrong side of the line. "Everything up to and including == 0.6.0.0 is hand-written and now frozen; from the next release on, each section is generated" reads as: v6 is finished, generation starts with v7. Generation is in effect now and 0.6.0.0 is the release it applies to. The misreading is not hypothetical - an agent hit it this week and concluded that whatever currently sits under == 0.6.0.0 is what v6 ships with. The trap is that "frozen" is doing two jobs in one sentence. It is a statement about text in the file (leave it alone) and it reads as a statement about a release (settled, quotable). The rewrite splits them: a section's state follows from whether its release has SHIPPED, and 0.6.0.0 has not, so its contents are working text that release-time generation will replace. Both wrong readings are named and denied explicitly, because a reader who has already formed one will not give it up to an implication. Also states the exception that previously had to be inferred. "Do not add entries in a feature PR" does not obviously permit a correction, and #198 made one - the Kafka client entry said 3.9.1 after the pom moved to 3.9.2. The test is now written down: changing an existing claim to be true is allowed, adding information about a change is the generator's job. Three locations carried the phrase (AGENTS.md's Changelog section and its "where things live" row, and the CHANGELOG header NOTE). A fourth, in docs/inflight/release-0600-blockers.md, had already copied it. README.adoc is unaffected - it stopped embedding the changelog in #113 and only links to it. Separately, the same section claimed the PR Checklist changelog gate "should never fire ... since PRs no longer touch the file". That was false when written: #51, #57, #105 and #106 all modify CHANGELOG.adoc and all predate the policy. The gate also does not enforce the policy in either direction - it only fails UNCITED entries, so #57's cited entries would pass while violating it, and it cannot tell an edit from an addition, so a correction like #198's trips it and needs the changelog-ref: N/A opt-out. The gate is left alone deliberately, with the reasoning in docs/inflight/ci-changelog-gate-scope.md. Rejecting every addition would fail the one edit the policy permits, since "adds an entry" and "corrects an entry" are both +* lines - the gate's own header records that fuzzy-matching removed bullets against added ones was built, and abandoned as its subtlest code. And the strict version's only escape is a self-declared PR-body line, so it would buy no enforcement over the written rule while putting an opt-out on every legitimate correction, and would block four PRs that followed the rule that was live when they were opened. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RtNUsxokE9g2pSEjBHZqNA
Dependency Review✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.Scanned FilesNone |
|
Claude finished @astubbs's task in 3m 32s —— View job Review complete ✅Tasks
SummaryDocs-only PR, one commit, no behaviour change. It corrects a genuine ambiguity left by #113: the old wording ("frozen ... from the next release on, each section is generated") put Verification performedI independently checked the load-bearing factual claims rather than taking the PR body at face value:
Observations (non-blocking)
No bugs or factual errors found. The rewording is internally consistent (AGENTS.md's two spots, |
✅ Duplicate Code ReportTwo engines run in parallel for cross-validation. Each has its own thresholds tuned to its baseline - the real safety net is the per-engine "max increase vs base" check. ✅ PMD CPD
No new clones introduced by this PR. ✅ jscpd (language-agnostic)
No new clones introduced by this PR. Powered by astubbs/duplicate-code-cross-check |
✅ SpotBugs ReportNo bugs found (new bugs only — baseline from base branch excluded). |
The note said what the AGENTS.md subsection above it already said. Its one unique contribution - why tightening the gate to reject every addition was rejected - now sits inline where the gate is described, so a reader gets the reasoning without a second hop. docs/inflight/ tracks work that is open and needs picking up. This is a settled decision with its rationale, which is what AGENTS.md is for. A note recording a decision already made is the stale-marker failure that directory's own charter warns about. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RtNUsxokE9g2pSEjBHZqNA
check-review-posted.sh:94 ran `printf | grep -q` under `set -o pipefail`. grep exits the instant it matches, printf then dies with EPIPE (141), and pipefail promotes that to the pipeline's status - so a comment citing the run made the check fail. It only bites when more than one pipe buffer (64 KiB) of comment data follows the match, which is why it looked intermittent. Observed on #198, #199, #204 and #210. On #210 the review comment sat 4.7 KB into the stream with a 127 KB similarity report behind it; the job log carries `printf: write error: Broken pipe` directly above the failure. The emitted error blamed the workflow-validation guard, which none of those PRs had tripped. The fix is the herestring this repo already prescribes: the same bug class is guarded against in bin/test-check-copyright-headers.sh, whose comment records it "seen live in CI". check-review-posted.sh shipped in that same commit without the guard. Two tests, both verified to fail against the old line: - functional: match found, then >64 KiB of further comments. The existing cases dance around this - case 5 buries the match but keeps it small, case 6 puts it last so nothing follows to fill the buffer. - structural: the checker may not pipe into grep -q or awk at all, mirroring the copyright scanner's guard, so the next instance of the class is caught rather than the next occurrence of this one. The error text is left alone: it misdiagnosed those four PRs only because of this bug, and becomes accurate again once the SIGPIPE path is gone. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RtNUsxokE9g2pSEjBHZqNA
Two follow-ups pushed1. The red
|
|
Claude finished @astubbs's task in 3m 45s —— View job Review complete ✅Tasks
SummaryTwo independent, well-scoped fixes, both docs/tooling with no runtime behavior change to the product itself. 1. Changelog policy wording ( 2. Verification performedI independently checked every load-bearing factual claim rather than trusting the PR body:
Observations (non-blocking)
No bugs or factual errors found. Every checkable claim held up against direct verification. |
#211 added a rule to bin/AGENTS.md - never pipe into `grep -q` under pipefail - while the repo broke it in four places. A rule shipped with known violations is not a rule. `writer | grep -q PATTERN` under `set -o pipefail` reports failure exactly when it MATCHES: grep exits on the first hit, the writer takes EPIPE (141), pipefail promotes that to the pipeline's status. It only fires once the writer still has more than one pipe buffer (64 KiB) to write, so it passes every small fixture and surfaces when real data grows. - check-review-posted.sh:94 - live. Reported "no review posted" on four PRs whose reviews had posted (#198, #199, #204, #210). - check-quarantine-owners.sh:98,110 - latent, and close. `git show` pipes a whole source file into `grep -q` inside an `if`. The largest file in the repo is 65,185 bytes against a 65,536-byte buffer: 351 bytes of headroom, on a file two open PRs are adding lines to. It would fail as "annotation missing", not as a pipe error. - quarantine-lane-report.sh:201 - the `||` makes a SIGPIPE take the wrong branch and silently retarget, rather than shielding it. All four become herestrings, which have no pipeline to fail. Adds bin/check-shell-sigpipe.sh, run in CI beside the copyright self-test (seconds, no JDK) and granted to the reviewer. Verified both directions: clean on this tree, exit 1 when the old line is reinstated. It skips itself, since its failure message necessarily contains the anti-pattern as the "wrong" half of a worked example. shellcheck does NOT detect this - run against the known-bad line, it passed clean. Hence a bespoke grep rather than adopting a linter. Note check-review-posted.sh:94 is also fixed in #210, which adds the functional regression test for it. Conflict expected and cheap; this PR fixes it because it is this PR that states the rule. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RtNUsxokE9g2pSEjBHZqNA
bin/test-check-review-posted.sh was missing from --allowedTools while its sibling bin/test-check-copyright-headers.sh was granted. So when #210 changed the review gate, the reviewer could not run the gate's self-test and verified the fix by reading instead - and said so - on the one PR where executing it mattered most. The workflow already runs that same script itself, at its "Self-test the review gate" step. Both spellings granted for each script: these are prefix matches, not globs, so Bash(bin/foo.sh:*) does not cover ./bin/foo.sh. Deliberately NOT widened to bin/*. The comment above the list explains why it is enumerated rather than Bash(*) - the job has no fork guard beyond sender.type != Bot and reads attacker-influencable text - and a directory glob gives that margin away just as effectively. Adds bin/AGENTS.md, because nothing failed to keep the allowlist in sync: no rule existed. `allowedTools` appears in zero docs, scripts or checks, so a new verification script stays invisible to the reviewer until someone remembers by hand. An automated "every script must be granted" check would be wrong - the list is a security boundary and most scripts belong off it - so the rule is written down instead: what earns a grant, what must never get one, that both spellings are needed, and that editing this workflow costs you the review on that PR. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RtNUsxokE9g2pSEjBHZqNA
`writer | grep -q` under `set -o pipefail` reports failure exactly when it MATCHES: grep exits on the first hit, the writer takes EPIPE (141), pipefail promotes that to the pipeline's status. It only fires once the writer still has more than one pipe buffer (64 KiB) to write, so it passes every small fixture and surfaces when real data grows. - check-review-posted.sh:94 - live. Reported "no review posted" on four PRs whose reviews had posted (#198, #199, #204, #210). - check-quarantine-owners.sh:98,110 - latent, and close. `git show` pipes a whole source file into `grep -q` inside an `if`. The largest file in the repo is 65,185 bytes against a 65,536-byte buffer: 351 bytes of headroom, on a file two open PRs are adding lines to. It would fail as "annotation missing", not as a pipe error, sending the reader nowhere near the cause. - quarantine-lane-report.sh:201 - the `||` makes a SIGPIPE take the wrong branch and silently retarget, rather than shielding it. All four become herestrings, which have no pipeline to fail. Kept separate from the guard that enforces this, so the fixes can be reviewed - and reverted - on their own. shellcheck does NOT detect this: run against the known-bad line, it passed clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RtNUsxokE9g2pSEjBHZqNA
bin/test-check-review-posted.sh was missing from --allowedTools while its sibling bin/test-check-copyright-headers.sh was granted. So when #210 changed the review gate, the reviewer could not run the gate's self-test and verified the fix by reading instead - and said so - on the one PR where executing it mattered most. The workflow already runs that same script itself, at its "Self-test the review gate" step. Both spellings granted for each script: these are prefix matches, not globs, so Bash(bin/foo.sh:*) does not cover ./bin/foo.sh. Deliberately NOT widened to bin/*. The comment above the list explains why it is enumerated rather than Bash(*) - the job has no fork guard beyond sender.type != Bot and reads attacker-influencable text - and a directory glob gives that margin away just as effectively. Adds bin/AGENTS.md, because nothing failed to keep the allowlist in sync: no rule existed. `allowedTools` appears in zero docs, scripts or checks, so a new verification script stays invisible to the reviewer until someone remembers by hand. An automated "every script must be granted" check would be wrong - the list is a security boundary and most scripts belong off it - so the rule is written down instead: what earns a grant, what must never get one, that both spellings are needed, and that editing this workflow costs you the review on that PR. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RtNUsxokE9g2pSEjBHZqNA
`writer | grep -q` under `set -o pipefail` reports failure exactly when it MATCHES: grep exits on the first hit, the writer takes EPIPE (141), pipefail promotes that to the pipeline's status. It only fires once the writer still has more than one pipe buffer (64 KiB) to write, so it passes every small fixture and surfaces when real data grows. - check-review-posted.sh:94 - live. Reported "no review posted" on four PRs whose reviews had posted (#198, #199, #204, #210). - check-quarantine-owners.sh:98,110 - latent, and close. `git show` pipes a whole source file into `grep -q` inside an `if`. The largest file in the repo is 65,185 bytes against a 65,536-byte buffer: 351 bytes of headroom, on a file two open PRs are adding lines to. It would fail as "annotation missing", not as a pipe error, sending the reader nowhere near the cause. - quarantine-lane-report.sh:201 - the `||` makes a SIGPIPE take the wrong branch and silently retarget, rather than shielding it. All four become herestrings, which have no pipeline to fail. Kept separate from the guard that enforces this, so the fixes can be reviewed - and reverted - on their own. shellcheck does NOT detect this: run against the known-bad line, it passed clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RtNUsxokE9g2pSEjBHZqNA
Picks up the two new required contexts (shell: sigpipe, workflows: action versions) so this PR can report them, and resolves the overlap with #211. check-review-posted.sh:94 - both branches fixed the same SIGPIPE line. The code was identical; only the explanatory comment differed. Took master's wording. test-check-review-posted.sh - this branch's structural guard (no `| grep -q` in the checker) is now redundant: bin/check-shell-sigpipe.sh landed on master with #211 and enforces that across every bin/*.sh, in every flag spelling, with its own self-test. Removed the local copy rather than keep two rules for one thing. Case 13 is KEPT. It is behavioural where the repo-wide guard is static: it drives the checker with a match followed by >64 KiB of comments and asserts exit 0, so it would catch a regression that reintroduced the failure by some route other than a literal `| grep -q` - which is the only shape the static guard sees. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RtNUsxokE9g2pSEjBHZqNA
|
Claude finished @astubbs's task in 4m 7s —— View job Review complete ✅Tasks
SummaryThis 1. Changelog policy wording. The old text from #113 put 2. Verification performed
One thing I could not execute directly: this sandbox required interactive approval to actually run shell scripts ( Observations (non-blocking)
No bugs or factual errors found. Every checkable claim in the PR body held up against direct, independent verification. |
…anch PR Checklist failed with "12 reference(s) below #1000 do not say which repo they mean". The gate arrived in 735b1d3 (#114), which merged 17 minutes AFTER #211 - so this branch was written before the rule existed. The fork's numbers sit inside upstream's range, so a bare number is a coin flip: #114 found 48 numbers that exist in BOTH repos meaning different things. All twelve here are fork PRs, now written astubbs#NN. Pointed, given this PR is about a reference convention. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RtNUsxokE9g2pSEjBHZqNA
|
Claude finished @astubbs's task in 6m 33s —— View job Review complete ✅Tasks
SummaryThe wording fix itself is sound: the old "from the next release on" phrasing did put However, digging into the two specific PRs cited as live evidence for "don't tighten the gate" turned up a real problem: the mechanism described for both examples is wrong, verified against the actual CI logs. Finding: the false-pass/false-fail examples in
|
The gate has only ever asked whether a machine reviewed the PR. The half that actually matters to the person merging - "have I read this one myself yet?" - was carried in his head, across a dozen open PRs, and it was getting dropped: "I always need to do a review myself and add a LGTM ... sometimes I forget or get confused. It would be much easier if there was just an action that highlighted that." So the gate now asserts two things, and names them separately when they fail: an automated review exists and finished (bin/check-review-posted.sh, unchanged), and the repository owner has submitted a review saying LGTM (bin/check-human-lgtm.sh, new). Both halves run on every gate run even when the other has already failed, so one look at the checks list says which is outstanding rather than only the first one to go red. WHAT COUNTS, AND WHY IT WAS DECIDED BY LOOKING The matching rule was settled against every LGTM this repo has actually received rather than against taste, because a memory aid that rejects the habit it is meant to remind you of is a nuisance people route around. All 18 of them, from #210 to #292, are the lower-case bare word `lgtm`, submitted as a COMMENTED review - not an approval, and never in the ordinary comment box. So: a submitted pull-request review, by astubbs, whose body contains `lgtm` as a whole word in any case, outside code fences and blockquotes, not followed by `?` and not preceded by a negation. Review state is not consulted, except that a PENDING review is ignored. All sixteen historical LGTM PRs pass the new checker against live API data; #287, #296 and #297 - reviewed, or not, but never stamped - fail it. Two of those clauses are load-bearing rather than fussy. An APPROVED review with an empty body does NOT count, because a silent Approve is the reflex this exists to interrupt. A DISMISSED review DOES, because the master ruleset dismisses approvals on push: had the rule consulted state it would have been head-sensitive through the back door, going green and then quietly red again after an unrelated typo fix. HEAD-INSENSITIVE, DELIBERATELY An LGTM on any commit of the PR counts for the whole PR, permanently. That matches the automated half, which abandoned freshness for reasons recorded in docs/inflight/parked-strict-review-gate-freshness.md, and it matches what was asked for: "it doesn't need to be on the latest head because I won't say LGTM until I'm confident that we're near the merge point anyway". A stamp that evaporates on the next push trains you to stop typing it. Two self-test cases assert this as a positive rather than as deleted code, so re-adding freshness breaks the suite. RENAME, AND WHY THERE ARE TEMPORARILY TWO JOBS `claude-review` no longer describes a check that also wants a human, so the job is now named `review: bot + human LGTM` - repo convention for a required check (`shell: sigpipe`, `deps: vulnerabilities`), with the two operands naming the two halves so a red entry in the checks list says which question is open before you open anything. The job name is a required status check matched by name, so the rename cannot be done in one step: drop the old name and the ruleset requires a context nothing produces, which blocks every open PR including this one. A `needs:`-based mirror was rejected outright - a job that skips when its dependency fails reports `skipped`, which GitHub counts as satisfying a required check, so the mirror would have gone green exactly when the gate went red. Instead a transitional job keeps reporting `claude-review` under the old contract, with duplicated steps and a stated deletion condition. The ruleset is swapped in one atomic PUT while both names report, and only then is the old job deleted; the commands are in the PR description. The gate stays read-only (`contents: read`, `pull-requests: read` - the reviews endpoint needs no new scope), bot-raised PRs stay exempt, and a fork PR still cannot satisfy it. A `pull_request_review` trigger, which would clear the human half the moment the LGTM lands, is deliberately NOT added and the reasoning is written into the workflow: on that event the sender is the reviewer, so a bot-submitted review would skip the job on its existing sender guard, and a skipped job satisfies a required check. That is this repo's most-repeated defect class pointed the wrong way. The LGTM therefore needs the same re-run the automated half already documents, unless it is left before the review is asked for. The self-test carries 42 cases, over half of them negative controls, including four that read the workflow file and assert it still feeds this checker the reviews endpoint with the marker the checker parses - a gate fed the wrong stream would report success without having asked the question. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BhF637Ywr7MiKkxaR11Up3
…correct the survey it was wrong about Review findings on this branch, all four verified by running before and after. CORRECTION TO THE PREVIOUS COMMIT ON THIS BRANCH. Its message says "18 owner LGTMs" in one place and "sixteen" in another. Both are wrong, and so was every copy of that claim in the tree. The real figure, re-derived from repos/astubbs/parallel-consumer/pulls/<n>/reviews over all 181 PRs, is 50 owner LGTM reviews across 38 PRs, from #63 to #292. That commit cannot be rewritten, so this one states the correction. 1. THE SELF-TEST COULD NOT SEE A DISABLED STEP, ONLY A DELETED ONE The four coupling cases were `grep -F` substring searches over the workflow TEXT, and a substring search cannot tell a step that RUNS from one that merely APPEARS. Measured on a scratch copy: `if: false` on the step, `|| true` on its pipeline, `; true`, `set +e`, `continue-on-error: true`, and commenting the entire step out ALL left the suite green at 42/42. One case - `grep -F 'bin/check-human-lgtm.sh'` - was satisfied by two header comments alone, so it stayed green with the step deleted outright. That is the failure class in docs/solutions/workflow-issues/a-check-that-reports-success-without-having-run.md occurring inside the guard written to prevent it, which is why it is worth more than its blast radius suggests. The coupling section now parses the step out of the workflow - comments dropped first, so a commented-out step reads as an absent one - and asserts on its structure: it exists, it reads the reviews endpoint, its marker matches, its `if:` is neither a never-true constant nor changed from the intended guard, nothing in it swallows the checker's exit status, and the checker invocation is its last command. All eight sabotages above now go red; the unsabotaged copy stays green. Deliberately awk rather than python3 + PyYAML, though PyYAML does import here. This suite runs as a step of a REQUIRED check, ahead of the gate it protects, so a dependency of it is a thing that can brick every open PR by being absent from a runner image. The indentation rules it needs are the only YAML involved. 2. THE EMPIRICAL CLAIM JUSTIFYING CASE-INSENSITIVITY WAS FALSE, IN FOUR PLACES Claimed: 18 LGTMs, #210 to #292, all the lower-case bare word. Actual: 50, across 38 PRs, #63 to #292. Forty-nine are `lgtm`; ONE, on #84, is `Lgtm`. Forty-six are the bare word alone on a line; four carry a trailing clause, one of which - #73's "lgtm, @claude how about you?" - ends in a question mark. The true data argues for the design harder than the false data did. `Lgtm` is a live counterexample proving case-insensitivity is load-bearing rather than merely generous: without it this repo has a stamp on record that the checker would call missing. #73 is a real passing body containing a `?`, which is why the `?` clause rejects only a `?` touching the token. Both are now self- test cases (8c, 8d). The one clause that WAS true and is load-bearing - all 50 are COMMENTED reviews, not approvals - is kept. The count also sat inside an assertion LABEL, so a passing test printed a false claim on every run. Labels now name the PR the body came from instead. 3. README DROPPED `claude-review` FROM "JOB NAMES ARE AN API" That list named only the new job, while `claude-review` is the context ruleset 15055005 actually requires throughout cutover steps 1-3. Someone reading it before performing step 5 could conclude the old name is no longer an API and delete the transitional job BEFORE the ruleset swap - the exact bricking the workflow header warns about, arrived at by following the docs. Restored, marked transitional, with the ordering spelled out. 4. THE CANONICAL CONTRACT WAS OWNED TWICE docs/ci.md declares itself canonical and says everything else links to it; this branch then added a seven-clause restatement of a rule bin/check-human-lgtm.sh heads "THE MATCHING RULE, IN FULL". Both cannot own it, and the drift was not hypothetical - the false survey above was pasted into four files and rotted in all four inside one PR. Split by altitude: docs/ci.md owns the GATE contract (what satisfies the gate), bin/check-human-lgtm.sh owns the MATCHING RULE (what satisfies the human half). The script wins the second because it is the executable truth - its prose sits beside the awk implementing it, and prose and code in one file cannot drift unnoticed. AGENTS.md's substantive clause, which sat two lines above the sentence forbidding exactly that, is reduced to rule-plus-pointer. FOUR CHECKER BUGS, EACH PINNED BY A CASE PROVEN TO FAIL WITHOUT THE FIX - A marker line bearing this run's token but a lost field was read as more of the PREVIOUS review's body, merging the next reviewer's words into the previous segment. A six-field marker for `mallory` after an `astubbs` segment reported "astubbs submitted a review containing LGTM". Not reachable from today's --jq, but one `; next` removes the forgery path. (21c) - scan() advanced PAST each token it examined, discarding the character the next candidate needs to see, so glued repeats walked through the whole-word guard: `LGTMLGTM` passed a rule under which neither half of it does, and `xLGTMLGTM` passed one that refuses `xLGTM`. (11b, 11c) - A trailing `\r` defeated the fence-close test, so a CRLF body never closed a fence and swallowed every LGTM after it. Not live - 0 of 365 owner bodies carry a CR - which is why it needed a case rather than a wait. (12f) - Three clauses survived deletion with the suite green: the marker NAME check, the fence info-string clause, and the fence-character clause (no `~~~` fence appeared anywhere in the suite). Inputs added that flip under each. (21b, 12d, 12e) Also: docs/ci.md said there were three human-half reds when there are four - the catch-all "Could not scan this PR's reviews" does not start with NO HUMAN LGTM ON THIS PR, which defeated the "tell them apart without opening the job" promise in the same paragraph. The fourth is now listed, and named as the one that means the instrument broke rather than the work is outstanding. DELIBERATELY NOT CHANGED. `not LGTM/LGTM` returns 0. The negator rule considers only the word TOUCHING the token, and the second token's preceding character is a slash; making it 1 needs a negation detector that reads past an intervening token, which is the unbounded cleverness the script refuses by name. Pinned as case 10e so nobody widens it while fixing the glued-repeat bug beside it. VERIFIED. 55 self-test cases green. Both gate self-tests, check-shell-sigpipe, check-copyright-headers, check-issue-refs, check-docs-data, check-action- versions: all pass. Eight workflow sabotages red after, six of them green before. Seven checker mutations each turn exactly the intended new case red. Replayed against live API data for all 78 PRs carrying reviews: 38 green, zero false positives, zero false negatives against an independently written matcher. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BhF637Ywr7MiKkxaR11Up3
Description
The changelog policy from #113 is correct. Only its stated timing is wrong, and the wrong timing has already misled a reader.
#113 (
e3b1601e) moved the changelog off per-PR entries and onto release-time generation from the commit log. The sentence it wrote to describe that says:That puts
0.6.0.0on the hand-written side of the line and starts generation after it. Generation is in effect now, and0.6.0.0is the release it applies to.git log -S "from the next release"confirmse3b1601eis the sole origin of the phrase; it reached three locations, and a fourth file had already copied the claim.Why the wording, not just the fact, is the bug
An agent read it this week and concluded "0.6.0.0 is frozen, therefore v6 ships with whatever is written under that heading now." Both halves of that follow from the sentence as written.
The trap is that "frozen" is doing two jobs in one word. It is a statement about text in a file - do not add to it - and it reads as a statement about a release - settled, quotable, final. The rewrite separates them and hangs the state on a fact the reader can check: has the release shipped?
== 0.5.xand below== 0.6.0.0- the release being cutBoth wrong readings are then named and denied outright, rather than left to be inferred - a reader who has already formed one will not give it up to an implication.
The exception that had to be inferred, and now doesn't
"Do not add entries in a feature PR" does not obviously permit a correction - yet #198 made one, changing a Dependencies entry that claimed the Kafka client stayed on
3.9.1after the pom had moved to3.9.2. The rule is now explicit, with a test an author can apply:Before / after
AGENTS.md§ Changelog== 0.6.0.0is hand-written and now frozen; from the next release on, each section is generated…"0.6.0.0itself" + the shipped/not-shipped table + the two explicit anti-readings + the correction exceptionAGENTS.md"where things live" row== 0.6.0.0; later sections are generated at release time"== 0.6.0.0included - is regenerated at release time … what is under it today is working text, not the notes v6 will publish"CHANGELOG.adocheaderNOTE::0.6.0.0is hand-written and frozen. From the next release on, each section is generated…"0.6.0.0below: it has not shipped, so what you are reading there is working text and not the notes that release will publish"docs/inflight/release-0600-blockers.md(4th site, already copied)CHANGELOG.adocis frozen up to== 0.6.0.0"== 0.6.0.0section is not final … do not quote it as the release notes"README.adocis untouched and needs no regeneration - #113 stopped it embedding the changelog, and neither it norsrc/docs/README_TEMPLATE.adocrestates the policy (verified). The== 0.6.0.0section's content is deliberately not edited; regenerating it is release-time work.Second defect: the changelog gate note was false when written
The same section claimed:
Four open PRs modify
CHANGELOG.adocright now - #51, #57, #105, #106 - all opened before #113 merged (verified viagh pr view --json files). Every PR predating the policy is in the same position.Worse, the gate never enforced the policy in the first place, and misses in both directions:
changelog-ref-gate.jsonly fails entries citing no issue. fix(core) astubbs#120: unbounded PCMetrics heap growth, with the confluentinc#905 hot-shard metric #57's entries all cite issues, so they pass the gate while violating the policy outright.changelog-ref: N/A - <reason>is the intended escape, and the workflow comment already names this exact case.The note now states this, and says plainly that a green gate is not compliance with the rule.
My call on tightening it: don't - documented instead
Reasoning is inlined in the
AGENTS.mdgate subsection itself, so a reader gets it without a second hop. The scope of the gate is unchanged and the citation check is not weakened. Three reasons:+*lines. The gate's own header records that fuzzy-matching removed bullets against added ones was built, became the largest and subtlest part of the file, and still mispaired entries built from a common template. Re-deriving it repeats known-failed work.changelog-ref: N/A - <reason>, a self-declared line in the PR body. The same line that legitimises a correction legitimises a policy-violating addition - so the strict gate buys no enforcement over the written rule, and costs an opt-out line on every legitimate correction.If entries do start reappearing after 0.6.0.0 ships, the mechanically honest version is a warning, not a gate: any
CHANGELOG.adocchange posts a comment asking the author to confirm it is a correction. No false failures, and it teaches the policy at the point of edit rather than at a release-time merge conflict.changelog-ref: N/A - the only
CHANGELOG.adocchange is the headerNOTE::describing how the file is maintained. No entry is added, edited or removed.Checklist
AGENTS.mdN/A, no behaviour change;.github/scripts/changelog-ref-gate.test.jswas run and passes unchangedN/A, no workflow or runner change (the gate is described, not modified)One test kept from the review-gate work
An earlier revision of this PR also fixed
bin/check-review-posted.sh:94-printf | grep -qunderset -o pipefail, which reports failure exactly when it matches, becausegrep -qexits on the first hit andpipefailpromotes the writer'sEPIPE. That fix has since landed on master via #211, so it is no longer part of this diff - merging master resolved it to a no-op.What survives here is case 13 in
bin/test-check-review-posted.sh: a match followed by >64 KiB of further comments must still pass.It is worth keeping alongside #211's guard rather than being redundant with it. That guard is static - it greps
bin/*.shfor a literal| grep -q. This case is behavioural: it drives the checker with a real oversized payload and asserts exit 0, so it catches a regression that reintroduces the failure by any route the static grep does not recognise.The existing suite could not have caught the original bug: case 5 buries the match in a multi-line body but keeps it small, and case 6 puts the match last, so nothing follows it to fill the pipe buffer.
References
Qualified as
astubbs#NNthroughout, per the gate added in #114 (735b1d3a), which merged after this branch was written. The fork's numbers sit inside upstream's range, and #114 found 48 that exist in both repos meaning different things.Known-red check
Check PR Dependenciesdoes not report on this branch - it needed an admin bypass to push. Unrelated to this PR's content.