Skip to content

Rectify incomplete merge-gate evidence - #321

Closed
lamemustafa wants to merge 53 commits into
masterfrom
tooling/merge-gate
Closed

lamemustafa wants to merge 53 commits into
masterfrom
tooling/merge-gate

Conversation

@lamemustafa

@lamemustafa lamemustafa commented Sep 12, 2026

Copy link
Copy Markdown
Owner

Closes #328. Addresses #317's incomplete merge-evidence failure mode.

Functional summary

Add a read-only preflight that binds its result to the full current PR head and master tip. It requires the exact protected-context set, a completed current provider review, independent acceptance, complete review/file/commit inventories, description evidence and compatibility manifests. Known failures return MUST NOT MERGE; missing or malformed evidence returns INDETERMINATE. The command never changes GitHub state.

Check and combined-status pagination validate complete, unique, current-head records. Large context and review payloads travel through files, avoiding Linux argument limits; failed diagnostic computation cannot become success. Required skipped checks remain blocking, while deliberate optional skips are allowed. A summary-only provider receipt needs an explicit independent-review attestation naming the complete head.

The diff parser preserves payload using Git's LF delimiter, including Unicode separators and CRLF content. Independently quoted paths, textual destinations and rename metadata reconcile with REST file records. Added payload, paths, PR text, commit messages and standard Git identity fields enter the bounded privacy heuristic. Horizontal Unicode spaces normalize without joining distinct lines. Coverage and metadata-only diagnostics redact private path shapes and bound every example. Null linked GitHub identities remain valid; ordinary names and email addresses are not automatically classified as private data. Binary changes require human inspection, and gitlinks remain indeterminate pending provenance, license and NOTICE review.

The description must link an actual checklist item at the complete candidate SHA and provide a concrete validation command. Populated canonical template fields are accepted; empty labels, hidden HTML comments (including unclosed comments), bare placeholders and unrelated sibling fields are insufficient. Raw body bytes remain available to privacy scanning and final drift detection. Checklist anchors require a real delimiter after the line number. Security impact accepts explicit None/N/A/no impact responses while rejecting pending or TODO. Implementation additions, including shell, PowerShell and SQL code, require all three P4 answers. Native and platform-sensitive paths require affected-host evidence or a justified unaffected-host statement; workflow and database migration paths require rollback notes. Renames retain their prior path for conditional checks. DSC/credential changes additionally need a fetched independent security-focused reviewer record naming the full head.

Scope, reuse, and impact

  • Existing component reused: gh, jq, Bash, Python and the repository's compatibility tooling; no new production dependency.
  • What is deleted (or why no deletion is justified): removes timestamp/short-prefix authentication and empty-result fallbacks; replaces incomplete API and line parsing with bounded, checked evidence handling.
  • What breaks if this is not built: incomplete or stale review/API evidence can be mistaken for permission to merge.
  • Net LOC change: +2663 (2666 added, 3 deleted).

Test or reproduction command

Candidate: 7ea1235396bb1ddf2cab0e9f5cdda079c65a2f17, including master ba1742cf0ec8f17c3f10d537a5bfa845739b946f.

  • python3 scripts/merge-gate.test.py: all 118 controls passed at immutable 2151a22923c1d222e0906d4691a595c9bcb97b40 (490.465 seconds, exit 0). Input hashes and the clean head matched before and after execution. Covers malformed and paginated evidence, large Linux context inputs, Unicode/CRLF paths, visible/raw body handling, conditional policy fields, privacy diagnostics and gitlink holds.
  • The normal merge of master ba1742cf0ec8f17c3f10d537a5bfa845739b946f preserved those three gate source files byte-for-byte. Compatibility tooling matches the accepted Rectify compatibility output mode preservation #334 tree; all 212 pins passed membership and actual-byte verification after regeneration.
  • The final PowerShell/SQL classification extension passed the three affected control methods (including subcases), exit 0: python3 scripts/merge-gate.test.py MergeGateControls.test_implementation_additions_need_all_three_p4_answers MergeGateControls.test_database_migration_paths_need_rollback_notes MergeGateControls.test_wrapped_canonical_migration_template_field_is_recognized. Full 118 execution is recorded at the earlier immutable head, not claimed for changed final bytes.
  • bash -n scripts/merge-gate.sh, Python compilation and git diff --check passed. Root reviewed the complete policy/parser changes; Luna independently accepted the immutable gate batch and final extension. Hosted provider review must separately complete on the exact published head.
  • macOS validation: local controls passed on macOS. Current Linux Workflow consistency passed all 118 gate tests in 339.076 seconds, exit 0. Its checked-out merge commit facf1cb292f0d9a16dfbf3b2aff7267fd5b0b98a has the exact candidate tree 98e191799f1ffe8235288239c5d7a0c53fc6667b, independently verified against both parents. Windows and macOS native checks and bundle smoke also pass; the compiler-cache retention skip is optional.
  • Windows validation: native application behavior is unaffected by this read-only shell tool; Windows shell execution is unqualified.

Migration compatibility

No application schema, persisted data, Tally transport or credential change. The preflight supplements branch protection and required native/live evidence. Its field checks and privacy heuristic do not replace human assessment of test results, source completeness, licensing or private-data exposure. Independent acceptance also verifies whether production-regression policy applies, that the branch is dedicated to its fix, and that the required rectification label is present. AGENTS.md defines no branch-name prefix or authoritative metadata classifier; the gate documents this semantic responsibility instead of inventing one. This PR is dedicated to the merge-gate defect and carries type:rectify.

