From 83b3b3db79e6fb7d0c66c13c31f79335a11b6c25 Mon Sep 17 00:00:00 2001 From: ndycode Date: Sun, 15 Mar 2026 05:30:58 +0800 Subject: [PATCH 01/16] chore: add anti-slop PR screening --- .github/pull_request_template.md | 18 ++++++----- .github/workflows/anti-slop.yml | 38 +++++++++++++++++++++++ CONTRIBUTING.md | 6 ++-- test/documentation.test.ts | 53 ++++++++++++++++++++++++++++++++ 4 files changed, 106 insertions(+), 9 deletions(-) create mode 100644 .github/workflows/anti-slop.yml diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 2f0e3eff..57beffed 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,3 +1,5 @@ + + ## Summary - @@ -14,14 +16,16 @@ - [ ] `npm test -- test/documentation.test.ts` - [ ] `npm run build` -## Docs and Governance Checklist +## Docs Impact + +- [ ] No docs update needed +- [ ] Docs updated in this PR +- [ ] Follow-up docs work needed + +## Governance Review -- [ ] README updated (if user-visible behavior changed) -- [ ] `docs/getting-started.md` updated (if onboarding flow changed) -- [ ] `docs/features.md` updated (if capability surface changed) -- [ ] relevant `docs/reference/*` pages updated (if commands/settings/paths changed) -- [ ] `docs/upgrade.md` updated (if migration behavior changed) -- [ ] `SECURITY.md` and `CONTRIBUTING.md` reviewed for alignment +- [ ] No CONTRIBUTING.md/SECURITY.md changes needed +- [ ] CONTRIBUTING.md and/or SECURITY.md reviewed or updated ## Risk and Rollback diff --git a/.github/workflows/anti-slop.yml b/.github/workflows/anti-slop.yml new file mode 100644 index 00000000..eac8a065 --- /dev/null +++ b/.github/workflows/anti-slop.yml @@ -0,0 +1,38 @@ +name: Anti Slop + +on: + pull_request_target: + branches: [main] + types: [opened, synchronize, reopened, ready_for_review, edited] + +permissions: + contents: read + issues: read + pull-requests: write + +jobs: + anti-slop: + name: PR Quality Screening + runs-on: ubuntu-latest + + steps: + - name: Run anti-slop checks + uses: peakoss/anti-slop@85daca1880e9e1af197fc06ea03349daf08f4202 # v0.2.1 + with: + github-token: ${{ github.token }} + max-failures: 4 + require-pr-template: true + strict-pr-template-sections: Validation + optional-pr-template-sections: Additional Notes + blocked-terms: | + WORKTREE_LANTERN_1455 + blocked-paths: "" + exempt-draft-prs: true + failure-add-pr-labels: needs-human-review + failure-pr-message: >- + This PR failed automated PR quality screening and was flagged for + maintainer review. Keep the PR template intact, include real + validation evidence, and remove any hidden template trap text before + requesting review again. + close-pr: false + lock-pr: false diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 295c5fbe..4b881559 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -59,8 +59,10 @@ For user-facing behavior changes, review these files at minimum: - `npm test` - `npm run build` 4. Include command output evidence in the PR description. -5. Document behavior changes and migration notes when needed. -6. Ensure no secrets or local runtime data are committed. +5. Keep `.github/pull_request_template.md` intact and fill every visible section when opening or updating the PR. +6. PRs are screened by automated PR quality checks; a `needs-human-review` label means maintainer follow-up is required before merge. +7. Document behavior changes and migration notes when needed. +8. Ensure no secrets or local runtime data are committed. Use `.github/pull_request_template.md` when opening the PR. diff --git a/test/documentation.test.ts b/test/documentation.test.ts index 1c696d36..9c0e1af0 100644 --- a/test/documentation.test.ts +++ b/test/documentation.test.ts @@ -405,12 +405,17 @@ describe("Documentation Integrity", () => { }); it("keeps governance templates and security reporting guidance present", () => { + const antiSlopWorkflow = ".github/workflows/anti-slop.yml"; const prTemplate = ".github/pull_request_template.md"; const issueConfig = ".github/ISSUE_TEMPLATE/config.yml"; const bugTemplate = ".github/ISSUE_TEMPLATE/bug_report.md"; const featureTemplate = ".github/ISSUE_TEMPLATE/feature_request.md"; const codeOfConduct = "CODE_OF_CONDUCT.md"; + expect( + existsSync(join(projectRoot, antiSlopWorkflow)), + `${antiSlopWorkflow} should exist`, + ).toBe(true); expect( existsSync(join(projectRoot, prTemplate)), `${prTemplate} should exist`, @@ -432,12 +437,58 @@ describe("Documentation Integrity", () => { `${codeOfConduct} should exist`, ).toBe(true); + const antiSlop = read(antiSlopWorkflow); + // pull_request_target runs with base-repo permissions, so the action must + // stay pinned to an immutable commit instead of a mutable tag or branch. + expect(antiSlop).toMatch(/uses:\s*peakoss\/anti-slop@[a-f0-9]{40}\b/i); + expect(antiSlop).toContain( + "peakoss/anti-slop@85daca1880e9e1af197fc06ea03349daf08f4202", + ); + expect(antiSlop).toContain("pull_request_target:"); + const pullRequestTargetStart = antiSlop.indexOf("pull_request_target:"); + const jobsMatch = antiSlop + .slice(pullRequestTargetStart) + .match(/\n\s*jobs:/); + const jobsStart = + jobsMatch?.index === undefined + ? -1 + : pullRequestTargetStart + jobsMatch.index; + const pullRequestTargetSection = antiSlop.slice( + pullRequestTargetStart, + jobsStart === -1 ? undefined : jobsStart, + ); + expect(pullRequestTargetSection).toContain("types:"); + for (const triggerType of [ + "opened", + "synchronize", + "reopened", + "ready_for_review", + "edited", + ]) { + expect(pullRequestTargetSection).toContain(triggerType); + } + expect(antiSlop).toContain("issues: read"); + expect(antiSlop).toContain("pull-requests: write"); + expect(antiSlop).toMatch( + /github-token:\s*\$\{\{\s*github\.token\s*\}\}/, + ); + expect(antiSlop).toContain("require-pr-template: true"); + expect(antiSlop).toContain("strict-pr-template-sections: Validation"); + expect(antiSlop).toContain("blocked-terms:"); + expect(antiSlop).toContain("WORKTREE_LANTERN_1455"); + expect(antiSlop).toContain("failure-add-pr-labels: needs-human-review"); + expect(antiSlop).toContain("close-pr: false"); + const prBody = read(prTemplate); + expect(prBody).toContain("WORKTREE_LANTERN_1455"); expect(prBody).toContain("npm run lint"); expect(prBody).toContain("npm run typecheck"); expect(prBody).toContain("npm test"); expect(prBody).toContain("npm test -- test/documentation.test.ts"); expect(prBody).toContain("npm run build"); + expect(prBody).toContain("## Docs Impact"); + expect(prBody).toContain("## Governance Review"); + expect(prBody).not.toContain("## Docs and Governance Checklist"); const security = read("SECURITY.md").toLowerCase(); expect(security).toContain("do not open a public issue"); @@ -450,6 +501,8 @@ describe("Documentation Integrity", () => { expect(contributing).toContain("npm run lint"); expect(contributing).toContain("npm test"); expect(contributing).toContain("npm run build"); + expect(contributing).toContain("needs-human-review"); + expect(contributing).toContain("pull_request_template.md"); const conduct = read("CODE_OF_CONDUCT.md").toLowerCase(); expect(conduct).toContain("respectful"); From 27a3fa57eb0aaf214c74dc0fdb6ea74f4244440b Mon Sep 17 00:00:00 2001 From: ndycode Date: Sun, 15 Mar 2026 11:42:59 +0800 Subject: [PATCH 02/16] fix: address anti-slop review feedback --- .github/workflows/anti-slop.yml | 4 +- CONTRIBUTING.md | 2 +- test/documentation.test.ts | 100 +++++++++++++++++++++----------- 3 files changed, 69 insertions(+), 37 deletions(-) diff --git a/.github/workflows/anti-slop.yml b/.github/workflows/anti-slop.yml index eac8a065..e15948a7 100644 --- a/.github/workflows/anti-slop.yml +++ b/.github/workflows/anti-slop.yml @@ -7,7 +7,7 @@ on: permissions: contents: read - issues: read + issues: write pull-requests: write jobs: @@ -22,7 +22,7 @@ jobs: github-token: ${{ github.token }} max-failures: 4 require-pr-template: true - strict-pr-template-sections: Validation + strict-pr-template-sections: Validation,Docs Impact,Governance Review optional-pr-template-sections: Additional Notes blocked-terms: | WORKTREE_LANTERN_1455 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4b881559..f16cbbc4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -59,7 +59,7 @@ For user-facing behavior changes, review these files at minimum: - `npm test` - `npm run build` 4. Include command output evidence in the PR description. -5. Keep `.github/pull_request_template.md` intact and fill every visible section when opening or updating the PR. +5. Keep the visible `.github/pull_request_template.md` section structure intact, fill every visible section, and do not copy hidden template comments into the PR description. 6. PRs are screened by automated PR quality checks; a `needs-human-review` label means maintainer follow-up is required before merge. 7. Document behavior changes and migration notes when needed. 8. Ensure no secrets or local runtime data are committed. diff --git a/test/documentation.test.ts b/test/documentation.test.ts index 9c0e1af0..98995f6e 100644 --- a/test/documentation.test.ts +++ b/test/documentation.test.ts @@ -10,6 +10,7 @@ import { import { tmpdir } from "node:os"; import { dirname, join, resolve } from "node:path"; import { describe, expect, it, vi } from "vitest"; +import { parse } from "yaml"; import { UI_COPY } from "../lib/ui/copy.js"; const projectRoot = resolve(process.cwd()); @@ -60,6 +61,36 @@ function read(filePath: string): string { return readFileSync(join(projectRoot, filePath), "utf-8"); } +interface AntiSlopWorkflowConfig { + on?: { + pull_request_target?: { + branches?: string[]; + types?: string[]; + }; + }; + permissions?: { + contents?: string; + issues?: string; + "pull-requests"?: string; + }; + jobs?: { + "anti-slop"?: { + steps?: Array<{ + name?: string; + uses?: string; + with?: { + "github-token"?: string; + "require-pr-template"?: boolean; + "strict-pr-template-sections"?: string; + "blocked-terms"?: string; + "failure-add-pr-labels"?: string; + "close-pr"?: boolean; + }; + }>; + }; + }; +} + function extractInternalLinks(markdown: string): string[] { return [...markdown.matchAll(/\[[^\]]+\]\(([^)]+)\)/g)] .map((match) => match[1]) @@ -438,46 +469,47 @@ describe("Documentation Integrity", () => { ).toBe(true); const antiSlop = read(antiSlopWorkflow); + const antiSlopConfig = parse(antiSlop) as AntiSlopWorkflowConfig; + const antiSlopStep = antiSlopConfig.jobs?.["anti-slop"]?.steps?.find( + (step) => step.name === "Run anti-slop checks", + ); // pull_request_target runs with base-repo permissions, so the action must // stay pinned to an immutable commit instead of a mutable tag or branch. - expect(antiSlop).toMatch(/uses:\s*peakoss\/anti-slop@[a-f0-9]{40}\b/i); - expect(antiSlop).toContain( + expect(antiSlopStep?.uses).toMatch( + /^peakoss\/anti-slop@[a-f0-9]{40}\b/i, + ); + expect(antiSlopStep?.uses).toBe( "peakoss/anti-slop@85daca1880e9e1af197fc06ea03349daf08f4202", ); - expect(antiSlop).toContain("pull_request_target:"); - const pullRequestTargetStart = antiSlop.indexOf("pull_request_target:"); - const jobsMatch = antiSlop - .slice(pullRequestTargetStart) - .match(/\n\s*jobs:/); - const jobsStart = - jobsMatch?.index === undefined - ? -1 - : pullRequestTargetStart + jobsMatch.index; - const pullRequestTargetSection = antiSlop.slice( - pullRequestTargetStart, - jobsStart === -1 ? undefined : jobsStart, + expect(antiSlopConfig.on?.pull_request_target?.branches).toEqual(["main"]); + expect( + antiSlopConfig.on?.pull_request_target?.types ?? [], + ).toEqual( + [ + "opened", + "synchronize", + "reopened", + "ready_for_review", + "edited", + ], ); - expect(pullRequestTargetSection).toContain("types:"); - for (const triggerType of [ - "opened", - "synchronize", - "reopened", - "ready_for_review", - "edited", - ]) { - expect(pullRequestTargetSection).toContain(triggerType); - } - expect(antiSlop).toContain("issues: read"); - expect(antiSlop).toContain("pull-requests: write"); - expect(antiSlop).toMatch( - /github-token:\s*\$\{\{\s*github\.token\s*\}\}/, + expect(antiSlopConfig.permissions).toMatchObject({ + contents: "read", + issues: "write", + "pull-requests": "write", + }); + expect(antiSlopStep?.with?.["github-token"]).toBe("${{ github.token }}"); + expect(antiSlopStep?.with?.["require-pr-template"]).toBe(true); + expect(antiSlopStep?.with?.["strict-pr-template-sections"]).toBe( + "Validation,Docs Impact,Governance Review", + ); + expect(antiSlopStep?.with?.["blocked-terms"]).toContain( + "WORKTREE_LANTERN_1455", + ); + expect(antiSlopStep?.with?.["failure-add-pr-labels"]).toBe( + "needs-human-review", ); - expect(antiSlop).toContain("require-pr-template: true"); - expect(antiSlop).toContain("strict-pr-template-sections: Validation"); - expect(antiSlop).toContain("blocked-terms:"); - expect(antiSlop).toContain("WORKTREE_LANTERN_1455"); - expect(antiSlop).toContain("failure-add-pr-labels: needs-human-review"); - expect(antiSlop).toContain("close-pr: false"); + expect(antiSlopStep?.with?.["close-pr"]).toBe(false); const prBody = read(prTemplate); expect(prBody).toContain("WORKTREE_LANTERN_1455"); From abbaba8465c115c4902b19f0fcedf800bfdedb03 Mon Sep 17 00:00:00 2001 From: ndycode Date: Sun, 15 Mar 2026 11:53:27 +0800 Subject: [PATCH 03/16] fix: tighten anti-slop workflow contract coverage --- package-lock.json | 3 ++- package.json | 3 ++- test/documentation.test.ts | 4 +++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index c71e670d..7d84a956 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35,7 +35,8 @@ "lint-staged": "^16.2.7", "typescript": "^5.9.3", "typescript-language-server": "^5.1.3", - "vitest": "^4.0.18" + "vitest": "^4.0.18", + "yaml": "^2.8.2" }, "engines": { "node": ">=18.0.0" diff --git a/package.json b/package.json index 13083dba..a0a46fda 100644 --- a/package.json +++ b/package.json @@ -112,7 +112,8 @@ "lint-staged": "^16.2.7", "typescript": "^5.9.3", "typescript-language-server": "^5.1.3", - "vitest": "^4.0.18" + "vitest": "^4.0.18", + "yaml": "^2.8.2" }, "dependencies": { "@openauthjs/openauth": "^0.4.3", diff --git a/test/documentation.test.ts b/test/documentation.test.ts index 98995f6e..b0489028 100644 --- a/test/documentation.test.ts +++ b/test/documentation.test.ts @@ -85,6 +85,7 @@ interface AntiSlopWorkflowConfig { "blocked-terms"?: string; "failure-add-pr-labels"?: string; "close-pr"?: boolean; + "lock-pr"?: boolean; }; }>; }; @@ -493,7 +494,7 @@ describe("Documentation Integrity", () => { "edited", ], ); - expect(antiSlopConfig.permissions).toMatchObject({ + expect(antiSlopConfig.permissions).toEqual({ contents: "read", issues: "write", "pull-requests": "write", @@ -510,6 +511,7 @@ describe("Documentation Integrity", () => { "needs-human-review", ); expect(antiSlopStep?.with?.["close-pr"]).toBe(false); + expect(antiSlopStep?.with?.["lock-pr"]).toBe(false); const prBody = read(prTemplate); expect(prBody).toContain("WORKTREE_LANTERN_1455"); From 750e5ee6c240dddcbca1ca35184987341aa503cb Mon Sep 17 00:00:00 2001 From: ndycode Date: Sun, 15 Mar 2026 13:49:43 +0800 Subject: [PATCH 04/16] fix(ci): keep only validation strict in anti-slop --- .github/workflows/anti-slop.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/anti-slop.yml b/.github/workflows/anti-slop.yml index e15948a7..a837b051 100644 --- a/.github/workflows/anti-slop.yml +++ b/.github/workflows/anti-slop.yml @@ -22,7 +22,7 @@ jobs: github-token: ${{ github.token }} max-failures: 4 require-pr-template: true - strict-pr-template-sections: Validation,Docs Impact,Governance Review + strict-pr-template-sections: Validation optional-pr-template-sections: Additional Notes blocked-terms: | WORKTREE_LANTERN_1455 From c80139ad6e7da5de1c21af2c28ca93b5f988793c Mon Sep 17 00:00:00 2001 From: ndycode Date: Sun, 15 Mar 2026 13:59:06 +0800 Subject: [PATCH 05/16] test(docs): align anti-slop workflow expectation --- test/documentation.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/documentation.test.ts b/test/documentation.test.ts index b0489028..c0d205cb 100644 --- a/test/documentation.test.ts +++ b/test/documentation.test.ts @@ -502,7 +502,7 @@ describe("Documentation Integrity", () => { expect(antiSlopStep?.with?.["github-token"]).toBe("${{ github.token }}"); expect(antiSlopStep?.with?.["require-pr-template"]).toBe(true); expect(antiSlopStep?.with?.["strict-pr-template-sections"]).toBe( - "Validation,Docs Impact,Governance Review", + "Validation", ); expect(antiSlopStep?.with?.["blocked-terms"]).toContain( "WORKTREE_LANTERN_1455", From 87121aa9b920d42ba0ec694d8ea49856f2dfff4c Mon Sep 17 00:00:00 2001 From: ndycode Date: Sun, 15 Mar 2026 14:03:34 +0800 Subject: [PATCH 06/16] fix(ci): clarify anti-slop workflow contract --- .github/pull_request_template.md | 4 ++++ .github/workflows/anti-slop.yml | 2 ++ test/documentation.test.ts | 10 ++++++++++ 3 files changed, 16 insertions(+) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 57beffed..9bf1e14f 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -18,12 +18,16 @@ ## Docs Impact +Pick one: + - [ ] No docs update needed - [ ] Docs updated in this PR - [ ] Follow-up docs work needed ## Governance Review +Pick one: + - [ ] No CONTRIBUTING.md/SECURITY.md changes needed - [ ] CONTRIBUTING.md and/or SECURITY.md reviewed or updated diff --git a/.github/workflows/anti-slop.yml b/.github/workflows/anti-slop.yml index a837b051..949222e9 100644 --- a/.github/workflows/anti-slop.yml +++ b/.github/workflows/anti-slop.yml @@ -1,6 +1,8 @@ name: Anti Slop on: + # `pull_request_target` is intentional so this workflow can label/comment on + # PRs. It must stay metadata-only: never checkout or run untrusted PR code. pull_request_target: branches: [main] types: [opened, synchronize, reopened, ready_for_review, edited] diff --git a/test/documentation.test.ts b/test/documentation.test.ts index c0d205cb..e37d5125 100644 --- a/test/documentation.test.ts +++ b/test/documentation.test.ts @@ -474,6 +474,10 @@ describe("Documentation Integrity", () => { const antiSlopStep = antiSlopConfig.jobs?.["anti-slop"]?.steps?.find( (step) => step.name === "Run anti-slop checks", ); + expect( + antiSlopStep, + 'step "Run anti-slop checks" not found in anti-slop.yml', + ).toBeDefined(); // pull_request_target runs with base-repo permissions, so the action must // stay pinned to an immutable commit instead of a mutable tag or branch. expect(antiSlopStep?.uses).toMatch( @@ -512,16 +516,22 @@ describe("Documentation Integrity", () => { ); expect(antiSlopStep?.with?.["close-pr"]).toBe(false); expect(antiSlopStep?.with?.["lock-pr"]).toBe(false); + expect(antiSlop).toContain("must stay metadata-only"); const prBody = read(prTemplate); expect(prBody).toContain("WORKTREE_LANTERN_1455"); + expect(prBody).toContain("## Summary"); + expect(prBody).toContain("## What Changed"); expect(prBody).toContain("npm run lint"); expect(prBody).toContain("npm run typecheck"); expect(prBody).toContain("npm test"); expect(prBody).toContain("npm test -- test/documentation.test.ts"); expect(prBody).toContain("npm run build"); expect(prBody).toContain("## Docs Impact"); + expect(prBody).toContain("Pick one:"); expect(prBody).toContain("## Governance Review"); + expect(prBody).toContain("## Risk and Rollback"); + expect(prBody).toContain("## Additional Notes"); expect(prBody).not.toContain("## Docs and Governance Checklist"); const security = read("SECURITY.md").toLowerCase(); From 63311bbd801f567587e1e9c392c14176d625377f Mon Sep 17 00:00:00 2001 From: ndycode Date: Sun, 15 Mar 2026 14:06:11 +0800 Subject: [PATCH 07/16] fix(ci): document anti-slop honeypot behavior --- .github/workflows/anti-slop.yml | 6 ++++-- test/documentation.test.ts | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/anti-slop.yml b/.github/workflows/anti-slop.yml index 949222e9..63f74662 100644 --- a/.github/workflows/anti-slop.yml +++ b/.github/workflows/anti-slop.yml @@ -26,6 +26,8 @@ jobs: require-pr-template: true strict-pr-template-sections: Validation optional-pr-template-sections: Additional Notes + # anti-slop strips HTML comments before blocked-term matching, so the + # hidden template comment only trips if copied into visible PR text. blocked-terms: | WORKTREE_LANTERN_1455 blocked-paths: "" @@ -34,7 +36,7 @@ jobs: failure-pr-message: >- This PR failed automated PR quality screening and was flagged for maintainer review. Keep the PR template intact, include real - validation evidence, and remove any hidden template trap text before - requesting review again. + validation evidence, and remove any copied hidden template trap text + from the visible PR body before requesting review again. close-pr: false lock-pr: false diff --git a/test/documentation.test.ts b/test/documentation.test.ts index e37d5125..c0a1c875 100644 --- a/test/documentation.test.ts +++ b/test/documentation.test.ts @@ -517,6 +517,7 @@ describe("Documentation Integrity", () => { expect(antiSlopStep?.with?.["close-pr"]).toBe(false); expect(antiSlopStep?.with?.["lock-pr"]).toBe(false); expect(antiSlop).toContain("must stay metadata-only"); + expect(antiSlop).toContain("strips HTML comments before blocked-term matching"); const prBody = read(prTemplate); expect(prBody).toContain("WORKTREE_LANTERN_1455"); From cd7b89f29df60111835f105778dde6b3c33e260c Mon Sep 17 00:00:00 2001 From: ndycode Date: Sun, 15 Mar 2026 14:57:33 +0800 Subject: [PATCH 08/16] test(docs): cover optional anti-slop section --- test/documentation.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/documentation.test.ts b/test/documentation.test.ts index c0a1c875..a6283629 100644 --- a/test/documentation.test.ts +++ b/test/documentation.test.ts @@ -82,6 +82,7 @@ interface AntiSlopWorkflowConfig { "github-token"?: string; "require-pr-template"?: boolean; "strict-pr-template-sections"?: string; + "optional-pr-template-sections"?: string; "blocked-terms"?: string; "failure-add-pr-labels"?: string; "close-pr"?: boolean; @@ -508,6 +509,9 @@ describe("Documentation Integrity", () => { expect(antiSlopStep?.with?.["strict-pr-template-sections"]).toBe( "Validation", ); + expect(antiSlopStep?.with?.["optional-pr-template-sections"]).toBe( + "Additional Notes", + ); expect(antiSlopStep?.with?.["blocked-terms"]).toContain( "WORKTREE_LANTERN_1455", ); From b1c927043cdcc16dcefdee6ab23022ab084b988d Mon Sep 17 00:00:00 2001 From: ndycode Date: Sun, 15 Mar 2026 15:19:18 +0800 Subject: [PATCH 09/16] test: cover anti-slop input contract details --- test/documentation.test.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/test/documentation.test.ts b/test/documentation.test.ts index a6283629..cbc3164e 100644 --- a/test/documentation.test.ts +++ b/test/documentation.test.ts @@ -83,6 +83,8 @@ interface AntiSlopWorkflowConfig { "require-pr-template"?: boolean; "strict-pr-template-sections"?: string; "optional-pr-template-sections"?: string; + "max-failures"?: number; + "exempt-draft-prs"?: boolean; "blocked-terms"?: string; "failure-add-pr-labels"?: string; "close-pr"?: boolean; @@ -512,6 +514,8 @@ describe("Documentation Integrity", () => { expect(antiSlopStep?.with?.["optional-pr-template-sections"]).toBe( "Additional Notes", ); + expect(antiSlopStep?.with?.["max-failures"]).toBe(4); + expect(antiSlopStep?.with?.["exempt-draft-prs"]).toBe(true); expect(antiSlopStep?.with?.["blocked-terms"]).toContain( "WORKTREE_LANTERN_1455", ); @@ -533,8 +537,9 @@ describe("Documentation Integrity", () => { expect(prBody).toContain("npm test -- test/documentation.test.ts"); expect(prBody).toContain("npm run build"); expect(prBody).toContain("## Docs Impact"); - expect(prBody).toContain("Pick one:"); + expect(prBody).toMatch(/## Docs Impact\s*\n+\s*Pick one:/); expect(prBody).toContain("## Governance Review"); + expect(prBody).toMatch(/## Governance Review\s*\n+\s*Pick one:/); expect(prBody).toContain("## Risk and Rollback"); expect(prBody).toContain("## Additional Notes"); expect(prBody).not.toContain("## Docs and Governance Checklist"); From 30cd36a6cc0894b6630d589ed94faf7fa9a88627 Mon Sep 17 00:00:00 2001 From: ndycode Date: Sun, 15 Mar 2026 15:24:38 +0800 Subject: [PATCH 10/16] test: cover anti-slop failure message contract --- .github/workflows/anti-slop.yml | 9 +++++---- test/documentation.test.ts | 6 ++++++ 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/.github/workflows/anti-slop.yml b/.github/workflows/anti-slop.yml index 63f74662..563a6594 100644 --- a/.github/workflows/anti-slop.yml +++ b/.github/workflows/anti-slop.yml @@ -34,9 +34,10 @@ jobs: exempt-draft-prs: true failure-add-pr-labels: needs-human-review failure-pr-message: >- - This PR failed automated PR quality screening and was flagged for - maintainer review. Keep the PR template intact, include real - validation evidence, and remove any copied hidden template trap text - from the visible PR body before requesting review again. + This PR failed automated PR quality screening and was labelled + needs-human-review for maintainer review. Keep the PR template + intact, include real validation evidence, and remove any copied + hidden template trap text from the visible PR body before requesting + review again. close-pr: false lock-pr: false diff --git a/test/documentation.test.ts b/test/documentation.test.ts index cbc3164e..ae3a4296 100644 --- a/test/documentation.test.ts +++ b/test/documentation.test.ts @@ -86,7 +86,9 @@ interface AntiSlopWorkflowConfig { "max-failures"?: number; "exempt-draft-prs"?: boolean; "blocked-terms"?: string; + "blocked-paths"?: string; "failure-add-pr-labels"?: string; + "failure-pr-message"?: string; "close-pr"?: boolean; "lock-pr"?: boolean; }; @@ -519,9 +521,13 @@ describe("Documentation Integrity", () => { expect(antiSlopStep?.with?.["blocked-terms"]).toContain( "WORKTREE_LANTERN_1455", ); + expect(antiSlopStep?.with?.["blocked-paths"]).toBe(""); expect(antiSlopStep?.with?.["failure-add-pr-labels"]).toBe( "needs-human-review", ); + expect(antiSlopStep?.with?.["failure-pr-message"]).toContain( + "needs-human-review", + ); expect(antiSlopStep?.with?.["close-pr"]).toBe(false); expect(antiSlopStep?.with?.["lock-pr"]).toBe(false); expect(antiSlop).toContain("must stay metadata-only"); From 8f311ea589ec0939f4314214c8121c71c3e096cd Mon Sep 17 00:00:00 2001 From: ndycode Date: Sun, 15 Mar 2026 15:36:01 +0800 Subject: [PATCH 11/16] docs: tighten anti-slop honeypot and governance template --- .github/pull_request_template.md | 1 + .github/workflows/anti-slop.yml | 3 +-- test/documentation.test.ts | 5 ++++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 9bf1e14f..a6d59983 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -30,6 +30,7 @@ Pick one: - [ ] No CONTRIBUTING.md/SECURITY.md changes needed - [ ] CONTRIBUTING.md and/or SECURITY.md reviewed or updated +- [ ] Follow-up CONTRIBUTING.md/SECURITY.md work needed ## Risk and Rollback diff --git a/.github/workflows/anti-slop.yml b/.github/workflows/anti-slop.yml index 563a6594..59d25160 100644 --- a/.github/workflows/anti-slop.yml +++ b/.github/workflows/anti-slop.yml @@ -28,8 +28,7 @@ jobs: optional-pr-template-sections: Additional Notes # anti-slop strips HTML comments before blocked-term matching, so the # hidden template comment only trips if copied into visible PR text. - blocked-terms: | - WORKTREE_LANTERN_1455 + blocked-terms: "WORKTREE_LANTERN_1455" blocked-paths: "" exempt-draft-prs: true failure-add-pr-labels: needs-human-review diff --git a/test/documentation.test.ts b/test/documentation.test.ts index ae3a4296..3928da74 100644 --- a/test/documentation.test.ts +++ b/test/documentation.test.ts @@ -518,7 +518,7 @@ describe("Documentation Integrity", () => { ); expect(antiSlopStep?.with?.["max-failures"]).toBe(4); expect(antiSlopStep?.with?.["exempt-draft-prs"]).toBe(true); - expect(antiSlopStep?.with?.["blocked-terms"]).toContain( + expect(antiSlopStep?.with?.["blocked-terms"]).toBe( "WORKTREE_LANTERN_1455", ); expect(antiSlopStep?.with?.["blocked-paths"]).toBe(""); @@ -546,6 +546,9 @@ describe("Documentation Integrity", () => { expect(prBody).toMatch(/## Docs Impact\s*\n+\s*Pick one:/); expect(prBody).toContain("## Governance Review"); expect(prBody).toMatch(/## Governance Review\s*\n+\s*Pick one:/); + expect(prBody).toContain( + "Follow-up CONTRIBUTING.md/SECURITY.md work needed", + ); expect(prBody).toContain("## Risk and Rollback"); expect(prBody).toContain("## Additional Notes"); expect(prBody).not.toContain("## Docs and Governance Checklist"); From 0c89ee1b5b65445053e3d45a7278a4246aa13a90 Mon Sep 17 00:00:00 2001 From: ndycode Date: Sun, 15 Mar 2026 15:51:06 +0800 Subject: [PATCH 12/16] ci: serialize anti-slop runs per pr --- .github/workflows/anti-slop.yml | 4 ++++ test/documentation.test.ts | 8 ++++++++ 2 files changed, 12 insertions(+) diff --git a/.github/workflows/anti-slop.yml b/.github/workflows/anti-slop.yml index 59d25160..eefcdd36 100644 --- a/.github/workflows/anti-slop.yml +++ b/.github/workflows/anti-slop.yml @@ -7,6 +7,10 @@ on: branches: [main] types: [opened, synchronize, reopened, ready_for_review, edited] +concurrency: + group: anti-slop-${{ github.event.pull_request.number }} + cancel-in-progress: true + permissions: contents: read issues: write diff --git a/test/documentation.test.ts b/test/documentation.test.ts index 3928da74..f2954849 100644 --- a/test/documentation.test.ts +++ b/test/documentation.test.ts @@ -68,6 +68,10 @@ interface AntiSlopWorkflowConfig { types?: string[]; }; }; + concurrency?: { + group?: string; + "cancel-in-progress"?: boolean; + }; permissions?: { contents?: string; issues?: string; @@ -503,6 +507,10 @@ describe("Documentation Integrity", () => { "edited", ], ); + expect(antiSlopConfig.concurrency).toEqual({ + group: "anti-slop-${{ github.event.pull_request.number }}", + "cancel-in-progress": true, + }); expect(antiSlopConfig.permissions).toEqual({ contents: "read", issues: "write", From fad9c7b137a6385787803fa94399281ba6eb81d3 Mon Sep 17 00:00:00 2001 From: ndycode Date: Sun, 15 Mar 2026 16:00:54 +0800 Subject: [PATCH 13/16] test: drop brittle anti-slop prose assertions --- .github/workflows/anti-slop.yml | 2 +- test/documentation.test.ts | 5 ----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/.github/workflows/anti-slop.yml b/.github/workflows/anti-slop.yml index eefcdd36..1f017ca9 100644 --- a/.github/workflows/anti-slop.yml +++ b/.github/workflows/anti-slop.yml @@ -13,7 +13,7 @@ concurrency: permissions: contents: read - issues: write + issues: write # required for GitHub's label API, which routes through Issues API even on PRs pull-requests: write jobs: diff --git a/test/documentation.test.ts b/test/documentation.test.ts index f2954849..3b1c5116 100644 --- a/test/documentation.test.ts +++ b/test/documentation.test.ts @@ -489,9 +489,6 @@ describe("Documentation Integrity", () => { ).toBeDefined(); // pull_request_target runs with base-repo permissions, so the action must // stay pinned to an immutable commit instead of a mutable tag or branch. - expect(antiSlopStep?.uses).toMatch( - /^peakoss\/anti-slop@[a-f0-9]{40}\b/i, - ); expect(antiSlopStep?.uses).toBe( "peakoss/anti-slop@85daca1880e9e1af197fc06ea03349daf08f4202", ); @@ -538,8 +535,6 @@ describe("Documentation Integrity", () => { ); expect(antiSlopStep?.with?.["close-pr"]).toBe(false); expect(antiSlopStep?.with?.["lock-pr"]).toBe(false); - expect(antiSlop).toContain("must stay metadata-only"); - expect(antiSlop).toContain("strips HTML comments before blocked-term matching"); const prBody = read(prTemplate); expect(prBody).toContain("WORKTREE_LANTERN_1455"); From 1a13b56df759dbfe637860942dc283417d0d8ad3 Mon Sep 17 00:00:00 2001 From: ndycode Date: Sun, 15 Mar 2026 16:08:49 +0800 Subject: [PATCH 14/16] docs: restore template doc file guidance --- .github/pull_request_template.md | 2 +- test/documentation.test.ts | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index a6d59983..ab6c3c3e 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -21,7 +21,7 @@ Pick one: - [ ] No docs update needed -- [ ] Docs updated in this PR +- [ ] Docs updated in this PR - [ ] Follow-up docs work needed ## Governance Review diff --git a/test/documentation.test.ts b/test/documentation.test.ts index 3b1c5116..5313c44f 100644 --- a/test/documentation.test.ts +++ b/test/documentation.test.ts @@ -547,6 +547,10 @@ describe("Documentation Integrity", () => { expect(prBody).toContain("npm run build"); expect(prBody).toContain("## Docs Impact"); expect(prBody).toMatch(/## Docs Impact\s*\n+\s*Pick one:/); + expect(prBody).toContain("docs/getting-started.md"); + expect(prBody).toContain("docs/features.md"); + expect(prBody).toContain("docs/reference/*"); + expect(prBody).toContain("docs/upgrade.md"); expect(prBody).toContain("## Governance Review"); expect(prBody).toMatch(/## Governance Review\s*\n+\s*Pick one:/); expect(prBody).toContain( From 61f9ca6183a50fb098b34fcc59a5d942eb891ac5 Mon Sep 17 00:00:00 2001 From: ndycode Date: Sun, 15 Mar 2026 16:30:04 +0800 Subject: [PATCH 15/16] ci: bound anti-slop runtime --- .github/workflows/anti-slop.yml | 1 + test/documentation.test.ts | 14 +++++++++----- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/.github/workflows/anti-slop.yml b/.github/workflows/anti-slop.yml index 1f017ca9..e7085272 100644 --- a/.github/workflows/anti-slop.yml +++ b/.github/workflows/anti-slop.yml @@ -20,6 +20,7 @@ jobs: anti-slop: name: PR Quality Screening runs-on: ubuntu-latest + timeout-minutes: 5 steps: - name: Run anti-slop checks diff --git a/test/documentation.test.ts b/test/documentation.test.ts index 5313c44f..d5215624 100644 --- a/test/documentation.test.ts +++ b/test/documentation.test.ts @@ -79,6 +79,7 @@ interface AntiSlopWorkflowConfig { }; jobs?: { "anti-slop"?: { + "timeout-minutes"?: number; steps?: Array<{ name?: string; uses?: string; @@ -480,9 +481,12 @@ describe("Documentation Integrity", () => { const antiSlop = read(antiSlopWorkflow); const antiSlopConfig = parse(antiSlop) as AntiSlopWorkflowConfig; - const antiSlopStep = antiSlopConfig.jobs?.["anti-slop"]?.steps?.find( + const antiSlopJob = antiSlopConfig.jobs?.["anti-slop"]; + const antiSlopStep = antiSlopJob?.steps?.find( (step) => step.name === "Run anti-slop checks", ); + const hiddenDocsImpactHint = + ""; expect( antiSlopStep, 'step "Run anti-slop checks" not found in anti-slop.yml', @@ -513,6 +517,7 @@ describe("Documentation Integrity", () => { issues: "write", "pull-requests": "write", }); + expect(antiSlopJob?.["timeout-minutes"]).toBe(5); expect(antiSlopStep?.with?.["github-token"]).toBe("${{ github.token }}"); expect(antiSlopStep?.with?.["require-pr-template"]).toBe(true); expect(antiSlopStep?.with?.["strict-pr-template-sections"]).toBe( @@ -547,10 +552,9 @@ describe("Documentation Integrity", () => { expect(prBody).toContain("npm run build"); expect(prBody).toContain("## Docs Impact"); expect(prBody).toMatch(/## Docs Impact\s*\n+\s*Pick one:/); - expect(prBody).toContain("docs/getting-started.md"); - expect(prBody).toContain("docs/features.md"); - expect(prBody).toContain("docs/reference/*"); - expect(prBody).toContain("docs/upgrade.md"); + // The concrete docs file list lives in a hidden HTML hint so the raw + // template stays specific even though GitHub hides it in rendered PRs. + expect(prBody).toContain(hiddenDocsImpactHint); expect(prBody).toContain("## Governance Review"); expect(prBody).toMatch(/## Governance Review\s*\n+\s*Pick one:/); expect(prBody).toContain( From 2393d9c4d4954e4c28c79cd1922c4423144f5fd2 Mon Sep 17 00:00:00 2001 From: ndycode Date: Sun, 15 Mar 2026 17:08:43 +0800 Subject: [PATCH 16/16] test: harden anti-slop honeypot coverage --- test/documentation.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/documentation.test.ts b/test/documentation.test.ts index d5215624..f73c08af 100644 --- a/test/documentation.test.ts +++ b/test/documentation.test.ts @@ -543,6 +543,8 @@ describe("Documentation Integrity", () => { const prBody = read(prTemplate); expect(prBody).toContain("WORKTREE_LANTERN_1455"); + const visiblePrBody = prBody.replace(//g, ""); + expect(visiblePrBody).not.toContain("WORKTREE_LANTERN_1455"); expect(prBody).toContain("## Summary"); expect(prBody).toContain("## What Changed"); expect(prBody).toContain("npm run lint");