Repository navigation
Conversation
… reads (red) Cases for R-153 and R-152: an excluded PR is refused, an unreadable label set or exclusion record refuses, a PR with neither passes as before, and the reads-fine path is held as hard as the refusals. Fails against the guard as it is, which says exclusions are not checked here. Refs #274
…raft cases (red) A missing exclusion record refuses, since the guard cannot tell none declared from a record that is gone. Adds the repo-prefix and label-prefix non-matches and gives the older fixtures a label list. Refs #282
…sion record A missing rulings file now stops the guard, since a missing record cannot be told from none declared; an empty file means none declared. The skill text names where the records live and how a match, an unreadable record and a missing label list each stop. Refs #282
arcavenai
left a comment
There was a problem hiding this comment.
Requesting changes at 09d3397 because three mutants of the match survive the verifier. The guard itself matches correctly: I probed the real script and every case below gives the right answer. The tests do not pin those cases, so a later edit could break them unnoticed. Three test cases fix it.
1. Surviving mutants. Each was run on the full verify-merge-guard.sh, and each gives 82 passed, 0 failed:
- Repo suffix match:
[[ "$repo" == *$xrepo ]]atmerge-guard:105. An exclusion fororg/repowould then stopxorg/repo. - Label suffix match:
"$lname" == *$xlabelat:108. An exclusion foracu.updatewould then stopnotacu.update. - Dot as any character:
"$lname" =~ ^${xlabel}$at:108, an anchored regex. An exclusion foracu.*would then stopacuprod, andacu.updatewould stopacuXupdate.
The existing non-match cases (:249-258) are all prefix-shaped: the excluded value is a prefix of the PR's, or the PR's value only starts like it. They kill the substring and prefix mutants, but not these three. Could you add three PROCEED cases:
- a repo with extra text before the excluded one (
xorg/repoagainstorg/repo); - a label with extra text before it (
notacu.updateagainstacu.update); acuprodagainstacu.*?
What the real guard does. I ran it with a stub gh and a one-line rulings file:
| exclusion | PR | result |
|---|---|---|
org/repo, acu.update |
xorg/repo, acu.update |
PROCEED |
org/repo, acu.update |
org/repo, notacu.update |
PROCEED |
org/repo, acu.update |
org/repo, acuXupdate |
PROCEED |
org/repo, acu.* |
org/repo, acuprod |
PROCEED |
org/repo, acu.* |
org/repo, acu.prod |
STOP |
org/repo, acu.* |
org/repo, acu.a/b |
STOP |
org/repo, acu.* |
org/repo, ACU.Prod |
STOP |
org/*, acu |
org/repo-docs, acu |
STOP |
org/infra, acu |
org/infra-docs, acu |
PROCEED |
org/repo, acu |
org/repo, acute |
PROCEED |
So the bash [[ == ]] pattern is anchored at both ends, . is literal, case is folded under nocasematch, and * matches across /. An exclusion with no glob is an exact, case-folded match. That * crosses / is ordinary for bash patterns, but SKILL.md calls them "shell patterns", and a reader may expect filename globbing, where * stops at /. One clause saying * matches any text, including /, would settle it.
2. A recommended change: who clears a missing-file STOP. Refusing on a missing file is right. A missing record and an empty one cannot be told apart, and the PR body states the cost: every guarded merge stops until the file exists. But the STOP text (merge-guard:61) tells whoever hits it to "Create the file (empty means none declared) and run it again". The seat that hits it is the seat trying to merge. Following that text, it records "none declared" for the operator and proceeds. Could the message, and SKILL.md, say the operator creates the file and enters their exclusions, and that a seat reports the STOP rather than creating it? SKILL.md also names $DIRECTOR_STATE/rulings.jsonl but not the default (~/.director/state, merge-guard:29) or how to create it. I would not block on the wording alone.
What holds.
- Order: the rulings file is read and parsed before any
ghcall (:59-71). Then the read-onlygh pr viewruns (:93), then the label check and the exclusion match (:97-113), all before the state (:115) and draft (:118) checks. The guard never merges. My mutant that moved the exclusion block after the draft check failed "STOP on the exclusion, before the other checks". - Fail-closed reads: a missing file (
:61), a path that is not a file (:62), an unreadable file (:63), a non-JSON line, a non-object line or an exclusion missing its repo or label (:64-71), and a view with no readable label list (:97-98) each STOP. - The verifier stubs gh. Its runs use
PATH="$root/bin:/usr/bin:/bin"with a stubghthat exits 97 on any unexpected call (verify-merge-guard.sh:85). I ran it underenv -iwith a scratch HOME in a localhost-only sandbox: 82 passed, 0 failed. - The author's six mutants, re-run: case-sensitive match, substring on repo, substring on label, torn line skipped, empty label allowed, unreadable file ignored. Each fails at least one case. So do mine for a missing file proceeding, a repo prefix match, a literal (unglobbed) label compare and an unanchored regex. The source was restored and the tree is clean.
- Signed: all three commits are verified.
- Text: no em dash and no non-ASCII. No private repository, host name, bus id or card slug appears in the diff, the commit messages or the PR body. prefile-scan hits are style only: a
~/path in the guard's own header comment, and "we" in a code comment atmerge-guard:96("a view we cannot read"), which "a view the guard cannot read" would avoid.
CI: Harden-Runner skipped; there are no other checks.
Seat: reviewer / reviewer
…ings file The exclusion match is anchored at both ends and a dot is literal, but no case pinned it: a suffix match on the repo or the label, or an anchored regex, passed the verifier. Add xorg/repo against org/repo, notacu.update against acu.update and acuprod against acu.*. A missing rulings file now tells the seat to report the STOP; the operator creates the file and enters the exclusions. SKILL.md gives the default path and says * matches any text, including /. Refs #282
arcavenai
left a comment
There was a problem hiding this comment.
Approving at 0d4067f. The three mutants that survived at 09d3397 are now killed by the full verifier. The missing-file STOP now tells a seat to report it, not to create the file. Both points from my last review are closed.
The three new PROCEED cases (verify-merge-guard.sh:260-271) are the ones asked for: xorg/repo against org/repo, notacu.update against acu.update, and acuprod against acu.*.
Mutants, re-run on the full verifier under env -i with a scratch HOME, in a sandbox that allows only localhost, with the stub gh. The baseline at this head gives 85 passed, 0 failed. Each mutant gives 84 passed and 1 failed, on its own case:
- repo suffix,
[[ "$repo" == *$xrepo ]]atmerge-guard:105, fails "xorg/repo against org/repo"; - label suffix,
"$lname" == *$xlabelat:108, fails thenotacu.updatecase; - dot as any character,
"$lname" =~ ^${xlabel}$at:108, fails "a dot in an exclusion is a literal dot".
The source was restored after each mutant, the tree was clean, and no process was left behind.
The STOP text and SKILL.md.
merge-guard:61now reads "The operator creates the file and enters their exclusions (an empty file means none declared); a seat reports this STOP and does not create the file".- The verifier pins both halves (
:290-292). The old "create the file" check is gone. - SKILL.md names the default
~/.director/state/rulings.jsonl, which matches the script's fallback atmerge-guard:59. It gives the same operator and seat split. It also adds the clause that*matches any text, including/, and that.is a literal dot. - The "a view we cannot read" comment at
:96now reads "a view the guard cannot read".
Text: no non-ASCII and no em dash in the diff or the commit message. The commit is verified.
CI: Harden-Runner skipped; there are no other checks.
Seat: reviewer / reviewer
Director's merge guard says in its own header that operator merge exclusions are not checked there, so a PR the operator ruled out can pass it and be merged. This PR makes the guard read the exclusion records and the PR's labels, and stop on a match or on anything it cannot read, which is what R-153 and R-152 ask of it.
Before any
ghcall the guard readsrulings.jsonl(path fromMERGE_GUARD_RULINGS, default$DIRECTOR_STATE/rulings.jsonl). An exclusion is one line,{"kind":"exclusion","repo":R,"label":L}; repo and label compare without case and may be shell patterns. The labels come from the samegh pr view. A PR in a matching repo carrying a matching label gets a STOP before any other check, so an excluded PR is refused whatever its review or check state.Fail closed, as the rest of the guard does:
A guard bug stops every merge, so the cases that must keep passing are tested as hard as the refusals: other record kinds and blank lines, exclusions for another repo or label, a repo or label that only starts like the excluded one, a PR with no labels, and a PR with neither labels nor records. A merge made outside the guard stays honor-only, as the design says.
Where the records live, which the design leaves open until K9: in
rulings.jsonlin the director's local state directory, the file design D already names for theexclusionkind. It is local and private, never in this repo, so exclusion data stays out of a public tree. I propose that path as the home K9 then writes to.One consequence to know before merging: because a missing file refuses, every merge through the guard stops on a host until someone creates that file (an empty file means none declared) and, for an exclusion to bite, writes the operator's lines into it. Not in this PR: the writer and the other kinds (K9), and seeding the operator's actual exclusions.
Red then green: the first commit adds the cases against the unchanged guard (20 fail). The second adds the guard change with the cases for a missing file and the prefix non-matches, three of which fail against it. The third makes a missing file refuse, and
verify-merge-guard.shpasses (82 passed, 0 failed). I also ran mutants of the new code (case-sensitive match, substring match on repo or label, torn line skipped, empty label allowed, unreadable file ignored); each is caught by a case.Refs #282
Refs: #274, #276