Rollback notes

Revert this PR to remove the advisory preflight and its Workflow consistency invocation. Branch protection remains in place. Run a fresh preflight immediately before a head-bound merge after relevant state changes.

Security impact

Read-only GitHub queries, numeric PR selection and fixed repository validation. Privacy diagnostics report counts instead of matched values. Credential and DSC paths are unchanged.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 12, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-13T00:17:16.490080Z 7ea1235 New commits
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b9ad301a1c

ℹ️ 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".

Comment thread scripts/merge-gate.sh Outdated
Comment thread scripts/merge-gate.sh Outdated
Comment thread scripts/merge-gate.sh Outdated
Comment thread scripts/merge-gate.sh Outdated
Comment thread scripts/merge-gate.sh Outdated
Comment thread scripts/merge-gate.sh Outdated
Comment thread scripts/merge-gate.sh Outdated
Comment thread scripts/merge-gate.sh Outdated
@lamemustafa
lamemustafa force-pushed the tooling/merge-gate branch 2 times, most recently from e0e6dc3 to 5bf8320 Compare September 12, 2026 07:50

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5bf83200bb

ℹ️ 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".

Comment thread scripts/merge-gate.sh Outdated
Comment thread scripts/merge-gate.sh Outdated
Comment thread scripts/merge-gate.sh Outdated
Comment thread scripts/merge-gate.sh Outdated
Comment thread scripts/merge-gate.sh Outdated
Comment thread scripts/merge-gate.sh Outdated
Comment thread scripts/merge-gate.sh Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 33bdc32182

ℹ️ 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".

Comment thread scripts/merge-gate.sh Outdated
Comment thread scripts/merge-gate.sh Outdated
Comment thread scripts/merge-gate.sh Outdated
Comment thread scripts/merge-gate.sh Outdated
Comment thread scripts/merge-gate.sh Outdated
Comment thread scripts/merge-gate.sh Outdated
Comment thread scripts/merge-gate.sh Outdated
t and others added 7 commits September 12, 2026 14:07
The gate for "may this PR be merged" has lived in one session's head, and it
has been got wrong from both directions in a single day.

On 2026-09-12 four PRs were merged before their reviews arrived — #312 by
three seconds — leaving eleven findings, four of them P1, on code already in
master. The gate added to stop that then deadlocked a clean PR, because a
Codex pass with no findings submits no review object at all, only a summary
row and a thumbs-up. Later the same day two separate sessions reported a PR
"clean, zero open threads" from a query issued seconds before its review
posted, and a third reported clean without noticing master had moved.

One rule underneath all of it: **a review is only evidence about the commit it
names.** "No findings yet" and "not looked yet" are indistinguishable unless
you read which commit was looked at — so the script reads the commit in the
summary table, not the review list and not the comment's timestamp, because
that comment is edited in place and its `created_at` means nothing.

Six checks, each carrying the incident that motivates it:

1. mergeable against its base;
2. based on master — `ci.yml` fires on `pull_request` into master only, so a
   stacked PR runs no CI at all and its green tick measures nothing;
3. every check concluded and none failing;
4. a Codex review naming *this* head, completed rather than running;
5. zero unresolved threads, paginated, since `required_conversation_resolution`
   makes this the gate rather than a courtesy — and a `first:100` page once hid
   19 open threads;
6. a privacy scan of the **whole merge diff**, not the author's own commits: a
   real transaction reference sat in a comment on public master through two
   PRs because each author scanned only what they wrote.

The scan was wrong twice while being written, both times in the direction that
reports clean:

- `XXXXX1234X` is a fabricated PAN and `X` is an uppercase letter, so it
  matched the PAN shape. A gate that cries wolf gets ignored, which is worse
  than no gate, so obvious placeholders are excluded — but by an explicit list,
  never by widening the shape, which would start excusing real values.
- the first placeholder pattern used a backreference, which is not ERE. grep
  errored, returned no matches, and the scan reported clean. That is the same
  shape as a control reporting zero because its branch never fired, so the
  pattern is now probed against a string it must match before being trusted.
- UUID tails are twelve hex characters and often all digits; the canonical
  `550e8400-…-446655440000` tripped it. UUIDs are stripped before scanning.

Verified in both directions rather than assumed: a diff carrying a real-shaped
GSTIN, mobile number and account number is flagged; one carrying only
placeholders and a UUID is not. Run against three live PRs it blocks each for
a different, correct reason — a running review, unresolved threads, and a
non-master base.

Usage: `scripts/merge-gate.sh <pr> [owner/name]`; exit 0 may merge, 1 must not.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… catch

Eight findings on the first revision, plus two more found by testing it rather
than reading it. A script whose whole purpose is rigour had ten holes, and the
two that matter most were found by running it against the very case it exists
for.

**The scan could not see the leak that motivated it.** The pattern was
`\b[0-9]{11,18}\b`. The reference it was written to catch is written
`HDF CH12345678901` — glued to letters — and `\b` does not match between `H`
and `1`. Verified directly: the old pattern returns nothing on that string.
The gate would have passed the exact commit it was built to stop. The word
boundaries are gone.

**It scanned removals as well as additions,** so it blocked the PR that
*deletes* leaked data while passing the one that adds it. Added lines only now.

