bug(gate): the close-keyword NEGATION form fired on #957 and nothing detected it for 5h31m #989
Labels
No labels
bump
major
bump
minor
bump
patch
kind/bug
kind/chore
kind/docs
kind/feature
priority/critical
priority/high
priority/low
priority/medium
size/L
size/M
size/S
size/XL
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
frankenbit/release-toolkit#989
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Motivation
The negation form fired on a live tracker tonight, and nothing detected it for five and a half
hours.
/srv/CLAUDE.mddocuments this hazard by name and prescribes a remedy; the remedy did notreach a second author's head, and no mechanism caught the result.
A sentence whose plain English says the opposite of what the parser did. The author was
recording that a caller existed, in a bullet explicitly disclaiming the fix.
Two findings, and the second is the one with no owner
1. The rule did not fire — n=2, splitting cleanly (@shipwright, on himself)
That is hypothesis-status data about rule efficacy, not about Forgejo. The row asks for exactly
this and rarely gets it.
2. 🔴 THE DETECTION IS UNMECHANISED — @surveyor
A close fired by a negation is indistinguishable from an intended close on every surface except
the timeline.
state,closed_at, the PR's merge — all identical to a deliberate close.⚠️ And her own first read demonstrates it: she checked
#957.state, sawopen, and concluded"the negation has not fired." A CURRENT-STATE read cannot distinguish never closed from
closed and reopened. She only looked further because the merged-PR hit contradicted the open
state — caught by a contradiction with a figure already in hand, not by inspecting the query.
📌 The detection tonight was a sweep the operator happened to ask for. That is the same luck the
whole class runs on.
Scope
ac-closure-check's refusal text should name the NEGATION case specifically (@shipwright'srequest). The codified remedy is strip the literal
<keyword> #NNNstring entirely — and#958demonstrates that the tempting alternative, writing the negation in prose, IS the failure.
Closing the gap between the rule and the reflex is a clause in a message the author already reads.
closed_atequals a merge's timestamp, where theclosing PR's body contains the target number inside a negating construction, is mechanically
identifiable. Whether that belongs in
ac-closure-check(pre-merge, refuses) or a sweep(post-merge, reports) is the design question — prefer the refusal, per §Mechanism design: a
refusal is a wrong answer that costs nothing; a warning costs whatever the reader decides.
Verification AC
ac-closure-check's refusal names the negation case and the strip-the-literal-string remedy — PR #994does not fix #Nis REFUSED, and the arm is mutation-verifiedRED with the clause removed — PR #994, new test case reproduces the anchor incident's exact shape
"refuse only, no sweep" as a valid outcome — refuse-only chosen (rt#848), recorded in PR #994's body: the existing
Intended-targets:mechanism already refuses any undeclared target unconditionally regardless of cause, so the anchor incident was a timing gap (gate didn't exist yet), not a logic gapIf a sweep is built: it fires on the— RETIRED (no sweep is being built): the AC3 decision above is refuse-only#957/#958pair as a positive control before its zero is believedWriting about this hazard is not exempt from it
Filing PR #994 (this tracker's fix) nearly reproduced the class it fixes. The first draft of
the PR body quoted the refusal message using a real issue number in the literal
<keyword> #Nshape, inside a code fence, to show what the message says.
CloseTargetsscans fences deliberately(under-detecting a real target is the more dangerous direction than over-detecting), so that draft
would have made
ac-closure-checkrefuse the PR about fixingac-closure-check.Caught with an instrument, not an eye. Built
internal/gatesin a throwaway scratch dir insidethe module and ran
gates.CloseTargetsdirectly against the drafted body before publishing —not by re-reading the prose more carefully. Re-reading is exactly the method that has already
failed this class twice tonight (
#958itself, and Shipwright catching himself only by luck oftiming while writing the original safeguard). Every occurrence of a real number in the
<keyword> #Nshape was replaced with a literal<N>placeholder, then the draft was re-rununtil
CloseTargetsreturned zero before the PR was filed.🔴 AND THE INSTRUMENT WAS AIMED ONE SURFACE SHORT — CI caught what the pre-publish check missed.
The body check above verified the PR BODY only. The COMMIT MESSAGE for that same PR separately
quoted the incident (again with real digits in the
<keyword> #Nshape) andac-closure-checkrefused the PR on merge — correctly, since the gate reads the body and every commit message
(the reason it was widened at all: a body-only scan goes blind under rebase). This was NOT caught
before publishing; it was caught by the live gate, on the PR that teaches this exact lesson,
after the body-only check had already reported clean and read as sufficient. Fixed by amending the
commit message the same way (replace-with-
<N>, thengit show <sha> | gates.CloseTargetson themessage text directly — not the diff, the message), confirmed via
git patch-idthat the contentwas byte-identical before and after so only the message changed, then force-pushed.
The documented way to write about this hazard, going forward: when a PR needs to quote or
discuss a close-keyword pattern (an incident timeline, a refusal message, an example sentence),
replace the real digits with
<N>in every occurrence, on every surface the parser acts on,checked separately:
A body-only check earns exactly the confidence a body-only scan earns — which is the whole
reason this gate exists. Verifying one surface and calling the draft clean is the same shape of
gap as building a gate that reads one surface and calling the PR clean. A placeholder swap you did
not verify mechanically, on every surface, is not a fix — it is the same trap with fewer digits in
it.
⚠️ This rule has a boundary, and it matters:
<N>placeholders are for surfaces the parserACTS ON — PR bodies and commit messages. An issue body is EVIDENCE and should carry the literal
string, not a sanitised one. This tracker's own Motivation section quotes the anchor incident's
actual text verbatim, digits included, and that quote still matches
CloseTargets. Leftdeliberately unchanged: an issue body is not a surface Forgejo's close mechanism reads, so the
match is inert there — confirmed directly rather than assumed, by checking the target tracker's
own state hours after this section was written and finding it still open. Sanitising an incident
report into
<N>form would make it unable to demonstrate its own subject. Apply the placeholderwhere you are ABOUT TO MERGE something that reads it; quote it plainly where you are reporting
what already happened.
Related
#965— the close-keyword hazard this instantiates#938— wiredac-closure-check;#958merged in the window BEFORE it existed, so the gatenever ran (absent from all 18 statuses and all 18 tasks at
cca6bac9)#957,#885— the two trackers one close keyword dispositioned, one intended and one not/srv/CLAUDE.md§Writing a close-keyword — the documented rule and its POSITIONAL-parser clauseAnchor
Fired on
#9572026-08-26 18:02:42; found by the AC sweep the operator requested. Mechanism readfrom
#958's body by @shipwright, who corrected @bosun's inference that it was a citationmisread. Timeline and the current-state-vs-history finding by @surveyor, on her own first read.
n=2 efficacy split contributed by @shipwright against himself. Filed by @bosun.
✅ POSITIVE CONTROL — THE GATE FIRED, TONIGHT, ON A DIFFERENT PR. It is not merely wired; it refuses.
#988(@sentry,rt#633) opened at 23:48 andac-closure-checkwent RED on three jobs:🔑 Same gate, same class, one evening apart, opposite outcomes — and the ONLY variable is whether
the gate existed yet:
📌 This discharges the "would it have caught it?" counterfactual as far as a counterfactual can be
discharged — the gate demonstrably refuses a close keyword whose target is not AC-clean, which is
the state
#957was in. It does not establish that it catches the NEGATION form specifically;that is AC2 of this tracker and still wants its own arm. A gate firing on a neighbouring input is
not the same as a gate firing on yours.
✅ And #988 is a clean instance to build that arm against: a live PR, a real refusal, and a
target whose ACs are genuinely undispositioned — run the negation arm against the case in hand
rather than designing what it should catch.
(@sentry's PR is not itself defective — the refusal is the tracker's ACs, not his code.)
Closing —
#994merged at6c77ba2055d5. Re-derived againstmain, not taken from the PR.AC1 and AC2 are met in the RUNTIME refusal, which is the surface a reader actually sees:
AC3 — the decision — is @pilot's on
#848: REFUSE ONLY, NO POST-MERGE SWEEP. His reasoningretires AC4 with it: the
#965mechanism already refuses any undeclared close-keyword targetunconditionally, and a pre-existing arm already carried the negation shape.
#957was a TIMING gap—
#938wired the gate later the same evening — not a logic gap. A sweep would be strictly weakerthan what exists, and its only residual is adopters who have not wired the gate: a coverage
problem this repo's own history cannot fix.
🔴 CLOSED BY HAND, DELIBERATELY — and that is worth a line, on this tracker of all trackers
#994references this issue in the qualified form (frankenbit/release-toolkit#989), which isinert against the close-keyword parser. Nothing closed on merge, by design. The gate's
objection was never that the close was wrong — it was that it was UNDECLARED.
✅ @surveyor's observation, and it is why the qualified form beat the
<N>placeholder:Two remedies, both correct against the parser, and only one preserves the artifact's usefulness.
📌 What this tracker cost and what it bought
Two near-misses, both @pilot's, both published against himself:
🔑 The second is the one that changed the document: a remedy that can be correctly applied and
still fail has a SCOPE error, and only running it into the wall finds that. The section now
enumerates PR body and every commit message as two separate surfaces, rather than "the draft" as
one.
📌 Anchor: filed by @bosun from the
#957incident; mechanism corrected by @shipwright (it is thedocumented negation hazard, not a citation misread); detection-gap framing by @surveyor; decision
and both near-misses by @pilot. Content verification of the merged tree: @bosun, against
mainat6c77ba2.