@webpieces/pr-gate 0.4.732 → 0.4.733
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -3
- package/src/dashboard/checklist-comment-renderer.js +6 -2
- package/src/dashboard/checklist-comment-renderer.js.map +1 -1
- package/src/scripts/commands/review-upsert-pr-command.d.ts +6 -2
- package/src/scripts/commands/review-upsert-pr-command.js +7 -3
- package/src/scripts/commands/review-upsert-pr-command.js.map +1 -1
- package/src/scripts/workflow/review-report.d.ts +4 -3
- package/src/scripts/workflow/review-report.js +4 -3
- package/src/scripts/workflow/review-report.js.map +1 -1
- package/src/scripts/workflow/reviewer-verdict-gate.js +1 -1
- package/src/scripts/workflow/reviewer-verdict-gate.js.map +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webpieces/pr-gate",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.733",
|
|
4
4
|
"description": "Gated PR system: 3-point squash-merge, merge validation gate, and red/yellow/green PR dashboard. Standalone scripts, no Nx dependency required.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
18
|
"@inversifyjs/binding-decorators": "1.1.5",
|
|
19
|
-
"@webpieces/ai-hook-rules": "0.4.
|
|
20
|
-
"@webpieces/rules-config": "0.4.
|
|
19
|
+
"@webpieces/ai-hook-rules": "0.4.733",
|
|
20
|
+
"@webpieces/rules-config": "0.4.733",
|
|
21
21
|
"inversify": "7.10.4",
|
|
22
22
|
"reflect-metadata": "0.2.2"
|
|
23
23
|
},
|
|
@@ -184,8 +184,12 @@ let ChecklistCommentRenderer = class ChecklistCommentRenderer {
|
|
|
184
184
|
return 'passed';
|
|
185
185
|
if (row.status === rules_config_1.CK_WARN)
|
|
186
186
|
return 'passed with concerns';
|
|
187
|
-
|
|
188
|
-
|
|
187
|
+
// Names the FILE the authorization lives in, so a reader of the PR can go and check the provenance
|
|
188
|
+
// rather than take "someone approved this" on trust. The reason, who authorized it and when are
|
|
189
|
+
// rendered verbatim in this row's section below.
|
|
190
|
+
if (row.status === rules_config_1.CK_OVERRIDDEN) {
|
|
191
|
+
return `OVERRIDDEN — a human authorized shipping it (recorded in override-${row.subagent}.json)`;
|
|
192
|
+
}
|
|
189
193
|
if (row.status === rules_config_1.CK_FAIL)
|
|
190
194
|
return 'FAILED review';
|
|
191
195
|
if (row.status === rules_config_1.CK_MISSING)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"checklist-comment-renderer.js","sourceRoot":"","sources":["../../../../../../packages/tooling/pr-gate/src/dashboard/checklist-comment-renderer.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n formatFileList, CK_PASS, CK_WARN, CK_OVERRIDDEN, CK_FAIL, CK_MISSING,\n} from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { ChecklistCommentRow } from './checklist-comment-row';\n\n/**\n * Hidden marker on the checklist review COMMENT — the \"2nd comment\" the PR description points at — so\n * wp-finish can find and PATCH its own comment on every push instead of appending a new one. Versioned so\n * the format can evolve without matching an old shape. v2 = the full roster (every DEFINED checklist,\n * matched or not) + tri-state verdicts.\n */\nexport const CHECKLIST_COMMENT_MARKER = '<!-- webpieces-checklists v2 -->';\n\nconst COMMENT_LIMIT = 65000; // under GitHub's 65536-char cap, with headroom for the marker + roll-up.\n\n// One checklist section for the combined comment (heading + verbatim reviewer output), so oversize\n// truncation can shrink the longest BODY without ever dropping a verdict heading.\nclass CommentSection {\n heading: string;\n body: string;\n\n constructor(heading: string, body: string) {\n this.heading = heading;\n this.body = body;\n }\n}\n\n/**\n * Renders the 2nd PR comment: the reviewer checklist — full roster plus each reviewer's verbatim output.\n *\n * Split out of `Dashboard` when the PR-description/commit-body swap pushed that file over the 700-line\n * cap, and the seam was already there to be found: this is ONE of the three surfaces the gated flow\n * writes, it shares no rendering helper with the other two (it speaks `ChecklistCommentRow`, where the\n * dashboard speaks `ChecklistRow`), and it owns the only size-fitting logic in the package. One class per\n * surface is the shape the rest of this change assumes — see `Dashboard.renderPrBody` and\n * `Dashboard.renderDetailComment`.\n *\n * `@injectable(bindingScopeValues.Singleton)` so it is drawn in the DI design and injected by type.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class ChecklistCommentRenderer {\n /**\n * The ONE combined PR comment. Two halves, in this order:\n *\n * 1. A roll-up plus the FULL ROSTER — every DEFINED checklist as a checkbox, each with a sub-bullet\n * stating exactly which globs fired against which files, or which did not and out of how many.\n * Skipped checklists are listed on purpose: skipping is the normal, healthy outcome, and a comment\n * that names only the reviewers that fired cannot distinguish \"evaluated and irrelevant\" from\n * \"never wired up\" — nor answer \"why did the DB reviewer run on my frontend PR?\".\n * 2. One section per reviewer that RAN, carrying its full `output` — the depth a verdict line throws\n * away. Overridden first, then warned, then passed: a reader should meet the exceptions first.\n *\n * Idempotent: keyed by the hidden marker so wp-finish PATCHes this same comment on every push.\n */\n render(\n rows: readonly ChecklistCommentRow[],\n provenanceVerified: boolean,\n baseResolved: boolean,\n ): string {\n const ran = this.ranOrdered(rows);\n const prov = provenanceVerified\n ? '_Each reviewer ran as its own independent subagent, verified from the Claude Code harness._'\n : '_⚠️ Reviewer provenance was NOT verified (no Claude Code session) — treat these as unverified._';\n // The roster lives in the HEADER, never in a section: fitComment only ever shrinks section bodies,\n // so a roster line can never be the thing an oversize comment silently drops.\n const lines: string[] = [CHECKLIST_COMMENT_MARKER, this.rollupHeader(rows, baseResolved)];\n // No reviewer ran ⇒ no provenance claim to make. Printing one either way would attest to nothing.\n if (ran.length > 0) lines.push(prov);\n lines.push('', `### Checklists (all ${rows.length})`);\n for (const row of rows) lines.push(this.rosterBullet(row));\n const header = lines.join('\\n');\n if (ran.length === 0) {\n return `${header}\\n\\n${this.nothingRanNote(rows)}`;\n }\n return this.fitComment(\n `${header}\\n\\n### Reviews that ran`,\n ran.map((r: ChecklistCommentRow): CommentSection => this.commentSection(r)),\n );\n }\n\n /**\n * The closing note when no reviewer produced a verdict. The original wording — \"every configured\n * checklist was evaluated and none of them applied\" — is an all-clear, and it becomes FALSE the moment a\n * checklist did apply and was declined. That sentence under a PR nobody reviewed is precisely the\n * misreport this feature could otherwise introduce, so the declined case gets its own words.\n */\n private nothingRanNote(rows: readonly ChecklistCommentRow[]): string {\n const declined = rows.filter((r: ChecklistCommentRow): boolean => this.declined(r));\n if (declined.length === 0) {\n return '_No reviewer had to run on this diff — every configured checklist was evaluated and none of them applied._';\n }\n return (\n `_No reviewer ran. ${declined.length} OPTIONAL checklist(s) DID apply to this diff and were not ` +\n `run; the rest were evaluated and did not apply._`\n );\n }\n\n // The roll-up line. `baseResolved:false` replaces it entirely: with no fork point the changed-file set is\n // EMPTY, so nothing matched — including patternless ALWAYS-RUNS checklists — and reporting that as\n // \"all skipped ✅\" would post a green all-clear for a PR where nothing was actually evaluated.\n private rollupHeader(rows: readonly ChecklistCommentRow[], baseResolved: boolean): string {\n if (!baseResolved) {\n return (\n `## 🔍 Company review checklists — ⚠️ NOT EVALUATED (${rows.length} defined)\\n` +\n `_No diff base (fork point of main) could be resolved, so no checklist was matched against ` +\n `anything. This is **not** an all-clear._`\n );\n }\n const ran = rows.filter((r: ChecklistCommentRow): boolean => this.reviewerRan(r));\n const declined = rows.filter((r: ChecklistCommentRow): boolean => this.declined(r));\n const skipped = rows.length - ran.length - declined.length;\n const parts: string[] = [];\n for (const pair of this.rollupCounts(ran)) parts.push(pair);\n const breakdown = parts.length > 0 ? ` (${parts.join(' · ')})` : '';\n const skip = skipped > 0 ? ` · ${skipped} skipped ✅` : '';\n // Counted SEPARATELY from \"skipped\", and without a ✅. A declined optional review is a legitimate\n // outcome, but it is not the same good news as a checklist that had nothing to look at — folding the\n // two together would let a PR that declined every optional review read as fully covered.\n const notRun = declined.length > 0 ? ` · ${declined.length} optional not run` : '';\n return `## 🔍 Company review checklists — ${rows.length} defined · ${ran.length} ran${breakdown}${skip}${notRun}`;\n }\n\n // `🟢 2 · 🟡 1` — only the non-zero buckets, so a clean run reads as one number rather than four.\n private rollupCounts(ran: readonly ChecklistCommentRow[]): string[] {\n const counts: string[] = [];\n const emojiFor: string[] = ['🟢', '🟡', '🟠'];\n const statusFor: string[] = [CK_PASS, CK_WARN, CK_OVERRIDDEN];\n statusFor.forEach((status: string, i: number): void => {\n const n = ran.filter((r: ChecklistCommentRow): boolean => r.status === status).length;\n if (n > 0) counts.push(`${emojiFor[i]} ${n}`);\n });\n return counts;\n }\n\n // One roster line + its why sub-bullet. A checked box means a reviewer ran; an unchecked one means the\n // checklist was evaluated and did not apply, which the words state as the good news it is.\n private rosterBullet(row: ChecklistCommentRow): string {\n const box = this.reviewerRan(row) ? '- [x]' : '- [ ]';\n return (\n `${box} ${this.verdictEmoji(row)} **${row.subagent}**${this.optionalTag(row)} — ` +\n `${this.verdictWords(row)}${this.evidenceSuffix(row)}\\n` +\n ` - ${this.whyLine(row)}`\n );\n }\n\n /**\n * Did a reviewer actually produce a verdict for this row?\n *\n * `row.ran` is really \"this checklist APPLIED to the diff\" — for a required checklist the two are the\n * same thing, because the PR cannot open otherwise, and the field was named before optional checklists\n * existed. For a DECLINED optional one they diverge, and using `ran` alone would put a checked box and a\n * reviewer section on a review that nobody performed.\n */\n private reviewerRan(row: ChecklistCommentRow): boolean {\n return row.ran && !this.declined(row);\n }\n\n // Applied, optional, and carrying no verdict — i.e. the human was offered this review and said no (or\n // `--no-optional` skipped the offer). Never true of a required checklist: one of those with no verdict\n // does not reach a PR at all.\n private declined(row: ChecklistCommentRow): boolean {\n return row.ran && !row.required && (row.status === CK_MISSING || row.status === '');\n }\n\n // Marks which rows the human could have declined. Without it a reader cannot tell a review that was\n // skippable from one that simply passed, and so cannot judge how much this PR was actually reviewed.\n private optionalTag(row: ChecklistCommentRow): string {\n return row.required ? '' : ' _(optional)_';\n }\n\n /**\n * Whether the reviewer demonstrably opened the diff, read from its own transcript. A QUALITY signal and\n * never a blocker (see SubagentProvenanceService.evidenceFor) — but published, because \"wrote a verdict\n * without reading the change\" is exactly what a reader of this comment would want to weigh.\n */\n private evidenceSuffix(row: ChecklistCommentRow): string {\n if (!this.reviewerRan(row) || row.diffRead === '') return '';\n return row.diffRead === 'yes' ? ' _(diff read ✓)_' : ' _(⚠️ no diff read recorded)_';\n }\n\n private verdictEmoji(row: ChecklistCommentRow): string {\n if (!this.reviewerRan(row)) return '⚪';\n if (row.status === CK_PASS) return '🟢';\n if (row.status === CK_WARN) return '🟡';\n if (row.status === CK_OVERRIDDEN) return '🟠';\n if (row.status === CK_FAIL) return '🔴';\n return '⚪';\n }\n\n // SHORT words for a roster line / section heading. Short on purpose: the reviewer's own output and any\n // override justification get their own section below, and a roster exists to be scanned.\n private verdictWords(row: ChecklistCommentRow): string {\n // Two different unchecked boxes, two different sentences. \"Not applicable\" is the diff's doing;\n // \"not run\" is a person's, and reporting the second as the first would quietly credit a review that\n // a human deliberately declined.\n if (this.declined(row)) return 'OPTIONAL — applied to this diff but was NOT run (not selected)';\n if (!row.ran) return 'skipped, not applicable to this diff (expected ✅)';\n if (row.status === CK_PASS) return 'passed';\n if (row.status === CK_WARN) return 'passed with concerns';\n if (row.status === CK_OVERRIDDEN) return 'OVERRIDDEN — shipped with a stated justification';\n if (row.status === CK_FAIL) return 'FAILED review';\n if (row.status === CK_MISSING) return 'no verdict written';\n return `unknown verdict (${row.status})`;\n }\n\n /**\n * WHY this checklist ran or did not — the line that answers \"why was this reviewer involved?\". Branches\n * on `configuredPatterns`, NEVER on `firedPatterns.length`: a patternless checklist and a skipped one\n * both fired zero globs and they mean opposite things, so keying off the fired list would tell every\n * skipped checklist's reader that the whole diff had been in its scope.\n */\n private whyLine(row: ChecklistCommentRow): string {\n const total = row.changedFileCount;\n if (row.configuredPatterns.length === 0) {\n // State the fact, not a suspicion. Patternless is a deliberate configuration — an always-runs\n // gate (every PR names a ticket, every PR has an owner) is exactly what it is FOR — so telling\n // every such row to \"add `patterns` if that is not intended\" nags the repos that meant it, on\n // every PR, forever. A reader who wants to know whether it was intended can read the config.\n return (\n `ALWAYS RUNS (no patterns) — whole diff in scope, ${total} changed file(s): ` +\n `${formatFileList(row.matchedFiles)}`\n );\n }\n const configured = this.asCode(row.configuredPatterns);\n if (row.firedPatterns.length === 0) {\n return `${configured} matched 0 of ${total} changed file(s)`;\n }\n return (\n `matched ${this.asCode(row.firedPatterns)} → ${row.matchedFiles.length} of ${total} ` +\n `changed file(s): ${formatFileList(row.matchedFiles)}`\n );\n }\n\n private asCode(patterns: readonly string[]): string {\n return patterns.map((p: string): string => `\\`${p}\\``).join(', ');\n }\n\n // Reviewers that ran, exceptions first (overridden → warned → passed) so a reader meets what needs\n // attention before a wall of green.\n private ranOrdered(rows: readonly ChecklistCommentRow[]): ChecklistCommentRow[] {\n const rank: string[] = [CK_OVERRIDDEN, CK_WARN, CK_PASS];\n return rows\n .filter((r: ChecklistCommentRow): boolean => this.reviewerRan(r))\n .slice()\n .sort(\n (a: ChecklistCommentRow, b: ChecklistCommentRow): number =>\n this.rankOf(rank, a.status) - this.rankOf(rank, b.status),\n );\n }\n\n private rankOf(rank: readonly string[], status: string): number {\n const idx = rank.indexOf(status);\n return idx < 0 ? rank.length : idx;\n }\n\n private commentSection(row: ChecklistCommentRow): CommentSection {\n const heading = `#### ${this.verdictEmoji(row)} ${row.subagent} — ${this.verdictWords(row)}`;\n const body =\n row.detail.trim() !== '' ? row.detail.trim() : '_(reviewer recorded no output)_';\n return new CommentSection(heading, body);\n }\n\n // Keep the comment under GitHub's size cap by shrinking the LONGEST section body first (so a short\n // overridden note is never cut to make room for a long passing one), never dropping a verdict heading.\n private fitComment(header: string, sections: CommentSection[]): string {\n const assemble = (): string =>\n `${header}\\n\\n${sections.map((s: CommentSection): string => `${s.heading}\\n\\n${s.body}`).join('\\n\\n')}`;\n const trunc = '\\n\\n…_[truncated to fit the GitHub comment size limit]_';\n let out = assemble();\n while (out.length > COMMENT_LIMIT) {\n const idx = this.longestBodyIndex(sections);\n if (idx < 0 || sections[idx].body.length <= trunc.length + 1) break;\n const over = out.length - COMMENT_LIMIT;\n const keep = Math.max(0, sections[idx].body.length - over - trunc.length - 8);\n sections[idx].body = sections[idx].body.slice(0, keep).trimEnd() + trunc;\n out = assemble();\n }\n return out;\n }\n\n private longestBodyIndex(sections: readonly CommentSection[]): number {\n let idx = -1;\n let max = -1;\n sections.forEach((s: CommentSection, i: number): void => {\n if (s.body.length > max) {\n max = s.body.length;\n idx = i;\n }\n });\n return idx;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"checklist-comment-renderer.js","sourceRoot":"","sources":["../../../../../../packages/tooling/pr-gate/src/dashboard/checklist-comment-renderer.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n formatFileList, CK_PASS, CK_WARN, CK_OVERRIDDEN, CK_FAIL, CK_MISSING,\n} from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { ChecklistCommentRow } from './checklist-comment-row';\n\n/**\n * Hidden marker on the checklist review COMMENT — the \"2nd comment\" the PR description points at — so\n * wp-finish can find and PATCH its own comment on every push instead of appending a new one. Versioned so\n * the format can evolve without matching an old shape. v2 = the full roster (every DEFINED checklist,\n * matched or not) + tri-state verdicts.\n */\nexport const CHECKLIST_COMMENT_MARKER = '<!-- webpieces-checklists v2 -->';\n\nconst COMMENT_LIMIT = 65000; // under GitHub's 65536-char cap, with headroom for the marker + roll-up.\n\n// One checklist section for the combined comment (heading + verbatim reviewer output), so oversize\n// truncation can shrink the longest BODY without ever dropping a verdict heading.\nclass CommentSection {\n heading: string;\n body: string;\n\n constructor(heading: string, body: string) {\n this.heading = heading;\n this.body = body;\n }\n}\n\n/**\n * Renders the 2nd PR comment: the reviewer checklist — full roster plus each reviewer's verbatim output.\n *\n * Split out of `Dashboard` when the PR-description/commit-body swap pushed that file over the 700-line\n * cap, and the seam was already there to be found: this is ONE of the three surfaces the gated flow\n * writes, it shares no rendering helper with the other two (it speaks `ChecklistCommentRow`, where the\n * dashboard speaks `ChecklistRow`), and it owns the only size-fitting logic in the package. One class per\n * surface is the shape the rest of this change assumes — see `Dashboard.renderPrBody` and\n * `Dashboard.renderDetailComment`.\n *\n * `@injectable(bindingScopeValues.Singleton)` so it is drawn in the DI design and injected by type.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class ChecklistCommentRenderer {\n /**\n * The ONE combined PR comment. Two halves, in this order:\n *\n * 1. A roll-up plus the FULL ROSTER — every DEFINED checklist as a checkbox, each with a sub-bullet\n * stating exactly which globs fired against which files, or which did not and out of how many.\n * Skipped checklists are listed on purpose: skipping is the normal, healthy outcome, and a comment\n * that names only the reviewers that fired cannot distinguish \"evaluated and irrelevant\" from\n * \"never wired up\" — nor answer \"why did the DB reviewer run on my frontend PR?\".\n * 2. One section per reviewer that RAN, carrying its full `output` — the depth a verdict line throws\n * away. Overridden first, then warned, then passed: a reader should meet the exceptions first.\n *\n * Idempotent: keyed by the hidden marker so wp-finish PATCHes this same comment on every push.\n */\n render(\n rows: readonly ChecklistCommentRow[],\n provenanceVerified: boolean,\n baseResolved: boolean,\n ): string {\n const ran = this.ranOrdered(rows);\n const prov = provenanceVerified\n ? '_Each reviewer ran as its own independent subagent, verified from the Claude Code harness._'\n : '_⚠️ Reviewer provenance was NOT verified (no Claude Code session) — treat these as unverified._';\n // The roster lives in the HEADER, never in a section: fitComment only ever shrinks section bodies,\n // so a roster line can never be the thing an oversize comment silently drops.\n const lines: string[] = [CHECKLIST_COMMENT_MARKER, this.rollupHeader(rows, baseResolved)];\n // No reviewer ran ⇒ no provenance claim to make. Printing one either way would attest to nothing.\n if (ran.length > 0) lines.push(prov);\n lines.push('', `### Checklists (all ${rows.length})`);\n for (const row of rows) lines.push(this.rosterBullet(row));\n const header = lines.join('\\n');\n if (ran.length === 0) {\n return `${header}\\n\\n${this.nothingRanNote(rows)}`;\n }\n return this.fitComment(\n `${header}\\n\\n### Reviews that ran`,\n ran.map((r: ChecklistCommentRow): CommentSection => this.commentSection(r)),\n );\n }\n\n /**\n * The closing note when no reviewer produced a verdict. The original wording — \"every configured\n * checklist was evaluated and none of them applied\" — is an all-clear, and it becomes FALSE the moment a\n * checklist did apply and was declined. That sentence under a PR nobody reviewed is precisely the\n * misreport this feature could otherwise introduce, so the declined case gets its own words.\n */\n private nothingRanNote(rows: readonly ChecklistCommentRow[]): string {\n const declined = rows.filter((r: ChecklistCommentRow): boolean => this.declined(r));\n if (declined.length === 0) {\n return '_No reviewer had to run on this diff — every configured checklist was evaluated and none of them applied._';\n }\n return (\n `_No reviewer ran. ${declined.length} OPTIONAL checklist(s) DID apply to this diff and were not ` +\n `run; the rest were evaluated and did not apply._`\n );\n }\n\n // The roll-up line. `baseResolved:false` replaces it entirely: with no fork point the changed-file set is\n // EMPTY, so nothing matched — including patternless ALWAYS-RUNS checklists — and reporting that as\n // \"all skipped ✅\" would post a green all-clear for a PR where nothing was actually evaluated.\n private rollupHeader(rows: readonly ChecklistCommentRow[], baseResolved: boolean): string {\n if (!baseResolved) {\n return (\n `## 🔍 Company review checklists — ⚠️ NOT EVALUATED (${rows.length} defined)\\n` +\n `_No diff base (fork point of main) could be resolved, so no checklist was matched against ` +\n `anything. This is **not** an all-clear._`\n );\n }\n const ran = rows.filter((r: ChecklistCommentRow): boolean => this.reviewerRan(r));\n const declined = rows.filter((r: ChecklistCommentRow): boolean => this.declined(r));\n const skipped = rows.length - ran.length - declined.length;\n const parts: string[] = [];\n for (const pair of this.rollupCounts(ran)) parts.push(pair);\n const breakdown = parts.length > 0 ? ` (${parts.join(' · ')})` : '';\n const skip = skipped > 0 ? ` · ${skipped} skipped ✅` : '';\n // Counted SEPARATELY from \"skipped\", and without a ✅. A declined optional review is a legitimate\n // outcome, but it is not the same good news as a checklist that had nothing to look at — folding the\n // two together would let a PR that declined every optional review read as fully covered.\n const notRun = declined.length > 0 ? ` · ${declined.length} optional not run` : '';\n return `## 🔍 Company review checklists — ${rows.length} defined · ${ran.length} ran${breakdown}${skip}${notRun}`;\n }\n\n // `🟢 2 · 🟡 1` — only the non-zero buckets, so a clean run reads as one number rather than four.\n private rollupCounts(ran: readonly ChecklistCommentRow[]): string[] {\n const counts: string[] = [];\n const emojiFor: string[] = ['🟢', '🟡', '🟠'];\n const statusFor: string[] = [CK_PASS, CK_WARN, CK_OVERRIDDEN];\n statusFor.forEach((status: string, i: number): void => {\n const n = ran.filter((r: ChecklistCommentRow): boolean => r.status === status).length;\n if (n > 0) counts.push(`${emojiFor[i]} ${n}`);\n });\n return counts;\n }\n\n // One roster line + its why sub-bullet. A checked box means a reviewer ran; an unchecked one means the\n // checklist was evaluated and did not apply, which the words state as the good news it is.\n private rosterBullet(row: ChecklistCommentRow): string {\n const box = this.reviewerRan(row) ? '- [x]' : '- [ ]';\n return (\n `${box} ${this.verdictEmoji(row)} **${row.subagent}**${this.optionalTag(row)} — ` +\n `${this.verdictWords(row)}${this.evidenceSuffix(row)}\\n` +\n ` - ${this.whyLine(row)}`\n );\n }\n\n /**\n * Did a reviewer actually produce a verdict for this row?\n *\n * `row.ran` is really \"this checklist APPLIED to the diff\" — for a required checklist the two are the\n * same thing, because the PR cannot open otherwise, and the field was named before optional checklists\n * existed. For a DECLINED optional one they diverge, and using `ran` alone would put a checked box and a\n * reviewer section on a review that nobody performed.\n */\n private reviewerRan(row: ChecklistCommentRow): boolean {\n return row.ran && !this.declined(row);\n }\n\n // Applied, optional, and carrying no verdict — i.e. the human was offered this review and said no (or\n // `--no-optional` skipped the offer). Never true of a required checklist: one of those with no verdict\n // does not reach a PR at all.\n private declined(row: ChecklistCommentRow): boolean {\n return row.ran && !row.required && (row.status === CK_MISSING || row.status === '');\n }\n\n // Marks which rows the human could have declined. Without it a reader cannot tell a review that was\n // skippable from one that simply passed, and so cannot judge how much this PR was actually reviewed.\n private optionalTag(row: ChecklistCommentRow): string {\n return row.required ? '' : ' _(optional)_';\n }\n\n /**\n * Whether the reviewer demonstrably opened the diff, read from its own transcript. A QUALITY signal and\n * never a blocker (see SubagentProvenanceService.evidenceFor) — but published, because \"wrote a verdict\n * without reading the change\" is exactly what a reader of this comment would want to weigh.\n */\n private evidenceSuffix(row: ChecklistCommentRow): string {\n if (!this.reviewerRan(row) || row.diffRead === '') return '';\n return row.diffRead === 'yes' ? ' _(diff read ✓)_' : ' _(⚠️ no diff read recorded)_';\n }\n\n private verdictEmoji(row: ChecklistCommentRow): string {\n if (!this.reviewerRan(row)) return '⚪';\n if (row.status === CK_PASS) return '🟢';\n if (row.status === CK_WARN) return '🟡';\n if (row.status === CK_OVERRIDDEN) return '🟠';\n if (row.status === CK_FAIL) return '🔴';\n return '⚪';\n }\n\n // SHORT words for a roster line / section heading. Short on purpose: the reviewer's own output and any\n // override justification get their own section below, and a roster exists to be scanned.\n private verdictWords(row: ChecklistCommentRow): string {\n // Two different unchecked boxes, two different sentences. \"Not applicable\" is the diff's doing;\n // \"not run\" is a person's, and reporting the second as the first would quietly credit a review that\n // a human deliberately declined.\n if (this.declined(row)) return 'OPTIONAL — applied to this diff but was NOT run (not selected)';\n if (!row.ran) return 'skipped, not applicable to this diff (expected ✅)';\n if (row.status === CK_PASS) return 'passed';\n if (row.status === CK_WARN) return 'passed with concerns';\n // Names the FILE the authorization lives in, so a reader of the PR can go and check the provenance\n // rather than take \"someone approved this\" on trust. The reason, who authorized it and when are\n // rendered verbatim in this row's section below.\n if (row.status === CK_OVERRIDDEN) {\n return `OVERRIDDEN — a human authorized shipping it (recorded in override-${row.subagent}.json)`;\n }\n if (row.status === CK_FAIL) return 'FAILED review';\n if (row.status === CK_MISSING) return 'no verdict written';\n return `unknown verdict (${row.status})`;\n }\n\n /**\n * WHY this checklist ran or did not — the line that answers \"why was this reviewer involved?\". Branches\n * on `configuredPatterns`, NEVER on `firedPatterns.length`: a patternless checklist and a skipped one\n * both fired zero globs and they mean opposite things, so keying off the fired list would tell every\n * skipped checklist's reader that the whole diff had been in its scope.\n */\n private whyLine(row: ChecklistCommentRow): string {\n const total = row.changedFileCount;\n if (row.configuredPatterns.length === 0) {\n // State the fact, not a suspicion. Patternless is a deliberate configuration — an always-runs\n // gate (every PR names a ticket, every PR has an owner) is exactly what it is FOR — so telling\n // every such row to \"add `patterns` if that is not intended\" nags the repos that meant it, on\n // every PR, forever. A reader who wants to know whether it was intended can read the config.\n return (\n `ALWAYS RUNS (no patterns) — whole diff in scope, ${total} changed file(s): ` +\n `${formatFileList(row.matchedFiles)}`\n );\n }\n const configured = this.asCode(row.configuredPatterns);\n if (row.firedPatterns.length === 0) {\n return `${configured} matched 0 of ${total} changed file(s)`;\n }\n return (\n `matched ${this.asCode(row.firedPatterns)} → ${row.matchedFiles.length} of ${total} ` +\n `changed file(s): ${formatFileList(row.matchedFiles)}`\n );\n }\n\n private asCode(patterns: readonly string[]): string {\n return patterns.map((p: string): string => `\\`${p}\\``).join(', ');\n }\n\n // Reviewers that ran, exceptions first (overridden → warned → passed) so a reader meets what needs\n // attention before a wall of green.\n private ranOrdered(rows: readonly ChecklistCommentRow[]): ChecklistCommentRow[] {\n const rank: string[] = [CK_OVERRIDDEN, CK_WARN, CK_PASS];\n return rows\n .filter((r: ChecklistCommentRow): boolean => this.reviewerRan(r))\n .slice()\n .sort(\n (a: ChecklistCommentRow, b: ChecklistCommentRow): number =>\n this.rankOf(rank, a.status) - this.rankOf(rank, b.status),\n );\n }\n\n private rankOf(rank: readonly string[], status: string): number {\n const idx = rank.indexOf(status);\n return idx < 0 ? rank.length : idx;\n }\n\n private commentSection(row: ChecklistCommentRow): CommentSection {\n const heading = `#### ${this.verdictEmoji(row)} ${row.subagent} — ${this.verdictWords(row)}`;\n const body =\n row.detail.trim() !== '' ? row.detail.trim() : '_(reviewer recorded no output)_';\n return new CommentSection(heading, body);\n }\n\n // Keep the comment under GitHub's size cap by shrinking the LONGEST section body first (so a short\n // overridden note is never cut to make room for a long passing one), never dropping a verdict heading.\n private fitComment(header: string, sections: CommentSection[]): string {\n const assemble = (): string =>\n `${header}\\n\\n${sections.map((s: CommentSection): string => `${s.heading}\\n\\n${s.body}`).join('\\n\\n')}`;\n const trunc = '\\n\\n…_[truncated to fit the GitHub comment size limit]_';\n let out = assemble();\n while (out.length > COMMENT_LIMIT) {\n const idx = this.longestBodyIndex(sections);\n if (idx < 0 || sections[idx].body.length <= trunc.length + 1) break;\n const over = out.length - COMMENT_LIMIT;\n const keep = Math.max(0, sections[idx].body.length - over - trunc.length - 8);\n sections[idx].body = sections[idx].body.slice(0, keep).trimEnd() + trunc;\n out = assemble();\n }\n return out;\n }\n\n private longestBodyIndex(sections: readonly CommentSection[]): number {\n let idx = -1;\n let max = -1;\n sections.forEach((s: CommentSection, i: number): void => {\n if (s.body.length > max) {\n max = s.body.length;\n idx = i;\n }\n });\n return idx;\n }\n}\n"]}
|
|
@@ -134,8 +134,12 @@ export declare class ReviewUpsertPrCommand {
|
|
|
134
134
|
*
|
|
135
135
|
* NO archive path is passed, deliberately, and this stage moves nothing. Retiring a verdict is finish's
|
|
136
136
|
* act on the refusal it is actually enforcing; doing it here would delete the live verdict of a branch
|
|
137
|
-
* that has not even been asked to finish yet, and `refusalError`'s un-archived wording — "
|
|
138
|
-
*
|
|
137
|
+
* that has not even been asked to finish yet, and `refusalError`'s un-archived wording — "fix it, then
|
|
138
|
+
* re-run" — is only correct while that verdict file is still there, which here it is.
|
|
139
|
+
*
|
|
140
|
+
* The review path IS passed, because the ship-anyway route the refusal prints is a command that writes
|
|
141
|
+
* `override-<id>.json` beside it. That command is as correct at this stage as at finish: the override
|
|
142
|
+
* is a separate file with its own writer, so nothing about it depends on the verdict having been moved.
|
|
139
143
|
*/
|
|
140
144
|
private refusals;
|
|
141
145
|
}
|
|
@@ -236,12 +236,16 @@ let ReviewUpsertPrCommand = class ReviewUpsertPrCommand {
|
|
|
236
236
|
*
|
|
237
237
|
* NO archive path is passed, deliberately, and this stage moves nothing. Retiring a verdict is finish's
|
|
238
238
|
* act on the refusal it is actually enforcing; doing it here would delete the live verdict of a branch
|
|
239
|
-
* that has not even been asked to finish yet, and `refusalError`'s un-archived wording — "
|
|
240
|
-
*
|
|
239
|
+
* that has not even been asked to finish yet, and `refusalError`'s un-archived wording — "fix it, then
|
|
240
|
+
* re-run" — is only correct while that verdict file is still there, which here it is.
|
|
241
|
+
*
|
|
242
|
+
* The review path IS passed, because the ship-anyway route the refusal prints is a command that writes
|
|
243
|
+
* `override-<id>.json` beside it. That command is as correct at this stage as at finish: the override
|
|
244
|
+
* is a separate file with its own writer, so nothing about it depends on the verdict having been moved.
|
|
241
245
|
*/
|
|
242
246
|
refusals(scan) {
|
|
243
247
|
const refused = this.reviewJsonService.refusedChecklists(scan.applicable, scan.results);
|
|
244
|
-
return refused.map((req) => new review_report_1.RefusedReviewer(req.id, this.reviewJsonService.refusalError(req, this.reviewJsonService.resolveVerdict(req, scan.results))));
|
|
248
|
+
return refused.map((req) => new review_report_1.RefusedReviewer(req.id, this.reviewJsonService.refusalError(req, this.reviewJsonService.resolveVerdict(req, scan.results), scan.reviewPath)));
|
|
245
249
|
}
|
|
246
250
|
};
|
|
247
251
|
exports.ReviewUpsertPrCommand = ReviewUpsertPrCommand;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"review-upsert-pr-command.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/commands/review-upsert-pr-command.ts"],"names":[],"mappings":";;;;AAAA,+CAAyB;AACzB,0DAGiC;AACjC,yCAA2D;AAC3D,+DAA4E;AAC5E,2EAAgE;AAChE,+DAA6E;AAC7E,+DAA0D;AAC1D,yEAAoE;AACpE,qEAAsG;AACtG,qEAAiE;AACjE,mDAA+C;AAC/C,yDAAuD;AACvD,qDAAkE;AAClE,yDAAqD;AACrD,qEAAgE;AAChE,qFAAgF;AAChF,6DAA6F;AAC7F,2EAAiG;AACjG,mEAAkF;AAElF,MAAM,GAAG,GAAG,0DAA0D,CAAC;AAEvE,kGAAkG;AAClG,MAAa,qBAAqB;IAC9B;;;;;OAKG;IACH,YAAY,CAAU;IAEtB,YAAY,YAAY,GAAG,KAAK;QAC5B,IAAI,CAAC,YAAY,GAAG,YAAY,CAAC;IACrC,CAAC;CACJ;AAZD,sDAYC;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEI,IAAM,qBAAqB,GAA3B,MAAM,qBAAqB;IAGT;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IAEA;IACA;IAnBrB,yDAAyD;IACzD,YACqB,cAA8B,EAC9B,YAA0B,EAC1B,OAAgB,EAChB,aAA4B,EAC5B,iBAAoC,EACpC,UAAsB,EACtB,QAAkB,EAClB,gBAAkC,EAClC,YAA0B,EAC1B,YAA8B,EAC9B,eAAgC,EAChC,eAAwC,EACxC,oBAAiD,EACjD,QAAmC,EACnC,iBAAoC;IACrD,oGAAoG;IACnF,iBAAoC,EACpC,YAA4B;QAjB5B,mBAAc,GAAd,cAAc,CAAgB;QAC9B,iBAAY,GAAZ,YAAY,CAAc;QAC1B,YAAO,GAAP,OAAO,CAAS;QAChB,kBAAa,GAAb,aAAa,CAAe;QAC5B,sBAAiB,GAAjB,iBAAiB,CAAmB;QACpC,eAAU,GAAV,UAAU,CAAY;QACtB,aAAQ,GAAR,QAAQ,CAAU;QAClB,qBAAgB,GAAhB,gBAAgB,CAAkB;QAClC,iBAAY,GAAZ,YAAY,CAAc;QAC1B,iBAAY,GAAZ,YAAY,CAAkB;QAC9B,oBAAe,GAAf,eAAe,CAAiB;QAChC,oBAAe,GAAf,eAAe,CAAyB;QACxC,yBAAoB,GAApB,oBAAoB,CAA6B;QACjD,aAAQ,GAAR,QAAQ,CAA2B;QACnC,sBAAiB,GAAjB,iBAAiB,CAAmB;QAEpC,sBAAiB,GAAjB,iBAAiB,CAAmB;QACpC,iBAAY,GAAZ,YAAY,CAAgB;IAC9C,CAAC;IAEJ;;;;;;;;;;OAUG;IACH,KAAK,CAAC,GAAG,CAAC,OAA8B,IAAI,qBAAqB,EAAE;QAC/D,MAAM,QAAQ,GAAG,IAAI,CAAC,cAAc,CAAC,eAAe,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;QACpE,MAAM,IAAI,CAAC,YAAY,CAAC,WAAW,CAC/B,QAAQ,EAAE,qCAAkB,EAAE,GAAkB,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC,CAAC;IAC1F,CAAC;IAEO,KAAK,CAAC,QAAQ,CAAC,QAAgB,EAAE,IAA2B;QAChE,IAAA,4BAAa,EAAC,QAAQ,EAAE,2BAA2B,CAAC,CAAC;QACrD,IAAA,4BAAa,EAAC,QAAQ,EAAE,gCAAgC,CAAC,CAAC;QAC1D,MAAM,WAAW,GAAG,IAAI,CAAC,YAAY,CAAC,cAAc,EAAE,CAAC;QACvD,MAAM,MAAM,GAAG,IAAA,8BAAe,EAAC,QAAQ,CAAC,CAAC,MAAM,CAAC;QAEhD,MAAM,cAAc,GAAG,MAAM,IAAI,CAAC,0BAA0B,CAAC,QAAQ,CAAC,CAAC;QACvE,IAAI,CAAC,OAAO,CAAC,eAAe,CAAC,QAAQ,CAAC,CAAC;QACvC,MAAM,aAAa,GAAG,MAAM,IAAI,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC;QAExD,MAAM,IAAI,GAAG,IAAI,CAAC,gBAAgB,CAAC,IAAI,CAAC,QAAQ,EAAE,MAAM,CAAC,UAAU,EAAE,IAAI,wCAAoB,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,CAAC,CAAE,mEAAmE;QAC/K,MAAM,SAAS,GAAG,IAAI,CAAC,cAAc,CAAC,QAAQ,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC;QAC3E,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,QAAQ,EAAE,WAAW,EAAE,IAAI,yCAAkB,CAC7D,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,cAAc,EAAE,IAAI,CAAC,aAAa,CAAC,mBAAmB,CAAC,QAAQ,CAAC,EAAE,aAAa,EACnG,SAAS,CAAC,GAAG,CAAC,CAAC,CAAmB,EAAU,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,CAC7D,CAAC,CAAC;QACH,IAAI,CAAC,mBAAmB,CAAC,QAAQ,CAAC,CAAC;QACnC,IAAI,CAAC,MAAM,CAAC,QAAQ,EAAE,WAAW,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,CAAC,CAAC;IAC9D,CAAC;IAED;;;;;;;OAOG;IACK,mBAAmB,CAAC,QAAgB;QACxC,MAAM,OAAO,GAAkB,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACrE,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,iBAAiB,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC;IACjE,CAAC;IAED;;;;;;;;;;;OAWG;IACK,KAAK,CAAC,0BAA0B,CAAC,QAAgB;QACrD,MAAM,IAAI,GAAG,IAAI,CAAC,UAAU,CAAC,WAAW,CAAC,QAAQ,EAAE,IAAI,CAAC,YAAY,CAAC,cAAc,EAAE,CAAC,CAAC;QACvF,MAAM,SAAS,GAAG,IAAI,CAAC,UAAU,CAAC,qBAAqB,CAAC,IAAI,CAAC,CAAC;QAC9D,MAAM,MAAM,GAAG,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,eAAe,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;QAC7E,IAAI,CAAC,SAAS,IAAI,CAAC,MAAM,IAAI,MAAM,CAAC,SAAS;YAAE,OAAO,IAAI,CAAC;QAC3D,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,GAAG,GAAG,GAAG,mCAAmC,GAAG,GAAG,GAAG,IAAI,CAAC,CAAC;QACpF,wDAAwD;QACxD,MAAM,IAAI,CAAC,QAAQ,CAAC,QAAQ,CACxB,QAAQ,EAAE,qBAAqB,EAAE,SAAS,EAC1C,IAAI,0BAAY,CAAC,MAAM,CAAC,aAAa,EAAE,MAAM,CAAC,YAAY,EAAE,MAAM,CAAC,YAAY,EAAE,MAAM,CAAC,QAAQ,CAAC,EACjG,IAAI,2BAAe,CAAC,MAAM,CAAC,eAAe,EAAE,KAAK,CAAC,CACrD,CAAC;QACF,OAAO,IAAI,CAAC;IAChB,CAAC;IAED;;;;OAIG;IACK,KAAK,CAAC,YAAY,CAAC,QAAgB;QACvC,MAAM,IAAI,CAAC,aAAa,CAAC,YAAY,CAAC,QAAQ,EAAE,IAAI,iCAAgB,CAChE,iBAAiB,EACjB,0BAA0B,EAC1B,wFAAwF,EACxF,6BAAY,CACf,CAAC,CAAC;QACH,+FAA+F;QAC/F,6FAA6F;QAC7F,gFAAgF;QAChF,4FAA4F;QAC5F,IAAI,CAAC,iBAAiB,CAAC,iCAAiC,CAAC,QAAQ,CAAC,CAAC;QACnE,OAAO,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IACpC,CAAC;IAED;;;;;OAKG;IACK,cAAc,CAAC,QAAgB,EAAE,WAAmB,EAAE,IAAmB,EAAE,MAAoB;QACnG,IAAI,IAAI,CAAC,KAAK,CAAC,UAAU;YAAE,OAAO,EAAE,CAAC;QACrC,MAAM,QAAQ,GAAG,IAAI,CAAC,YAAY,CAAC,WAAW,CAC1C,QAAQ,EAAE,WAAW,EAAE,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,YAAY,EAAE,MAAM,CAAC,iBAAiB,CAAC,CAAC;QACpF,MAAM,OAAO,GAAG,IAAI,CAAC,YAAY,CAAC,UAAU,CAAC,QAAQ,EAAE,WAAW,CAAC,CAAC;QACpE,iGAAiG;QACjG,oGAAoG;QACpG,gGAAgG;QAChG,iGAAiG;QACjG,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,eAAe,CAAC,MAAM,CACtC,QAAQ,EAAE,WAAW,EAAE,IAAI,CAAC,KAAK,EAAE,eAAe,EAAE,IAAI,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC;QACpF,MAAM,SAAS,GAAG,IAAI,CAAC,eAAe,CAAC,KAAK,CAAC,QAAQ,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC;QACxF,MAAM,GAAG,GAAG,IAAI,CAAC,oBAAoB,CAAC,kBAAkB,CAAC,QAAQ,EAAE,WAAW,CAAC,CAAC;QAChF,EAAE,CAAC,MAAM,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,yDAAyD;QAC3G,EAAE,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACvC,KAAK,MAAM,CAAC,IAAI,SAAS,EAAE,CAAC;YACxB,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,oBAAoB,CAAC,OAAO,CAAC,QAAQ,EAAE,WAAW,EAAE,CAAC,CAAC,QAAQ,CAAC,EACjF,IAAI,CAAC,oBAAoB,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;QAC7C,CAAC;QACD,OAAO,SAAS,CAAC;IACrB,CAAC;IAED;;;;;;;OAOG;IACH,yDAAyD;IACjD,MAAM,CACV,QAAgB,EAAE,WAAmB,EAAE,IAAmB,EAAE,SAAsC,EAClG,IAA2B;QAE3B,MAAM,KAAK,GAAG,IAAI,iCAAiB,CAAC,QAAQ,EAAE,WAAW,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC;QAC5E,KAAK,CAAC,YAAY,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC;QACzC,KAAK,CAAC,eAAe,GAAG,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC;QAC/C,KAAK,CAAC,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE,CAAC;QACvC,KAAK,CAAC,YAAY,GAAG,IAAI,CAAC,YAAY,CAAC,KAAK,EAAE,CAAC;QAC/C,KAAK,CAAC,SAAS,GAAG,SAAS,CAAC,KAAK,EAAE,CAAC;QACpC,KAAK,CAAC,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;QACpC,KAAK,CAAC,YAAY,GAAG,IAAI,CAAC,YAAY,CAAC;QACvC,+FAA+F;QAC/F,gGAAgG;QAChG,kEAAkE;QAClE,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;IAC3D,CAAC;IAED;;;;;;;;;;;;;OAaG;IACK,QAAQ,CAAC,IAAmB;QAChC,MAAM,OAAO,GAAG,IAAI,CAAC,iBAAiB,CAAC,iBAAiB,CAAC,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;QACxF,OAAO,OAAO,CAAC,GAAG,CAAC,CAAC,GAAsB,EAAmB,EAAE,CAAC,IAAI,+BAAe,CAC/E,GAAG,CAAC,EAAE,EAAE,IAAI,CAAC,iBAAiB,CAAC,YAAY,CAAC,GAAG,EAAE,IAAI,CAAC,iBAAiB,CAAC,cAAc,CAAC,GAAG,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;IACrH,CAAC;CACJ,CAAA;AAlMY,sDAAqB;gCAArB,qBAAqB;IADjC,IAAA,sBAAU,EAAC,8BAAkB,CAAC,SAAS,CAAC;6CAIA,6BAAc;QAChB,mCAAY;QACjB,kBAAO;QACD,8BAAa;QACT,uCAAiB;QACxB,wBAAU;QACZ,oBAAQ;QACA,oCAAgB;QACpB,4BAAY;QACZ,oCAAgB;QACb,mCAAe;QACf,mDAAuB;QAClB,0CAA2B;QACvC,gDAAyB;QAChB,kCAAiB;QAEjB,gCAAiB;QACtB,iCAAc;GApBxC,qBAAqB,CAkMjC","sourcesContent":["import * as fs from 'fs';\nimport {\n loadAndValidate, writeTemplate, PrGateConfig, RepoRootFinder, RequiredChecklist,\n ReviewerBriefing, ReviewerInstructionsService, ReviewJsonService,\n} from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { ActiveHatch, ActiveHatchReport } from '../workflow/active-hatches';\nimport { AiBranchName } from '../workflow/git-readAiBranchName';\nimport { BuildAffected, BuildGateOptions } from '../workflow/build-affected';\nimport { REVIEW_STAGE } from '../workflow/build-gate-log';\nimport { BuildArtifactGate } from '../workflow/build-artifact-gate';\nimport { ChecklistScan, ChecklistScanOptions, ChecklistScanner } from '../workflow/checklist-scanner';\nimport { DiffMaterializer } from '../workflow/diff-materializer';\nimport { GitExec } from '../workflow/git-exec';\nimport { MergeContext } from '../workflow/merge-start';\nimport { MergeEnd, MergeEndOptions } from '../workflow/merge-end';\nimport { MergeState } from '../workflow/merge-state';\nimport { PrContextWriter } from '../workflow/pr-context-writer';\nimport { ReviewerBriefingBuilder } from '../workflow/reviewer-briefing-builder';\nimport { RefusedReviewer, ReviewReport, ReviewReportInput } from '../workflow/review-report';\nimport { ReviewStageReceipt, ReviewStageReceiptService } from '../workflow/review-stage-receipt';\nimport { StageOutputLog, REVIEW_CONSOLE_LOG } from '../workflow/stage-output-log';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n/** The `--no-optional` decision, as data (per CLAUDE.md) rather than a bare boolean parameter. */\nexport class ReviewUpsertPrOptions {\n /**\n * true ⇒ the human ALREADY said to submit without the optional reviews, so stage ② must not print the\n * block telling the AI to offer them. It changes nothing about what BLOCKS: required checklists are\n * unaffected, and an optional checklist that already has a red verdict on this branch still refuses the\n * PR at finish. This only suppresses the offer.\n */\n skipOptional: boolean;\n\n constructor(skipOptional = false) {\n this.skipOptional = skipOptional;\n }\n}\n\n/**\n * `wp-review-upsert-pr` — STAGE ② of the three-stage PR flow, between `wp-start-upsert-pr` (sync from main)\n * and `wp-finish-upsert-pr` (post the PR).\n *\n * It exists because reviewing came before verifying. Under the old two-command flow the 3-point merge was\n * finalized and the build gate ran inside `wp-finish-upsert-pr` — i.e. AFTER the reviewers had already run.\n * A reviewer could therefore spend a full review on an unresolved merge, or on code that does not compile,\n * and only afterwards would the branch be checked. This command puts the verification first:\n *\n * 1. finalize + VALIDATE any in-progress 3-point merge (moved here from finish)\n * 2. assert a clean tree (meaningful now — step 1 committed the resolution)\n * 3. run the build gate (on the merged, committed tree)\n * 4. scan the checklists, EXTRACT the diff, BRIEF the reviewers\n * 5. write the stage receipt, then print what to spawn + the review.json schema\n *\n * Unlike the report-only command it replaces — which always exited 0, because reporting is not gating — this\n * command CAN FAIL, at steps 1, 2 and 3. That is the point: it fails BEFORE any reviewer is spawned, so a\n * branch with an unresolved merge or a broken build costs zero reviewer tokens.\n *\n * The ordering of 1 → 2 → 3 is load-bearing. MergeEnd stages and commits the conflict resolution, so the\n * tree is clean by the time the build runs; building first would either build a dirty tree (proving nothing\n * about what ships) or refuse a tree that is legitimately mid-resolution.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class ReviewUpsertPrCommand {\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(\n private readonly repoRootFinder: RepoRootFinder,\n private readonly aiBranchName: AiBranchName,\n private readonly gitExec: GitExec,\n private readonly buildAffected: BuildAffected,\n private readonly buildArtifactGate: BuildArtifactGate,\n private readonly mergeState: MergeState,\n private readonly mergeEnd: MergeEnd,\n private readonly checklistScanner: ChecklistScanner,\n private readonly reviewReport: ReviewReport,\n private readonly materializer: DiffMaterializer,\n private readonly prContextWriter: PrContextWriter,\n private readonly briefingBuilder: ReviewerBriefingBuilder,\n private readonly reviewerInstructions: ReviewerInstructionsService,\n private readonly receipts: ReviewStageReceiptService,\n private readonly activeHatchReport: ActiveHatchReport,\n // Injected only to RESOLVE + RENDER refusals (see refusals()). This stage never archives a verdict.\n private readonly reviewJsonService: ReviewJsonService,\n private readonly stageConsole: StageOutputLog,\n ) {}\n\n /**\n * Everything below runs with this stage's console CAPTURED to a file (see StageOutputLog). Only the\n * lines an agent must act on before it can do anything else reach the terminal — the build heartbeat\n * and result, and the closing instruction block. The merge validation, the hatch report, the checklist\n * scan and the diff extraction are all in the file, one `grep` away.\n *\n * This is the FIRST half of the fix for the piped-command watchdog kill: an agent pipes a command it\n * expects to flood, a pipe withholds every byte until exit, and the harness kills a command that has\n * printed nothing for 600 seconds — after a full build. Guarding the pipe without first shrinking the\n * output would only have traded that kill for a flooded context.\n */\n async run(opts: ReviewUpsertPrOptions = new ReviewUpsertPrOptions()): Promise<void> {\n const repoRoot = this.repoRootFinder.resolveRepoRoot(process.cwd());\n await this.stageConsole.withCapture(\n repoRoot, REVIEW_CONSOLE_LOG, (): Promise<void> => this.runStage(repoRoot, opts));\n }\n\n private async runStage(repoRoot: string, opts: ReviewUpsertPrOptions): Promise<void> {\n writeTemplate(repoRoot, 'webpieces.git-workflow.md');\n writeTemplate(repoRoot, 'webpieces.review-checklists.md');\n const featureName = this.aiBranchName.getFeatureName();\n const config = loadAndValidate(repoRoot).prGate;\n\n const mergeValidated = await this.finalizeAnyInProgressMerge(repoRoot);\n this.gitExec.assertCleanTree(repoRoot);\n const buildPassedAt = await this.runBuildGate(repoRoot);\n\n const scan = this.checklistScanner.scan(repoRoot, config.checklists, new ChecklistScanOptions(false, '')); // '' — THIS command writes the context itself, after materializing\n const briefings = this.briefReviewers(repoRoot, featureName, scan, config);\n this.receipts.write(repoRoot, featureName, new ReviewStageReceipt(\n scan.basis.headSha, mergeValidated, this.buildAffected.resolveBuildCommand(repoRoot), buildPassedAt,\n briefings.map((b: ReviewerBriefing): string => b.subagent),\n ));\n this.reportActiveHatches(repoRoot);\n this.report(repoRoot, featureName, scan, briefings, opts);\n }\n\n /**\n * List every rule that is currently switched off in webpieces.config.json. NON-BLOCKING and printed\n * before the instruction block, because it is context for the review rather than a step in it.\n *\n * A hatch is the one config change that makes the build QUIETER, so no gate, reviewer or red check\n * will ever mention it — which is how a repo ends up with rules off for weeks that nobody chose to\n * leave off. This is the only place in the flow that says so out loud.\n */\n private reportActiveHatches(repoRoot: string): void {\n const hatches: ActiveHatch[] = this.activeHatchReport.scan(repoRoot);\n process.stdout.write(this.activeHatchReport.render(hatches));\n }\n\n /**\n * Validate + commit + finalize a conflict resolution, if one is in flight. MOVED here from\n * `wp-finish-upsert-pr` so that exactly one command owns merge finalization — two owners is the drift\n * failure `PrContextWriter`'s own docstring records having already happened once in this codebase.\n *\n * MergeEnd's three checks are what \"did the AI do the 3-point merge right?\" means concretely: no\n * conflict markers left in the scoped files, `git diff --diff-filter=U` empty, and a non-empty\n * merge-explanation.md beside every conflicted file.\n *\n * Returns true when a merge was finalized OR there was none to finalize; it throws rather than returning\n * false when validation fails, so the receipt can never record an unvalidated merge as validated.\n */\n private async finalizeAnyInProgressMerge(repoRoot: string): Promise<boolean> {\n const home = this.mergeState.mergeDirFor(repoRoot, this.aiBranchName.getFeatureName());\n const activeDir = this.mergeState.findActiveMergeRunDir(home);\n const marker = activeDir ? this.mergeState.readMergeMarker(activeDir) : null;\n if (!activeDir || !marker || marker.validated) return true;\n process.stdout.write('\\n' + SEP + '① Validating your 3-point merge\\n' + SEP + '\\n');\n // pushRemote:false — finish still owns the single push.\n await this.mergeEnd.mergeEnd(\n repoRoot, 'wp-review-upsert-pr', activeDir,\n new MergeContext(marker.currentBranch, marker.squashBranch, marker.backupBranch, marker.prNumber),\n new MergeEndOptions(marker.conflictedFiles, false),\n );\n return true;\n }\n\n /**\n * The build gate, run BEFORE any reviewer is briefed. Returns the ISO time it passed, for the receipt —\n * which is what lets `wp-finish-upsert-pr` skip its own gate when HEAD has not moved, so moving the gate\n * earlier did not turn one build into two.\n */\n private async runBuildGate(repoRoot: string): Promise<string> {\n await this.buildAffected.runBuildGate(repoRoot, new BuildGateOptions(\n '🛠️ Build gate',\n 'pnpm wp-review-upsert-pr',\n 'Build failed — NO reviewer was briefed and no diff was extracted. Fix it, then re-run.',\n REVIEW_STAGE,\n ));\n // Repo-wide: the build must not have left anything uncommitted AND unstaged. Runs HERE, not in\n // finish, because this is the stage that ran buildCommand and is therefore holding the dirty\n // tree at the exact moment the question is answerable. Replaces the per-project\n // validate-di-graph-unchanged nx target — see BuildArtifactGate for why that one had to go.\n this.buildArtifactGate.assertBuildLeftNothingUncommitted(repoRoot);\n return new Date().toISOString();\n }\n\n /**\n * Extract the diff once, then write one instructions file per reviewer.\n *\n * The changed-file set and the basis both come off the SCAN rather than being recomputed, so the diff a\n * reviewer reads covers exactly the files its checklist matched against.\n */\n private briefReviewers(repoRoot: string, featureName: string, scan: ChecklistScan, config: PrGateConfig): ReviewerBriefing[] {\n if (scan.basis.unresolved) return [];\n const manifest = this.materializer.materialize(\n repoRoot, featureName, scan.basis, scan.changedFiles, config.reviewDiffExclude);\n const diffDir = this.materializer.diffDirFor(repoRoot, featureName);\n // Write pr-context.json ONCE, here — after materializing, so `diffDir` is populated on the first\n // write. The scan deliberately did not write it (ChecklistScanOptions.contextStage === ''); it used\n // to, which meant this command wrote the same file twice, the first time with an empty diffDir.\n // `scan.changedFiles` is passed through so the context is not recomputed from a second git call.\n scan.context = this.prContextWriter.ensure(\n repoRoot, featureName, scan.basis, 'stage2-review', scan.changedFiles, diffDir);\n const briefings = this.briefingBuilder.build(repoRoot, scan, manifest, diffDir, config);\n const dir = this.reviewerInstructions.instructionsDirFor(repoRoot, featureName);\n fs.rmSync(dir, { recursive: true, force: true }); // stale instructions read as current are worse than none\n fs.mkdirSync(dir, { recursive: true });\n for (const b of briefings) {\n fs.writeFileSync(this.reviewerInstructions.pathFor(repoRoot, featureName, b.subagent),\n this.reviewerInstructions.render(b));\n }\n return briefings;\n }\n\n /**\n * What the AI must do next — rendered by `ReviewReport`, which owns the ordering.\n *\n * The rendering moved OUT of this command so the ordering could be asserted on as a string. It had a\n * real bug: the zero-checklist notice signed off with \"Carry on and run: pnpm wp-finish-upsert-pr\"\n * while the block right beneath it said to write review.json first. An agent that reads top to bottom\n * obeyed the first line and posted a PR with no review at all.\n */\n // eslint-disable-next-line @typescript-eslint/max-params\n private report(\n repoRoot: string, featureName: string, scan: ChecklistScan, briefings: readonly ReviewerBriefing[],\n opts: ReviewUpsertPrOptions,\n ): void {\n const input = new ReviewReportInput(repoRoot, featureName, scan.reviewPath);\n input.definedCount = scan.defined.length;\n input.applicableCount = scan.applicable.length;\n input.reviewed = scan.reviewed.slice();\n input.formatErrors = scan.formatErrors.slice();\n input.briefings = briefings.slice();\n input.refused = this.refusals(scan);\n input.skipOptional = opts.skipOptional;\n // `say`: this block IS the next action — which reviewers to spawn, where review.json goes, and\n // the command after that. Capturing it into the log would leave the terminal with a pointer and\n // no instruction, which is the one thing this stage may never do.\n this.stageConsole.say(this.reviewReport.render(input));\n }\n\n /**\n * The applicable checklists that ALREADY ANSWERED and refused, each with its refusal rendered by the ONE\n * renderer `wp-finish-upsert-pr` also uses.\n *\n * Stage ② had the identical defect finish did: a refused checklist has no passing verdict, so it is not\n * in `scan.reviewed`, so it was printed as an ordinary owed reviewer and re-instructed word for word —\n * and an agent that obeys spawns it against unchanged code, gets the same refusal, and loops one stage\n * earlier than the loop that was reported.\n *\n * NO archive path is passed, deliberately, and this stage moves nothing. Retiring a verdict is finish's\n * act on the refusal it is actually enforcing; doing it here would delete the live verdict of a branch\n * that has not even been asked to finish yet, and `refusalError`'s un-archived wording — \"set an override\n * in review-<id>.json\" — is only correct while that file is still there, which here it is.\n */\n private refusals(scan: ChecklistScan): RefusedReviewer[] {\n const refused = this.reviewJsonService.refusedChecklists(scan.applicable, scan.results);\n return refused.map((req: RequiredChecklist): RefusedReviewer => new RefusedReviewer(\n req.id, this.reviewJsonService.refusalError(req, this.reviewJsonService.resolveVerdict(req, scan.results))));\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"review-upsert-pr-command.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/commands/review-upsert-pr-command.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'fs';\nimport {\n loadAndValidate, writeTemplate, PrGateConfig, RepoRootFinder, RequiredChecklist,\n ReviewerBriefing, ReviewerInstructionsService, ReviewJsonService,\n} from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { ActiveHatch, ActiveHatchReport } from '../workflow/active-hatches';\nimport { AiBranchName } from '../workflow/git-readAiBranchName';\nimport { BuildAffected, BuildGateOptions } from '../workflow/build-affected';\nimport { REVIEW_STAGE } from '../workflow/build-gate-log';\nimport { BuildArtifactGate } from '../workflow/build-artifact-gate';\nimport { ChecklistScan, ChecklistScanOptions, ChecklistScanner } from '../workflow/checklist-scanner';\nimport { DiffMaterializer } from '../workflow/diff-materializer';\nimport { GitExec } from '../workflow/git-exec';\nimport { MergeContext } from '../workflow/merge-start';\nimport { MergeEnd, MergeEndOptions } from '../workflow/merge-end';\nimport { MergeState } from '../workflow/merge-state';\nimport { PrContextWriter } from '../workflow/pr-context-writer';\nimport { ReviewerBriefingBuilder } from '../workflow/reviewer-briefing-builder';\nimport { RefusedReviewer, ReviewReport, ReviewReportInput } from '../workflow/review-report';\nimport { ReviewStageReceipt, ReviewStageReceiptService } from '../workflow/review-stage-receipt';\nimport { StageOutputLog, REVIEW_CONSOLE_LOG } from '../workflow/stage-output-log';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n/** The `--no-optional` decision, as data (per CLAUDE.md) rather than a bare boolean parameter. */\nexport class ReviewUpsertPrOptions {\n /**\n * true ⇒ the human ALREADY said to submit without the optional reviews, so stage ② must not print the\n * block telling the AI to offer them. It changes nothing about what BLOCKS: required checklists are\n * unaffected, and an optional checklist that already has a red verdict on this branch still refuses the\n * PR at finish. This only suppresses the offer.\n */\n skipOptional: boolean;\n\n constructor(skipOptional = false) {\n this.skipOptional = skipOptional;\n }\n}\n\n/**\n * `wp-review-upsert-pr` — STAGE ② of the three-stage PR flow, between `wp-start-upsert-pr` (sync from main)\n * and `wp-finish-upsert-pr` (post the PR).\n *\n * It exists because reviewing came before verifying. Under the old two-command flow the 3-point merge was\n * finalized and the build gate ran inside `wp-finish-upsert-pr` — i.e. AFTER the reviewers had already run.\n * A reviewer could therefore spend a full review on an unresolved merge, or on code that does not compile,\n * and only afterwards would the branch be checked. This command puts the verification first:\n *\n * 1. finalize + VALIDATE any in-progress 3-point merge (moved here from finish)\n * 2. assert a clean tree (meaningful now — step 1 committed the resolution)\n * 3. run the build gate (on the merged, committed tree)\n * 4. scan the checklists, EXTRACT the diff, BRIEF the reviewers\n * 5. write the stage receipt, then print what to spawn + the review.json schema\n *\n * Unlike the report-only command it replaces — which always exited 0, because reporting is not gating — this\n * command CAN FAIL, at steps 1, 2 and 3. That is the point: it fails BEFORE any reviewer is spawned, so a\n * branch with an unresolved merge or a broken build costs zero reviewer tokens.\n *\n * The ordering of 1 → 2 → 3 is load-bearing. MergeEnd stages and commits the conflict resolution, so the\n * tree is clean by the time the build runs; building first would either build a dirty tree (proving nothing\n * about what ships) or refuse a tree that is legitimately mid-resolution.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class ReviewUpsertPrCommand {\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(\n private readonly repoRootFinder: RepoRootFinder,\n private readonly aiBranchName: AiBranchName,\n private readonly gitExec: GitExec,\n private readonly buildAffected: BuildAffected,\n private readonly buildArtifactGate: BuildArtifactGate,\n private readonly mergeState: MergeState,\n private readonly mergeEnd: MergeEnd,\n private readonly checklistScanner: ChecklistScanner,\n private readonly reviewReport: ReviewReport,\n private readonly materializer: DiffMaterializer,\n private readonly prContextWriter: PrContextWriter,\n private readonly briefingBuilder: ReviewerBriefingBuilder,\n private readonly reviewerInstructions: ReviewerInstructionsService,\n private readonly receipts: ReviewStageReceiptService,\n private readonly activeHatchReport: ActiveHatchReport,\n // Injected only to RESOLVE + RENDER refusals (see refusals()). This stage never archives a verdict.\n private readonly reviewJsonService: ReviewJsonService,\n private readonly stageConsole: StageOutputLog,\n ) {}\n\n /**\n * Everything below runs with this stage's console CAPTURED to a file (see StageOutputLog). Only the\n * lines an agent must act on before it can do anything else reach the terminal — the build heartbeat\n * and result, and the closing instruction block. The merge validation, the hatch report, the checklist\n * scan and the diff extraction are all in the file, one `grep` away.\n *\n * This is the FIRST half of the fix for the piped-command watchdog kill: an agent pipes a command it\n * expects to flood, a pipe withholds every byte until exit, and the harness kills a command that has\n * printed nothing for 600 seconds — after a full build. Guarding the pipe without first shrinking the\n * output would only have traded that kill for a flooded context.\n */\n async run(opts: ReviewUpsertPrOptions = new ReviewUpsertPrOptions()): Promise<void> {\n const repoRoot = this.repoRootFinder.resolveRepoRoot(process.cwd());\n await this.stageConsole.withCapture(\n repoRoot, REVIEW_CONSOLE_LOG, (): Promise<void> => this.runStage(repoRoot, opts));\n }\n\n private async runStage(repoRoot: string, opts: ReviewUpsertPrOptions): Promise<void> {\n writeTemplate(repoRoot, 'webpieces.git-workflow.md');\n writeTemplate(repoRoot, 'webpieces.review-checklists.md');\n const featureName = this.aiBranchName.getFeatureName();\n const config = loadAndValidate(repoRoot).prGate;\n\n const mergeValidated = await this.finalizeAnyInProgressMerge(repoRoot);\n this.gitExec.assertCleanTree(repoRoot);\n const buildPassedAt = await this.runBuildGate(repoRoot);\n\n const scan = this.checklistScanner.scan(repoRoot, config.checklists, new ChecklistScanOptions(false, '')); // '' — THIS command writes the context itself, after materializing\n const briefings = this.briefReviewers(repoRoot, featureName, scan, config);\n this.receipts.write(repoRoot, featureName, new ReviewStageReceipt(\n scan.basis.headSha, mergeValidated, this.buildAffected.resolveBuildCommand(repoRoot), buildPassedAt,\n briefings.map((b: ReviewerBriefing): string => b.subagent),\n ));\n this.reportActiveHatches(repoRoot);\n this.report(repoRoot, featureName, scan, briefings, opts);\n }\n\n /**\n * List every rule that is currently switched off in webpieces.config.json. NON-BLOCKING and printed\n * before the instruction block, because it is context for the review rather than a step in it.\n *\n * A hatch is the one config change that makes the build QUIETER, so no gate, reviewer or red check\n * will ever mention it — which is how a repo ends up with rules off for weeks that nobody chose to\n * leave off. This is the only place in the flow that says so out loud.\n */\n private reportActiveHatches(repoRoot: string): void {\n const hatches: ActiveHatch[] = this.activeHatchReport.scan(repoRoot);\n process.stdout.write(this.activeHatchReport.render(hatches));\n }\n\n /**\n * Validate + commit + finalize a conflict resolution, if one is in flight. MOVED here from\n * `wp-finish-upsert-pr` so that exactly one command owns merge finalization — two owners is the drift\n * failure `PrContextWriter`'s own docstring records having already happened once in this codebase.\n *\n * MergeEnd's three checks are what \"did the AI do the 3-point merge right?\" means concretely: no\n * conflict markers left in the scoped files, `git diff --diff-filter=U` empty, and a non-empty\n * merge-explanation.md beside every conflicted file.\n *\n * Returns true when a merge was finalized OR there was none to finalize; it throws rather than returning\n * false when validation fails, so the receipt can never record an unvalidated merge as validated.\n */\n private async finalizeAnyInProgressMerge(repoRoot: string): Promise<boolean> {\n const home = this.mergeState.mergeDirFor(repoRoot, this.aiBranchName.getFeatureName());\n const activeDir = this.mergeState.findActiveMergeRunDir(home);\n const marker = activeDir ? this.mergeState.readMergeMarker(activeDir) : null;\n if (!activeDir || !marker || marker.validated) return true;\n process.stdout.write('\\n' + SEP + '① Validating your 3-point merge\\n' + SEP + '\\n');\n // pushRemote:false — finish still owns the single push.\n await this.mergeEnd.mergeEnd(\n repoRoot, 'wp-review-upsert-pr', activeDir,\n new MergeContext(marker.currentBranch, marker.squashBranch, marker.backupBranch, marker.prNumber),\n new MergeEndOptions(marker.conflictedFiles, false),\n );\n return true;\n }\n\n /**\n * The build gate, run BEFORE any reviewer is briefed. Returns the ISO time it passed, for the receipt —\n * which is what lets `wp-finish-upsert-pr` skip its own gate when HEAD has not moved, so moving the gate\n * earlier did not turn one build into two.\n */\n private async runBuildGate(repoRoot: string): Promise<string> {\n await this.buildAffected.runBuildGate(repoRoot, new BuildGateOptions(\n '🛠️ Build gate',\n 'pnpm wp-review-upsert-pr',\n 'Build failed — NO reviewer was briefed and no diff was extracted. Fix it, then re-run.',\n REVIEW_STAGE,\n ));\n // Repo-wide: the build must not have left anything uncommitted AND unstaged. Runs HERE, not in\n // finish, because this is the stage that ran buildCommand and is therefore holding the dirty\n // tree at the exact moment the question is answerable. Replaces the per-project\n // validate-di-graph-unchanged nx target — see BuildArtifactGate for why that one had to go.\n this.buildArtifactGate.assertBuildLeftNothingUncommitted(repoRoot);\n return new Date().toISOString();\n }\n\n /**\n * Extract the diff once, then write one instructions file per reviewer.\n *\n * The changed-file set and the basis both come off the SCAN rather than being recomputed, so the diff a\n * reviewer reads covers exactly the files its checklist matched against.\n */\n private briefReviewers(repoRoot: string, featureName: string, scan: ChecklistScan, config: PrGateConfig): ReviewerBriefing[] {\n if (scan.basis.unresolved) return [];\n const manifest = this.materializer.materialize(\n repoRoot, featureName, scan.basis, scan.changedFiles, config.reviewDiffExclude);\n const diffDir = this.materializer.diffDirFor(repoRoot, featureName);\n // Write pr-context.json ONCE, here — after materializing, so `diffDir` is populated on the first\n // write. The scan deliberately did not write it (ChecklistScanOptions.contextStage === ''); it used\n // to, which meant this command wrote the same file twice, the first time with an empty diffDir.\n // `scan.changedFiles` is passed through so the context is not recomputed from a second git call.\n scan.context = this.prContextWriter.ensure(\n repoRoot, featureName, scan.basis, 'stage2-review', scan.changedFiles, diffDir);\n const briefings = this.briefingBuilder.build(repoRoot, scan, manifest, diffDir, config);\n const dir = this.reviewerInstructions.instructionsDirFor(repoRoot, featureName);\n fs.rmSync(dir, { recursive: true, force: true }); // stale instructions read as current are worse than none\n fs.mkdirSync(dir, { recursive: true });\n for (const b of briefings) {\n fs.writeFileSync(this.reviewerInstructions.pathFor(repoRoot, featureName, b.subagent),\n this.reviewerInstructions.render(b));\n }\n return briefings;\n }\n\n /**\n * What the AI must do next — rendered by `ReviewReport`, which owns the ordering.\n *\n * The rendering moved OUT of this command so the ordering could be asserted on as a string. It had a\n * real bug: the zero-checklist notice signed off with \"Carry on and run: pnpm wp-finish-upsert-pr\"\n * while the block right beneath it said to write review.json first. An agent that reads top to bottom\n * obeyed the first line and posted a PR with no review at all.\n */\n // eslint-disable-next-line @typescript-eslint/max-params\n private report(\n repoRoot: string, featureName: string, scan: ChecklistScan, briefings: readonly ReviewerBriefing[],\n opts: ReviewUpsertPrOptions,\n ): void {\n const input = new ReviewReportInput(repoRoot, featureName, scan.reviewPath);\n input.definedCount = scan.defined.length;\n input.applicableCount = scan.applicable.length;\n input.reviewed = scan.reviewed.slice();\n input.formatErrors = scan.formatErrors.slice();\n input.briefings = briefings.slice();\n input.refused = this.refusals(scan);\n input.skipOptional = opts.skipOptional;\n // `say`: this block IS the next action — which reviewers to spawn, where review.json goes, and\n // the command after that. Capturing it into the log would leave the terminal with a pointer and\n // no instruction, which is the one thing this stage may never do.\n this.stageConsole.say(this.reviewReport.render(input));\n }\n\n /**\n * The applicable checklists that ALREADY ANSWERED and refused, each with its refusal rendered by the ONE\n * renderer `wp-finish-upsert-pr` also uses.\n *\n * Stage ② had the identical defect finish did: a refused checklist has no passing verdict, so it is not\n * in `scan.reviewed`, so it was printed as an ordinary owed reviewer and re-instructed word for word —\n * and an agent that obeys spawns it against unchanged code, gets the same refusal, and loops one stage\n * earlier than the loop that was reported.\n *\n * NO archive path is passed, deliberately, and this stage moves nothing. Retiring a verdict is finish's\n * act on the refusal it is actually enforcing; doing it here would delete the live verdict of a branch\n * that has not even been asked to finish yet, and `refusalError`'s un-archived wording — \"fix it, then\n * re-run\" — is only correct while that verdict file is still there, which here it is.\n *\n * The review path IS passed, because the ship-anyway route the refusal prints is a command that writes\n * `override-<id>.json` beside it. That command is as correct at this stage as at finish: the override\n * is a separate file with its own writer, so nothing about it depends on the verdict having been moved.\n */\n private refusals(scan: ChecklistScan): RefusedReviewer[] {\n const refused = this.reviewJsonService.refusedChecklists(scan.applicable, scan.results);\n return refused.map((req: RequiredChecklist): RefusedReviewer => new RefusedReviewer(\n req.id,\n this.reviewJsonService.refusalError(\n req, this.reviewJsonService.resolveVerdict(req, scan.results), scan.reviewPath)));\n }\n}\n"]}
|
|
@@ -9,9 +9,10 @@ import { ChecklistNotice } from './checklist-notice';
|
|
|
9
9
|
* agent obeys that block, the reviewer re-reads unchanged code, refuses again — the loop, one stage earlier.
|
|
10
10
|
*
|
|
11
11
|
* The message is rendered by the COMMAND rather than in here for one reason: stage ② must not archive
|
|
12
|
-
* anything. `refusalError` called with no archive path says "fix it, then re-run
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* anything. `refusalError` called with no archive path says "fix it, then re-run", which is only true while
|
|
13
|
+
* the verdict file is still live — and at stage ② it is. Retiring a verdict is finish's act, on the refusal
|
|
14
|
+
* it is actually acting on. (The ship-anyway route it prints — the command that writes `override-<id>.json`
|
|
15
|
+
* — is correct at either stage: that is a different file, with its own writer, and no archive touches it.)
|
|
15
16
|
*/
|
|
16
17
|
export declare class RefusedReviewer {
|
|
17
18
|
checklistId: string;
|
|
@@ -15,9 +15,10 @@ const SEP = '━━━━━━━━━━━━━━━━━━━━━━
|
|
|
15
15
|
* agent obeys that block, the reviewer re-reads unchanged code, refuses again — the loop, one stage earlier.
|
|
16
16
|
*
|
|
17
17
|
* The message is rendered by the COMMAND rather than in here for one reason: stage ② must not archive
|
|
18
|
-
* anything. `refusalError` called with no archive path says "fix it, then re-run
|
|
19
|
-
*
|
|
20
|
-
*
|
|
18
|
+
* anything. `refusalError` called with no archive path says "fix it, then re-run", which is only true while
|
|
19
|
+
* the verdict file is still live — and at stage ② it is. Retiring a verdict is finish's act, on the refusal
|
|
20
|
+
* it is actually acting on. (The ship-anyway route it prints — the command that writes `override-<id>.json`
|
|
21
|
+
* — is correct at either stage: that is a different file, with its own writer, and no archive touches it.)
|
|
21
22
|
*/
|
|
22
23
|
class RefusedReviewer {
|
|
23
24
|
checklistId;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"review-report.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/review-report.ts"],"names":[],"mappings":"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","sourcesContent":["import { injectable, bindingScopeValues } from 'inversify';\nimport { reviewJsonSchemaHint, RequiredChecklist, ReviewerBriefing, ReviewerInstructionsService } from '@webpieces/rules-config';\nimport { ChecklistNotice } from './checklist-notice';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n/**\n * One reviewer that ALREADY ANSWERED on this branch and refused, with the refusal rendered by\n * `ReviewJsonService.refusalError` — the ONE wording, shared with `wp-finish-upsert-pr`. Data-only.\n *\n * Stage ② carries these because it had the same defect finish did: a refused checklist has no passing\n * verdict, so it is \"owed\", so it got an ordinary spawn block identical to a reviewer that never ran. An\n * agent obeys that block, the reviewer re-reads unchanged code, refuses again — the loop, one stage earlier.\n *\n * The message is rendered by the COMMAND rather than in here for one reason: stage ② must not archive\n * anything. `refusalError` called with no archive path says \"fix it, then re-run; or set an override in\n * review-<id>.json\", which is only true while that file is still live — and at stage ② it is. Retiring a\n * verdict is finish's act, on the refusal it is actually acting on.\n */\nexport class RefusedReviewer {\n checklistId: string;\n message: string;\n\n constructor(checklistId: string, message: string) {\n this.checklistId = checklistId;\n this.message = message;\n }\n}\n\n/**\n * Everything the closing block of `wp-review-upsert-pr` needs. Data-only, and a class rather than an\n * object literal per CLAUDE.md. The three identifying paths are constructor args; the rest are optional\n * facts about the scan that default to \"nothing\", so a repo with no checklists constructs it in one line.\n */\nexport class ReviewReportInput {\n repoRoot: string;\n featureName: string;\n reviewPath: string; // the branch's review.json — the file the AI must write next\n definedCount: number; // how many checklists pr-gate.checklists defines\n applicableCount: number; // how many of them apply to THIS diff (0 ⇒ the notice, not spawn blocks)\n reviewed: RequiredChecklist[]; // already have a passing verdict on this branch\n formatErrors: string[]; // verdict files that exist but cannot be read as verdicts\n briefings: ReviewerBriefing[]; // one per applicable checklist, already written to disk\n refused: RefusedReviewer[]; // of the owed ones, those that already ran and said no (see RefusedReviewer)\n /**\n * `--no-optional` was passed: the human has already said to submit without the optional reviews, so the\n * block that offers them is replaced by a one-line statement that they were skipped. It never suppresses\n * a REQUIRED reviewer, and it is not a gate — nothing about what blocks the PR changes.\n */\n skipOptional: boolean;\n\n constructor(repoRoot: string, featureName: string, reviewPath: string) {\n this.repoRoot = repoRoot;\n this.featureName = featureName;\n this.reviewPath = reviewPath;\n this.definedCount = 0;\n this.applicableCount = 0;\n this.reviewed = [];\n this.formatErrors = [];\n this.briefings = [];\n this.refused = [];\n this.skipOptional = false;\n }\n}\n\n/**\n * Renders the closing block of `wp-review-upsert-pr` — the checklist verdict, then EXACTLY ONE \"what to do\n * next\".\n *\n * Extracted from the command so it can be asserted on as a rendered string, because the ordering IS the\n * contract. The bug it was extracted to fix: the zero-checklist notice ended with \"Carry on and run: pnpm\n * wp-finish-upsert-pr\", and the block printed directly beneath it said to write review.json first and\n * finish afterwards. Two next-steps, in the wrong order, and an agent that follows instructions literally —\n * which is the entire reason this command prints them — took the first one and opened a PR with no review.\n * That is precisely the failure the three-stage flow exists to prevent (reported on PR #519).\n *\n * The invariants, enforced by review-report.spec.ts:\n * 1. `wp-finish-upsert-pr` is named as a thing to run EXACTLY ONCE in the whole block.\n * 2. The review.json instruction comes BEFORE it — and BEFORE the spawn blocks (see nextSteps).\n * 3. With zero checklists the all-clear precedes any configuration guidance.\n * 4. REQUIRED reviewers are spawned unasked; OPTIONAL ones are only ever OFFERED, in one batched\n * question. The two never share a step, because one instruction says \"do it\" and the other says\n * \"ask first\", and an agent reading a merged list will act on the stronger of the two.\n *\n * Pure string building, no I/O. `@injectable(bindingScopeValues.Singleton)` so it is injected by type.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class ReviewReport {\n constructor(\n private readonly checklistNotice: ChecklistNotice,\n private readonly reviewerInstructions: ReviewerInstructionsService,\n ) {}\n\n render(input: ReviewReportInput): string {\n return '\\n' + SEP + this.header(input) + SEP\n + this.scanVerdict(input)\n + this.nextSteps(input);\n }\n\n /**\n * Name what this block is actually about. A repo with reviewers owed is being told to SPAWN; a repo with\n * none is not, and promising subagents it does not have is the same kind of noise as explaining checklist\n * configuration to a repo that configured none. Keyed on what is actually ACTIONABLE rather than on the\n * applicable count: every applicable checklist already having a verdict means nothing to spawn, and so\n * does a branch whose only outstanding reviews are optional ones the human already waved off.\n *\n * \"spawn\" and \"ask about\" are separate headings because they are separate obligations. A branch owing\n * only optional reviews has nothing the AI may do unilaterally, and a heading that says SPAWN is the\n * single line most likely to make it do exactly that.\n */\n private header(input: ReviewReportInput): string {\n if (this.requiredOwed(input).length > 0) return '② Review, spawn subagent reviewers, then finish\\n';\n if (this.offerableOwed(input).length > 0) return '② Review, offer the optional reviewers, then finish\\n';\n return '② Review, then finish\\n';\n }\n\n /**\n * What the SCAN found: either \"nothing applies here\" or the already-reviewed / unreadable-verdict lines.\n * Verdicts, not instructions — every instruction lives in nextSteps() below, so no line up here can be\n * mistaken for the next action.\n */\n private scanVerdict(input: ReviewReportInput): string {\n if (input.applicableCount === 0) return '\\n' + this.checklistNotice.build(input.definedCount);\n const lines: string[] = [];\n // The prohibition rides on the REUSE line itself, not only in the all-clear below, because the\n // all-clear is not printed when anything is still owed — and \"some reviewers are reused, others\n // must be spawned\" is exactly the shape in which an agent re-spawns the reused ones too.\n for (const r of input.reviewed) {\n lines.push(` ✓ ${r.subagent} — already reviewed on this branch; verdict STANDS, do NOT re-spawn (review-${r.id}.json)`);\n }\n // A verdict file that EXISTS but is unreadable as a verdict is called out here. Without it this\n // reports the checklist as simply owed, and the AI re-runs a reviewer that already ran instead of\n // correcting the file sitting right there.\n for (const e of input.formatErrors) lines.push(` ⛔ ${e}`);\n lines.push(...this.skippedLines(input));\n if (this.actionableOwed(input).length === 0) lines.push('', this.allClear(input));\n if (lines.length === 0) return '';\n return '\\n' + lines.join('\\n') + '\\n';\n }\n\n /**\n * `--no-optional`, stated as a VERDICT rather than left silent. Named individually, not just counted: the\n * whole reason the human is allowed to skip these is that they know this diff, and the only way they can\n * catch \"wait, not THAT one\" is to see which ones went unreviewed.\n */\n private skippedLines(input: ReviewReportInput): string[] {\n const skipped = this.optionalOwed(input);\n if (!input.skipOptional || skipped.length === 0) return [];\n return [\n '',\n ` ⏭️ ${skipped.length} OPTIONAL checklist(s) matched this diff and were SKIPPED (--no-optional):`,\n ...skipped.map((b: ReviewerBriefing): string => ` ${b.subagent} — ${this.why(b)}`),\n ' Not blocking. Drop the flag and re-run this command to offer them after all.',\n ];\n }\n\n /**\n * The all-clear, then the RULE that makes it actionable.\n *\n * \"nothing to spawn\" on its own is a description of the current state, and an agent that has just been\n * told a state — rather than a rule — treats re-spawning as a judgement call it is entitled to make. It\n * then makes it, reasoning (correctly, on the facts) that the carried-forward verdicts judged an earlier\n * tree. Reviews here are once per branch BY CONSTRUCTION: a passing review-<id>.json satisfies its\n * checklist for the branch's whole life, and `wp-finish-upsert-pr` never archives one the way it archives\n * review.json. So the reuse is deliberate — it is what keeps post-PR iteration from re-paying for every\n * matched reviewer — and the output has to say so, because the alternative reading is the expensive one.\n *\n * The overwrite warning is not decoration. A re-spawned reviewer writes to the SAME verdict path, so a\n * gratuitous re-run does not merely cost a subagent — it destroys the verdict that was already banked.\n *\n * It must NOT claim everything was reviewed when optional reviews were skipped — that is the one sentence\n * that would turn a deliberate skip into a false record of a review that happened.\n */\n private allClear(input: ReviewReportInput): string {\n const headline = input.skipOptional && this.optionalOwed(input).length > 0\n ? '✅ Nothing left to spawn — every REQUIRED checklist is reviewed (optional ones skipped above).'\n : '✅ Every checklist that applies is already reviewed — nothing to spawn.';\n return headline + '\\n' + this.oncePerBranchRule();\n }\n\n /**\n * The once-per-branch rule, stated as a prohibition rather than left to be inferred from a ✅.\n *\n * Printed whenever verdicts are being reused — which is every iteration of a PR after the first, i.e. the\n * majority of stage-② runs on any branch that gets review feedback.\n */\n private oncePerBranchRule(): string {\n return (\n ' Reviews here are ONCE PER BRANCH: a passing verdict carries forward to every later iteration\\n' +\n ' of this PR, deliberately, so post-PR edits cost no reviewer tokens. Do NOT re-spawn a reviewer\\n' +\n ' listed above to \"re-check\" the newer code — it burns a full subagent run AND overwrites the\\n' +\n ' verdict file it already wrote. The only reviewers you may spawn are ones a STEP below names.'\n );\n }\n\n /**\n * The ONE instruction block, and the last thing the stage prints. Numbered rather than prose-linked\n * (\"Then… Finally…\") so that skipping step 1 is visibly skipping a step, and worded so no earlier line\n * can be mistaken for the real next action.\n *\n * review.json is STEP 1 and the spawn blocks are STEP 2 — that ORDER is the contract, not a preference.\n * This block used to print the spawn blocks first and then say to write review.json \"WHILE any reviewer\n * subagents above are still running\", which does not merely permit spawning first, it instructs it.\n * Harmless for a reviewer that only reads the diff; wrong for one that judges the PR's stated INTENT —\n * its title, summary or risk level — because review.json is the only place that intent lives. Such a\n * reviewer either finds no file (a false RED and a wasted reviewer run) or, on a second run of this\n * stage on the same branch, finds the PREVIOUS run's file and validates a title that no longer exists —\n * a false GREEN, with nothing in the output saying which of the two happened. A consuming repo had to\n * write itself a rule telling its agents to DISOBEY this block to work around it.\n *\n * What the old ordering bought was overlap on a single local file write, not a subagent round-trip.\n *\n * The schema hint is rendered by ReviewJsonService — the single renderer — so the shape printed here\n * can never drift from the shape `wp-finish-upsert-pr` validates.\n */\n private nextSteps(input: ReviewReportInput): string {\n const required = this.requiredOwed(input);\n const offerable = this.offerableOwed(input);\n // Numbered by what is actually PRINTED, so the numbers a reader sees are 1..n with no gaps: write\n // review.json, then a spawn step only if anything must run, then an offer step only if anything may.\n let step = 1;\n const write = this.writeReviewStep(input.reviewPath, step++);\n const spawn = required.length === 0 ? '' : this.spawnStep(input, required, step++);\n const offer = offerable.length === 0 ? '' : this.offerStep(input, offerable, step++);\n return '\\n' + SEP\n + `▶ NEXT — ${step} steps, in this order. Step 1 is NOT optional:\\n` + SEP + '\\n'\n + write + spawn + offer + this.finishStep(step, required.length + offerable.length > 0);\n }\n\n private writeReviewStep(reviewPath: string, step: number): string {\n return (\n `STEP ${step} — review your own changes, then write the review file. Write it FIRST — BEFORE you spawn\\n` +\n ' anything below. finish REFUSES without it, and a reviewer subagent may READ it: a\\n' +\n ' checklist that judges the PR title, summary or risk level reads exactly this file, so\\n' +\n ' writing it afterwards races that reviewer into seeing nothing — or, on a re-run of this\\n' +\n ' stage, into judging the PREVIOUS run\\'s review of code that has since changed.\\n\\n' +\n reviewJsonSchemaHint(reviewPath) + '\\n\\n'\n );\n }\n\n /**\n * The REQUIRED reviewers — one copy-paste block each, and nothing at all when none is owed. The prompt is\n * deliberately a POINTER and nothing else: the generated instructions file is the contract, so anything\n * restated here is a second copy that can go stale — which is exactly how a removed `success` field\n * outlived its own removal in print.\n *\n * These are spawned WITHOUT asking. They are the checklists the repo declared `required: true`, which is\n * the repo saying the decision was already made; putting them to the human again would re-open a question\n * the config exists to settle.\n */\n private spawnStep(input: ReviewReportInput, owed: readonly ReviewerBriefing[], step: number): string {\n const lines: string[] = [\n `STEP ${step} — only once that file is written, spawn these ${owed.length} REQUIRED reviewer subagent(s) — a`,\n ' SEPARATE one each. They block the PR, so do NOT ask whether to run them. You may NOT',\n ' review your own work, and you may NOT write a reviewer\\'s verdict file on its behalf.',\n '',\n ];\n lines.push(...this.refusedWarning(input, owed));\n for (const b of owed) lines.push(...this.oneSpawnBlock(input, b));\n lines.push('');\n return lines.join('\\n');\n }\n\n /**\n * STEP n — the OPTIONAL reviewers: listed, never spawned unasked.\n *\n * This is the whole point of `required: false`. A one-line bug fix in a repo whose checklists key on a\n * glob as broad as every TypeScript file otherwise pays for a dozen subagent reviews, and the only party\n * who can judge whether this particular diff is worth them is the human looking at it.\n *\n * ONE batched multi-select question, explicitly. Asked one at a time, a human answering \"no\" nine times\n * is being worn down rather than consulted, and by the third question the cheap thing is to say yes to\n * everything — which is the state this feature exists to leave. The \"None\" option has to be spelled out\n * too: an agent that offers a list without an explicit way to decline it has not really offered a choice.\n *\n * The blocking consequence is stated because it is the one non-obvious part of the contract: `required`\n * governs whether a reviewer must RUN, not whether its answer counts. Choosing to run one and then\n * shrugging off a red verdict would make the whole exercise theater.\n */\n private offerStep(input: ReviewReportInput, offerable: readonly ReviewerBriefing[], step: number): string {\n const lines: string[] = [\n `STEP ${step} — these ${offerable.length} OPTIONAL review checklist(s) matched this diff. They do NOT block the`,\n ' PR, and you may NOT decide for the human whether to run them.',\n '',\n ' ASK THE HUMAN, in ONE multi-select question listing all of them plus an explicit',\n ' \"None — required only\" choice. Do not ask one question per reviewer. Then spawn ONLY',\n ' what they picked, the same way as any other reviewer.',\n '',\n ' If they pick none, that is a complete answer: go straight to the final step. If they',\n ' told you up front to submit without reviews, re-run this stage as',\n ' `pnpm wp-review-upsert-pr --no-optional` and this step disappears.',\n '',\n ' NOTE: whichever ones you DO run, their verdicts count in full — a red verdict from an',\n ' optional reviewer blocks the PR exactly like a required one.',\n '',\n ];\n lines.push(...this.refusedWarning(input, offerable));\n for (const b of offerable) lines.push(...this.oneSpawnBlock(input, b));\n lines.push('');\n return lines.join('\\n');\n }\n\n // Said up front, not only beside the block: an agent that has decided to spawn everything listed here\n // needs to know BEFORE it starts that one of these entries is not a spawn-shaped task. Scoped to the\n // group being printed — a refusal among the REQUIRED reviewers is not a caveat on the optional list.\n private refusedWarning(input: ReviewReportInput, group: readonly ReviewerBriefing[]): string[] {\n const ids = new Set(group.map((b: ReviewerBriefing): string => b.checklistId));\n const n = input.refused.filter((r: RefusedReviewer): boolean => ids.has(r.checklistId)).length;\n if (n === 0) return [];\n return [\n ` ${n} of them already ANSWERED and refused (marked ⛔ below). Do not spawn`,\n ' those against unchanged code — fix what they found first; the fix is the prerequisite.',\n '',\n ];\n }\n\n private finishStep(stepNumber: number, anyReviewers: boolean): string {\n // \"every reviewer you ran\" rather than \"every reviewer above\": with an optional list the human may\n // legitimately have run none of them, and a precondition naming reviewers that were declined reads as\n // an unmeetable one.\n const precondition = anyReviewers\n ? 'once every reviewer you ran has written its verdict file'\n : 'once that file exists';\n return (\n `STEP ${stepNumber} — only ${precondition}, run: pnpm wp-finish-upsert-pr\\n` +\n ' (The build gate is already green for this commit — finish reuses it unless HEAD moves.)\\n\\n'\n );\n }\n\n // The briefings with no passing verdict yet — the ONE definition of \"owed\", shared by the header, the\n // scan verdict and the step numbering, so they cannot disagree about whether there is anything to spawn.\n private owedReviewers(input: ReviewReportInput): ReviewerBriefing[] {\n const reviewedIds = new Set(input.reviewed.map((r: RequiredChecklist): string => r.id));\n return input.briefings.filter((b: ReviewerBriefing): boolean => !reviewedIds.has(b.checklistId));\n }\n\n // Owed AND blocking — spawned without asking.\n private requiredOwed(input: ReviewReportInput): ReviewerBriefing[] {\n return this.owedReviewers(input).filter((b: ReviewerBriefing): boolean => b.required);\n }\n\n // Owed and optional. Still listed under `--no-optional` (as a skip verdict), just never as a step.\n private optionalOwed(input: ReviewReportInput): ReviewerBriefing[] {\n return this.owedReviewers(input).filter((b: ReviewerBriefing): boolean => !b.required);\n }\n\n // The optional ones the human is actually to be ASKED about — none, once they have already answered.\n private offerableOwed(input: ReviewReportInput): ReviewerBriefing[] {\n return input.skipOptional ? [] : this.optionalOwed(input);\n }\n\n // Everything the AI still has to act on. Distinct from `owedReviewers`: a skipped optional checklist is\n // owed a verdict it will never get, and treating it as pending work is what would print a spawn\n // instruction for a review the human just declined.\n private actionableOwed(input: ReviewReportInput): ReviewerBriefing[] {\n return [...this.requiredOwed(input), ...this.offerableOwed(input)];\n }\n\n /**\n * One reviewer's block. A reviewer that already REFUSED gets the SAME spawn coordinates but a different\n * lead-in, because the action before spawning is different: its own words are printed, and the spawn is\n * explicitly conditioned on having fixed the finding first.\n *\n * It keeps its spawn block rather than being dropped from the list, because the reviewer genuinely does\n * still owe a fresh verdict — dropping it would leave nothing anywhere saying how to get one. What must\n * not happen is a bare \"spawn this\" that reads identically to a reviewer that never ran, which is the\n * loop this exists to break.\n */\n private oneSpawnBlock(input: ReviewReportInput, b: ReviewerBriefing): string[] {\n const instructionsFile = this.reviewerInstructions.pathFor(input.repoRoot, input.featureName, b.subagent);\n return [\n ...this.leadIn(input, b),\n ` subagent_type: ${b.subagent}`,\n ' prompt: Read your instructions file FIRST and follow it exactly:',\n ` ${instructionsFile}`,\n '',\n ];\n }\n\n // The lines above the spawn coordinates: normally just why this reviewer is in scope; for one that\n // already refused, its verdict verbatim plus the order the two actions must happen in.\n private leadIn(input: ReviewReportInput, b: ReviewerBriefing): string[] {\n const refusal = input.refused.find((r: RefusedReviewer): boolean => r.checklistId === b.checklistId);\n if (!refusal) return [` ▶ ${b.subagent} — ${this.why(b)}`, ...this.docLine(b)];\n return [\n ` ⛔ ${b.subagent} — ALREADY REVIEWED THIS BRANCH AND REFUSED. It will refuse again on unchanged code.`,\n ` ${refusal.message}`,\n ' FIX THE FINDING FIRST (or record a human-authored override). ONLY THEN spawn it again, to',\n ' write a fresh verdict:',\n ];\n }\n\n /**\n * The checklist's guidance doc, for OPTIONAL reviewers only.\n *\n * \"4 file(s) matched\" plus a broad glob does not tell a human what the review would actually look AT,\n * and they are being asked to decide exactly that. Omitted for required reviewers: there is no decision\n * to inform there — the reviewer runs either way, and the doc is already in its instructions file.\n */\n private docLine(b: ReviewerBriefing): string[] {\n if (b.required || b.docPath === '') return [];\n return [` reviews against: ${b.docPath}`];\n }\n\n // Why this one is in scope. A patternless checklist is NOT \"matched\" — it always runs, over the whole\n // diff, and saying so is what tells a repo its checklist is firing on docs-only PRs by design.\n private why(b: ReviewerBriefing): string {\n if (b.matchedPatterns.length === 0) {\n return `ALWAYS RUNS (no \"patterns\" configured), whole diff in scope — ${b.myFiles.length} file(s)`;\n }\n return `${b.myFiles.length} file(s) matched ${b.matchedPatterns.map((p: string): string => `\"${p}\"`).join(', ')}`;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"review-report.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/review-report.ts"],"names":[],"mappings":";;;;AAAA,yCAA2D;AAC3D,0DAAiI;AACjI,yDAAqD;AAErD,MAAM,GAAG,GAAG,0DAA0D,CAAC;AAEvE;;;;;;;;;;;;;GAaG;AACH,MAAa,eAAe;IACxB,WAAW,CAAS;IACpB,OAAO,CAAS;IAEhB,YAAY,WAAmB,EAAE,OAAe;QAC5C,IAAI,CAAC,WAAW,GAAG,WAAW,CAAC;QAC/B,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;IAC3B,CAAC;CACJ;AARD,0CAQC;AAED;;;;GAIG;AACH,MAAa,iBAAiB;IAC1B,QAAQ,CAAS;IACjB,WAAW,CAAS;IACpB,UAAU,CAAS,CAAa,6DAA6D;IAC7F,YAAY,CAAS,CAAW,iDAAiD;IACjF,eAAe,CAAS,CAAQ,yEAAyE;IACzG,QAAQ,CAAsB,CAAE,gDAAgD;IAChF,YAAY,CAAW,CAAS,0DAA0D;IAC1F,SAAS,CAAqB,CAAE,wDAAwD;IACxF,OAAO,CAAoB,CAAK,6EAA6E;IAC7G;;;;OAIG;IACH,YAAY,CAAU;IAEtB,YAAY,QAAgB,EAAE,WAAmB,EAAE,UAAkB;QACjE,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,WAAW,GAAG,WAAW,CAAC;QAC/B,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;QAC7B,IAAI,CAAC,YAAY,GAAG,CAAC,CAAC;QACtB,IAAI,CAAC,eAAe,GAAG,CAAC,CAAC;QACzB,IAAI,CAAC,QAAQ,GAAG,EAAE,CAAC;QACnB,IAAI,CAAC,YAAY,GAAG,EAAE,CAAC;QACvB,IAAI,CAAC,SAAS,GAAG,EAAE,CAAC;QACpB,IAAI,CAAC,OAAO,GAAG,EAAE,CAAC;QAClB,IAAI,CAAC,YAAY,GAAG,KAAK,CAAC;IAC9B,CAAC;CACJ;AA7BD,8CA6BC;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AAEI,IAAM,YAAY,GAAlB,MAAM,YAAY;IAEA;IACA;IAFrB,YACqB,eAAgC,EAChC,oBAAiD;QADjD,oBAAe,GAAf,eAAe,CAAiB;QAChC,yBAAoB,GAApB,oBAAoB,CAA6B;IACnE,CAAC;IAEJ,MAAM,CAAC,KAAwB;QAC3B,OAAO,IAAI,GAAG,GAAG,GAAG,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,GAAG;cACtC,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC;cACvB,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;IAChC,CAAC;IAED;;;;;;;;;;OAUG;IACK,MAAM,CAAC,KAAwB;QACnC,IAAI,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,MAAM,GAAG,CAAC;YAAE,OAAO,mDAAmD,CAAC;QACpG,IAAI,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC,MAAM,GAAG,CAAC;YAAE,OAAO,uDAAuD,CAAC;QACzG,OAAO,yBAAyB,CAAC;IACrC,CAAC;IAED;;;;OAIG;IACK,WAAW,CAAC,KAAwB;QACxC,IAAI,KAAK,CAAC,eAAe,KAAK,CAAC;YAAE,OAAO,IAAI,GAAG,IAAI,CAAC,eAAe,CAAC,KAAK,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC;QAC9F,MAAM,KAAK,GAAa,EAAE,CAAC;QAC3B,+FAA+F;QAC/F,gGAAgG;QAChG,yFAAyF;QACzF,KAAK,MAAM,CAAC,IAAI,KAAK,CAAC,QAAQ,EAAE,CAAC;YAC7B,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,QAAQ,+EAA+E,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC;QAC7H,CAAC;QACD,gGAAgG;QAChG,kGAAkG;QAClG,2CAA2C;QAC3C,KAAK,MAAM,CAAC,IAAI,KAAK,CAAC,YAAY;YAAE,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;QAC3D,KAAK,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,CAAC;QACxC,IAAI,IAAI,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC,MAAM,KAAK,CAAC;YAAE,KAAK,CAAC,IAAI,CAAC,EAAE,EAAE,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC;QAClF,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,EAAE,CAAC;QAClC,OAAO,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC1C,CAAC;IAED;;;;OAIG;IACK,YAAY,CAAC,KAAwB;QACzC,MAAM,OAAO,GAAG,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC;QACzC,IAAI,CAAC,KAAK,CAAC,YAAY,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,EAAE,CAAC;QAC3D,OAAO;YACH,EAAE;YACF,SAAS,OAAO,CAAC,MAAM,4EAA4E;YACnG,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,CAAmB,EAAU,EAAE,CAAC,UAAU,CAAC,CAAC,QAAQ,MAAM,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;YACxF,oFAAoF;SACvF,CAAC;IACN,CAAC;IAED;;;;;;;;;;;;;;;;OAgBG;IACK,QAAQ,CAAC,KAAwB;QACrC,MAAM,QAAQ,GAAG,KAAK,CAAC,YAAY,IAAI,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,MAAM,GAAG,CAAC;YACtE,CAAC,CAAC,+FAA+F;YACjG,CAAC,CAAC,wEAAwE,CAAC;QAC/E,OAAO,QAAQ,GAAG,IAAI,GAAG,IAAI,CAAC,iBAAiB,EAAE,CAAC;IACtD,CAAC;IAED;;;;;OAKG;IACK,iBAAiB;QACrB,OAAO,CACH,mGAAmG;YACnG,qGAAqG;YACrG,kGAAkG;YAClG,iGAAiG,CACpG,CAAC;IACN,CAAC;IAED;;;;;;;;;;;;;;;;;;;OAmBG;IACK,SAAS,CAAC,KAAwB;QACtC,MAAM,QAAQ,GAAG,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC;QAC1C,MAAM,SAAS,GAAG,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;QAC5C,kGAAkG;QAClG,qGAAqG;QACrG,IAAI,IAAI,GAAG,CAAC,CAAC;QACb,MAAM,KAAK,GAAG,IAAI,CAAC,eAAe,CAAC,KAAK,CAAC,UAAU,EAAE,IAAI,EAAE,CAAC,CAAC;QAC7D,MAAM,KAAK,GAAG,QAAQ,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;QACnF,MAAM,KAAK,GAAG,SAAS,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACrF,OAAO,IAAI,GAAG,GAAG;cACX,YAAY,IAAI,kDAAkD,GAAG,GAAG,GAAG,IAAI;cAC/E,KAAK,GAAG,KAAK,GAAG,KAAK,GAAG,IAAI,CAAC,UAAU,CAAC,IAAI,EAAE,QAAQ,CAAC,MAAM,GAAG,SAAS,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IAChG,CAAC;IAEO,eAAe,CAAC,UAAkB,EAAE,IAAY;QACpD,OAAO,CACH,QAAQ,IAAI,6FAA6F;YACzG,8FAA8F;YAC9F,kGAAkG;YAClG,oGAAoG;YACpG,6FAA6F;YAC7F,IAAA,mCAAoB,EAAC,UAAU,CAAC,GAAG,MAAM,CAC5C,CAAC;IACN,CAAC;IAED;;;;;;;;;OASG;IACK,SAAS,CAAC,KAAwB,EAAE,IAAiC,EAAE,IAAY;QACvF,MAAM,KAAK,GAAa;YACpB,QAAQ,IAAI,kDAAkD,IAAI,CAAC,MAAM,oCAAoC;YAC7G,+FAA+F;YAC/F,gGAAgG;YAChG,EAAE;SACL,CAAC;QACF,KAAK,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,cAAc,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC,CAAC;QAChD,KAAK,MAAM,CAAC,IAAI,IAAI;YAAE,KAAK,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,aAAa,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC;QAClE,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACf,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC5B,CAAC;IAED;;;;;;;;;;;;;;;OAeG;IACK,SAAS,CAAC,KAAwB,EAAE,SAAsC,EAAE,IAAY;QAC5F,MAAM,KAAK,GAAa;YACpB,QAAQ,IAAI,YAAY,SAAS,CAAC,MAAM,wEAAwE;YAChH,wEAAwE;YACxE,EAAE;YACF,2FAA2F;YAC3F,+FAA+F;YAC/F,gEAAgE;YAChE,EAAE;YACF,+FAA+F;YAC/F,4EAA4E;YAC5E,6EAA6E;YAC7E,EAAE;YACF,gGAAgG;YAChG,uEAAuE;YACvE,EAAE;SACL,CAAC;QACF,KAAK,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,cAAc,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC,CAAC;QACrD,KAAK,MAAM,CAAC,IAAI,SAAS;YAAE,KAAK,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,aAAa,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC;QACvE,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACf,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC5B,CAAC;IAED,sGAAsG;IACtG,qGAAqG;IACrG,qGAAqG;IAC7F,cAAc,CAAC,KAAwB,EAAE,KAAkC;QAC/E,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAmB,EAAU,EAAE,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC;QAC/E,MAAM,CAAC,GAAG,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAkB,EAAW,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,MAAM,CAAC;QAC/F,IAAI,CAAC,KAAK,CAAC;YAAE,OAAO,EAAE,CAAC;QACvB,OAAO;YACH,YAAY,CAAC,sEAAsE;YACnF,iGAAiG;YACjG,EAAE;SACL,CAAC;IACN,CAAC;IAEO,UAAU,CAAC,UAAkB,EAAE,YAAqB;QACxD,mGAAmG;QACnG,sGAAsG;QACtG,qBAAqB;QACrB,MAAM,YAAY,GAAG,YAAY;YAC7B,CAAC,CAAC,0DAA0D;YAC5D,CAAC,CAAC,uBAAuB,CAAC;QAC9B,OAAO,CACH,QAAQ,UAAU,WAAW,YAAY,oCAAoC;YAC7E,sGAAsG,CACzG,CAAC;IACN,CAAC;IAED,sGAAsG;IACtG,yGAAyG;IACjG,aAAa,CAAC,KAAwB;QAC1C,MAAM,WAAW,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAoB,EAAU,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;QACxF,OAAO,KAAK,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAmB,EAAW,EAAE,CAAC,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC;IACrG,CAAC;IAED,8CAA8C;IACtC,YAAY,CAAC,KAAwB;QACzC,OAAO,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC,CAAmB,EAAW,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC;IAC1F,CAAC;IAED,mGAAmG;IAC3F,YAAY,CAAC,KAAwB;QACzC,OAAO,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC,CAAmB,EAAW,EAAE,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC;IAC3F,CAAC;IAED,qGAAqG;IAC7F,aAAa,CAAC,KAAwB;QAC1C,OAAO,KAAK,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC;IAC9D,CAAC;IAED,wGAAwG;IACxG,gGAAgG;IAChG,oDAAoD;IAC5C,cAAc,CAAC,KAAwB;QAC3C,OAAO,CAAC,GAAG,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC,EAAE,GAAG,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC,CAAC;IACvE,CAAC;IAED;;;;;;;;;OASG;IACK,aAAa,CAAC,KAAwB,EAAE,CAAmB;QAC/D,MAAM,gBAAgB,GAAG,IAAI,CAAC,oBAAoB,CAAC,OAAO,CAAC,KAAK,CAAC,QAAQ,EAAE,KAAK,CAAC,WAAW,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC;QAC1G,OAAO;YACH,GAAG,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC;YACxB,wBAAwB,CAAC,CAAC,QAAQ,EAAE;YACpC,+EAA+E;YAC/E,wBAAwB,gBAAgB,EAAE;YAC1C,EAAE;SACL,CAAC;IACN,CAAC;IAED,mGAAmG;IACnG,uFAAuF;IAC/E,MAAM,CAAC,KAAwB,EAAE,CAAmB;QACxD,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAkB,EAAW,EAAE,CAAC,CAAC,CAAC,WAAW,KAAK,CAAC,CAAC,WAAW,CAAC,CAAC;QACrG,IAAI,CAAC,OAAO;YAAE,OAAO,CAAC,OAAO,CAAC,CAAC,QAAQ,MAAM,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,GAAG,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;QAChF,OAAO;YACH,OAAO,CAAC,CAAC,QAAQ,sFAAsF;YACvG,SAAS,OAAO,CAAC,OAAO,EAAE;YAC1B,iGAAiG;YACjG,8BAA8B;SACjC,CAAC;IACN,CAAC;IAED;;;;;;OAMG;IACK,OAAO,CAAC,CAAmB;QAC/B,IAAI,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC,OAAO,KAAK,EAAE;YAAE,OAAO,EAAE,CAAC;QAC9C,OAAO,CAAC,0BAA0B,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC;IACnD,CAAC;IAED,sGAAsG;IACtG,+FAA+F;IACvF,GAAG,CAAC,CAAmB;QAC3B,IAAI,CAAC,CAAC,eAAe,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACjC,OAAO,iEAAiE,CAAC,CAAC,OAAO,CAAC,MAAM,UAAU,CAAC;QACvG,CAAC;QACD,OAAO,GAAG,CAAC,CAAC,OAAO,CAAC,MAAM,oBAAoB,CAAC,CAAC,eAAe,CAAC,GAAG,CAAC,CAAC,CAAS,EAAU,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;IACtH,CAAC;CACJ,CAAA;AArUY,oCAAY;uBAAZ,YAAY;IADxB,IAAA,sBAAU,EAAC,8BAAkB,CAAC,SAAS,CAAC;6CAGC,kCAAe;QACV,0CAA2B;GAH7D,YAAY,CAqUxB","sourcesContent":["import { injectable, bindingScopeValues } from 'inversify';\nimport { reviewJsonSchemaHint, RequiredChecklist, ReviewerBriefing, ReviewerInstructionsService } from '@webpieces/rules-config';\nimport { ChecklistNotice } from './checklist-notice';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n/**\n * One reviewer that ALREADY ANSWERED on this branch and refused, with the refusal rendered by\n * `ReviewJsonService.refusalError` — the ONE wording, shared with `wp-finish-upsert-pr`. Data-only.\n *\n * Stage ② carries these because it had the same defect finish did: a refused checklist has no passing\n * verdict, so it is \"owed\", so it got an ordinary spawn block identical to a reviewer that never ran. An\n * agent obeys that block, the reviewer re-reads unchanged code, refuses again — the loop, one stage earlier.\n *\n * The message is rendered by the COMMAND rather than in here for one reason: stage ② must not archive\n * anything. `refusalError` called with no archive path says \"fix it, then re-run\", which is only true while\n * the verdict file is still live — and at stage ② it is. Retiring a verdict is finish's act, on the refusal\n * it is actually acting on. (The ship-anyway route it prints — the command that writes `override-<id>.json`\n * — is correct at either stage: that is a different file, with its own writer, and no archive touches it.)\n */\nexport class RefusedReviewer {\n checklistId: string;\n message: string;\n\n constructor(checklistId: string, message: string) {\n this.checklistId = checklistId;\n this.message = message;\n }\n}\n\n/**\n * Everything the closing block of `wp-review-upsert-pr` needs. Data-only, and a class rather than an\n * object literal per CLAUDE.md. The three identifying paths are constructor args; the rest are optional\n * facts about the scan that default to \"nothing\", so a repo with no checklists constructs it in one line.\n */\nexport class ReviewReportInput {\n repoRoot: string;\n featureName: string;\n reviewPath: string; // the branch's review.json — the file the AI must write next\n definedCount: number; // how many checklists pr-gate.checklists defines\n applicableCount: number; // how many of them apply to THIS diff (0 ⇒ the notice, not spawn blocks)\n reviewed: RequiredChecklist[]; // already have a passing verdict on this branch\n formatErrors: string[]; // verdict files that exist but cannot be read as verdicts\n briefings: ReviewerBriefing[]; // one per applicable checklist, already written to disk\n refused: RefusedReviewer[]; // of the owed ones, those that already ran and said no (see RefusedReviewer)\n /**\n * `--no-optional` was passed: the human has already said to submit without the optional reviews, so the\n * block that offers them is replaced by a one-line statement that they were skipped. It never suppresses\n * a REQUIRED reviewer, and it is not a gate — nothing about what blocks the PR changes.\n */\n skipOptional: boolean;\n\n constructor(repoRoot: string, featureName: string, reviewPath: string) {\n this.repoRoot = repoRoot;\n this.featureName = featureName;\n this.reviewPath = reviewPath;\n this.definedCount = 0;\n this.applicableCount = 0;\n this.reviewed = [];\n this.formatErrors = [];\n this.briefings = [];\n this.refused = [];\n this.skipOptional = false;\n }\n}\n\n/**\n * Renders the closing block of `wp-review-upsert-pr` — the checklist verdict, then EXACTLY ONE \"what to do\n * next\".\n *\n * Extracted from the command so it can be asserted on as a rendered string, because the ordering IS the\n * contract. The bug it was extracted to fix: the zero-checklist notice ended with \"Carry on and run: pnpm\n * wp-finish-upsert-pr\", and the block printed directly beneath it said to write review.json first and\n * finish afterwards. Two next-steps, in the wrong order, and an agent that follows instructions literally —\n * which is the entire reason this command prints them — took the first one and opened a PR with no review.\n * That is precisely the failure the three-stage flow exists to prevent (reported on PR #519).\n *\n * The invariants, enforced by review-report.spec.ts:\n * 1. `wp-finish-upsert-pr` is named as a thing to run EXACTLY ONCE in the whole block.\n * 2. The review.json instruction comes BEFORE it — and BEFORE the spawn blocks (see nextSteps).\n * 3. With zero checklists the all-clear precedes any configuration guidance.\n * 4. REQUIRED reviewers are spawned unasked; OPTIONAL ones are only ever OFFERED, in one batched\n * question. The two never share a step, because one instruction says \"do it\" and the other says\n * \"ask first\", and an agent reading a merged list will act on the stronger of the two.\n *\n * Pure string building, no I/O. `@injectable(bindingScopeValues.Singleton)` so it is injected by type.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class ReviewReport {\n constructor(\n private readonly checklistNotice: ChecklistNotice,\n private readonly reviewerInstructions: ReviewerInstructionsService,\n ) {}\n\n render(input: ReviewReportInput): string {\n return '\\n' + SEP + this.header(input) + SEP\n + this.scanVerdict(input)\n + this.nextSteps(input);\n }\n\n /**\n * Name what this block is actually about. A repo with reviewers owed is being told to SPAWN; a repo with\n * none is not, and promising subagents it does not have is the same kind of noise as explaining checklist\n * configuration to a repo that configured none. Keyed on what is actually ACTIONABLE rather than on the\n * applicable count: every applicable checklist already having a verdict means nothing to spawn, and so\n * does a branch whose only outstanding reviews are optional ones the human already waved off.\n *\n * \"spawn\" and \"ask about\" are separate headings because they are separate obligations. A branch owing\n * only optional reviews has nothing the AI may do unilaterally, and a heading that says SPAWN is the\n * single line most likely to make it do exactly that.\n */\n private header(input: ReviewReportInput): string {\n if (this.requiredOwed(input).length > 0) return '② Review, spawn subagent reviewers, then finish\\n';\n if (this.offerableOwed(input).length > 0) return '② Review, offer the optional reviewers, then finish\\n';\n return '② Review, then finish\\n';\n }\n\n /**\n * What the SCAN found: either \"nothing applies here\" or the already-reviewed / unreadable-verdict lines.\n * Verdicts, not instructions — every instruction lives in nextSteps() below, so no line up here can be\n * mistaken for the next action.\n */\n private scanVerdict(input: ReviewReportInput): string {\n if (input.applicableCount === 0) return '\\n' + this.checklistNotice.build(input.definedCount);\n const lines: string[] = [];\n // The prohibition rides on the REUSE line itself, not only in the all-clear below, because the\n // all-clear is not printed when anything is still owed — and \"some reviewers are reused, others\n // must be spawned\" is exactly the shape in which an agent re-spawns the reused ones too.\n for (const r of input.reviewed) {\n lines.push(` ✓ ${r.subagent} — already reviewed on this branch; verdict STANDS, do NOT re-spawn (review-${r.id}.json)`);\n }\n // A verdict file that EXISTS but is unreadable as a verdict is called out here. Without it this\n // reports the checklist as simply owed, and the AI re-runs a reviewer that already ran instead of\n // correcting the file sitting right there.\n for (const e of input.formatErrors) lines.push(` ⛔ ${e}`);\n lines.push(...this.skippedLines(input));\n if (this.actionableOwed(input).length === 0) lines.push('', this.allClear(input));\n if (lines.length === 0) return '';\n return '\\n' + lines.join('\\n') + '\\n';\n }\n\n /**\n * `--no-optional`, stated as a VERDICT rather than left silent. Named individually, not just counted: the\n * whole reason the human is allowed to skip these is that they know this diff, and the only way they can\n * catch \"wait, not THAT one\" is to see which ones went unreviewed.\n */\n private skippedLines(input: ReviewReportInput): string[] {\n const skipped = this.optionalOwed(input);\n if (!input.skipOptional || skipped.length === 0) return [];\n return [\n '',\n ` ⏭️ ${skipped.length} OPTIONAL checklist(s) matched this diff and were SKIPPED (--no-optional):`,\n ...skipped.map((b: ReviewerBriefing): string => ` ${b.subagent} — ${this.why(b)}`),\n ' Not blocking. Drop the flag and re-run this command to offer them after all.',\n ];\n }\n\n /**\n * The all-clear, then the RULE that makes it actionable.\n *\n * \"nothing to spawn\" on its own is a description of the current state, and an agent that has just been\n * told a state — rather than a rule — treats re-spawning as a judgement call it is entitled to make. It\n * then makes it, reasoning (correctly, on the facts) that the carried-forward verdicts judged an earlier\n * tree. Reviews here are once per branch BY CONSTRUCTION: a passing review-<id>.json satisfies its\n * checklist for the branch's whole life, and `wp-finish-upsert-pr` never archives one the way it archives\n * review.json. So the reuse is deliberate — it is what keeps post-PR iteration from re-paying for every\n * matched reviewer — and the output has to say so, because the alternative reading is the expensive one.\n *\n * The overwrite warning is not decoration. A re-spawned reviewer writes to the SAME verdict path, so a\n * gratuitous re-run does not merely cost a subagent — it destroys the verdict that was already banked.\n *\n * It must NOT claim everything was reviewed when optional reviews were skipped — that is the one sentence\n * that would turn a deliberate skip into a false record of a review that happened.\n */\n private allClear(input: ReviewReportInput): string {\n const headline = input.skipOptional && this.optionalOwed(input).length > 0\n ? '✅ Nothing left to spawn — every REQUIRED checklist is reviewed (optional ones skipped above).'\n : '✅ Every checklist that applies is already reviewed — nothing to spawn.';\n return headline + '\\n' + this.oncePerBranchRule();\n }\n\n /**\n * The once-per-branch rule, stated as a prohibition rather than left to be inferred from a ✅.\n *\n * Printed whenever verdicts are being reused — which is every iteration of a PR after the first, i.e. the\n * majority of stage-② runs on any branch that gets review feedback.\n */\n private oncePerBranchRule(): string {\n return (\n ' Reviews here are ONCE PER BRANCH: a passing verdict carries forward to every later iteration\\n' +\n ' of this PR, deliberately, so post-PR edits cost no reviewer tokens. Do NOT re-spawn a reviewer\\n' +\n ' listed above to \"re-check\" the newer code — it burns a full subagent run AND overwrites the\\n' +\n ' verdict file it already wrote. The only reviewers you may spawn are ones a STEP below names.'\n );\n }\n\n /**\n * The ONE instruction block, and the last thing the stage prints. Numbered rather than prose-linked\n * (\"Then… Finally…\") so that skipping step 1 is visibly skipping a step, and worded so no earlier line\n * can be mistaken for the real next action.\n *\n * review.json is STEP 1 and the spawn blocks are STEP 2 — that ORDER is the contract, not a preference.\n * This block used to print the spawn blocks first and then say to write review.json \"WHILE any reviewer\n * subagents above are still running\", which does not merely permit spawning first, it instructs it.\n * Harmless for a reviewer that only reads the diff; wrong for one that judges the PR's stated INTENT —\n * its title, summary or risk level — because review.json is the only place that intent lives. Such a\n * reviewer either finds no file (a false RED and a wasted reviewer run) or, on a second run of this\n * stage on the same branch, finds the PREVIOUS run's file and validates a title that no longer exists —\n * a false GREEN, with nothing in the output saying which of the two happened. A consuming repo had to\n * write itself a rule telling its agents to DISOBEY this block to work around it.\n *\n * What the old ordering bought was overlap on a single local file write, not a subagent round-trip.\n *\n * The schema hint is rendered by ReviewJsonService — the single renderer — so the shape printed here\n * can never drift from the shape `wp-finish-upsert-pr` validates.\n */\n private nextSteps(input: ReviewReportInput): string {\n const required = this.requiredOwed(input);\n const offerable = this.offerableOwed(input);\n // Numbered by what is actually PRINTED, so the numbers a reader sees are 1..n with no gaps: write\n // review.json, then a spawn step only if anything must run, then an offer step only if anything may.\n let step = 1;\n const write = this.writeReviewStep(input.reviewPath, step++);\n const spawn = required.length === 0 ? '' : this.spawnStep(input, required, step++);\n const offer = offerable.length === 0 ? '' : this.offerStep(input, offerable, step++);\n return '\\n' + SEP\n + `▶ NEXT — ${step} steps, in this order. Step 1 is NOT optional:\\n` + SEP + '\\n'\n + write + spawn + offer + this.finishStep(step, required.length + offerable.length > 0);\n }\n\n private writeReviewStep(reviewPath: string, step: number): string {\n return (\n `STEP ${step} — review your own changes, then write the review file. Write it FIRST — BEFORE you spawn\\n` +\n ' anything below. finish REFUSES without it, and a reviewer subagent may READ it: a\\n' +\n ' checklist that judges the PR title, summary or risk level reads exactly this file, so\\n' +\n ' writing it afterwards races that reviewer into seeing nothing — or, on a re-run of this\\n' +\n ' stage, into judging the PREVIOUS run\\'s review of code that has since changed.\\n\\n' +\n reviewJsonSchemaHint(reviewPath) + '\\n\\n'\n );\n }\n\n /**\n * The REQUIRED reviewers — one copy-paste block each, and nothing at all when none is owed. The prompt is\n * deliberately a POINTER and nothing else: the generated instructions file is the contract, so anything\n * restated here is a second copy that can go stale — which is exactly how a removed `success` field\n * outlived its own removal in print.\n *\n * These are spawned WITHOUT asking. They are the checklists the repo declared `required: true`, which is\n * the repo saying the decision was already made; putting them to the human again would re-open a question\n * the config exists to settle.\n */\n private spawnStep(input: ReviewReportInput, owed: readonly ReviewerBriefing[], step: number): string {\n const lines: string[] = [\n `STEP ${step} — only once that file is written, spawn these ${owed.length} REQUIRED reviewer subagent(s) — a`,\n ' SEPARATE one each. They block the PR, so do NOT ask whether to run them. You may NOT',\n ' review your own work, and you may NOT write a reviewer\\'s verdict file on its behalf.',\n '',\n ];\n lines.push(...this.refusedWarning(input, owed));\n for (const b of owed) lines.push(...this.oneSpawnBlock(input, b));\n lines.push('');\n return lines.join('\\n');\n }\n\n /**\n * STEP n — the OPTIONAL reviewers: listed, never spawned unasked.\n *\n * This is the whole point of `required: false`. A one-line bug fix in a repo whose checklists key on a\n * glob as broad as every TypeScript file otherwise pays for a dozen subagent reviews, and the only party\n * who can judge whether this particular diff is worth them is the human looking at it.\n *\n * ONE batched multi-select question, explicitly. Asked one at a time, a human answering \"no\" nine times\n * is being worn down rather than consulted, and by the third question the cheap thing is to say yes to\n * everything — which is the state this feature exists to leave. The \"None\" option has to be spelled out\n * too: an agent that offers a list without an explicit way to decline it has not really offered a choice.\n *\n * The blocking consequence is stated because it is the one non-obvious part of the contract: `required`\n * governs whether a reviewer must RUN, not whether its answer counts. Choosing to run one and then\n * shrugging off a red verdict would make the whole exercise theater.\n */\n private offerStep(input: ReviewReportInput, offerable: readonly ReviewerBriefing[], step: number): string {\n const lines: string[] = [\n `STEP ${step} — these ${offerable.length} OPTIONAL review checklist(s) matched this diff. They do NOT block the`,\n ' PR, and you may NOT decide for the human whether to run them.',\n '',\n ' ASK THE HUMAN, in ONE multi-select question listing all of them plus an explicit',\n ' \"None — required only\" choice. Do not ask one question per reviewer. Then spawn ONLY',\n ' what they picked, the same way as any other reviewer.',\n '',\n ' If they pick none, that is a complete answer: go straight to the final step. If they',\n ' told you up front to submit without reviews, re-run this stage as',\n ' `pnpm wp-review-upsert-pr --no-optional` and this step disappears.',\n '',\n ' NOTE: whichever ones you DO run, their verdicts count in full — a red verdict from an',\n ' optional reviewer blocks the PR exactly like a required one.',\n '',\n ];\n lines.push(...this.refusedWarning(input, offerable));\n for (const b of offerable) lines.push(...this.oneSpawnBlock(input, b));\n lines.push('');\n return lines.join('\\n');\n }\n\n // Said up front, not only beside the block: an agent that has decided to spawn everything listed here\n // needs to know BEFORE it starts that one of these entries is not a spawn-shaped task. Scoped to the\n // group being printed — a refusal among the REQUIRED reviewers is not a caveat on the optional list.\n private refusedWarning(input: ReviewReportInput, group: readonly ReviewerBriefing[]): string[] {\n const ids = new Set(group.map((b: ReviewerBriefing): string => b.checklistId));\n const n = input.refused.filter((r: RefusedReviewer): boolean => ids.has(r.checklistId)).length;\n if (n === 0) return [];\n return [\n ` ${n} of them already ANSWERED and refused (marked ⛔ below). Do not spawn`,\n ' those against unchanged code — fix what they found first; the fix is the prerequisite.',\n '',\n ];\n }\n\n private finishStep(stepNumber: number, anyReviewers: boolean): string {\n // \"every reviewer you ran\" rather than \"every reviewer above\": with an optional list the human may\n // legitimately have run none of them, and a precondition naming reviewers that were declined reads as\n // an unmeetable one.\n const precondition = anyReviewers\n ? 'once every reviewer you ran has written its verdict file'\n : 'once that file exists';\n return (\n `STEP ${stepNumber} — only ${precondition}, run: pnpm wp-finish-upsert-pr\\n` +\n ' (The build gate is already green for this commit — finish reuses it unless HEAD moves.)\\n\\n'\n );\n }\n\n // The briefings with no passing verdict yet — the ONE definition of \"owed\", shared by the header, the\n // scan verdict and the step numbering, so they cannot disagree about whether there is anything to spawn.\n private owedReviewers(input: ReviewReportInput): ReviewerBriefing[] {\n const reviewedIds = new Set(input.reviewed.map((r: RequiredChecklist): string => r.id));\n return input.briefings.filter((b: ReviewerBriefing): boolean => !reviewedIds.has(b.checklistId));\n }\n\n // Owed AND blocking — spawned without asking.\n private requiredOwed(input: ReviewReportInput): ReviewerBriefing[] {\n return this.owedReviewers(input).filter((b: ReviewerBriefing): boolean => b.required);\n }\n\n // Owed and optional. Still listed under `--no-optional` (as a skip verdict), just never as a step.\n private optionalOwed(input: ReviewReportInput): ReviewerBriefing[] {\n return this.owedReviewers(input).filter((b: ReviewerBriefing): boolean => !b.required);\n }\n\n // The optional ones the human is actually to be ASKED about — none, once they have already answered.\n private offerableOwed(input: ReviewReportInput): ReviewerBriefing[] {\n return input.skipOptional ? [] : this.optionalOwed(input);\n }\n\n // Everything the AI still has to act on. Distinct from `owedReviewers`: a skipped optional checklist is\n // owed a verdict it will never get, and treating it as pending work is what would print a spawn\n // instruction for a review the human just declined.\n private actionableOwed(input: ReviewReportInput): ReviewerBriefing[] {\n return [...this.requiredOwed(input), ...this.offerableOwed(input)];\n }\n\n /**\n * One reviewer's block. A reviewer that already REFUSED gets the SAME spawn coordinates but a different\n * lead-in, because the action before spawning is different: its own words are printed, and the spawn is\n * explicitly conditioned on having fixed the finding first.\n *\n * It keeps its spawn block rather than being dropped from the list, because the reviewer genuinely does\n * still owe a fresh verdict — dropping it would leave nothing anywhere saying how to get one. What must\n * not happen is a bare \"spawn this\" that reads identically to a reviewer that never ran, which is the\n * loop this exists to break.\n */\n private oneSpawnBlock(input: ReviewReportInput, b: ReviewerBriefing): string[] {\n const instructionsFile = this.reviewerInstructions.pathFor(input.repoRoot, input.featureName, b.subagent);\n return [\n ...this.leadIn(input, b),\n ` subagent_type: ${b.subagent}`,\n ' prompt: Read your instructions file FIRST and follow it exactly:',\n ` ${instructionsFile}`,\n '',\n ];\n }\n\n // The lines above the spawn coordinates: normally just why this reviewer is in scope; for one that\n // already refused, its verdict verbatim plus the order the two actions must happen in.\n private leadIn(input: ReviewReportInput, b: ReviewerBriefing): string[] {\n const refusal = input.refused.find((r: RefusedReviewer): boolean => r.checklistId === b.checklistId);\n if (!refusal) return [` ▶ ${b.subagent} — ${this.why(b)}`, ...this.docLine(b)];\n return [\n ` ⛔ ${b.subagent} — ALREADY REVIEWED THIS BRANCH AND REFUSED. It will refuse again on unchanged code.`,\n ` ${refusal.message}`,\n ' FIX THE FINDING FIRST (or record a human-authored override). ONLY THEN spawn it again, to',\n ' write a fresh verdict:',\n ];\n }\n\n /**\n * The checklist's guidance doc, for OPTIONAL reviewers only.\n *\n * \"4 file(s) matched\" plus a broad glob does not tell a human what the review would actually look AT,\n * and they are being asked to decide exactly that. Omitted for required reviewers: there is no decision\n * to inform there — the reviewer runs either way, and the doc is already in its instructions file.\n */\n private docLine(b: ReviewerBriefing): string[] {\n if (b.required || b.docPath === '') return [];\n return [` reviews against: ${b.docPath}`];\n }\n\n // Why this one is in scope. A patternless checklist is NOT \"matched\" — it always runs, over the whole\n // diff, and saying so is what tells a repo its checklist is firing on docs-only PRs by design.\n private why(b: ReviewerBriefing): string {\n if (b.matchedPatterns.length === 0) {\n return `ALWAYS RUNS (no \"patterns\" configured), whole diff in scope — ${b.myFiles.length} file(s)`;\n }\n return `${b.myFiles.length} file(s) matched ${b.matchedPatterns.map((p: string): string => `\"${p}\"`).join(', ')}`;\n }\n}\n"]}
|
|
@@ -138,7 +138,7 @@ let ReviewerVerdictGate = class ReviewerVerdictGate {
|
|
|
138
138
|
*/
|
|
139
139
|
retireAndReport(scan, req) {
|
|
140
140
|
const verdict = this.reviewJsonService.resolveVerdict(req, scan.results);
|
|
141
|
-
return this.reviewJsonService.refusalError(req, verdict, this.archiveOrWarn(scan.reviewPath, req.id));
|
|
141
|
+
return this.reviewJsonService.refusalError(req, verdict, scan.reviewPath, this.archiveOrWarn(scan.reviewPath, req.id));
|
|
142
142
|
}
|
|
143
143
|
// The archive path, or '' when there was nothing to move or the move failed (see retireAndReport).
|
|
144
144
|
archiveOrWarn(reviewPath, checklistId) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reviewer-verdict-gate.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/reviewer-verdict-gate.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n ChecklistInstructionsService, InformAiError, RequiredChecklist, ReviewJsonService, toError,\n} from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { ChecklistScan } from './checklist-scanner';\n\n/**\n * The gate `wp-finish-upsert-pr` runs before it parses review.json: REFUSE the PR while any applicable\n * checklist is not clear, and say — per checklist — which of three different things went wrong.\n *\n * It is its own class, not a private method on the command, for two reasons. It is the one piece of finish\n * that both MUTATES the branch (it retires red verdicts) and produces the text an AI acts on, so it is the\n * piece most worth asserting on directly; and the command it came out of is already the largest file in this\n * package, against a hard `@webpieces/max-file-lines` limit.\n *\n * THE BUG THIS EXISTS TO FIX. The message used to be one bucket and one imperative — \"You MUST run these N\n * reviewer subagent(s)\" — for three unrelated states. Handed to an agent, the literal, obedient response for\n * a checklist that had ALREADY REFUSED was to spawn the reviewer again; it re-read the same unchanged code,\n * refused again, and the loop cost a full subagent run per pass while the reviewer's actual finding was\n * never printed at all. Three states, three actions, in this order:\n *\n * 1. UNREADABLE (`scan.formatErrors`) -> fix four characters of JSON. Not a reviewer problem.\n * 2. REFUSED (CK_FAIL) -> fix the finding (or get a HUMAN override). Not a missing step.\n * 3. NEVER RAN (the rest) -> spawn the subagent. The ONLY case where that is the right move.\n *\n * `@injectable(bindingScopeValues.Singleton)` so it is injected by type and drawn in the DI design.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class ReviewerVerdictGate {\n constructor(\n private readonly reviewJsonService: ReviewJsonService,\n private readonly instructions: ChecklistInstructionsService,\n ) {}\n\n /**\n * Refuse the PR while ANY applicable checklist still owes a passing verdict — naming exactly what each\n * one needs. No-op for a repo with no applicable checklists.\n *\n * SIDE EFFECT, deliberately: every REFUSED checklist's verdict file is RETIRED to review-<id>.json.old on\n * the way out (see {@link retireAndReport}). Only red verdicts are ever moved — a green, yellow or\n * overridden one is reused across finish attempts and retiring it would force a needless subagent re-run.\n */\n assertEveryReviewerRan(scan: ChecklistScan): void {\n if (scan.outstanding.length === 0) return;\n // The split is computed from the verdicts the SCAN already loaded, never from a second read of disk:\n // a re-read here could disagree with the set that produced `outstanding`, and a checklist that is\n // \"outstanding\" per one read and \"clear\" per another belongs to neither section of the message.\n const refused = this.reviewJsonService.refusedChecklists(scan.outstanding, scan.results);\n const refusedIds = new Set(refused.map((r: RequiredChecklist): string => r.id));\n const neverRan = scan.outstanding.filter((r: RequiredChecklist): boolean => !refusedIds.has(r.id));\n // Rendered BEFORE the throw and, per checklist, with the verdict resolved before its file is moved —\n // the message quotes the reviewer's own words, so losing them to the move would defeat the point.\n const refusals = refused.map((req: RequiredChecklist): string => this.retireAndReport(scan, req));\n throw new InformAiError(\n this.headline(scan, refused, neverRan)\n + this.unreadableSection(scan)\n + this.refusedSection(refusals)\n + this.neverRanSection(scan, neverRan)\n + this.footer(refused.length > 0),\n );\n }\n\n /**\n * Say plainly how many REFUSED and how many never ran.\n *\n * The old headline said all of them \"have no passing verdict yet\", which is technically true of a refusal\n * and reads as though nobody had looked. It is the sentence that framed a decision as an omission.\n */\n private headline(scan: ChecklistScan, refused: readonly RequiredChecklist[], neverRan: readonly RequiredChecklist[]): string {\n const lines = [\n `⛔ NO PR — ${scan.outstanding.length} of ${scan.applicable.length} review checklist(s) that apply to this branch are not clear:`,\n ];\n if (refused.length > 0) {\n lines.push(` • ${refused.length} REFUSED — a reviewer ran, judged this change, and said no: ${this.instructions.names(refused)}`);\n }\n if (neverRan.length > 0) {\n lines.push(` • ${neverRan.length} never ran — no verdict has been written yet: ${this.instructions.names(neverRan)}`);\n }\n return lines.join('\\n') + this.skippedNote(scan) + '\\n\\n';\n }\n\n /**\n * The optional checklists nobody ran, stated OUT LOUD on the refusal.\n *\n * They are not why the PR is being refused and this block says so — but a message that lists only what\n * blocked reads as \"everything else was reviewed\", and here that is false: those reviews were declined,\n * not passed. The one moment a human is looking at the review state is the moment to be honest about\n * which reviews did not happen, and it is also the prompt an AI needs to offer them again if the human\n * has changed their mind now that something else came back red.\n */\n private skippedNote(scan: ChecklistScan): string {\n if (scan.optionalNotRun.length === 0) return '';\n return `\\n (Not blocking: ${scan.optionalNotRun.length} OPTIONAL checklist(s) were not run — `\n + `${this.instructions.names(scan.optionalNotRun)}. They do not have to run; re-run\\n`\n + ' pnpm wp-review-upsert-pr if you now want to offer them.)';\n }\n\n // Unreadable verdict files come FIRST. A reviewer that wrote its verdict in the removed `success` format\n // is otherwise indistinguishable from one that never ran, and the AI would go re-run a subagent instead\n // of correcting four characters of JSON.\n private unreadableSection(scan: ChecklistScan): string {\n if (scan.formatErrors.length === 0) return '';\n return `${scan.formatErrors.length} verdict file(s) are in an UNREADABLE format:\\n\\n`\n + scan.formatErrors.map((e: string): string => ` • ${e}`).join('\\n') + '\\n\\n';\n }\n\n /**\n * The refusals — SECOND, above anything that says to spawn a subagent, because a spawn instruction is the\n * one line an agent acts on first, and acting on it here IS the loop.\n */\n private refusedSection(refusals: readonly string[]): string {\n if (refusals.length === 0) return '';\n return '⛔ REFUSED — these reviewers ALREADY ANSWERED. Re-spawning one against unchanged code only buys\\n'\n + ' the same answer: fix what it found (or get a HUMAN to authorize an override), THEN review again.\\n\\n'\n + refusals.map((r: string): string => ` • ${r}`).join('\\n\\n') + '\\n\\n';\n }\n\n /**\n * The reviewers that genuinely never ran — LAST, and listing ONLY these. When there are none this block\n * is absent entirely, which is the whole point: the \"You MUST run these N reviewer subagent(s)\"\n * imperative must not appear at all on a run whose only problem is a refusal.\n */\n private neverRanSection(scan: ChecklistScan, neverRan: readonly RequiredChecklist[]): string {\n if (neverRan.length === 0) return '';\n return '❓ NO VERDICT YET — nothing has been written for these, so they must actually be run:\\n\\n'\n + `${this.instructions.render(neverRan, scan.reviewPath, scan.context)}\\n\\n`;\n }\n\n /**\n * Retire ONE refused checklist's verdict and render the refusal that reports it.\n *\n * ORDER IS LOAD-BEARING. The verdict is resolved from the already-loaded results, so the finding is in\n * hand before the file moves; the move then happens; and only then is the message rendered, because it\n * names the archive path and must not claim a move that did not happen.\n *\n * The move is NON-FATAL. A finish that died after retiring a verdict would leave the AI with a red\n * verdict gone from the live path and no message saying where it went or why the PR was refused — a\n * silent gap, and the worst possible outcome for a feature whose entire purpose is a durable record. So a\n * failed move warns and the refusal is reported without an archive path (the file is then still live,\n * which the un-archived wording of `refusalError` describes correctly).\n */\n private retireAndReport(scan: ChecklistScan, req: RequiredChecklist): string {\n const verdict = this.reviewJsonService.resolveVerdict(req, scan.results);\n return this.reviewJsonService.refusalError(req, verdict, this.archiveOrWarn(scan.reviewPath, req.id));\n }\n\n // The archive path, or '' when there was nothing to move or the move failed (see retireAndReport).\n private archiveOrWarn(reviewPath: string, checklistId: string): string {\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: a failed archive must never swallow the refusal it belongs to\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return this.reviewJsonService.archiveChecklistResult(reviewPath, checklistId);\n } catch (err: unknown) {\n const error = toError(err);\n process.stderr.write(\n `⚠️ Could not retire the refused verdict for \"${checklistId}\" (non-fatal — the refusal is still `\n + `reported below): ${error.message}\\n`);\n return '';\n }\n }\n\n // Re-running stage 2 is normally optional. It stops being optional after a refusal, because the only way\n // past one is to CHANGE the code — which is exactly the condition that makes the extracted diff and every\n // reviewer briefing stale.\n private footer(anyRefused: boolean): string {\n if (anyRefused) {\n return 'Then re-run: pnpm wp-review-upsert-pr (the code changed, so the extracted diff and the reviewer\\n'\n + 'briefings are stale), re-run the reviewer(s) above, and finally: pnpm wp-finish-upsert-pr';\n }\n return 'Then re-run: pnpm wp-finish-upsert-pr\\n'\n + '(Each reviewer\\'s generated instructions file is already written — re-running pnpm wp-review-upsert-pr\\n'\n + ' is only needed if the code changed since it ran.)';\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"reviewer-verdict-gate.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/reviewer-verdict-gate.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n ChecklistInstructionsService, InformAiError, RequiredChecklist, ReviewJsonService, toError,\n} from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { ChecklistScan } from './checklist-scanner';\n\n/**\n * The gate `wp-finish-upsert-pr` runs before it parses review.json: REFUSE the PR while any applicable\n * checklist is not clear, and say — per checklist — which of three different things went wrong.\n *\n * It is its own class, not a private method on the command, for two reasons. It is the one piece of finish\n * that both MUTATES the branch (it retires red verdicts) and produces the text an AI acts on, so it is the\n * piece most worth asserting on directly; and the command it came out of is already the largest file in this\n * package, against a hard `@webpieces/max-file-lines` limit.\n *\n * THE BUG THIS EXISTS TO FIX. The message used to be one bucket and one imperative — \"You MUST run these N\n * reviewer subagent(s)\" — for three unrelated states. Handed to an agent, the literal, obedient response for\n * a checklist that had ALREADY REFUSED was to spawn the reviewer again; it re-read the same unchanged code,\n * refused again, and the loop cost a full subagent run per pass while the reviewer's actual finding was\n * never printed at all. Three states, three actions, in this order:\n *\n * 1. UNREADABLE (`scan.formatErrors`) -> fix four characters of JSON. Not a reviewer problem.\n * 2. REFUSED (CK_FAIL) -> fix the finding (or get a HUMAN override). Not a missing step.\n * 3. NEVER RAN (the rest) -> spawn the subagent. The ONLY case where that is the right move.\n *\n * `@injectable(bindingScopeValues.Singleton)` so it is injected by type and drawn in the DI design.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class ReviewerVerdictGate {\n constructor(\n private readonly reviewJsonService: ReviewJsonService,\n private readonly instructions: ChecklistInstructionsService,\n ) {}\n\n /**\n * Refuse the PR while ANY applicable checklist still owes a passing verdict — naming exactly what each\n * one needs. No-op for a repo with no applicable checklists.\n *\n * SIDE EFFECT, deliberately: every REFUSED checklist's verdict file is RETIRED to review-<id>.json.old on\n * the way out (see {@link retireAndReport}). Only red verdicts are ever moved — a green, yellow or\n * overridden one is reused across finish attempts and retiring it would force a needless subagent re-run.\n */\n assertEveryReviewerRan(scan: ChecklistScan): void {\n if (scan.outstanding.length === 0) return;\n // The split is computed from the verdicts the SCAN already loaded, never from a second read of disk:\n // a re-read here could disagree with the set that produced `outstanding`, and a checklist that is\n // \"outstanding\" per one read and \"clear\" per another belongs to neither section of the message.\n const refused = this.reviewJsonService.refusedChecklists(scan.outstanding, scan.results);\n const refusedIds = new Set(refused.map((r: RequiredChecklist): string => r.id));\n const neverRan = scan.outstanding.filter((r: RequiredChecklist): boolean => !refusedIds.has(r.id));\n // Rendered BEFORE the throw and, per checklist, with the verdict resolved before its file is moved —\n // the message quotes the reviewer's own words, so losing them to the move would defeat the point.\n const refusals = refused.map((req: RequiredChecklist): string => this.retireAndReport(scan, req));\n throw new InformAiError(\n this.headline(scan, refused, neverRan)\n + this.unreadableSection(scan)\n + this.refusedSection(refusals)\n + this.neverRanSection(scan, neverRan)\n + this.footer(refused.length > 0),\n );\n }\n\n /**\n * Say plainly how many REFUSED and how many never ran.\n *\n * The old headline said all of them \"have no passing verdict yet\", which is technically true of a refusal\n * and reads as though nobody had looked. It is the sentence that framed a decision as an omission.\n */\n private headline(scan: ChecklistScan, refused: readonly RequiredChecklist[], neverRan: readonly RequiredChecklist[]): string {\n const lines = [\n `⛔ NO PR — ${scan.outstanding.length} of ${scan.applicable.length} review checklist(s) that apply to this branch are not clear:`,\n ];\n if (refused.length > 0) {\n lines.push(` • ${refused.length} REFUSED — a reviewer ran, judged this change, and said no: ${this.instructions.names(refused)}`);\n }\n if (neverRan.length > 0) {\n lines.push(` • ${neverRan.length} never ran — no verdict has been written yet: ${this.instructions.names(neverRan)}`);\n }\n return lines.join('\\n') + this.skippedNote(scan) + '\\n\\n';\n }\n\n /**\n * The optional checklists nobody ran, stated OUT LOUD on the refusal.\n *\n * They are not why the PR is being refused and this block says so — but a message that lists only what\n * blocked reads as \"everything else was reviewed\", and here that is false: those reviews were declined,\n * not passed. The one moment a human is looking at the review state is the moment to be honest about\n * which reviews did not happen, and it is also the prompt an AI needs to offer them again if the human\n * has changed their mind now that something else came back red.\n */\n private skippedNote(scan: ChecklistScan): string {\n if (scan.optionalNotRun.length === 0) return '';\n return `\\n (Not blocking: ${scan.optionalNotRun.length} OPTIONAL checklist(s) were not run — `\n + `${this.instructions.names(scan.optionalNotRun)}. They do not have to run; re-run\\n`\n + ' pnpm wp-review-upsert-pr if you now want to offer them.)';\n }\n\n // Unreadable verdict files come FIRST. A reviewer that wrote its verdict in the removed `success` format\n // is otherwise indistinguishable from one that never ran, and the AI would go re-run a subagent instead\n // of correcting four characters of JSON.\n private unreadableSection(scan: ChecklistScan): string {\n if (scan.formatErrors.length === 0) return '';\n return `${scan.formatErrors.length} verdict file(s) are in an UNREADABLE format:\\n\\n`\n + scan.formatErrors.map((e: string): string => ` • ${e}`).join('\\n') + '\\n\\n';\n }\n\n /**\n * The refusals — SECOND, above anything that says to spawn a subagent, because a spawn instruction is the\n * one line an agent acts on first, and acting on it here IS the loop.\n */\n private refusedSection(refusals: readonly string[]): string {\n if (refusals.length === 0) return '';\n return '⛔ REFUSED — these reviewers ALREADY ANSWERED. Re-spawning one against unchanged code only buys\\n'\n + ' the same answer: fix what it found (or get a HUMAN to authorize an override), THEN review again.\\n\\n'\n + refusals.map((r: string): string => ` • ${r}`).join('\\n\\n') + '\\n\\n';\n }\n\n /**\n * The reviewers that genuinely never ran — LAST, and listing ONLY these. When there are none this block\n * is absent entirely, which is the whole point: the \"You MUST run these N reviewer subagent(s)\"\n * imperative must not appear at all on a run whose only problem is a refusal.\n */\n private neverRanSection(scan: ChecklistScan, neverRan: readonly RequiredChecklist[]): string {\n if (neverRan.length === 0) return '';\n return '❓ NO VERDICT YET — nothing has been written for these, so they must actually be run:\\n\\n'\n + `${this.instructions.render(neverRan, scan.reviewPath, scan.context)}\\n\\n`;\n }\n\n /**\n * Retire ONE refused checklist's verdict and render the refusal that reports it.\n *\n * ORDER IS LOAD-BEARING. The verdict is resolved from the already-loaded results, so the finding is in\n * hand before the file moves; the move then happens; and only then is the message rendered, because it\n * names the archive path and must not claim a move that did not happen.\n *\n * The move is NON-FATAL. A finish that died after retiring a verdict would leave the AI with a red\n * verdict gone from the live path and no message saying where it went or why the PR was refused — a\n * silent gap, and the worst possible outcome for a feature whose entire purpose is a durable record. So a\n * failed move warns and the refusal is reported without an archive path (the file is then still live,\n * which the un-archived wording of `refusalError` describes correctly).\n */\n private retireAndReport(scan: ChecklistScan, req: RequiredChecklist): string {\n const verdict = this.reviewJsonService.resolveVerdict(req, scan.results);\n return this.reviewJsonService.refusalError(\n req, verdict, scan.reviewPath, this.archiveOrWarn(scan.reviewPath, req.id));\n }\n\n // The archive path, or '' when there was nothing to move or the move failed (see retireAndReport).\n private archiveOrWarn(reviewPath: string, checklistId: string): string {\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: a failed archive must never swallow the refusal it belongs to\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return this.reviewJsonService.archiveChecklistResult(reviewPath, checklistId);\n } catch (err: unknown) {\n const error = toError(err);\n process.stderr.write(\n `⚠️ Could not retire the refused verdict for \"${checklistId}\" (non-fatal — the refusal is still `\n + `reported below): ${error.message}\\n`);\n return '';\n }\n }\n\n // Re-running stage 2 is normally optional. It stops being optional after a refusal, because the only way\n // past one is to CHANGE the code — which is exactly the condition that makes the extracted diff and every\n // reviewer briefing stale.\n private footer(anyRefused: boolean): string {\n if (anyRefused) {\n return 'Then re-run: pnpm wp-review-upsert-pr (the code changed, so the extracted diff and the reviewer\\n'\n + 'briefings are stale), re-run the reviewer(s) above, and finally: pnpm wp-finish-upsert-pr';\n }\n return 'Then re-run: pnpm wp-finish-upsert-pr\\n'\n + '(Each reviewer\\'s generated instructions file is already written — re-running pnpm wp-review-upsert-pr\\n'\n + ' is only needed if the code changed since it ran.)';\n }\n}\n"]}
|