Then the eight from review, each confirmed against the tool it names:

- `BEHIND` was not blocked. `mergeable` can read `MERGEABLE` while
  `mergeStateStatus` is `BEHIND`, meaning the head never saw the current base —
  so its CI and its review describe a tree that no longer exists.
- `gh pr checks` buckets are `pass|fail|pending|skipping|cancel`. The regex
  looked for `cancelled`, so a cancelled check counted as neither failing nor
  pending and read as success. Now the JSON buckets are read instead of the
  text columns, which a check name containing spaces mis-splits anyway.
- A review reported `Failed` was treated exactly like `Completed`. A failed run
  means nothing looked at the code — the original bug inverted. Only
  `Completed` passes.
- `--repo owner/name` was documented and not parsed: `REPO` became the literal
  `--repo`. Proper option parsing, and `OWNER/NAME` is validated.
- On a transient GraphQL failure the thread query fell back to a hardcoded
  repository with the same PR number. A same-numbered PR elsewhere with no
  open threads would have read as a clean result. The fallback is gone.
- The issue-comments query was unpaginated; the Codex summary is an ordinary
  comment and the default page is 30.
- Nothing bound the merge to the reviewed commit. The script now prints the
  merge command carrying `--match-head-commit <sha>`.
- Draft and non-OPEN states were not checked.

Dropping `\b` made hex digests visible — a sha256 in a lockfile contains long
digit runs by chance — so digests and UUIDs are stripped before scanning.
Both are machine-generated and neither can carry a client identifier; the
placeholder list was deliberately *not* loosened, because loosening the shape
starts excusing real values.

Verified in both directions rather than assumed. On a 4,799-line real diff:
zero false positives. On constructed input: the glued-to-letters reference, a
real-shaped GSTIN and a real-shaped mobile number are all flagged, while a PR
that only removes a leak is not. Every exit path exercised — malformed
`--repo`, missing value, unknown option, no arguments all return 2; a stale
head returns 1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Running the gate across the whole open queue surfaced two false positives, both
worth fixing rather than tolerating — a gate that cries wolf gets ignored,
which is the failure mode this one is least able to afford.

`00000000005551` in a constructed test page is a padded MICR fixture, not an
account number. Excused by `^0{6,}[0-9]{1,5}$`, kept deliberately narrow: a
real account number can begin with a zero or two, so only a run of six or more
leading zeros — plainly synthetic — is excused. Verified that three
real-shaped account numbers with one and two leading zeros are still flagged.

The other was this script flagging itself. A comment named the canonical RFC
example UUID with its middle elided, so the UUID stripper could not match it
while its digits still read as an identifier. The comment no longer carries
example digits at all: a literal in a comment is a literal in the diff, and
this scan reads its own file like any other — which is exactly how the
reference that motivated the gate reached public master in the first place.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Round two on the gate: seven findings, two of them holes a determined actor
could walk through, and one the gate found in its own author the moment it ran.

**The review row was spoofable.** The summary was located by a marker string in
a comment body — and a marker is just text. Any PR participant could post an
ordinary comment carrying that marker and a `Completed` row for the current
SHA, and the gate would have recorded a review that never happened. It now
requires the comment's author to be `chatgpt-codex-connector[bot]` with a `Bot`
account type, checked alongside the marker.

**A binary file was an invisible pass.** `gh pr diff` emits `Binary files …
differ` instead of content, so a screenshot or PDF of a client statement
produced a scan with nothing to find and a clean verdict — the exact shape of a
control reporting zero because it cannot see, which this repository has written
down twice. Binary changes now block and are named for inspection by hand.

**Exemptions are reported rather than silent.** Stripping UUIDs and hex digests
keeps the false-positive rate low enough that the gate gets read at all, but a
blanket exemption nobody can see is how a real value gets erased. The count of
exempted lines is now printed.

Four more, each confirmed:

- `mergeStateStatus=BLOCKED` fell into a wildcard that printed `ok`, reading as
  approval for a state GitHub is refusing. It now prints a note, and an
  unrecognised state blocks rather than being guessed at.
- `reviewThreads` blocked permanently once a PR passed 100 threads, since
  `hasNextPage` stays true however many are resolved. A PR accumulates threads
  by being reviewed carefully, so the rule punished exactly the PRs it should
  trust. It paginates now.
- `gh pr checks` exits nonzero both when a check fails and when the query
  fails, so empty stdout from a broken query was reported as "no checks" — a
  statement about the PR rather than about the request. The two are now
  distinguished by whether anything reached stderr.
- AGENTS.md:23 requires every PR to link a completed line in
  `review-checklist.md`, and the gate did not check the repository's own stated
  pre-merge rule. It does now — and immediately blocked this PR, whose
  description did not carry one.

Each fix exercised rather than assumed: the author filter admits only the Bot
account; the binary matcher fires on a binary patch and not on a text one; the
thread loop terminates and counts across pages; the checklist rule blocks a PR
without the link.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Round three: seven findings, three of them P1, and one is a third blind spot
in the privacy scan — the third distinct way the scan could be handed data it
would not look at.

**`++` at the start of a line hid a value from the scan.** The filter dropped
`^+++` to skip the unified-diff file header, but an added line whose own
content begins with `++` produces exactly that prefix. `++ customer ABCDE1234F`
yielded no scannable text at all. The header is now identified structurally —
`+++ b/<path>` or `+++ /dev/null`, never the bare prefix — so content reaches
the scan and headers still do not.

