mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-16 16:16:32 +03:00
Simplify issue gate: presence-only structure check, no auto-close sweep
Required sections now only need their headings present; the gate no longer judges what sits under them. The daily sweep and its 5-day auto-close are removed: no-template issues still close immediately, partial failures stay open with a label and one warning. The bug template gains a required "How did you run impeccable?" section so reports confirm docs-intended usage instead of pasting custom scripts. AI-assisted: written with an AI agent under maintainer direction. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -5,20 +5,17 @@ import { dirname, join } from 'node:path';
|
||||
|
||||
import {
|
||||
AI_LABEL,
|
||||
CLOSE_MARKER,
|
||||
GATE_LABEL,
|
||||
GATE_MARKER,
|
||||
REJECT_MARKER,
|
||||
evaluateComment,
|
||||
evaluateIssue,
|
||||
evaluateSweep,
|
||||
loadTemplates,
|
||||
parseArgs,
|
||||
proseWordCount,
|
||||
} from '../scripts/github/issue-gate.mjs';
|
||||
|
||||
const REPO_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const NOW = '2026-08-05T12:00:00Z';
|
||||
|
||||
let templates;
|
||||
before(() => {
|
||||
@@ -38,6 +35,10 @@ Running the detect command against a directory crashes with a TypeError.
|
||||
|
||||
A findings report.
|
||||
|
||||
## How did you run impeccable?
|
||||
|
||||
Via \`npx impeccable detect\` as documented.
|
||||
|
||||
## Provider & environment
|
||||
|
||||
- **Provider** (Cursor / Claude Code / Gemini CLI / Codex / Copilot / Kiro / OpenCode): Cursor
|
||||
@@ -76,7 +77,12 @@ describe('issue gate templates', () => {
|
||||
it('derives required sections from the real templates', () => {
|
||||
const bug = templates.find((template) => template.file === 'bug_report.md');
|
||||
const feature = templates.find((template) => template.file === 'feature_request.md');
|
||||
assert.deepEqual(bug.requiredSections, ['what happened', 'steps to reproduce', 'provider & environment']);
|
||||
assert.deepEqual(bug.requiredSections, [
|
||||
'what happened',
|
||||
'steps to reproduce',
|
||||
'how did you run impeccable',
|
||||
'provider & environment',
|
||||
]);
|
||||
assert.deepEqual(feature.requiredSections, ['what problem does this solve', 'proposed solution']);
|
||||
});
|
||||
});
|
||||
@@ -117,10 +123,7 @@ describe('issue body gate', () => {
|
||||
assert.ok(plan.comment.includes(REJECT_MARKER));
|
||||
});
|
||||
|
||||
it('flags untouched template placeholders as empty sections', () => {
|
||||
const bugTemplate = templates.find((template) => template.file === 'bug_report.md');
|
||||
const untouched = [...bugTemplate.scaffoldLines].length > 0;
|
||||
assert.ok(untouched);
|
||||
it('accepts headings without judging the content under them', () => {
|
||||
const plan = evaluateIssue(issue({
|
||||
body: [
|
||||
'## What happened?',
|
||||
@@ -133,6 +136,8 @@ describe('issue body gate', () => {
|
||||
'2. ',
|
||||
'3. ',
|
||||
'',
|
||||
'## How did you run impeccable?',
|
||||
'',
|
||||
'## Provider & environment',
|
||||
'',
|
||||
'- **Provider** (Cursor / Claude Code / Gemini CLI / Codex / Copilot / Kiro / OpenCode):',
|
||||
@@ -140,20 +145,20 @@ describe('issue body gate', () => {
|
||||
'- **OS**: ',
|
||||
].join('\n'),
|
||||
}), { templates });
|
||||
assert.equal(plan.verdict, 'needs-work');
|
||||
assert.equal(plan.reasons.length, 3);
|
||||
assert.match(plan.reasons[0], /section "what happened" is empty/);
|
||||
assert.equal(plan.shouldClose, false);
|
||||
assert.deepEqual(plan.labelsToAdd, [GATE_LABEL]);
|
||||
assert.equal(plan.verdict, 'pass');
|
||||
});
|
||||
|
||||
it('names a missing required section', () => {
|
||||
it('names missing required sections', () => {
|
||||
const plan = evaluateIssue(issue({
|
||||
body: [
|
||||
'## What happened?',
|
||||
'',
|
||||
'The build fails.',
|
||||
'',
|
||||
'## How did you run impeccable?',
|
||||
'',
|
||||
'As documented, via /impeccable audit.',
|
||||
'',
|
||||
'## Provider & environment',
|
||||
'',
|
||||
'- **OS**: macOS 15 and provider Cursor 2.4.0',
|
||||
@@ -179,7 +184,7 @@ describe('issue body gate', () => {
|
||||
assert.equal(plan.verdict, 'pass');
|
||||
});
|
||||
|
||||
it('treats author sub-headings as section content, not section boundaries', () => {
|
||||
it('ignores author sub-headings between template sections', () => {
|
||||
const plan = evaluateIssue(issue({
|
||||
body: [
|
||||
'## What happened?',
|
||||
@@ -196,6 +201,10 @@ describe('issue body gate', () => {
|
||||
'',
|
||||
'1. Seed a Claude install first, then run the same command.',
|
||||
'',
|
||||
'## How did you run impeccable?',
|
||||
'',
|
||||
'CLI, as documented.',
|
||||
'',
|
||||
'## Provider & environment',
|
||||
'',
|
||||
'- **OS**: macOS 15, CLI 4.0.4',
|
||||
@@ -220,6 +229,17 @@ describe('issue body gate', () => {
|
||||
assert.equal(member.exempt, true);
|
||||
});
|
||||
|
||||
it('warns a needs-work issue only once', () => {
|
||||
const plan = evaluateIssue(issue({
|
||||
body: FILLED_BUG_BODY + '\n' + words(700),
|
||||
labels: [GATE_LABEL],
|
||||
comments: [comment('github-actions[bot]', '2026-08-01T01:00:00Z', GATE_MARKER)],
|
||||
}), { templates });
|
||||
assert.equal(plan.verdict, 'needs-work');
|
||||
assert.equal(plan.shouldComment, false);
|
||||
assert.deepEqual(plan.labelsToAdd, []);
|
||||
});
|
||||
|
||||
it('comments only once per issue', () => {
|
||||
const plan = evaluateIssue(issue({
|
||||
body: 'no structure here',
|
||||
@@ -261,45 +281,6 @@ describe('issue body gate', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('sweep', () => {
|
||||
it('closes an issue still failing five days after the warning', () => {
|
||||
const plan = evaluateSweep(issue({
|
||||
body: 'still no structure',
|
||||
labels: [GATE_LABEL],
|
||||
comments: [comment('github-actions[bot]', '2026-07-30T12:00:00Z', GATE_MARKER)],
|
||||
}), { templates, now: NOW });
|
||||
assert.equal(plan.shouldSweepClose, false);
|
||||
|
||||
const oversized = evaluateSweep(issue({
|
||||
body: FILLED_BUG_BODY + '\n' + words(700),
|
||||
labels: [GATE_LABEL],
|
||||
comments: [comment('github-actions[bot]', '2026-07-30T12:00:00Z', GATE_MARKER)],
|
||||
}), { templates, now: NOW });
|
||||
assert.equal(oversized.shouldSweepClose, true);
|
||||
assert.match(oversized.sweepComment, /failing for 6 days/);
|
||||
assert.ok(oversized.sweepComment.includes(CLOSE_MARKER));
|
||||
});
|
||||
|
||||
it('does not close before the warning has aged past close-days', () => {
|
||||
const plan = evaluateSweep(issue({
|
||||
body: FILLED_BUG_BODY + '\n' + words(700),
|
||||
labels: [GATE_LABEL],
|
||||
comments: [comment('github-actions[bot]', '2026-08-03T12:00:00Z', GATE_MARKER)],
|
||||
}), { templates, now: NOW });
|
||||
assert.equal(plan.shouldSweepClose, false);
|
||||
});
|
||||
|
||||
it('lets a fixed issue pass instead of closing it', () => {
|
||||
const plan = evaluateSweep(issue({
|
||||
labels: [GATE_LABEL],
|
||||
comments: [comment('github-actions[bot]', '2026-07-25T12:00:00Z', GATE_MARKER)],
|
||||
}), { templates, now: NOW });
|
||||
assert.equal(plan.verdict, 'pass');
|
||||
assert.equal(plan.shouldSweepClose, false);
|
||||
assert.deepEqual(plan.labelsToRemove, [GATE_LABEL]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('comment gate', () => {
|
||||
const base = {
|
||||
commentAuthorLogin: 'outside-reporter',
|
||||
|
||||
Reference in New Issue
Block a user