From 2402650491b5899af4bd37ec1f8c9bb62bd9c9ed Mon Sep 17 00:00:00 2001 From: ArchonVII Date: Sat, 20 Jun 2026 13:35:53 -0500 Subject: [PATCH] fix(pr-contract): allow placeholder prose Remove the standalone placeholder token from the PR contract detector so completed prose can mention placeholder matching without blocking ready-for-review. Explicit unfilled markers remain rejected by the same scanner. Refs #90 --- .../90-pr-contract-placeholder-matching.md | 4 +++ docs/repo-update-log.md | 9 +++++ scripts/pr-contract.mjs | 32 +++++++++++++++-- scripts/pr-contract.test.mjs | 34 +++++++++++++++++++ 4 files changed, 77 insertions(+), 2 deletions(-) create mode 100644 .changelog/unreleased/90-pr-contract-placeholder-matching.md diff --git a/.changelog/unreleased/90-pr-contract-placeholder-matching.md b/.changelog/unreleased/90-pr-contract-placeholder-matching.md new file mode 100644 index 0000000..6be562c --- /dev/null +++ b/.changelog/unreleased/90-pr-contract-placeholder-matching.md @@ -0,0 +1,4 @@ +### Fixed + +- Relaxed PR contract placeholder matching so completed prose may use the word + "placeholder" while explicit unfilled markers still fail validation. diff --git a/docs/repo-update-log.md b/docs/repo-update-log.md index 87f177c..e4a4086 100644 --- a/docs/repo-update-log.md +++ b/docs/repo-update-log.md @@ -15,6 +15,15 @@ This log records agent-visible repository changes that should be easy to audit l - **Propagation:** none | pending | completed ``` +## 2026-06-20 - PR contract placeholder prose matching + +- **Issue/PR:** #90 / #91 +- **Branch:** agent/codex/90-pr-contract-placeholder-matching +- **Changed paths:** scripts/pr-contract.mjs, scripts/pr-contract.test.mjs, .changelog/unreleased/90-pr-contract-placeholder-matching.md, docs/repo-update-log.md +- **What changed:** Relaxed the shared PR contract validator so normal completed prose can use the word "placeholder". Explicit unfilled markers such as TODO, TBD, N/A, unset issue links, and `` remain rejected. +- **Verification:** `npx vitest run scripts/pr-contract.test.mjs` passed 23/23 tests. `git diff --check` exited 0 with CRLF warnings. `npm test` on local Windows failed before executing unchanged `scripts/doc-policy-lint.test.mjs` with `SyntaxError: Invalid or unexpected token`; latest upstream `Self-test (scripts)` on `main` passed on GitHub Actions run 27698751593, and this PR should use the PR self-test for full-suite Linux signal. +- **Propagation:** pending `v1` tag movement after merge. + ## 2026-06-15 - Warning-only document policy lint workflow - **Issue/PR:** #70 / (pending) diff --git a/scripts/pr-contract.mjs b/scripts/pr-contract.mjs index f72d2b5..069eea7 100644 --- a/scripts/pr-contract.mjs +++ b/scripts/pr-contract.mjs @@ -15,7 +15,8 @@ const DEFAULT_REQUIRED_HEADINGS = [ const TITLE_RE = /^(feat|fix|refactor|test|docs|style|chore|perf|ci|build|revert)(\([^)]+\))?: .+/; const ISSUE_RE = /\b(Closes|Fixes|Refs)\s+#\d+\b/i; -const PLACEHOLDER_RE = /\b(TODO|TBD|FIXME|FILL ME|FILL IN|REPLACE THIS|PLACEHOLDER|NOT YET|N\/A|NONE YET)\b|#\s*(?:___|<[^>]+>)|/i; +// "placeholder" is valid completed prose; reject explicit unfilled markers. +const PLACEHOLDER_RE = /\b(TODO|TBD|FIXME|FILL ME|FILL IN|REPLACE THIS|NOT YET|N\/A|NONE YET)\b|#\s*(?:___|<[^>]+>)|/i; const CHECKED_RE = /^\s*-\s+\[[xX]\]\s+(.+?)\s*$/; const UNCHECKED_RE = /^\s*-\s+\[\s\]\s+(.+?)\s*$/; const GENERIC_VERIFICATION_RE = /\b(automated ci checks? green|ci[- ]?green|ci checks? pass(?:ed|es)?|checks? green|all checks? pass(?:ed|es)?|tests? pass(?:ed|es)?)\b/i; @@ -399,7 +400,34 @@ function hasSubstantiveContent(text) { } function hasPlaceholder(text) { - return PLACEHOLDER_RE.test(text || ''); + return PLACEHOLDER_RE.test(text || '') || hasLiteralPlaceholderFiller(text); +} + +function hasLiteralPlaceholderFiller(text) { + const raw = stripHtmlComments(text); + const candidates = [ + raw, + ...raw.split(/\r?\n/), + ]; + return candidates.some((candidate) => { + const cleaned = normalizePlaceholderCandidate(candidate); + const words = cleaned.toLowerCase().match(/[a-z]+/g) || []; + return words.length > 0 + && ( + words.every((word) => word === 'placeholder') + || words.join(' ') === 'placeholder text' + ); + }); +} + +function normalizePlaceholderCandidate(text) { + return String(text || '') + .replace(/^[-*]\s+\[[ xX]\]\s+/, '') + .replace(/^[-*]\s+/, '') + .replace(/^(?:feat|fix|refactor|test|docs|style|chore|perf|ci|build|revert)(?:\([^)]+\))?:\s*/i, '') + .replace(/^[A-Za-z][A-Za-z -]{0,40}:\s*/, '') + .replace(/[`"'[\]{}()<>]/g, ' ') + .trim(); } // Remove HTML comments before placeholder scanning so a template's own diff --git a/scripts/pr-contract.test.mjs b/scripts/pr-contract.test.mjs index 855d08d..1a79689 100644 --- a/scripts/pr-contract.test.mjs +++ b/scripts/pr-contract.test.mjs @@ -208,6 +208,23 @@ describe('validatePrTemplate', () => { // The "still fails" cases matter as much as the "now passes" cases: the parser is // context-aware, not weaker. describe('context-aware parser (acceptance table)', () => { + it('passes when visible completed prose uses the word "placeholder"', () => { + const body = validBody + .replace( + '- Add strict PR contract validation before ready-for-review.', + '- Allow completed PR prose to mention placeholder matching without failing the contract.', + ) + .replace( + 'Validated the reusable workflow tests locally with `npm test`; no warnings were emitted.', + 'Validated that placeholder wording in visible completed prose is accepted.', + ); + const result = validatePrContract(input({ + title: 'fix(policy): allow placeholder prose', + body, + })); + expect(result.ok).toBe(true); + }); + it('passes when the template HTML comment (with the word "placeholder") survives but visible fields are filled', () => { const body = [ '