**A seven-hex prefix is 28 bits and can be ground deliberately.** The review
row was matched by substring, so an author could push a commit sharing the
reviewed prefix and have a stale `Completed` row vouch for code nobody read.
Codex publishes only seven characters, so the comparison cannot be
strengthened — but a ground commit must be created *after* the review it
impersonates, and that is checkable. The row's own timestamp is now compared
against the head commit's committer date, and a review that predates its head
is refused. The match is also anchored to the backtick cell, since an
unanchored substring matched the row's timestamp and URL too — neither of
which is a claim about a commit.

**Identifiers were matched uppercase-only.** A GSTIN or PAN written in lower or
mixed case is the same identifier, and prose is exactly where it would be
written that way. Matching is case-insensitive now, with the placeholder list
applied to the uppercased form so `xxxxx1234x` stays excused.

Four more:

- A binary **deletion** emits `Binary files a/x and /dev/null differ` and was
  counted like an addition, so the gate blocked the PR removing a leaked
  screenshot. Same inversion as scanning removed lines, in a different rule.
- `--match-head-commit` validates only the head, so the base can be changed
  after the check without moving the head. The printed command now re-reads
  the base and compares it before merging.
- A failed comments query with empty stdout was reported as "no Codex review
  summary at all" — "I could not ask" stated as "there is no review", which is
  the exact substitution this script exists to prevent.
- `--repo=` with an empty value was accepted.

Both of the first two fixes were wrong on first writing and caught by their own
controls: the header filter still swallowed `+++ customer …` until it matched
`b/` and `/dev/null` explicitly, and the case-insensitive match used `-O` for
`-o`. Every rule is now exercised in both directions — content reaches the scan
while real headers do not, a lowercase PAN is flagged while a lowercase
placeholder is not, a binary add blocks while a binary delete does not.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three sessions hit the same defect independently in one day: a pinned file's
bytes moved and the compatibility surface was not resealed. Once by editing,
once by running `cargo fmt` *after* resealing, and once by rebasing — where the
rebase takes the base's manifest and the author touches nothing.

That spread of causes is the point. The rule people had written down was "the
reseal is the last step before `git add`", and each of us broke it while
believing we were following it, because each filed it under the *situation* we
had just been in rather than under the actual invariant: **any operation that
can change the bytes of a pinned file — an edit, a formatter, a merge, a
rebase — invalidates the seal, and the reseal runs after the last of them.**

Nothing in the local loop re-reads pins before a commit, so CI's gate is the
only thing that notices, and every instance therefore reaches a reviewer
instead of its author. That makes it a class, not a set of mistakes, and a
class is worth closing here rather than writing down again.

The check needs no checkout: read the 211 pinned paths from the manifest at the
PR head, intersect with the PR's changed files, and require the manifest to
have moved if any of them did.

It is deliberately weaker than CI's gate and says so: it proves the reseal was
*performed*, not that the hashes are *right*. Only the real gate proves that.
But every instance observed was a reseal that never ran at all, so this catches
the whole observed failure while costing one API call.

Verified in three directions rather than two: a pinned file changed without the
manifest blocks; the same change with the manifest passes; a PR touching the
manifest alone has nothing to reseal and passes. Against live PRs, #314 reports
one pinned file with the manifest moved alongside it, and #320 reports nothing
pinned.

Credit where due — this was suggested by the lane on #288, which had just been
bitten by the `cargo fmt` variant, on the grounds that closing the class beats
closing the instances. It was right.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… literals

A comment explaining the `++` blind spot used a realistic PAN as its example,
and the newly case-insensitive matcher flagged it. Correctly: the scan reads
this file like any other, and **a literal that illustrates a leak pattern is
the pattern**.

Third time a comment here has tripped the check — a UUID, a partial UUID, now a
PAN. Each time it was the check working. The rule is now written in the comment
so the next person adding an example reads it first: describe the shape, never
spell it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c19255ab89

ℹ️ 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".

Comment thread scripts/merge-gate.sh Outdated
Comment thread scripts/merge-gate.sh Outdated
Comment thread scripts/merge-gate.sh Outdated
Comment thread scripts/merge-gate.sh Outdated
Comment thread scripts/merge-gate.sh Outdated
@lamemustafa lamemustafa changed the title tooling: write the merge gate down, with each rule's failure beside it Rectify incomplete merge-gate evidence Sep 12, 2026
t and others added 8 commits September 12, 2026 17:34
The gate for "may this PR be merged" has lived in one session's head, and it
has been got wrong from both directions in a single day.

On 2026-09-12 four PRs were merged before their reviews arrived — #312 by
three seconds — leaving eleven findings, four of them P1, on code already in
master. The gate added to stop that then deadlocked a clean PR, because a
Codex pass with no findings submits no review object at all, only a summary
row and a thumbs-up. Later the same day two separate sessions reported a PR
"clean, zero open threads" from a query issued seconds before its review
posted, and a third reported clean without noticing master had moved.

One rule underneath all of it: **a review is only evidence about the commit it
names.** "No findings yet" and "not looked yet" are indistinguishable unless
you read which commit was looked at — so the script reads the commit in the
summary table, not the review list and not the comment's timestamp, because
that comment is edited in place and its `created_at` means nothing.

Six checks, each carrying the incident that motivates it:

