feat(gates): make an outstanding-issues id collision a red gate - #1410
Conversation
Ledger #112. The `issues:next-id` marker is a plain HTML comment that every editor read-modify-writes with no lock, and this file — unlike the branch review ledger — has NO `merge=union` driver. So two agents allocating in the same hour collide, and the collision surfaces as an ordinary content conflict that a hurried resolution can settle by taking one side wholesale and dropping the other's rows. On 2026-07-29 that happened three times in one hour on a single PR, and nothing noticed, because no gate read this file's structure at all. `npm run check:outstanding-issues` now does, in `verify:cheap` and in the `static-pr` CI job — the gate-manifest check refuses a local gate CI does not run, which is how I learned to add the second one. It fails on: - a duplicate id (a merge that kept both sides under one number) - an id in BOTH tables (an archive move that copied instead of moving) - a marker at or below the highest id (a merge that lost the bump) - a row whose cell count differs from its table's declared width - a missing heading or marker Verified by replaying the actual collision against the real file rather than only against fixtures: two rows claiming `#110` produce "#110 appears 2 times (lines 151, 164)", and a lost marker bump produces "issues:next-id=113 is not above the highest id #114". Two things the first draft got wrong, both caught by running it rather than reading it: Table width was inferred from the modal row width, which cannot flag the anomaly when a table holds one row. It now comes from the separator row, which is where a table actually declares its shape. The separator regex then had to include the inner pipes — without them it only ever matched a two-column table, so every real table silently had no declared width and the check was inert. And `cells()` split on every pipe, so its first run against the live file reported row #42 as malformed. That row is correctly escaped (`absent \| valid \| invalid`); the checker was wrong. It now splits on unescaped pipes only. A gate with false positives is a gate people switch off. Scope is deliberately structural. It says nothing about whether a row's content is right, because that is a judgement a gate cannot make, and pretending otherwise would make it noisy enough to ignore. #112 is archived rather than left open-and-done, with the limit stated: the underlying race is NOT fixed. Ids are still allocated by read-modify-write with no lock and the file still has no union driver. What changed is that a collision can no longer land silently. verify:cheap exit 0 — 28 gates, Tests 4450 passed | 4 skipped. format:check clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FvU8z73P6TXUXoYBqN5K1P
|
This pull request has been ignored for the connected project Preview Branches by Supabase. |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (4)
📝 WalkthroughWalkthroughA new ChangesOutstanding issues integrity
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant verifyCheap
participant StaticPR
participant checkOutstandingIssues
participant OutstandingIssuesLedger
verifyCheap->>checkOutstandingIssues: run self-test and ledger check
StaticPR->>checkOutstandingIssues: run ledger integrity check
checkOutstandingIssues->>OutstandingIssuesLedger: read and validate markdown
OutstandingIssuesLedger-->>checkOutstandingIssues: ledger contents
checkOutstandingIssues-->>verifyCheap: pass or fail
checkOutstandingIssues-->>StaticPR: pass or fail
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d4445166b2
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Both review findings are right, and both are the same failure this gate keeps producing in draft: a check that looks like it covers a case and does not. A row whose id cell is `001`, `#OO1` or empty did not match `ROW`, so it was dropped before every structural check — duplicate detection, the marker comparison, the width check — and the file reported green while carrying exactly the malformed row the gate advertises. Rows are now recognised by being a table body row at all (not a header, not a separator) and the id shape is validated rather than assumed, so an unparseable id is a reported problem instead of an invisible one. `markdown.match(MARKER)` returned only the first marker, so a conflict that kept two left a stale allocation the checker never saw. With markers 116 and 115 and a highest id of #115, nothing failed — and a later editor following the stale 115 reuses an id, which is the exact outcome this gate exists to prevent. Exactly one marker is now required. Four adversarial self-tests, all verified red against the previous logic: self-test FAILED: a dropped # on an id — expected 1 problem(s), got 0 self-test FAILED: a letter O for a zero — expected 1 problem(s), got 0 self-test FAILED: an empty id cell — expected 1 problem(s), got 0 self-test FAILED: a second next-id marker kept by a conflict — got 0 verify:cheap exit 0 — Tests 4450 passed | 4 skipped. format:check clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FvU8z73P6TXUXoYBqN5K1P
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 412afc59e4
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Three review rounds each found another row shape the detector silently dropped, because it asked "does this line look like a row?" and anything that did not was invisible to every check below. Patching the predicate a fourth time would only move the blind spot, so this inverts it: a table body is defined positionally, from its separator to the next heading or blank line, and every line in that span must be a well-formed row. Running that against the real file rather than its fixtures exposed the defect the previous logic was structurally unable to report. The archive section carries blank lines part way down its rows. GFM ends a table at the first blank line, so 56 of the 60 archived rows have been rendering as a paragraph of literal pipe characters, not as table rows. The prior checker passed that file with zero problems while counting all 114 rows. Rows outside every table are now their own reported failure, and the two stray blank lines are removed. Cell content is byte-identical; the row set sorts equal before and after, and the remaining churn is Prettier realigning what is now a single 60-row table. Also fixed while proving the new cases red: - ids are compared against a canonical zero-padded form, so `#1` and `#1` can no longer both exist as one allocation split across a merge - a deleted separator is reported rather than silently disabling the width check for its whole section - column counts are checked per block, against the width that block's own separator declares - body scanning uses an ATX heading test rather than startsWith("#"), because a row that loses its leading pipe begins `#1` — every id row does, so the old test ended the body early and hid the damaged row Six new self-test cases; five were green under the previous logic. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FvU8z73P6TXUXoYBqN5K1P
…collision # Conflicts: # docs/outstanding-issues.md
Co-authored-by: BigSimmo <BigSimmo@users.noreply.github.com>
Co-authored-by: BigSimmo <BigSimmo@users.noreply.github.com>
The #113 archive row cited `c2edda18` as PR #1405's merge commit. That was the branch tip, which the squash merge discarded, so the claim was already unverifiable when it was written: git merge-base --is-ancestor c2edda1 origin/main -> fails git merge-base --is-ancestor 020c126 origin/main -> ok 020c126 fix(mode-nav): size slots to their content ... (#1405) In a ledger whose whole purpose is durable, auditable resolution history, a resolution nobody can check is worse than no resolution note. The trap is that this repo mixes merge strategies: #1407 and #1410 landed as merge commits, so their branch SHAs stay reachable; #1405 was squashed and its did not. Copying the PR head works three times in four, which is exactly the kind of rule that survives review until it doesn't. Verified after the edit: `c2edda18` no longer appears anywhere in the file, the replacement is an ancestor of main, and `check:outstanding-issues` still passes with `117 rows (55 open, 62 archived), unique ids, next-id=118 above the highest`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FvU8z73P6TXUXoYBqN5K1P
* process: cut future PR conflict churn and silent CI gaps Add outstanding-issues ID/marker/union guards (#112), a read-only pull_request_target mergeability signal for dirty heads (#116), and an anti-conflict CI-speed operating procedure that prefers bundling and format-before-push without weakening required gates or touching active PRs. Co-authored-by: BigSimmo <BigSimmo@users.noreply.github.com> * docs(ledger): record PR #1416 merge-readiness as not ready Main advanced with #1410 overlapping the outstanding-issues gate; merge-tree is conflicting. Unique value remains the anti-conflict playbook, #116 signal, and merge=union. Co-authored-by: BigSimmo <BigSimmo@users.noreply.github.com> * style(issues): format outstanding-issues after main sync Prettier realigns the archive table after the #116 close and #112 note update. Co-authored-by: BigSimmo <BigSimmo@users.noreply.github.com> * docs(ledger): supersede #1416 merge-readiness as ready Record the post-sync review: merge-tree clean, unique #116/process value kept, duplicate #112 checker dropped in favor of #1410. Co-authored-by: BigSimmo <BigSimmo@users.noreply.github.com> * docs(ledger): pin #1416 READY review to final tip SHA Co-authored-by: BigSimmo <BigSimmo@users.noreply.github.com> * fix(test): shrink zip-bomb fixture to stop CI coverage timeout Unit coverage failed because reject-high-compression-ratio allocated and deflated 24MB of zeros under the 30s Vitest timeout. 1MB still exceeds the 150:1 admission ratio (~480:1) without the CI flake. Co-authored-by: BigSimmo <BigSimmo@users.noreply.github.com> * fix: refresh mergeability after base advances * fix: preserve ledger rotations during merges * fix: resolve issue ledger merge collision --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: BigSimmo <BigSimmo@users.noreply.github.com>
Summary
Fixes ledger
#112. Theissues:next-idmarker is a plain HTML comment that every editor read-modify-writes with no lock, anddocs/outstanding-issues.md— unlikedocs/branch-review-ledger.md— has nomerge=uniondriver. So two agents allocating in the same hour collide, and the collision surfaces as an ordinary content conflict that a hurried resolution can settle by taking one side wholesale and dropping the other's rows.On 2026-07-29 that happened three times in one hour on a single PR (#1391 lost
#096/#097, then#098/#099, then collided again on#108/#109). Nothing noticed, because no gate read this file's structure at all.npm run check:outstanding-issuesnow does — inverify:cheapand in thestatic-prCI job. It fails on:#1,#0001,#OO1, a dropped#) — two spellings of one allocationScope is deliberately structural. It says nothing about whether a row's content is right, because that is a judgement a gate cannot make, and pretending otherwise would make it noisy enough to ignore.
The live defect this found
Running the checker against the real file rather than its fixtures exposed a rendering defect that has been on
mainand that no earlier version of the gate was structurally able to report.The archive section carried blank lines part way down its rows. GFM ends a table at the first blank line, so 56 of the 60 archived rows were rendering as a paragraph of literal pipe characters, not as table rows. The pre-fix checker passed that file with zero problems while counting all 114 rows — it counted them and never noticed they were outside a table:
The two stray blank lines are removed here. Cell content is unchanged — the row set sorts byte-identical before and after — and the remaining churn in
docs/outstanding-issues.mdis Prettier realigning what is now a single 60-row table. The guard reports115 rows (55 open, 60 archived)where the pre-fix reader saw 58.How the row detector was rebuilt, and why
Three review rounds each found another row shape the detector silently dropped. Each time the cause was the same: the detector asked "does this line look like a row?", so anything that did not look like one was invisible to every check below — including the checks meant to report it. Patching the predicate a fourth time would only have moved the blind spot.
So detection is now positional. A table body is everything from its separator to the next heading or blank line — exactly what Markdown treats as one table — and every line in that span must be a well-formed row. A line that is not one is a reported problem rather than a skipped line. Detection no longer depends on the row being parseable, which is what kept regenerating the bug.
Two further defects fell out of that work:
startsWith("#"). A row that loses its leading pipe begins#001 | …— every id row does — so the scan mistook the damaged row for a heading, ended the body one line early, and hid precisely the row it was meant to catch. Now/^#{1,6}(\s|$)/, since an ATX heading requires whitespace after its hashes.Verification
Replayed the actual collision against the real file, not just fixtures:
Every new self-test case was run against the previous logic to confirm it was actually green before, rather than assuming the test was meaningful:
--self-testcovers 16 failure shapes plus a well-formed file and an escaped pipe, mirroring thecheck-branch-review-ledgerconventionnpm run verify:cheap— exit 0,Test Files 432 passed (432),Tests 4470 passed | 4 skipped (4474)npm run check:gate-manifest— allverify:cheapgates enforced in CInpm run format:checkclean; ESLint cleanmainresolved and verified row-by-row: same 115-id set asmain, none lost, none added, no duplicatesThree things the first draft got wrong, all caught by running it rather than reading it — worth recording, because each would have shipped an inert or actively annoying gate:
/^\|[\s:-]+\|$/excluded the inner pipes, so it only ever matched a two-column table. Every real table therefore had no declared width and that check was silently inert — the self-test caught it.cells()split on every pipe, so the checker's first run against the live file reported row#042as malformed. That row is correctly escaped (absent \| valid \| invalid) — the checker was wrong, not the file. It now splits on unescaped pipes only. A gate with false positives is a gate people switch off.Risk and rollout
verify:cheapand one CI step; no product code. The one content change is the removal of two blank lines from a docs file, with the row set proven identical. The realistic failure mode is a false positive blocking an unrelated PR, which is why (3) above mattered and why the scope stays structural.Clinical Governance Preflight
No ingestion, answer generation, search/ranking, document access, privacy or clinical output path is touched.
Clinical KB Database(sjrfecxgysukkwxsowpy)RAG impact: no retrieval behaviour change — a docs checker, one npm script, one CI step, and one docs file's whitespace; no retrieval, ranking, selection, or answer-generation code is touched.
Notes
#112is archived rather than left open-and-done, and its outcome states the limit plainly: the underlying race is not fixed. Ids are still allocated by read-modify-write with no lock, and the file still has no union merge driver. What changed is that a collision can no longer land silently. A real fix would bemerge=unionplus allocation that does not depend on reading a counter — worth doing, but a different change, and row-level union alone would not prevent two rows sharing a number.🤖 Generated with Claude Code
https://claude.ai/code/session_01FvU8z73P6TXUXoYBqN5K1P
Summary by CodeRabbit
Bug Fixes
Documentation