mirror of
https://github.com/proffesor-for-testing/agentic-qe.git
synced 2026-09-19 08:45:47 +08:00
70713b0b45
Replaces the honor-system PR template checkbox with a parser that fails any PR whose body either (a) unchecks or deletes the load-bearing failure-modes checkbox, or (b) contains dismissal phrases like "I believe it's unlikely" without a same-paragraph tracking issue reference. Both rules hard-fail. Covers the 3 phrases explicitly named in #408 plus 8 additional dismissal patterns that shipped or could ship the same class of reasoning that caused the v3.9.1–v3.9.4 init regression series. - scripts/check-pr-body.cjs: standalone Node parser, ESM-safe via .cjs, zero dependencies, exported for self-test. Skips the template's own checkbox paragraph when scanning for forbidden phrases — the stock instruction text quotes the phrase as a teaching example and would otherwise trip the linter on every clean PR. - .github/workflows/pr-template-check.yml: two jobs. lint-pr-body runs the parser against the actual PR body (dependabot[bot] allowlisted). self-test runs the parser against 11 fixtures and asserts both exit code and a content-specific substring in the output, so a parser regression fails loudly instead of silently passing bad PRs. - tests/fixtures/pr-body/: 3 valid fixtures (clean, tracked-phrase, tracked-edge-case), 6 invalid fixtures (unchecked, missing section, forbidden phrase, multi-violation, highly-unlikely, can't-see-how), 2 historical fixtures (the actual v3.9.3 PR body which fails for missing section, plus a synthetic body quoting #401's postmortem reference which fails for forbidden phrase). Rollout: workflow is intentionally not required at merge time on initial landing. Maintainer should promote it to a required status check in branch protection once it has been observed to behave correctly on live PRs — see the header comment in .github/workflows/pr-template-check.yml. Verified locally with the same self-test logic the workflow will run: 11/11 fixtures pass with content-aware assertions, and a mutation test (breaking the checkbox-state logic) correctly causes the valid fixture to fail, proving the self-test would catch a parser regression. Closes #408 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
234 lines
9.0 KiB
JavaScript
Executable File
234 lines
9.0 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
//
|
|
// scripts/check-pr-body.js
|
|
//
|
|
// Parses a PR body and enforces the two rules from issue #401:
|
|
// 1. The load-bearing failure-modes checkbox (added by the #401 follow-up
|
|
// PR template update) must be checked.
|
|
// 2. No "hand-wavy dismissal" reasoning about failure modes — phrases
|
|
// like "I believe it's unlikely" and "can't see how this would fail"
|
|
// must be accompanied by a tracking issue reference (#NNN) in the
|
|
// same paragraph.
|
|
//
|
|
// Both rules are HARD FAILS. Forbidden phrases with a same-paragraph
|
|
// `#NNN` reference are allowed — the phrase is acceptable when the
|
|
// failure mode is at least being tracked.
|
|
//
|
|
// Usage:
|
|
// node scripts/check-pr-body.js <path-to-pr-body.md>
|
|
// cat pr-body.md | node scripts/check-pr-body.js -
|
|
//
|
|
// Exit codes:
|
|
// 0 PR body is valid
|
|
// 1 PR body has one or more violations (details on stderr)
|
|
// 2 usage / input error
|
|
//
|
|
// Refs: https://github.com/proffesor-for-testing/agentic-qe/issues/408
|
|
|
|
'use strict';
|
|
|
|
const fs = require('fs');
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Rule 1: the load-bearing failure-modes checkbox
|
|
// -----------------------------------------------------------------------------
|
|
//
|
|
// The template anchors on the phrase "Every failure mode mentioned in this
|
|
// PR description". We match on that phrase (not on the surrounding
|
|
// markdown) so minor reformatting — extra whitespace, line breaks inside
|
|
// the bullet, slightly different bold syntax — doesn't defeat the check.
|
|
//
|
|
// Returns: { present: bool, checked: bool, matchedLine: string|null }
|
|
function findRequiredCheckbox(body) {
|
|
const ANCHOR = /-\s*\[([ xX])\]\s*\*{0,2}\s*Every failure mode mentioned in this PR description/;
|
|
const match = body.match(ANCHOR);
|
|
if (!match) {
|
|
return { present: false, checked: false, matchedLine: null };
|
|
}
|
|
const state = match[1];
|
|
return {
|
|
present: true,
|
|
checked: state === 'x' || state === 'X',
|
|
matchedLine: match[0],
|
|
};
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Rule 2: forbidden dismissal phrases
|
|
// -----------------------------------------------------------------------------
|
|
//
|
|
// Each pattern is anchored on enough surrounding context to avoid false
|
|
// positives on legitimate usage. For example, "I believe" is not
|
|
// forbidden; "I believe it's unlikely" (applied to a failure mode) is.
|
|
//
|
|
// Label is used in error output so the author knows which rule they hit.
|
|
const FORBIDDEN_PHRASES = [
|
|
{ label: "I believe it's unlikely", re: /\bI\s+believe\s+it'?s\s+unlikely\b/i },
|
|
{ label: "I don't think this can happen", re: /\bI\s+don'?t\s+think\s+this\s+can\s+happen\b/i },
|
|
{ label: "I don't think this will happen", re: /\bI\s+don'?t\s+think\s+this\s+will\s+happen\b/i },
|
|
{ label: "unlikely to happen", re: /\bunlikely\s+to\s+happen\b/i },
|
|
{ label: "highly unlikely", re: /\bhighly\s+unlikely\b/i },
|
|
{ label: "shouldn't happen but", re: /\bshouldn'?t\s+happen\s+but\b/i },
|
|
{ label: "shouldn't happen in practice", re: /\bshouldn'?t\s+happen\s+in\s+practice\b/i },
|
|
{ label: "probably won't happen", re: /\bprobably\s+won'?t\s+happen\b/i },
|
|
{ label: "can't see how this would fail/break/happen",
|
|
re: /\bcan'?t\s+see\s+how\s+this\s+would\s+(fail|break|happen)\b/i },
|
|
{ label: "edge case we don't need", re: /\bedge\s+case\s+we\s+don'?t\s+need\b/i },
|
|
{ label: "I doubt this will/can", re: /\bI\s+doubt\s+this\s+(will|can)\b/i },
|
|
{ label: "in theory this won't/can't/shouldn't",
|
|
re: /\bin\s+theory\s+this\s+(won'?t|can'?t|shouldn'?t)\b/i },
|
|
{ label: "theoretical concern", re: /\btheoretical\s+concern\b/i },
|
|
];
|
|
|
|
// Splits body into paragraphs (blank-line separated). Each returned
|
|
// paragraph has its original start index preserved so we can point at
|
|
// the exact location later if needed.
|
|
function paragraphs(body) {
|
|
const paras = [];
|
|
const lines = body.split('\n');
|
|
let current = [];
|
|
let startLine = 0;
|
|
for (let i = 0; i < lines.length; i++) {
|
|
if (lines[i].trim() === '') {
|
|
if (current.length > 0) {
|
|
paras.push({ text: current.join('\n'), startLine });
|
|
current = [];
|
|
}
|
|
startLine = i + 1;
|
|
} else {
|
|
if (current.length === 0) startLine = i;
|
|
current.push(lines[i]);
|
|
}
|
|
}
|
|
if (current.length > 0) {
|
|
paras.push({ text: current.join('\n'), startLine });
|
|
}
|
|
return paras;
|
|
}
|
|
|
|
// A phrase is "tracked" when the SAME paragraph that contains the
|
|
// forbidden phrase also contains an issue reference like `#123`. This
|
|
// lets authors acknowledge a failure mode AND link a tracking issue
|
|
// without getting blocked.
|
|
function hasIssueReference(paragraphText) {
|
|
return /#\d+\b/.test(paragraphText);
|
|
}
|
|
|
|
// Matches the template's own checkbox paragraph. The stock instruction
|
|
// text contains the phrase "I don't think this can happen but..." as
|
|
// an example of what NOT to write — so if we scanned that paragraph
|
|
// like any other, the template itself would trip the linter. Skip any
|
|
// paragraph whose text matches this anchor.
|
|
const TEMPLATE_CHECKBOX_ANCHOR = /Every failure mode mentioned in this PR description/;
|
|
|
|
function findForbiddenPhraseViolations(body) {
|
|
const violations = [];
|
|
const paras = paragraphs(body);
|
|
for (const para of paras) {
|
|
// Skip the template's self-describing checkbox paragraph — it
|
|
// contains the forbidden phrase as a teaching example, not as
|
|
// author reasoning.
|
|
if (TEMPLATE_CHECKBOX_ANCHOR.test(para.text)) {
|
|
continue;
|
|
}
|
|
for (const phrase of FORBIDDEN_PHRASES) {
|
|
if (phrase.re.test(para.text) && !hasIssueReference(para.text)) {
|
|
violations.push({
|
|
label: phrase.label,
|
|
paragraphLine: para.startLine + 1, // 1-indexed for human output
|
|
paragraphPreview: para.text.length > 200
|
|
? para.text.slice(0, 200) + '…'
|
|
: para.text,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
return violations;
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Entry point
|
|
// -----------------------------------------------------------------------------
|
|
function main() {
|
|
const arg = process.argv[2];
|
|
if (!arg) {
|
|
console.error('Usage: check-pr-body.js <path> | check-pr-body.js -');
|
|
process.exit(2);
|
|
}
|
|
|
|
let body;
|
|
try {
|
|
if (arg === '-') {
|
|
body = fs.readFileSync(0, 'utf8'); // stdin
|
|
} else {
|
|
body = fs.readFileSync(arg, 'utf8');
|
|
}
|
|
} catch (err) {
|
|
console.error(`ERROR: could not read ${arg}: ${err.message}`);
|
|
process.exit(2);
|
|
}
|
|
|
|
const errors = [];
|
|
|
|
// Rule 1: checkbox
|
|
const checkbox = findRequiredCheckbox(body);
|
|
if (!checkbox.present) {
|
|
errors.push(
|
|
'Required failure-modes checkbox is missing from PR body.\n' +
|
|
' The PR template (see .github/PULL_REQUEST_TEMPLATE.md) includes a\n' +
|
|
' checkbox anchored on the phrase "Every failure mode mentioned in\n' +
|
|
' this PR description". Removing the entire Required-check section\n' +
|
|
' is not allowed — reinstate it and mark it [x] if accurate.\n' +
|
|
' Context: https://github.com/proffesor-for-testing/agentic-qe/issues/401'
|
|
);
|
|
} else if (!checkbox.checked) {
|
|
errors.push(
|
|
'Required failure-modes checkbox is present but unchecked.\n' +
|
|
' Matched line: ' + checkbox.matchedLine + '\n' +
|
|
' Every failure mode mentioned in this PR description must have\n' +
|
|
' either (a) a test that exercises it, or (b) a linked tracking\n' +
|
|
' issue. Tick the box when that is true, or add the missing tests\n' +
|
|
' or tracking issues first.\n' +
|
|
' Context: https://github.com/proffesor-for-testing/agentic-qe/issues/401'
|
|
);
|
|
}
|
|
|
|
// Rule 2: forbidden phrases
|
|
const phraseViolations = findForbiddenPhraseViolations(body);
|
|
for (const v of phraseViolations) {
|
|
errors.push(
|
|
`Forbidden dismissal phrase "${v.label}" at line ${v.paragraphLine} ` +
|
|
'without a tracking issue reference in the same paragraph.\n' +
|
|
' Paragraph preview:\n' +
|
|
' > ' + v.paragraphPreview.split('\n').join('\n > ') + '\n' +
|
|
' Either test the failure mode, or open a tracking issue and\n' +
|
|
' reference it (e.g. "see #NNN") in the same paragraph as the\n' +
|
|
' phrase. "I believe it\'s unlikely" is not verification.\n' +
|
|
' Context: https://github.com/proffesor-for-testing/agentic-qe/issues/401'
|
|
);
|
|
}
|
|
|
|
if (errors.length === 0) {
|
|
console.log('PR body passes template checks (checkbox checked, no untracked dismissal phrases).');
|
|
process.exit(0);
|
|
}
|
|
|
|
console.error(`PR body check failed with ${errors.length} violation(s):\n`);
|
|
for (let i = 0; i < errors.length; i++) {
|
|
console.error(`[${i + 1}] ${errors[i]}\n`);
|
|
}
|
|
process.exit(1);
|
|
}
|
|
|
|
if (require.main === module) {
|
|
main();
|
|
}
|
|
|
|
// Exported for self-test
|
|
module.exports = {
|
|
findRequiredCheckbox,
|
|
findForbiddenPhraseViolations,
|
|
paragraphs,
|
|
FORBIDDEN_PHRASES,
|
|
};
|