1. mergeable against its base;
2. based on master — `ci.yml` fires on `pull_request` into master only, so a
   stacked PR runs no CI at all and its green tick measures nothing;
3. every check concluded and none failing;
4. a Codex review naming *this* head, completed rather than running;
5. zero unresolved threads, paginated, since `required_conversation_resolution`
   makes this the gate rather than a courtesy — and a `first:100` page once hid
   19 open threads;
6. a privacy scan of the **whole merge diff**, not the author's own commits: a
   real transaction reference sat in a comment on public master through two
   PRs because each author scanned only what they wrote.

The scan was wrong twice while being written, both times in the direction that
reports clean:

- `XXXXX1234X` is a fabricated PAN and `X` is an uppercase letter, so it
  matched the PAN shape. A gate that cries wolf gets ignored, which is worse
  than no gate, so obvious placeholders are excluded — but by an explicit list,
  never by widening the shape, which would start excusing real values.
- the first placeholder pattern used a backreference, which is not ERE. grep
  errored, returned no matches, and the scan reported clean. That is the same
  shape as a control reporting zero because its branch never fired, so the
  pattern is now probed against a string it must match before being trusted.
- UUID tails are twelve hex characters and often all digits; the canonical
  `550e8400-…-446655440000` tripped it. UUIDs are stripped before scanning.

Verified in both directions rather than assumed: a diff carrying a real-shaped
GSTIN, mobile number and account number is flagged; one carrying only
placeholders and a UUID is not. Run against three live PRs it blocks each for
a different, correct reason — a running review, unresolved threads, and a
non-master base.

Usage: `scripts/merge-gate.sh <pr> [owner/name]`; exit 0 may merge, 1 must not.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… catch

Eight findings on the first revision, plus two more found by testing it rather
than reading it. A script whose whole purpose is rigour had ten holes, and the
two that matter most were found by running it against the very case it exists
for.

**The scan could not see the leak that motivated it.** The pattern was
`\b[0-9]{11,18}\b`. The reference it was written to catch is written
`HDF CH12345678901` — glued to letters — and `\b` does not match between `H`
and `1`. Verified directly: the old pattern returns nothing on that string.
The gate would have passed the exact commit it was built to stop. The word
boundaries are gone.

**It scanned removals as well as additions,** so it blocked the PR that
*deletes* leaked data while passing the one that adds it. Added lines only now.

Then the eight from review, each confirmed against the tool it names:

- `BEHIND` was not blocked. `mergeable` can read `MERGEABLE` while
  `mergeStateStatus` is `BEHIND`, meaning the head never saw the current base —
  so its CI and its review describe a tree that no longer exists.
- `gh pr checks` buckets are `pass|fail|pending|skipping|cancel`. The regex
  looked for `cancelled`, so a cancelled check counted as neither failing nor
  pending and read as success. Now the JSON buckets are read instead of the
  text columns, which a check name containing spaces mis-splits anyway.
- A review reported `Failed` was treated exactly like `Completed`. A failed run
  means nothing looked at the code — the original bug inverted. Only
  `Completed` passes.
- `--repo owner/name` was documented and not parsed: `REPO` became the literal
  `--repo`. Proper option parsing, and `OWNER/NAME` is validated.
- On a transient GraphQL failure the thread query fell back to a hardcoded
  repository with the same PR number. A same-numbered PR elsewhere with no
  open threads would have read as a clean result. The fallback is gone.
- The issue-comments query was unpaginated; the Codex summary is an ordinary
  comment and the default page is 30.
- Nothing bound the merge to the reviewed commit. The script now prints the
  merge command carrying `--match-head-commit <sha>`.
- Draft and non-OPEN states were not checked.

Dropping `\b` made hex digests visible — a sha256 in a lockfile contains long
digit runs by chance — so digests and UUIDs are stripped before scanning.
Both are machine-generated and neither can carry a client identifier; the
placeholder list was deliberately *not* loosened, because loosening the shape
starts excusing real values.

Verified in both directions rather than assumed. On a 4,799-line real diff:
zero false positives. On constructed input: the glued-to-letters reference, a
real-shaped GSTIN and a real-shaped mobile number are all flagged, while a PR
that only removes a leak is not. Every exit path exercised — malformed
`--repo`, missing value, unknown option, no arguments all return 2; a stale
head returns 1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Running the gate across the whole open queue surfaced two false positives, both
worth fixing rather than tolerating — a gate that cries wolf gets ignored,
which is the failure mode this one is least able to afford.

`00000000005551` in a constructed test page is a padded MICR fixture, not an
account number. Excused by `^0{6,}[0-9]{1,5}$`, kept deliberately narrow: a
real account number can begin with a zero or two, so only a run of six or more
leading zeros — plainly synthetic — is excused. Verified that three
real-shaped account numbers with one and two leading zeros are still flagged.

The other was this script flagging itself. A comment named the canonical RFC
example UUID with its middle elided, so the UUID stripper could not match it
while its digits still read as an identifier. The comment no longer carries
example digits at all: a literal in a comment is a literal in the diff, and
this scan reads its own file like any other — which is exactly how the
reference that motivated the gate reached public master in the first place.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Round two on the gate: seven findings, two of them holes a determined actor
could walk through, and one the gate found in its own author the moment it ran.

