From ec4225f712aae273e7312a8cb9db3ac823ae8a29 Mon Sep 17 00:00:00 2001 From: FlyBot Dev-4 Date: Tue, 3 Nov 2026 07:07:18 +0000 Subject: [PATCH] enqueue-guard: a lift that quotes the PASS it answers clears the stop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The guard told a held author to "clear it on the record with a newer comment that lets this tip merge", then refused to read that comment. A lift usually quotes the PASS it answers, and `parseReport` reads a conclusion line anywhere below the first line as the body's own conclusion, so the stop filter dropped the whole comment: the hold stood, `enqueue-guard` stayed red, and the one action the message asked for was the one action its reader discarded. The filter now keeps a conclusion-shaped body when that body asks to lift the stop and carries no stop or hold of its own (`responseOnly`), so a lift phrased as "You may merge this tip now" clears the stop even when it quotes the PASS. A quoted conclusion with no lift phrase still clears nothing, and a PASS that says "record only — do not merge" is not a lift. Red first on live main `910f6302`: `.github/scripts/enqueue-guard.test.mjs` → 106 tests / 105 pass / 1 fail (`not ok 105`, `false !== true`). With the filter widened: 106 / 106 / 0; `merge-gate.test.mjs` 51/51, `review-gate.test.mjs` 68/68, the two "record only" cases 2/2. P-479 (bench, local only) Fixes #2281 Owner: Dev-4 Written-by: Dev-4 Reviewer: Birfy Tier: T3 Branch: dev/4-enqueue-lift-verdict-cite Base: 910f6302ccce53d321afbb0eb5bdc183b489dc6a --- .github/scripts/enqueue-guard.test.mjs | 43 ++++++++++++++++++++++++++ .github/scripts/enqueue-rules.mjs | 14 +++++++-- 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/.github/scripts/enqueue-guard.test.mjs b/.github/scripts/enqueue-guard.test.mjs index 05fae621e..6448fe89c 100644 --- a/.github/scripts/enqueue-guard.test.mjs +++ b/.github/scripts/enqueue-guard.test.mjs @@ -1117,3 +1117,46 @@ test('#602 (2026-11-01): an inline stop written before the PASS at this tip is n const r = run({ comments: [comment(report(), '2026-11-01T17:35:30Z', { id: 9103 })], reviewComments: [early] }) assert.equal(r.ok, true, r.reasons.join('; ')) }) + +// ---- P-479, 2026-11-03: the lift that answers a hold was itself thrown away. `parseReport` treats a line +// beginning `Verdict:` below the first line as this body's verdict (#643/#640 keep a quoted block from +// overwriting a conclusion only when the first line already is one), so a lift comment that quotes the PASS +// it answers parses as a conclusion, and the stop filter at `enqueue-rules.mjs:199` dropped the whole +// comment. The guard then told the author to answer the hold on the record — the one thing its own reader +// refused to see. The filter keeps a conclusion-shaped body when that body also asks to lift the hold; the +// second case is the counterweight: quoting a PASS without a lift phrase still clears nothing. + +test('P-479 (2026-11-03): a lift that quotes the PASS it answers still clears the stop', () => { + const lift = note({ + id: 6596858166, + login: 'hugo', + body: [ + 'The PASS for this head has landed. You may merge this tip now.', + '', + '`Verdict: PASS at ' + HEAD + '` names exactly this head.', + '', + 'Written-by: FlyBot Lead', + ].join('\n'), + at: '2026-11-03T06:15:25Z', + }) + const r = run({ + comments: [comment(report(), '2026-11-03T05:00:00Z', { id: 6596712952 }), note({ id: 6596354014, at: '2026-11-03T05:38:25Z' }), lift], + }) + assert.equal(r.ok, true, r.reasons.join('; ')) + assert.equal(r.stopRequest, null, 'the lift is the answer to the stop, so no stop stands') +}) + +test('P-479 (2026-11-03): a conclusion-shaped comment without a lift phrase does not clear the stop', () => { + const quoted = note({ + id: 6596858167, + login: 'hugo', + body: ['The PASS for this head has landed.', '', '`Verdict: PASS at ' + HEAD + '` names exactly this head.'].join('\n'), + at: '2026-11-03T06:15:25Z', + }) + const r = run({ + comments: [comment(report(), '2026-11-03T05:00:00Z', { id: 6596712952 }), note({ id: 6596354014, at: '2026-11-03T05:38:25Z' }), quoted], + }) + assert.equal(r.ok, false, 'a quoted conclusion is not an answer to the hold') + assert.match(r.reasons.join('; '), /asks not to merge/) + assert.match(r.reasons.join('; '), /6596354014/, 'the refusal still names the stop that stands') +}) diff --git a/.github/scripts/enqueue-rules.mjs b/.github/scripts/enqueue-rules.mjs index d386dea94..79890a918 100644 --- a/.github/scripts/enqueue-rules.mjs +++ b/.github/scripts/enqueue-rules.mjs @@ -182,6 +182,14 @@ const withoutQuotes = (text) => String(text ?? '') .replace(/"[^"\n]*"/g, ' ') .replace(/“[^”\n]*”/g, ' ') +// A body that parses as a conclusion but asks only to lift a stop — no ask to stop of its own. This is the +// lift that quotes the PASS it answers (#2246 on 2026-11-03); "record only — do not merge" in a PASS is the +// marker `recordOnlyAt` reads, so it is not one of these. +const responseOnly = (body) => { + const text = withoutQuotes(body) + return LIFT_REQUEST.test(text) && !STOP_REQUEST.test(text) && !HOLD_LINE.test(text) +} + // The stop request still standing, or null. `since` is the timestamp of the conclusion the decision used, in // ms; null reads the whole list (no anchor, or one that cannot be read — a guard that cannot find the PASS it // judged must not narrow what it refuses). @@ -195,8 +203,10 @@ export function stopRequestAt(reports, { author, since } = {}) { // whichever box it was typed in, and the callers already pass both (`enqueue-guard.mjs:69-71`, // `merge-gate.mjs:219-221`). A body carrying a conclusion is the conclusion, not a request: // "record only — do not merge" in a PASS is the marker `recordOnlyAt` already reads, and reading it - // here too would make every parked PASS its own stop. - .filter(({ r }) => !parseReport(r?.body).verdict) + // here too would make every parked PASS its own stop. The one exception is the answer to a stop: a + // lift that quotes the PASS it answers parses as a conclusion too (#2246 on 2026-11-03), and dropping + // it left the guard telling the author to do the one thing its own reader refused to see. + .filter(({ r }) => !parseReport(r?.body).verdict || responseOnly(r?.body)) .filter(({ t }) => after == null || t > after) .sort((a, b) => a.t - b.t || a.i - b.i) let stop = null