**The review row was spoofable.** The summary was located by a marker string in
a comment body — and a marker is just text. Any PR participant could post an
ordinary comment carrying that marker and a `Completed` row for the current
SHA, and the gate would have recorded a review that never happened. It now
requires the comment's author to be `chatgpt-codex-connector[bot]` with a `Bot`
account type, checked alongside the marker.

**A binary file was an invisible pass.** `gh pr diff` emits `Binary files …
differ` instead of content, so a screenshot or PDF of a client statement
produced a scan with nothing to find and a clean verdict — the exact shape of a
control reporting zero because it cannot see, which this repository has written
down twice. Binary changes now block and are named for inspection by hand.

**Exemptions are reported rather than silent.** Stripping UUIDs and hex digests
keeps the false-positive rate low enough that the gate gets read at all, but a
blanket exemption nobody can see is how a real value gets erased. The count of
exempted lines is now printed.

Four more, each confirmed:

- `mergeStateStatus=BLOCKED` fell into a wildcard that printed `ok`, reading as
  approval for a state GitHub is refusing. It now prints a note, and an
  unrecognised state blocks rather than being guessed at.
- `reviewThreads` blocked permanently once a PR passed 100 threads, since
  `hasNextPage` stays true however many are resolved. A PR accumulates threads
  by being reviewed carefully, so the rule punished exactly the PRs it should
  trust. It paginates now.
- `gh pr checks` exits nonzero both when a check fails and when the query
  fails, so empty stdout from a broken query was reported as "no checks" — a
  statement about the PR rather than about the request. The two are now
  distinguished by whether anything reached stderr.
- AGENTS.md:23 requires every PR to link a completed line in
  `review-checklist.md`, and the gate did not check the repository's own stated
  pre-merge rule. It does now — and immediately blocked this PR, whose
  description did not carry one.

Each fix exercised rather than assumed: the author filter admits only the Bot
account; the binary matcher fires on a binary patch and not on a text one; the
thread loop terminates and counts across pages; the checklist rule blocks a PR
without the link.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Round three: seven findings, three of them P1, and one is a third blind spot
in the privacy scan — the third distinct way the scan could be handed data it
would not look at.

**`++` at the start of a line hid a value from the scan.** The filter dropped
`^+++` to skip the unified-diff file header, but an added line whose own
content begins with `++` produces exactly that prefix. `++ customer ABCDE1234F`
yielded no scannable text at all. The header is now identified structurally —
`+++ b/<path>` or `+++ /dev/null`, never the bare prefix — so content reaches
the scan and headers still do not.

**A seven-hex prefix is 28 bits and can be ground deliberately.** The review
row was matched by substring, so an author could push a commit sharing the
reviewed prefix and have a stale `Completed` row vouch for code nobody read.
Codex publishes only seven characters, so the comparison cannot be
strengthened — but a ground commit must be created *after* the review it
impersonates, and that is checkable. The row's own timestamp is now compared
against the head commit's committer date, and a review that predates its head
is refused. The match is also anchored to the backtick cell, since an
unanchored substring matched the row's timestamp and URL too — neither of
which is a claim about a commit.

**Identifiers were matched uppercase-only.** A GSTIN or PAN written in lower or
mixed case is the same identifier, and prose is exactly where it would be
written that way. Matching is case-insensitive now, with the placeholder list
applied to the uppercased form so `xxxxx1234x` stays excused.

Four more:

- A binary **deletion** emits `Binary files a/x and /dev/null differ` and was
  counted like an addition, so the gate blocked the PR removing a leaked
  screenshot. Same inversion as scanning removed lines, in a different rule.
- `--match-head-commit` validates only the head, so the base can be changed
  after the check without moving the head. The printed command now re-reads
  the base and compares it before merging.
- A failed comments query with empty stdout was reported as "no Codex review
  summary at all" — "I could not ask" stated as "there is no review", which is
  the exact substitution this script exists to prevent.
- `--repo=` with an empty value was accepted.

Both of the first two fixes were wrong on first writing and caught by their own
controls: the header filter still swallowed `+++ customer …` until it matched
`b/` and `/dev/null` explicitly, and the case-insensitive match used `-O` for
`-o`. Every rule is now exercised in both directions — content reaches the scan
while real headers do not, a lowercase PAN is flagged while a lowercase
placeholder is not, a binary add blocks while a binary delete does not.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three sessions hit the same defect independently in one day: a pinned file's
bytes moved and the compatibility surface was not resealed. Once by editing,
once by running `cargo fmt` *after* resealing, and once by rebasing — where the
rebase takes the base's manifest and the author touches nothing.

That spread of causes is the point. The rule people had written down was "the
reseal is the last step before `git add`", and each of us broke it while
believing we were following it, because each filed it under the *situation* we
had just been in rather than under the actual invariant: **any operation that
can change the bytes of a pinned file — an edit, a formatter, a merge, a
rebase — invalidates the seal, and the reseal runs after the last of them.**

Nothing in the local loop re-reads pins before a commit, so CI's gate is the
only thing that notices, and every instance therefore reaches a reviewer
instead of its author. That makes it a class, not a set of mistakes, and a
class is worth closing here rather than writing down again.

The check needs no checkout: read the 211 pinned paths from the manifest at the
PR head, intersect with the PR's changed files, and require the manifest to
have moved if any of them did.

It is deliberately weaker than CI's gate and says so: it proves the reseal was
*performed*, not that the hashes are *right*. Only the real gate proves that.
But every instance observed was a reseal that never ran at all, so this catches
the whole observed failure while costing one API call.

Verified in three directions rather than two: a pinned file changed without the
manifest blocks; the same change with the manifest passes; a PR touching the
manifest alone has nothing to reseal and passes. Against live PRs, #314 reports
one pinned file with the manifest moved alongside it, and #320 reports nothing
pinned.

Credit where due — this was suggested by the lane on #288, which had just been
bitten by the `cargo fmt` variant, on the grounds that closing the class beats
closing the instances. It was right.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… literals

A comment explaining the `++` blind spot used a realistic PAN as its example,
and the newly case-insensitive matcher flagged it. Correctly: the scan reads
this file like any other, and **a literal that illustrates a leak pattern is
the pattern**.

Third time a comment here has tripped the check — a UUID, a partial UUID, now a
PAN. Each time it was the check working. The rule is now written in the comment
so the next person adding an example reads it first: describe the shape, never
spell it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Round four: five findings, three P1, and the first retracts last round's fix.

**The timestamp defence was void.** To authenticate a review against a ground
seven-hex prefix, the previous revision compared the review's time against the
head commit's committer date. `GIT_COMMITTER_DATE` is set by whoever creates the
commit: an author able to grind a prefix can also backdate it. The check read as
coverage and provided none, which is worse than the gap it papered over.

Replaced with something an author cannot set: **uniqueness.** A ground collision
puts two commits sharing the prefix in the PR, so the PR's own commit list is
counted and more than one match blocks.

The residual is stated in the code rather than implied away: uniqueness within
the PR does not exclude a collision created elsewhere and force-pushed as the
sole commit. Closing that needs Codex to publish a full SHA. Until then this is
a deterrent, not a proof, and it says so.

**A phone number written normally was invisible.** `+91 98765 43210` scans as
three short segments, under both the 10-digit and 11-digit thresholds. The first
fix stripped every separator to make a projection — and invented eight findings
on a clean diff, joining two adjacent dates into a sixteen-digit run and
`CE_ADR_0016_E` into a PAN shape. A gate that cries wolf gets ignored, which
costs more than this catches. Separators are now permitted only *inside* a
phone-shaped run, and the result is re-checked as a ten-digit Indian mobile, so
nothing unrelated is fused. Verified: five separator styles caught, and all
three invented findings gone.

**A filename is content.** Header lines are dropped from the scan, so a file
whose basename carries an identifier passed with safe contents. Destination
paths are added back as their own scannable text.

Two more: an unreadable compatibility manifest was treated as an absent one and
the pin check silently skipped — it now asks whether the path exists and fails
closed if it does; and `gh pr view --json files` hard-codes `files(first: 100)`,
so a PR touching more than 100 files returned a partial list and a pinned file
outside the first page read as untouched. The paginated REST endpoint is used
instead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 26baf27ed3

ℹ️ 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".

Comment thread scripts/merge-gate.sh Outdated
Comment thread scripts/merge-gate.sh
Comment thread scripts/merge-gate.sh Outdated
Comment thread scripts/merge-gate.sh
Comment thread scripts/merge-gate.sh
Comment thread scripts/merge-gate.sh
Comment thread scripts/merge_gate_diff.py Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bd0ab9ad52

ℹ️ 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".

Comment thread scripts/merge-gate.sh Outdated
Comment thread scripts/merge-gate.sh
Comment thread scripts/merge-gate.sh Outdated
Comment thread scripts/merge-gate.sh Outdated
Comment thread scripts/merge-gate.sh Outdated
Comment thread scripts/merge-gate.sh
Comment thread scripts/merge-gate.sh Outdated
Comment thread scripts/merge-gate.sh Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0c0db06f85

ℹ️ 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".

Comment thread scripts/merge-gate.sh Outdated
Comment thread scripts/merge_gate_diff.py Outdated
Comment thread scripts/merge_gate_diff.py Outdated
Comment thread scripts/merge-gate.sh Outdated
Comment thread scripts/merge-gate.sh
Comment thread scripts/merge-gate.sh Outdated
Comment thread scripts/merge-gate.sh
Comment thread scripts/merge-gate.sh
Comment thread scripts/merge_gate_diff.py

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 830c0289bf

ℹ️ 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".

Comment thread scripts/merge-gate.sh Outdated
Comment thread scripts/merge-gate.sh
Comment thread scripts/merge-gate.sh Outdated
Comment thread scripts/merge-gate.sh
Comment thread scripts/merge-gate.sh Outdated
Comment thread scripts/merge-gate.sh
Comment thread scripts/merge-gate.sh
Comment thread scripts/merge-gate.sh

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7ea1235396

ℹ️ 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".

Comment thread scripts/merge-gate.sh
Comment thread scripts/merge-gate.sh
Comment thread scripts/merge-gate.sh
Comment thread .github/workflows/ci.yml
Comment thread scripts/merge-gate.sh
@lamemustafa

Copy link
Copy Markdown
Owner Author

The reviewed implementation is carried forward in #335 at 99623f78d42e329f8db1b15dc887927f839c654e, based on current master.

The final preflight for this PR passes the protected-check, current-base, description and compatibility checks, but rejects synthetic identifier examples embedded in older commit messages. The replacement preserves the runtime gate, parser, workflow and compatibility artifacts exactly; its sole source difference assembles the same grouped-number test values from smaller pieces. The focused grouped-number control passes, and the privacy rule remains unchanged.

This PR remains the original review and authorship record. It stays open until #335 has its own completed provider review, current CI, independent acceptance and final preflight. Any new finding on this head will still be assessed against the replacement.

@lamemustafa

Copy link
Copy Markdown
Owner Author

Closing in favour of #335, which supersedes this PR.

Both PRs rewrite the same six files (ci.yml, both compatibility manifests, merge-gate.sh, merge-gate.test.py, merge_gate_diff.py); #335 additionally adds merge_gate_fake_gh.py and merge_gate_privacy.py. Merging both would double-apply the same rewrite. #335's own description already records that it replaces this PR.

This PR's review discussion and authorship record are preserved here; the 95 resolved threads on it are not being discarded, and the work they produced lives on in #335's branch.

Closed as superseded, not rejected.

lamemustafa added a commit that referenced this pull request Sep 15, 2026
* Rectify incomplete merge preflight evidence

Carry forward the reviewed implementation from PR #321. The original PR preserves its review and authorship record. Assemble synthetic grouped-number probes without embedding complete identifier-shaped literals in source.

* Rectify merge gate evidence controls

* fix: reject structural-only policy summaries and reseal workflow

* fix: fail closed on merge-gate review evidence

* Harden merge-gate review evidence

* Tighten platform evidence continuations

* Bound platform evidence list continuations

* Avoid literal home paths in gate fixtures

* fix(gate): close review evidence gaps

* fix(gate): harden platform and credential checks

* test(gate): cover renamed sensitive paths

* fix(gate): require affirmative platform evidence

* test: avoid privacy scan fixture false positives

* fix: require security review for bank import credentials

* fix: require security review for cache token handling

* fix: strengthen merge-gate security evidence

* test: qualify workflow security fixtures

* fix: extend merge gate credential and privacy coverage

* fix: extend merge gate credential boundaries

* rectify: modularize merge gate privacy policy

* rectify: validate skipped CI jobs at final fence

* rectify: review credential dependency lockfiles

* rectify: harden privacy and security review gates

Avoid static secret-like literals in the regression fixture so external secret scanning can evaluate the final PR range without suppressing the privacy-gate coverage.

* rectify: classify quoted credential assignments

* rectify: harden merge gate final fences

* merge-gate: cut to only what runs as a real required CI check

The merge-gate tooling was 3977 lines that gated nothing automatically
(a manual CLI; master's ci.yml never invoked it). Review produced 20
unresolved threads and 27 tracking issues. Per the owner's decision,
keep only what can run as a real required CI check and delete the rest.

Kept:
- Compatibility-surface validation (read_surface_paths() and callers).
- Privacy/PII scan (merge_gate_privacy.py, merge_gate_diff.py in full,
  plus their input pipeline: commit author/committer metadata
  validation and the diff-coverage scan in merge-gate.sh).
- A new, minimal (38-line) check that a review exists naming the
  current head SHA, closing #317: PRs merged 2-8 minutes after
  opening, where "zero unresolved threads" meant "review had not
  started". Treats "no review evidence for this head" as FAIL.

Cut entirely:
- PR/head/base identity binding, branch-protection context
  derivation, and the checks rollup.
- PR body/review-checklist/P4/platform-evidence validation.
- Skipped-CI-job allowlist and security-reviewer-comment validation.
- scripts/merge_gate_fake_gh.py (a GitHub API test double that existed
  only to test the cut concerns).

merge-gate.sh: 1587 -> 552 lines. merge-gate.test.py shrunk to cover
only the kept surface, backed by a new, much smaller fake `gh`
(merge_gate_test_gh.py) replacing the deleted one.

Also fixes 7 verified live defects in the kept code (#346, #358, #360,
#365, #366, #373, #377), each with a regression test in
merge-gate.test.py verified to fail against the pre-fix code and pass
after.

docs/proposed-merge-gate-ci.md proposes the CI wiring to actually make
this a required check (a new workflow file, not touching .github/ per
this change's scope) and explains why the review-evidence check needs
pull_request_review/issue_comment triggers, not just push/synchronize.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* test(merge-gate): lock in coverage for all 11 surviving PR #335 privacy findings

Investigated all 11 unresolved review findings against the promoted-to-
required scripts/merge_gate_privacy.py. All 11 turned out to already be
fixed on fix/merge-gate-cut-336: each fix landed in an earlier commit
(d16416d, 7a29762, c033dd4, 57799cb, f41d634) that is an ancestor of
this branch's head, confirmed by direct behavioral testing against the
scanner plus git ancestry checks.

Add PrivacyScannerFindingsPR335, a direct-unit-test class against
merge_gate_privacy.scan(), with one positive + one negative test per
finding (22 tests total) so this behavior has explicit regression coverage
-- no prior test exercised these specific sub-cases. Each was verified to
fail when the specific historical defect described in its review thread is
reintroduced, and pass once reverted back out (see task report for the
per-finding before/after transcripts).

All fixtures use fabricated values (fake UUIDs, .test-domain emails,
placeholder credential strings, truncated non-decodable PEM bodies) --
no real PII or credentials.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: t <dev@example.invalid>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type:rectify Regression rectification

Projects

None yet

Development

Successfully merging this pull request may close these issues.

merge-gate: the phone normaliser fuses adjacent numbers, flagging ordinary date ranges

1 participant