@webpieces/pr-gate 0.4.564 → 0.4.565
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 +2 -2
- package/src/dashboard/checklist-comment-row.d.ts +42 -0
- package/src/dashboard/checklist-comment-row.js +59 -0
- package/src/dashboard/checklist-comment-row.js.map +1 -0
- package/src/dashboard/dashboard.d.ts +19 -29
- package/src/dashboard/dashboard.js +57 -53
- package/src/dashboard/dashboard.js.map +1 -1
- package/src/scripts/commands/finish-upsert-pr-command.d.ts +9 -0
- package/src/scripts/commands/finish-upsert-pr-command.js +24 -4
- package/src/scripts/commands/finish-upsert-pr-command.js.map +1 -1
- package/src/scripts/commands/review-upsert-pr-command.d.ts +12 -1
- package/src/scripts/commands/review-upsert-pr-command.js +20 -4
- package/src/scripts/commands/review-upsert-pr-command.js.map +1 -1
- package/src/scripts/pr-gate-app.d.ts +2 -2
- package/src/scripts/pr-gate-app.js +2 -2
- package/src/scripts/pr-gate-app.js.map +1 -1
- package/src/scripts/workflow/checklist-detector.js +1 -1
- package/src/scripts/workflow/checklist-detector.js.map +1 -1
- package/src/scripts/workflow/checklist-scanner.d.ts +21 -1
- package/src/scripts/workflow/checklist-scanner.js +31 -3
- package/src/scripts/workflow/checklist-scanner.js.map +1 -1
- package/src/scripts/workflow/review-report.d.ts +62 -1
- package/src/scripts/workflow/review-report.js +165 -34
- package/src/scripts/workflow/review-report.js.map +1 -1
- package/src/scripts/workflow/reviewer-briefing-builder.js +1 -0
- package/src/scripts/workflow/reviewer-briefing-builder.js.map +1 -1
- package/src/scripts/workflow/reviewer-verdict-gate.d.ts +10 -0
- package/src/scripts/workflow/reviewer-verdict-gate.js +17 -1
- package/src/scripts/workflow/reviewer-verdict-gate.js.map +1 -1
- package/src/scripts/wp-review-upsert-pr.js +9 -2
- package/src/scripts/wp-review-upsert-pr.js.map +1 -1
|
@@ -62,7 +62,17 @@ export declare class ChecklistScan {
|
|
|
62
62
|
* message, which is exactly the loop this field exists to break.
|
|
63
63
|
*/
|
|
64
64
|
results: ChecklistResult[];
|
|
65
|
-
|
|
65
|
+
/**
|
|
66
|
+
* The OPTIONAL (`required: false`) applicable checklists carrying no verdict file — i.e. nobody ran them.
|
|
67
|
+
*
|
|
68
|
+
* Deliberately NOT in `outstanding` when filtering: that is the exemption that makes `required: false`
|
|
69
|
+
* mean something. Carried out as its own set rather than merely subtracted, because both readers need it
|
|
70
|
+
* BY NAME and neither can recover it from what is left: stage ② offers exactly these to the human, and
|
|
71
|
+
* the PR dashboard must publish them as "not run" rather than let a shorter roster imply everything
|
|
72
|
+
* passed. An optional checklist that ran and went RED is absent from here and stays in `outstanding`.
|
|
73
|
+
*/
|
|
74
|
+
optionalNotRun: RequiredChecklist[];
|
|
75
|
+
constructor(defined: ChecklistDefinition[], applicable: RequiredChecklist[], reviewed: RequiredChecklist[], outstanding: RequiredChecklist[], context: ChecklistReviewContext, reviewPath: string, forkPoint: string, roster: ChecklistRoster, formatErrors: string[], basis?: DiffBasis, changedFiles?: string[], results?: ChecklistResult[], optionalNotRun?: RequiredChecklist[]);
|
|
66
76
|
}
|
|
67
77
|
/**
|
|
68
78
|
* The ONE computation of which reviewer subagents a branch owes, shared by `wp-review-upsert-pr` (which lists) and
|
|
@@ -101,6 +111,16 @@ export declare class ChecklistScanner {
|
|
|
101
111
|
* here leaves this a function of its inputs rather than of the filesystem.
|
|
102
112
|
*/
|
|
103
113
|
scan(repoRoot: string, defined: ChecklistDefinition[], opts: ChecklistScanOptions): ChecklistScan;
|
|
114
|
+
/**
|
|
115
|
+
* What `wp-finish-upsert-pr` actually REFUSES on: everything still owing a verdict, minus the optional
|
|
116
|
+
* checklists nobody ran.
|
|
117
|
+
*
|
|
118
|
+
* The subtraction happens HERE — in the one place `outstanding` is computed — and not in the gate, so
|
|
119
|
+
* there is a single answer to "does this branch owe review?". When the gate did its own filtering, the
|
|
120
|
+
* command that lists and the command that blocks were two implementations of the same question, which is
|
|
121
|
+
* exactly the divergence this class's docstring exists to prevent.
|
|
122
|
+
*/
|
|
123
|
+
private blocking;
|
|
104
124
|
/**
|
|
105
125
|
* Every file changed since the fork point, INCLUDING uncommitted and untracked ones. Two non-default
|
|
106
126
|
* options, both load-bearing:
|
|
@@ -77,11 +77,21 @@ class ChecklistScan {
|
|
|
77
77
|
* message, which is exactly the loop this field exists to break.
|
|
78
78
|
*/
|
|
79
79
|
results;
|
|
80
|
+
/**
|
|
81
|
+
* The OPTIONAL (`required: false`) applicable checklists carrying no verdict file — i.e. nobody ran them.
|
|
82
|
+
*
|
|
83
|
+
* Deliberately NOT in `outstanding` when filtering: that is the exemption that makes `required: false`
|
|
84
|
+
* mean something. Carried out as its own set rather than merely subtracted, because both readers need it
|
|
85
|
+
* BY NAME and neither can recover it from what is left: stage ② offers exactly these to the human, and
|
|
86
|
+
* the PR dashboard must publish them as "not run" rather than let a shorter roster imply everything
|
|
87
|
+
* passed. An optional checklist that ran and went RED is absent from here and stays in `outstanding`.
|
|
88
|
+
*/
|
|
89
|
+
optionalNotRun;
|
|
80
90
|
// eslint-disable-next-line @typescript-eslint/max-params
|
|
81
91
|
constructor(defined, applicable, reviewed, outstanding, context, reviewPath, forkPoint, roster, formatErrors, basis = new diff_basis_1.DiffBasis(), changedFiles = [],
|
|
82
92
|
// Defaulted so a caller that only cares about the X/N/Z counts (and every existing test construction)
|
|
83
93
|
// stays a one-liner; the scanner itself always passes the real set.
|
|
84
|
-
results = []) {
|
|
94
|
+
results = [], optionalNotRun = []) {
|
|
85
95
|
this.defined = defined;
|
|
86
96
|
this.applicable = applicable;
|
|
87
97
|
this.reviewed = reviewed;
|
|
@@ -94,6 +104,7 @@ class ChecklistScan {
|
|
|
94
104
|
this.basis = basis;
|
|
95
105
|
this.changedFiles = changedFiles;
|
|
96
106
|
this.results = results;
|
|
107
|
+
this.optionalNotRun = optionalNotRun;
|
|
97
108
|
}
|
|
98
109
|
}
|
|
99
110
|
exports.ChecklistScan = ChecklistScan;
|
|
@@ -155,10 +166,27 @@ let ChecklistScanner = class ChecklistScanner {
|
|
|
155
166
|
const results = this.reviewJsonService.loadChecklistResults(reviewPath, applicable);
|
|
156
167
|
const stillOwed = this.reviewJsonService.pendingChecklists(applicable, results);
|
|
157
168
|
const owedIds = new Set(stillOwed.map((r) => r.id));
|
|
169
|
+
// NOT `!owedIds.has(...)`-with-the-optional-exemption-folded-in: an optional checklist nobody ran is
|
|
170
|
+
// neither reviewed nor blocking, and calling it "reviewed" would put a ✓ on the dashboard for a review
|
|
171
|
+
// that never happened.
|
|
158
172
|
const reviewed = applicable.filter((r) => !owedIds.has(r.id));
|
|
159
|
-
|
|
173
|
+
const optionalNotRun = this.reviewJsonService.optionalWithoutVerdict(applicable, results);
|
|
174
|
+
return new ChecklistScan(defined, applicable, reviewed, opts.filterAlreadyReviewed ? this.blocking(stillOwed, optionalNotRun) : applicable, opts.contextStage === ''
|
|
160
175
|
? this.prContextWriter.contextFor(repoRoot, featureName, basis)
|
|
161
|
-
: this.prContextWriter.ensure(repoRoot, featureName, basis, opts.contextStage, changedFiles), reviewPath, base, roster, this.reviewJsonService.checklistFormatErrors(applicable, results), basis, changedFiles, results);
|
|
176
|
+
: this.prContextWriter.ensure(repoRoot, featureName, basis, opts.contextStage, changedFiles), reviewPath, base, roster, this.reviewJsonService.checklistFormatErrors(applicable, results), basis, changedFiles, results, optionalNotRun);
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* What `wp-finish-upsert-pr` actually REFUSES on: everything still owing a verdict, minus the optional
|
|
180
|
+
* checklists nobody ran.
|
|
181
|
+
*
|
|
182
|
+
* The subtraction happens HERE — in the one place `outstanding` is computed — and not in the gate, so
|
|
183
|
+
* there is a single answer to "does this branch owe review?". When the gate did its own filtering, the
|
|
184
|
+
* command that lists and the command that blocks were two implementations of the same question, which is
|
|
185
|
+
* exactly the divergence this class's docstring exists to prevent.
|
|
186
|
+
*/
|
|
187
|
+
blocking(stillOwed, optionalNotRun) {
|
|
188
|
+
const skipped = new Set(optionalNotRun.map((r) => r.id));
|
|
189
|
+
return stillOwed.filter((r) => !skipped.has(r.id));
|
|
162
190
|
}
|
|
163
191
|
/**
|
|
164
192
|
* Every file changed since the fork point, INCLUDING uncommitted and untracked ones. Two non-default
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"checklist-scanner.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/checklist-scanner.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n ChangedFilesOptions, ChecklistDefinition, ChecklistResult, ChecklistReviewContext, DiffScope,\n RequiredChecklist, ReviewJsonService, reviewJsonPath,\n} from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { AiBranchName } from './git-readAiBranchName';\nimport { ChecklistDetector, ChecklistRoster } from './checklist-detector';\nimport { DiffBasis, DiffBasisResolver } from './diff-basis';\nimport { PrContextWriter } from './pr-context-writer';\n\n/** How a caller wants the scan filtered. Data-only (per CLAUDE.md). */\nexport class ChecklistScanOptions {\n /**\n * false — `outstanding` is every applicable checklist (what `wp-review-upsert-pr` LISTS).\n * true — `outstanding` drops the ones already carrying a passing/overridden verdict, leaving only what\n * still owes review (what `wp-finish-upsert-pr` BLOCKS on).\n */\n filterAlreadyReviewed: boolean;\n /**\n * Which per-stage snapshot this scan should persist ('stage3-finish', …), or '' for \"do not write\n * pr-context.json here\".\n *\n * '' exists for stage ②, which cannot write yet: the context records `diffDir`, and the diff has not\n * been materialized at scan time. Letting the scan write anyway meant stage ② wrote the file twice,\n * ~60 lines apart, the first time with an empty diffDir. Now it writes once, after materializing.\n *\n * The DEFAULT is deliberately non-empty, so writing is what you get by NOT thinking about it. A\n * reviewer block that lost its diff command because nobody had written pr-context.json is a bug this\n * codebase has already shipped once (see PrContextWriter's docstring); opting out must be an explicit,\n * visible act by a caller that takes responsibility for writing it later — not an omission.\n */\n contextStage: string;\n\n constructor(filterAlreadyReviewed = false, contextStage = 'stage-scan') {\n this.filterAlreadyReviewed = filterAlreadyReviewed;\n this.contextStage = contextStage;\n }\n}\n\n/**\n * The answer to \"what review does this branch owe?\", in the X → N → Z terms the commands report:\n * X = `defined` — every checklist in pr-gate.checklists\n * N = `applicable` — those whose patterns matched (or that have no patterns, so always run)\n * Z = `outstanding` — of N, those still owing a verdict (only when filterAlreadyReviewed)\n * Data-only.\n */\nexport class ChecklistScan {\n defined: ChecklistDefinition[]; // X\n applicable: RequiredChecklist[]; // N\n reviewed: RequiredChecklist[]; // N − Z: already have a passing/warned/overridden review-<id>.json\n outstanding: RequiredChecklist[]; // Z (== applicable when not filtering)\n context: ChecklistReviewContext; // fork-point sha + pr-context.json path\n reviewPath: string; // the branch's review.json; verdict files sit beside it\n forkPoint: string; // '' when no fork point resolved\n // ALL X, matched or not, with why — what the PR comment publishes as its roster. Skipped checklists are\n // absent from `applicable` by construction, and recovering them downstream would mean a second\n // changed-file computation with different semantics (see the class comment).\n roster: ChecklistRoster;\n // Verdict files that exist but cannot be read as a verdict (e.g. still using the removed `success`).\n // Carried on the SCAN because wp-finish-upsert-pr refuses on missing reviewers before it ever parses\n // review.json — a complaint raised only in there would never reach the AI.\n formatErrors: string[];\n /**\n * The basis the matching ACTUALLY ran against. Carried out so a caller that materializes the diff\n * (stage ②) reuses the identical range instead of resolving its own — two independent resolutions is\n * exactly how the changed-file set and the printed `git diff` command came to disagree.\n */\n basis: DiffBasis;\n changedFiles: string[]; // the full changed-file set the matching ran against\n /**\n * The verdict files the scan ALREADY read, carried out rather than dropped.\n *\n * `outstanding` answers \"who still owes a verdict?\" but not \"why\" — and the two reasons demand opposite\n * actions from the reader: a reviewer that never ran must be SPAWNED, a reviewer that ran and REFUSED\n * must not be (it will refuse again; the finding has to be fixed first). Telling them apart means\n * resolving each checklist's verdict, and without the results here every caller either re-reads the same\n * files off disk — a second read that can disagree with this one — or merges the two cases into one\n * message, which is exactly the loop this field exists to break.\n */\n results: ChecklistResult[];\n\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(\n defined: ChecklistDefinition[],\n applicable: RequiredChecklist[],\n reviewed: RequiredChecklist[],\n outstanding: RequiredChecklist[],\n context: ChecklistReviewContext,\n reviewPath: string,\n forkPoint: string,\n roster: ChecklistRoster,\n formatErrors: string[],\n basis: DiffBasis = new DiffBasis(),\n changedFiles: string[] = [],\n // Defaulted so a caller that only cares about the X/N/Z counts (and every existing test construction)\n // stays a one-liner; the scanner itself always passes the real set.\n results: ChecklistResult[] = [],\n ) {\n this.defined = defined;\n this.applicable = applicable;\n this.reviewed = reviewed;\n this.outstanding = outstanding;\n this.context = context;\n this.reviewPath = reviewPath;\n this.forkPoint = forkPoint;\n this.roster = roster;\n this.formatErrors = formatErrors;\n this.basis = basis;\n this.changedFiles = changedFiles;\n this.results = results;\n }\n}\n\n/**\n * The ONE computation of which reviewer subagents a branch owes, shared by `wp-review-upsert-pr` (which lists) and\n * `wp-finish-upsert-pr` (which blocks). They previously each assembled this from the same parts in slightly\n * different ways, and any divergence means the command that reports and the command that gates disagree.\n *\n * Two properties are deliberate and load-bearing:\n *\n * 1. **The base is the FORK POINT of main, computed directly** — never `DiffScope.resolveBase`, which\n * overlays `NX_BASE`/`NX_HEAD` from the environment. That made review coverage depend on an env var.\n * It now arrives via {@link DiffBasisResolver}, which injects ForkPoint; same sha, one resolution.\n * 2. **UNCOMMITTED work counts.** `getChangedFiles` is called with NO head, which is the branch of it that\n * diffs base → WORKING TREE and unions in untracked files. Passing a head would diff commit-to-commit and\n * silently miss staged, unstaged and untracked changes — so a checklist matching only uncommitted files\n * would never fire and its reviewer would never be listed.\n * 3. **The range and the command it prints are the SAME basis.** Property 2 used to be true of the file set\n * only: reviewers were handed `git diff <base> HEAD`, which on a dirty tree covers a different range and\n * prints nothing. Both now derive from one {@link DiffBasis}, carried on the scan so a materializing\n * caller cannot re-resolve and drift.\n *\n * `@injectable(bindingScopeValues.Singleton)` so it is injected by type and drawn in the DI design.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class ChecklistScanner {\n constructor(\n private readonly aiBranchName: AiBranchName,\n private readonly checklistDetector: ChecklistDetector,\n private readonly diffScope: DiffScope,\n private readonly diffBasisResolver: DiffBasisResolver,\n private readonly prContextWriter: PrContextWriter,\n private readonly reviewJsonService: ReviewJsonService,\n ) {}\n\n /**\n * `defined` is the caller's ALREADY-VALIDATED `prGate.checklists`. The scanner deliberately does not load\n * config itself: `loadAndValidate` is the one gate on the checklist set (it rejects a non-array\n * `checklists`, including the removed `{ doc }` manifest shape, and verifies every guidance doc and\n * reviewer-agent file exists), both callers already run it for other fields, and keeping the read out of\n * here leaves this a function of its inputs rather than of the filesystem.\n */\n scan(repoRoot: string, defined: ChecklistDefinition[], opts: ChecklistScanOptions): ChecklistScan {\n const featureName = this.aiBranchName.getFeatureName();\n const reviewPath = reviewJsonPath(repoRoot, featureName);\n // ONE basis for the file set, the reproduce command and any downstream materialization. The fork\n // point still comes from ForkPoint (never DiffScope.resolveBase) — DiffBasisResolver injects it.\n const basis = this.diffBasisResolver.resolve(repoRoot);\n const base = basis.base;\n // ONE changed-file computation feeds both the roster (all X) and the applicable set (N). `detect` is\n // pure and defined as the roster minus its empty entries, so the two cannot disagree about a match.\n const changedFiles = this.changedFiles(repoRoot, base);\n const roster = new ChecklistRoster(\n this.checklistDetector.roster(defined, changedFiles), changedFiles.length, base !== '');\n const applicable = this.checklistDetector.toRequired(this.checklistDetector.detect(defined, changedFiles));\n const results = this.reviewJsonService.loadChecklistResults(reviewPath, applicable);\n const stillOwed = this.reviewJsonService.pendingChecklists(applicable, results);\n const owedIds = new Set(stillOwed.map((r: RequiredChecklist): string => r.id));\n const reviewed = applicable.filter((r: RequiredChecklist): boolean => !owedIds.has(r.id));\n return new ChecklistScan(\n defined,\n applicable,\n reviewed,\n opts.filterAlreadyReviewed ? stillOwed : applicable,\n opts.contextStage === ''\n ? this.prContextWriter.contextFor(repoRoot, featureName, basis)\n : this.prContextWriter.ensure(repoRoot, featureName, basis, opts.contextStage, changedFiles),\n reviewPath,\n base,\n roster,\n this.reviewJsonService.checklistFormatErrors(applicable, results),\n basis,\n changedFiles,\n results,\n );\n }\n\n /**\n * Every file changed since the fork point, INCLUDING uncommitted and untracked ones. Two non-default\n * options, both load-bearing:\n *\n * `tsOnly:false` — the default drops every *.sql / Dockerfile / .env* file a checklist most wants\n * to key on, which would silently shrink the set a reviewer is pointed at.\n * `includeDeletions:true` — the default is `--diff-filter=d`, so a DELETED file is invisible. A PR that\n * deletes a migration, an auth check or a terraform rule changed exactly what a\n * checklist exists to catch, and under the default no checklist fires at all.\n */\n private changedFiles(repoRoot: string, base: string): string[] {\n if (base === '') return [];\n const opts = new ChangedFilesOptions();\n opts.tsOnly = false;\n opts.includeDeletions = true;\n // No head argument — see the class comment. This is what includes the working tree.\n return this.diffScope.getChangedFiles(repoRoot, base, undefined, opts);\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"checklist-scanner.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/checklist-scanner.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n ChangedFilesOptions, ChecklistDefinition, ChecklistResult, ChecklistReviewContext, DiffScope,\n RequiredChecklist, ReviewJsonService, reviewJsonPath,\n} from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { AiBranchName } from './git-readAiBranchName';\nimport { ChecklistDetector, ChecklistRoster } from './checklist-detector';\nimport { DiffBasis, DiffBasisResolver } from './diff-basis';\nimport { PrContextWriter } from './pr-context-writer';\n\n/** How a caller wants the scan filtered. Data-only (per CLAUDE.md). */\nexport class ChecklistScanOptions {\n /**\n * false — `outstanding` is every applicable checklist (what `wp-review-upsert-pr` LISTS).\n * true — `outstanding` drops the ones already carrying a passing/overridden verdict, leaving only what\n * still owes review (what `wp-finish-upsert-pr` BLOCKS on).\n */\n filterAlreadyReviewed: boolean;\n /**\n * Which per-stage snapshot this scan should persist ('stage3-finish', …), or '' for \"do not write\n * pr-context.json here\".\n *\n * '' exists for stage ②, which cannot write yet: the context records `diffDir`, and the diff has not\n * been materialized at scan time. Letting the scan write anyway meant stage ② wrote the file twice,\n * ~60 lines apart, the first time with an empty diffDir. Now it writes once, after materializing.\n *\n * The DEFAULT is deliberately non-empty, so writing is what you get by NOT thinking about it. A\n * reviewer block that lost its diff command because nobody had written pr-context.json is a bug this\n * codebase has already shipped once (see PrContextWriter's docstring); opting out must be an explicit,\n * visible act by a caller that takes responsibility for writing it later — not an omission.\n */\n contextStage: string;\n\n constructor(filterAlreadyReviewed = false, contextStage = 'stage-scan') {\n this.filterAlreadyReviewed = filterAlreadyReviewed;\n this.contextStage = contextStage;\n }\n}\n\n/**\n * The answer to \"what review does this branch owe?\", in the X → N → Z terms the commands report:\n * X = `defined` — every checklist in pr-gate.checklists\n * N = `applicable` — those whose patterns matched (or that have no patterns, so always run)\n * Z = `outstanding` — of N, those still owing a verdict (only when filterAlreadyReviewed)\n * Data-only.\n */\nexport class ChecklistScan {\n defined: ChecklistDefinition[]; // X\n applicable: RequiredChecklist[]; // N\n reviewed: RequiredChecklist[]; // N − Z: already have a passing/warned/overridden review-<id>.json\n outstanding: RequiredChecklist[]; // Z (== applicable when not filtering)\n context: ChecklistReviewContext; // fork-point sha + pr-context.json path\n reviewPath: string; // the branch's review.json; verdict files sit beside it\n forkPoint: string; // '' when no fork point resolved\n // ALL X, matched or not, with why — what the PR comment publishes as its roster. Skipped checklists are\n // absent from `applicable` by construction, and recovering them downstream would mean a second\n // changed-file computation with different semantics (see the class comment).\n roster: ChecklistRoster;\n // Verdict files that exist but cannot be read as a verdict (e.g. still using the removed `success`).\n // Carried on the SCAN because wp-finish-upsert-pr refuses on missing reviewers before it ever parses\n // review.json — a complaint raised only in there would never reach the AI.\n formatErrors: string[];\n /**\n * The basis the matching ACTUALLY ran against. Carried out so a caller that materializes the diff\n * (stage ②) reuses the identical range instead of resolving its own — two independent resolutions is\n * exactly how the changed-file set and the printed `git diff` command came to disagree.\n */\n basis: DiffBasis;\n changedFiles: string[]; // the full changed-file set the matching ran against\n /**\n * The verdict files the scan ALREADY read, carried out rather than dropped.\n *\n * `outstanding` answers \"who still owes a verdict?\" but not \"why\" — and the two reasons demand opposite\n * actions from the reader: a reviewer that never ran must be SPAWNED, a reviewer that ran and REFUSED\n * must not be (it will refuse again; the finding has to be fixed first). Telling them apart means\n * resolving each checklist's verdict, and without the results here every caller either re-reads the same\n * files off disk — a second read that can disagree with this one — or merges the two cases into one\n * message, which is exactly the loop this field exists to break.\n */\n results: ChecklistResult[];\n /**\n * The OPTIONAL (`required: false`) applicable checklists carrying no verdict file — i.e. nobody ran them.\n *\n * Deliberately NOT in `outstanding` when filtering: that is the exemption that makes `required: false`\n * mean something. Carried out as its own set rather than merely subtracted, because both readers need it\n * BY NAME and neither can recover it from what is left: stage ② offers exactly these to the human, and\n * the PR dashboard must publish them as \"not run\" rather than let a shorter roster imply everything\n * passed. An optional checklist that ran and went RED is absent from here and stays in `outstanding`.\n */\n optionalNotRun: RequiredChecklist[];\n\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(\n defined: ChecklistDefinition[],\n applicable: RequiredChecklist[],\n reviewed: RequiredChecklist[],\n outstanding: RequiredChecklist[],\n context: ChecklistReviewContext,\n reviewPath: string,\n forkPoint: string,\n roster: ChecklistRoster,\n formatErrors: string[],\n basis: DiffBasis = new DiffBasis(),\n changedFiles: string[] = [],\n // Defaulted so a caller that only cares about the X/N/Z counts (and every existing test construction)\n // stays a one-liner; the scanner itself always passes the real set.\n results: ChecklistResult[] = [],\n optionalNotRun: RequiredChecklist[] = [],\n ) {\n this.defined = defined;\n this.applicable = applicable;\n this.reviewed = reviewed;\n this.outstanding = outstanding;\n this.context = context;\n this.reviewPath = reviewPath;\n this.forkPoint = forkPoint;\n this.roster = roster;\n this.formatErrors = formatErrors;\n this.basis = basis;\n this.changedFiles = changedFiles;\n this.results = results;\n this.optionalNotRun = optionalNotRun;\n }\n}\n\n/**\n * The ONE computation of which reviewer subagents a branch owes, shared by `wp-review-upsert-pr` (which lists) and\n * `wp-finish-upsert-pr` (which blocks). They previously each assembled this from the same parts in slightly\n * different ways, and any divergence means the command that reports and the command that gates disagree.\n *\n * Two properties are deliberate and load-bearing:\n *\n * 1. **The base is the FORK POINT of main, computed directly** — never `DiffScope.resolveBase`, which\n * overlays `NX_BASE`/`NX_HEAD` from the environment. That made review coverage depend on an env var.\n * It now arrives via {@link DiffBasisResolver}, which injects ForkPoint; same sha, one resolution.\n * 2. **UNCOMMITTED work counts.** `getChangedFiles` is called with NO head, which is the branch of it that\n * diffs base → WORKING TREE and unions in untracked files. Passing a head would diff commit-to-commit and\n * silently miss staged, unstaged and untracked changes — so a checklist matching only uncommitted files\n * would never fire and its reviewer would never be listed.\n * 3. **The range and the command it prints are the SAME basis.** Property 2 used to be true of the file set\n * only: reviewers were handed `git diff <base> HEAD`, which on a dirty tree covers a different range and\n * prints nothing. Both now derive from one {@link DiffBasis}, carried on the scan so a materializing\n * caller cannot re-resolve and drift.\n *\n * `@injectable(bindingScopeValues.Singleton)` so it is injected by type and drawn in the DI design.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class ChecklistScanner {\n constructor(\n private readonly aiBranchName: AiBranchName,\n private readonly checklistDetector: ChecklistDetector,\n private readonly diffScope: DiffScope,\n private readonly diffBasisResolver: DiffBasisResolver,\n private readonly prContextWriter: PrContextWriter,\n private readonly reviewJsonService: ReviewJsonService,\n ) {}\n\n /**\n * `defined` is the caller's ALREADY-VALIDATED `prGate.checklists`. The scanner deliberately does not load\n * config itself: `loadAndValidate` is the one gate on the checklist set (it rejects a non-array\n * `checklists`, including the removed `{ doc }` manifest shape, and verifies every guidance doc and\n * reviewer-agent file exists), both callers already run it for other fields, and keeping the read out of\n * here leaves this a function of its inputs rather than of the filesystem.\n */\n scan(repoRoot: string, defined: ChecklistDefinition[], opts: ChecklistScanOptions): ChecklistScan {\n const featureName = this.aiBranchName.getFeatureName();\n const reviewPath = reviewJsonPath(repoRoot, featureName);\n // ONE basis for the file set, the reproduce command and any downstream materialization. The fork\n // point still comes from ForkPoint (never DiffScope.resolveBase) — DiffBasisResolver injects it.\n const basis = this.diffBasisResolver.resolve(repoRoot);\n const base = basis.base;\n // ONE changed-file computation feeds both the roster (all X) and the applicable set (N). `detect` is\n // pure and defined as the roster minus its empty entries, so the two cannot disagree about a match.\n const changedFiles = this.changedFiles(repoRoot, base);\n const roster = new ChecklistRoster(\n this.checklistDetector.roster(defined, changedFiles), changedFiles.length, base !== '');\n const applicable = this.checklistDetector.toRequired(this.checklistDetector.detect(defined, changedFiles));\n const results = this.reviewJsonService.loadChecklistResults(reviewPath, applicable);\n const stillOwed = this.reviewJsonService.pendingChecklists(applicable, results);\n const owedIds = new Set(stillOwed.map((r: RequiredChecklist): string => r.id));\n // NOT `!owedIds.has(...)`-with-the-optional-exemption-folded-in: an optional checklist nobody ran is\n // neither reviewed nor blocking, and calling it \"reviewed\" would put a ✓ on the dashboard for a review\n // that never happened.\n const reviewed = applicable.filter((r: RequiredChecklist): boolean => !owedIds.has(r.id));\n const optionalNotRun = this.reviewJsonService.optionalWithoutVerdict(applicable, results);\n return new ChecklistScan(\n defined,\n applicable,\n reviewed,\n opts.filterAlreadyReviewed ? this.blocking(stillOwed, optionalNotRun) : applicable,\n opts.contextStage === ''\n ? this.prContextWriter.contextFor(repoRoot, featureName, basis)\n : this.prContextWriter.ensure(repoRoot, featureName, basis, opts.contextStage, changedFiles),\n reviewPath,\n base,\n roster,\n this.reviewJsonService.checklistFormatErrors(applicable, results),\n basis,\n changedFiles,\n results,\n optionalNotRun,\n );\n }\n\n /**\n * What `wp-finish-upsert-pr` actually REFUSES on: everything still owing a verdict, minus the optional\n * checklists nobody ran.\n *\n * The subtraction happens HERE — in the one place `outstanding` is computed — and not in the gate, so\n * there is a single answer to \"does this branch owe review?\". When the gate did its own filtering, the\n * command that lists and the command that blocks were two implementations of the same question, which is\n * exactly the divergence this class's docstring exists to prevent.\n */\n private blocking(stillOwed: readonly RequiredChecklist[], optionalNotRun: readonly RequiredChecklist[]): RequiredChecklist[] {\n const skipped = new Set(optionalNotRun.map((r: RequiredChecklist): string => r.id));\n return stillOwed.filter((r: RequiredChecklist): boolean => !skipped.has(r.id));\n }\n\n /**\n * Every file changed since the fork point, INCLUDING uncommitted and untracked ones. Two non-default\n * options, both load-bearing:\n *\n * `tsOnly:false` — the default drops every *.sql / Dockerfile / .env* file a checklist most wants\n * to key on, which would silently shrink the set a reviewer is pointed at.\n * `includeDeletions:true` — the default is `--diff-filter=d`, so a DELETED file is invisible. A PR that\n * deletes a migration, an auth check or a terraform rule changed exactly what a\n * checklist exists to catch, and under the default no checklist fires at all.\n */\n private changedFiles(repoRoot: string, base: string): string[] {\n if (base === '') return [];\n const opts = new ChangedFilesOptions();\n opts.tsOnly = false;\n opts.includeDeletions = true;\n // No head argument — see the class comment. This is what includes the working tree.\n return this.diffScope.getChangedFiles(repoRoot, base, undefined, opts);\n }\n}\n"]}
|
|
@@ -33,6 +33,12 @@ export declare class ReviewReportInput {
|
|
|
33
33
|
formatErrors: string[];
|
|
34
34
|
briefings: ReviewerBriefing[];
|
|
35
35
|
refused: RefusedReviewer[];
|
|
36
|
+
/**
|
|
37
|
+
* `--no-optional` was passed: the human has already said to submit without the optional reviews, so the
|
|
38
|
+
* block that offers them is replaced by a one-line statement that they were skipped. It never suppresses
|
|
39
|
+
* a REQUIRED reviewer, and it is not a gate — nothing about what blocks the PR changes.
|
|
40
|
+
*/
|
|
41
|
+
skipOptional: boolean;
|
|
36
42
|
constructor(repoRoot: string, featureName: string, reviewPath: string);
|
|
37
43
|
}
|
|
38
44
|
/**
|
|
@@ -50,6 +56,9 @@ export declare class ReviewReportInput {
|
|
|
50
56
|
* 1. `wp-finish-upsert-pr` is named as a thing to run EXACTLY ONCE in the whole block.
|
|
51
57
|
* 2. The review.json instruction comes BEFORE it — and BEFORE the spawn blocks (see nextSteps).
|
|
52
58
|
* 3. With zero checklists the all-clear precedes any configuration guidance.
|
|
59
|
+
* 4. REQUIRED reviewers are spawned unasked; OPTIONAL ones are only ever OFFERED, in one batched
|
|
60
|
+
* question. The two never share a step, because one instruction says "do it" and the other says
|
|
61
|
+
* "ask first", and an agent reading a merged list will act on the stronger of the two.
|
|
53
62
|
*
|
|
54
63
|
* Pure string building, no I/O. `@injectable(bindingScopeValues.Singleton)` so it is injected by type.
|
|
55
64
|
*/
|
|
@@ -58,6 +67,17 @@ export declare class ReviewReport {
|
|
|
58
67
|
private readonly reviewerInstructions;
|
|
59
68
|
constructor(checklistNotice: ChecklistNotice, reviewerInstructions: ReviewerInstructionsService);
|
|
60
69
|
render(input: ReviewReportInput): string;
|
|
70
|
+
/**
|
|
71
|
+
* Name what this block is actually about. A repo with reviewers owed is being told to SPAWN; a repo with
|
|
72
|
+
* none is not, and promising subagents it does not have is the same kind of noise as explaining checklist
|
|
73
|
+
* configuration to a repo that configured none. Keyed on what is actually ACTIONABLE rather than on the
|
|
74
|
+
* applicable count: every applicable checklist already having a verdict means nothing to spawn, and so
|
|
75
|
+
* does a branch whose only outstanding reviews are optional ones the human already waved off.
|
|
76
|
+
*
|
|
77
|
+
* "spawn" and "ask about" are separate headings because they are separate obligations. A branch owing
|
|
78
|
+
* only optional reviews has nothing the AI may do unilaterally, and a heading that says SPAWN is the
|
|
79
|
+
* single line most likely to make it do exactly that.
|
|
80
|
+
*/
|
|
61
81
|
private header;
|
|
62
82
|
/**
|
|
63
83
|
* What the SCAN found: either "nothing applies here" or the already-reviewed / unreadable-verdict lines.
|
|
@@ -65,6 +85,13 @@ export declare class ReviewReport {
|
|
|
65
85
|
* mistaken for the next action.
|
|
66
86
|
*/
|
|
67
87
|
private scanVerdict;
|
|
88
|
+
/**
|
|
89
|
+
* `--no-optional`, stated as a VERDICT rather than left silent. Named individually, not just counted: the
|
|
90
|
+
* whole reason the human is allowed to skip these is that they know this diff, and the only way they can
|
|
91
|
+
* catch "wait, not THAT one" is to see which ones went unreviewed.
|
|
92
|
+
*/
|
|
93
|
+
private skippedLines;
|
|
94
|
+
private allClear;
|
|
68
95
|
/**
|
|
69
96
|
* The ONE instruction block, and the last thing the stage prints. Numbered rather than prose-linked
|
|
70
97
|
* ("Then… Finally…") so that skipping step 1 is visibly skipping a step, and worded so no earlier line
|
|
@@ -88,14 +115,40 @@ export declare class ReviewReport {
|
|
|
88
115
|
private nextSteps;
|
|
89
116
|
private writeReviewStep;
|
|
90
117
|
/**
|
|
91
|
-
*
|
|
118
|
+
* The REQUIRED reviewers — one copy-paste block each, and nothing at all when none is owed. The prompt is
|
|
92
119
|
* deliberately a POINTER and nothing else: the generated instructions file is the contract, so anything
|
|
93
120
|
* restated here is a second copy that can go stale — which is exactly how a removed `success` field
|
|
94
121
|
* outlived its own removal in print.
|
|
122
|
+
*
|
|
123
|
+
* These are spawned WITHOUT asking. They are the checklists the repo declared `required: true`, which is
|
|
124
|
+
* the repo saying the decision was already made; putting them to the human again would re-open a question
|
|
125
|
+
* the config exists to settle.
|
|
95
126
|
*/
|
|
96
127
|
private spawnStep;
|
|
128
|
+
/**
|
|
129
|
+
* STEP n — the OPTIONAL reviewers: listed, never spawned unasked.
|
|
130
|
+
*
|
|
131
|
+
* This is the whole point of `required: false`. A one-line bug fix in a repo whose checklists key on a
|
|
132
|
+
* glob as broad as every TypeScript file otherwise pays for a dozen subagent reviews, and the only party
|
|
133
|
+
* who can judge whether this particular diff is worth them is the human looking at it.
|
|
134
|
+
*
|
|
135
|
+
* ONE batched multi-select question, explicitly. Asked one at a time, a human answering "no" nine times
|
|
136
|
+
* is being worn down rather than consulted, and by the third question the cheap thing is to say yes to
|
|
137
|
+
* everything — which is the state this feature exists to leave. The "None" option has to be spelled out
|
|
138
|
+
* too: an agent that offers a list without an explicit way to decline it has not really offered a choice.
|
|
139
|
+
*
|
|
140
|
+
* The blocking consequence is stated because it is the one non-obvious part of the contract: `required`
|
|
141
|
+
* governs whether a reviewer must RUN, not whether its answer counts. Choosing to run one and then
|
|
142
|
+
* shrugging off a red verdict would make the whole exercise theater.
|
|
143
|
+
*/
|
|
144
|
+
private offerStep;
|
|
145
|
+
private refusedWarning;
|
|
97
146
|
private finishStep;
|
|
98
147
|
private owedReviewers;
|
|
148
|
+
private requiredOwed;
|
|
149
|
+
private optionalOwed;
|
|
150
|
+
private offerableOwed;
|
|
151
|
+
private actionableOwed;
|
|
99
152
|
/**
|
|
100
153
|
* One reviewer's block. A reviewer that already REFUSED gets the SAME spawn coordinates but a different
|
|
101
154
|
* lead-in, because the action before spawning is different: its own words are printed, and the spawn is
|
|
@@ -108,5 +161,13 @@ export declare class ReviewReport {
|
|
|
108
161
|
*/
|
|
109
162
|
private oneSpawnBlock;
|
|
110
163
|
private leadIn;
|
|
164
|
+
/**
|
|
165
|
+
* The checklist's guidance doc, for OPTIONAL reviewers only.
|
|
166
|
+
*
|
|
167
|
+
* "4 file(s) matched" plus a broad glob does not tell a human what the review would actually look AT,
|
|
168
|
+
* and they are being asked to decide exactly that. Omitted for required reviewers: there is no decision
|
|
169
|
+
* to inform there — the reviewer runs either way, and the doc is already in its instructions file.
|
|
170
|
+
*/
|
|
171
|
+
private docLine;
|
|
111
172
|
private why;
|
|
112
173
|
}
|
|
@@ -43,6 +43,12 @@ class ReviewReportInput {
|
|
|
43
43
|
formatErrors; // verdict files that exist but cannot be read as verdicts
|
|
44
44
|
briefings; // one per applicable checklist, already written to disk
|
|
45
45
|
refused; // of the owed ones, those that already ran and said no (see RefusedReviewer)
|
|
46
|
+
/**
|
|
47
|
+
* `--no-optional` was passed: the human has already said to submit without the optional reviews, so the
|
|
48
|
+
* block that offers them is replaced by a one-line statement that they were skipped. It never suppresses
|
|
49
|
+
* a REQUIRED reviewer, and it is not a gate — nothing about what blocks the PR changes.
|
|
50
|
+
*/
|
|
51
|
+
skipOptional;
|
|
46
52
|
constructor(repoRoot, featureName, reviewPath) {
|
|
47
53
|
this.repoRoot = repoRoot;
|
|
48
54
|
this.featureName = featureName;
|
|
@@ -53,6 +59,7 @@ class ReviewReportInput {
|
|
|
53
59
|
this.formatErrors = [];
|
|
54
60
|
this.briefings = [];
|
|
55
61
|
this.refused = [];
|
|
62
|
+
this.skipOptional = false;
|
|
56
63
|
}
|
|
57
64
|
}
|
|
58
65
|
exports.ReviewReportInput = ReviewReportInput;
|
|
@@ -71,6 +78,9 @@ exports.ReviewReportInput = ReviewReportInput;
|
|
|
71
78
|
* 1. `wp-finish-upsert-pr` is named as a thing to run EXACTLY ONCE in the whole block.
|
|
72
79
|
* 2. The review.json instruction comes BEFORE it — and BEFORE the spawn blocks (see nextSteps).
|
|
73
80
|
* 3. With zero checklists the all-clear precedes any configuration guidance.
|
|
81
|
+
* 4. REQUIRED reviewers are spawned unasked; OPTIONAL ones are only ever OFFERED, in one batched
|
|
82
|
+
* question. The two never share a step, because one instruction says "do it" and the other says
|
|
83
|
+
* "ask first", and an agent reading a merged list will act on the stronger of the two.
|
|
74
84
|
*
|
|
75
85
|
* Pure string building, no I/O. `@injectable(bindingScopeValues.Singleton)` so it is injected by type.
|
|
76
86
|
*/
|
|
@@ -86,14 +96,23 @@ let ReviewReport = class ReviewReport {
|
|
|
86
96
|
+ this.scanVerdict(input)
|
|
87
97
|
+ this.nextSteps(input);
|
|
88
98
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
99
|
+
/**
|
|
100
|
+
* Name what this block is actually about. A repo with reviewers owed is being told to SPAWN; a repo with
|
|
101
|
+
* none is not, and promising subagents it does not have is the same kind of noise as explaining checklist
|
|
102
|
+
* configuration to a repo that configured none. Keyed on what is actually ACTIONABLE rather than on the
|
|
103
|
+
* applicable count: every applicable checklist already having a verdict means nothing to spawn, and so
|
|
104
|
+
* does a branch whose only outstanding reviews are optional ones the human already waved off.
|
|
105
|
+
*
|
|
106
|
+
* "spawn" and "ask about" are separate headings because they are separate obligations. A branch owing
|
|
107
|
+
* only optional reviews has nothing the AI may do unilaterally, and a heading that says SPAWN is the
|
|
108
|
+
* single line most likely to make it do exactly that.
|
|
109
|
+
*/
|
|
93
110
|
header(input) {
|
|
94
|
-
if (this.
|
|
95
|
-
return '② Review, then finish\n';
|
|
96
|
-
|
|
111
|
+
if (this.requiredOwed(input).length > 0)
|
|
112
|
+
return '② Review, spawn subagent reviewers, then finish\n';
|
|
113
|
+
if (this.offerableOwed(input).length > 0)
|
|
114
|
+
return '② Review, offer the optional reviewers, then finish\n';
|
|
115
|
+
return '② Review, then finish\n';
|
|
97
116
|
}
|
|
98
117
|
/**
|
|
99
118
|
* What the SCAN found: either "nothing applies here" or the already-reviewed / unreadable-verdict lines.
|
|
@@ -112,13 +131,37 @@ let ReviewReport = class ReviewReport {
|
|
|
112
131
|
// correcting the file sitting right there.
|
|
113
132
|
for (const e of input.formatErrors)
|
|
114
133
|
lines.push(` ⛔ ${e}`);
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
134
|
+
lines.push(...this.skippedLines(input));
|
|
135
|
+
if (this.actionableOwed(input).length === 0)
|
|
136
|
+
lines.push('', this.allClear(input));
|
|
118
137
|
if (lines.length === 0)
|
|
119
138
|
return '';
|
|
120
139
|
return '\n' + lines.join('\n') + '\n';
|
|
121
140
|
}
|
|
141
|
+
/**
|
|
142
|
+
* `--no-optional`, stated as a VERDICT rather than left silent. Named individually, not just counted: the
|
|
143
|
+
* whole reason the human is allowed to skip these is that they know this diff, and the only way they can
|
|
144
|
+
* catch "wait, not THAT one" is to see which ones went unreviewed.
|
|
145
|
+
*/
|
|
146
|
+
skippedLines(input) {
|
|
147
|
+
const skipped = this.optionalOwed(input);
|
|
148
|
+
if (!input.skipOptional || skipped.length === 0)
|
|
149
|
+
return [];
|
|
150
|
+
return [
|
|
151
|
+
'',
|
|
152
|
+
` ⏭️ ${skipped.length} OPTIONAL checklist(s) matched this diff and were SKIPPED (--no-optional):`,
|
|
153
|
+
...skipped.map((b) => ` ${b.subagent} — ${this.why(b)}`),
|
|
154
|
+
' Not blocking. Drop the flag and re-run this command to offer them after all.',
|
|
155
|
+
];
|
|
156
|
+
}
|
|
157
|
+
// The all-clear. It must NOT claim everything was reviewed when optional reviews were skipped — that is
|
|
158
|
+
// the one sentence that would turn a deliberate skip into a false record of a review that happened.
|
|
159
|
+
allClear(input) {
|
|
160
|
+
if (input.skipOptional && this.optionalOwed(input).length > 0) {
|
|
161
|
+
return '✅ Nothing left to spawn — every REQUIRED checklist is reviewed (optional ones skipped above).';
|
|
162
|
+
}
|
|
163
|
+
return '✅ Every checklist that applies is already reviewed — nothing to spawn.';
|
|
164
|
+
}
|
|
122
165
|
/**
|
|
123
166
|
* The ONE instruction block, and the last thing the stage prints. Numbered rather than prose-linked
|
|
124
167
|
* ("Then… Finally…") so that skipping step 1 is visibly skipping a step, and worded so no earlier line
|
|
@@ -140,16 +183,20 @@ let ReviewReport = class ReviewReport {
|
|
|
140
183
|
* can never drift from the shape `wp-finish-upsert-pr` validates.
|
|
141
184
|
*/
|
|
142
185
|
nextSteps(input) {
|
|
143
|
-
const
|
|
144
|
-
const
|
|
186
|
+
const required = this.requiredOwed(input);
|
|
187
|
+
const offerable = this.offerableOwed(input);
|
|
188
|
+
// Numbered by what is actually PRINTED, so the numbers a reader sees are 1..n with no gaps: write
|
|
189
|
+
// review.json, then a spawn step only if anything must run, then an offer step only if anything may.
|
|
190
|
+
let step = 1;
|
|
191
|
+
const write = this.writeReviewStep(input.reviewPath, step++);
|
|
192
|
+
const spawn = required.length === 0 ? '' : this.spawnStep(input, required, step++);
|
|
193
|
+
const offer = offerable.length === 0 ? '' : this.offerStep(input, offerable, step++);
|
|
145
194
|
return '\n' + SEP
|
|
146
|
-
+ `▶ NEXT — ${
|
|
147
|
-
+ this.
|
|
148
|
-
+ this.spawnStep(input, owed)
|
|
149
|
-
+ this.finishStep(stepCount);
|
|
195
|
+
+ `▶ NEXT — ${step} steps, in this order. Step 1 is NOT optional:\n` + SEP + '\n'
|
|
196
|
+
+ write + spawn + offer + this.finishStep(step, required.length + offerable.length > 0);
|
|
150
197
|
}
|
|
151
|
-
writeReviewStep(reviewPath) {
|
|
152
|
-
return (
|
|
198
|
+
writeReviewStep(reviewPath, step) {
|
|
199
|
+
return (`STEP ${step} — review your own changes, then write the review file. Write it FIRST — BEFORE you spawn\n` +
|
|
153
200
|
' anything below. finish REFUSES without it, and a reviewer subagent may READ it: a\n' +
|
|
154
201
|
' checklist that judges the PR title, summary or risk level reads exactly this file, so\n' +
|
|
155
202
|
' writing it afterwards races that reviewer into seeing nothing — or, on a re-run of this\n' +
|
|
@@ -157,33 +204,87 @@ let ReviewReport = class ReviewReport {
|
|
|
157
204
|
(0, rules_config_1.reviewJsonSchemaHint)(reviewPath) + '\n\n');
|
|
158
205
|
}
|
|
159
206
|
/**
|
|
160
|
-
*
|
|
207
|
+
* The REQUIRED reviewers — one copy-paste block each, and nothing at all when none is owed. The prompt is
|
|
161
208
|
* deliberately a POINTER and nothing else: the generated instructions file is the contract, so anything
|
|
162
209
|
* restated here is a second copy that can go stale — which is exactly how a removed `success` field
|
|
163
210
|
* outlived its own removal in print.
|
|
211
|
+
*
|
|
212
|
+
* These are spawned WITHOUT asking. They are the checklists the repo declared `required: true`, which is
|
|
213
|
+
* the repo saying the decision was already made; putting them to the human again would re-open a question
|
|
214
|
+
* the config exists to settle.
|
|
164
215
|
*/
|
|
165
|
-
spawnStep(input, owed) {
|
|
166
|
-
if (owed.length === 0)
|
|
167
|
-
return '';
|
|
216
|
+
spawnStep(input, owed, step) {
|
|
168
217
|
const lines = [
|
|
169
|
-
`STEP
|
|
170
|
-
' one each.
|
|
171
|
-
' file on its behalf.',
|
|
218
|
+
`STEP ${step} — only once that file is written, spawn these ${owed.length} REQUIRED reviewer subagent(s) — a`,
|
|
219
|
+
' SEPARATE one each. They block the PR, so do NOT ask whether to run them. You may NOT',
|
|
220
|
+
' review your own work, and you may NOT write a reviewer\'s verdict file on its behalf.',
|
|
172
221
|
'',
|
|
173
222
|
];
|
|
174
|
-
|
|
175
|
-
// needs to know BEFORE it starts that one of these entries is not a spawn-shaped task.
|
|
176
|
-
if (input.refused.length > 0) {
|
|
177
|
-
lines.push(` ${input.refused.length} of them already ANSWERED and refused (marked ⛔ below). Do not spawn`, ' those against unchanged code — fix what they found first; the fix is the prerequisite.', '');
|
|
178
|
-
}
|
|
223
|
+
lines.push(...this.refusedWarning(input, owed));
|
|
179
224
|
for (const b of owed)
|
|
180
225
|
lines.push(...this.oneSpawnBlock(input, b));
|
|
181
226
|
lines.push('');
|
|
182
227
|
return lines.join('\n');
|
|
183
228
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
229
|
+
/**
|
|
230
|
+
* STEP n — the OPTIONAL reviewers: listed, never spawned unasked.
|
|
231
|
+
*
|
|
232
|
+
* This is the whole point of `required: false`. A one-line bug fix in a repo whose checklists key on a
|
|
233
|
+
* glob as broad as every TypeScript file otherwise pays for a dozen subagent reviews, and the only party
|
|
234
|
+
* who can judge whether this particular diff is worth them is the human looking at it.
|
|
235
|
+
*
|
|
236
|
+
* ONE batched multi-select question, explicitly. Asked one at a time, a human answering "no" nine times
|
|
237
|
+
* is being worn down rather than consulted, and by the third question the cheap thing is to say yes to
|
|
238
|
+
* everything — which is the state this feature exists to leave. The "None" option has to be spelled out
|
|
239
|
+
* too: an agent that offers a list without an explicit way to decline it has not really offered a choice.
|
|
240
|
+
*
|
|
241
|
+
* The blocking consequence is stated because it is the one non-obvious part of the contract: `required`
|
|
242
|
+
* governs whether a reviewer must RUN, not whether its answer counts. Choosing to run one and then
|
|
243
|
+
* shrugging off a red verdict would make the whole exercise theater.
|
|
244
|
+
*/
|
|
245
|
+
offerStep(input, offerable, step) {
|
|
246
|
+
const lines = [
|
|
247
|
+
`STEP ${step} — these ${offerable.length} OPTIONAL review checklist(s) matched this diff. They do NOT block the`,
|
|
248
|
+
' PR, and you may NOT decide for the human whether to run them.',
|
|
249
|
+
'',
|
|
250
|
+
' ASK THE HUMAN, in ONE multi-select question listing all of them plus an explicit',
|
|
251
|
+
' "None — required only" choice. Do not ask one question per reviewer. Then spawn ONLY',
|
|
252
|
+
' what they picked, the same way as any other reviewer.',
|
|
253
|
+
'',
|
|
254
|
+
' If they pick none, that is a complete answer: go straight to the final step. If they',
|
|
255
|
+
' told you up front to submit without reviews, re-run this stage as',
|
|
256
|
+
' `pnpm wp-review-upsert-pr --no-optional` and this step disappears.',
|
|
257
|
+
'',
|
|
258
|
+
' NOTE: whichever ones you DO run, their verdicts count in full — a red verdict from an',
|
|
259
|
+
' optional reviewer blocks the PR exactly like a required one.',
|
|
260
|
+
'',
|
|
261
|
+
];
|
|
262
|
+
lines.push(...this.refusedWarning(input, offerable));
|
|
263
|
+
for (const b of offerable)
|
|
264
|
+
lines.push(...this.oneSpawnBlock(input, b));
|
|
265
|
+
lines.push('');
|
|
266
|
+
return lines.join('\n');
|
|
267
|
+
}
|
|
268
|
+
// Said up front, not only beside the block: an agent that has decided to spawn everything listed here
|
|
269
|
+
// needs to know BEFORE it starts that one of these entries is not a spawn-shaped task. Scoped to the
|
|
270
|
+
// group being printed — a refusal among the REQUIRED reviewers is not a caveat on the optional list.
|
|
271
|
+
refusedWarning(input, group) {
|
|
272
|
+
const ids = new Set(group.map((b) => b.checklistId));
|
|
273
|
+
const n = input.refused.filter((r) => ids.has(r.checklistId)).length;
|
|
274
|
+
if (n === 0)
|
|
275
|
+
return [];
|
|
276
|
+
return [
|
|
277
|
+
` ${n} of them already ANSWERED and refused (marked ⛔ below). Do not spawn`,
|
|
278
|
+
' those against unchanged code — fix what they found first; the fix is the prerequisite.',
|
|
279
|
+
'',
|
|
280
|
+
];
|
|
281
|
+
}
|
|
282
|
+
finishStep(stepNumber, anyReviewers) {
|
|
283
|
+
// "every reviewer you ran" rather than "every reviewer above": with an optional list the human may
|
|
284
|
+
// legitimately have run none of them, and a precondition naming reviewers that were declined reads as
|
|
285
|
+
// an unmeetable one.
|
|
286
|
+
const precondition = anyReviewers
|
|
287
|
+
? 'once every reviewer you ran has written its verdict file'
|
|
187
288
|
: 'once that file exists';
|
|
188
289
|
return (`STEP ${stepNumber} — only ${precondition}, run: pnpm wp-finish-upsert-pr\n` +
|
|
189
290
|
' (The build gate is already green for this commit — finish reuses it unless HEAD moves.)\n\n');
|
|
@@ -194,6 +295,24 @@ let ReviewReport = class ReviewReport {
|
|
|
194
295
|
const reviewedIds = new Set(input.reviewed.map((r) => r.id));
|
|
195
296
|
return input.briefings.filter((b) => !reviewedIds.has(b.checklistId));
|
|
196
297
|
}
|
|
298
|
+
// Owed AND blocking — spawned without asking.
|
|
299
|
+
requiredOwed(input) {
|
|
300
|
+
return this.owedReviewers(input).filter((b) => b.required);
|
|
301
|
+
}
|
|
302
|
+
// Owed and optional. Still listed under `--no-optional` (as a skip verdict), just never as a step.
|
|
303
|
+
optionalOwed(input) {
|
|
304
|
+
return this.owedReviewers(input).filter((b) => !b.required);
|
|
305
|
+
}
|
|
306
|
+
// The optional ones the human is actually to be ASKED about — none, once they have already answered.
|
|
307
|
+
offerableOwed(input) {
|
|
308
|
+
return input.skipOptional ? [] : this.optionalOwed(input);
|
|
309
|
+
}
|
|
310
|
+
// Everything the AI still has to act on. Distinct from `owedReviewers`: a skipped optional checklist is
|
|
311
|
+
// owed a verdict it will never get, and treating it as pending work is what would print a spawn
|
|
312
|
+
// instruction for a review the human just declined.
|
|
313
|
+
actionableOwed(input) {
|
|
314
|
+
return [...this.requiredOwed(input), ...this.offerableOwed(input)];
|
|
315
|
+
}
|
|
197
316
|
/**
|
|
198
317
|
* One reviewer's block. A reviewer that already REFUSED gets the SAME spawn coordinates but a different
|
|
199
318
|
* lead-in, because the action before spawning is different: its own words are printed, and the spawn is
|
|
@@ -219,7 +338,7 @@ let ReviewReport = class ReviewReport {
|
|
|
219
338
|
leadIn(input, b) {
|
|
220
339
|
const refusal = input.refused.find((r) => r.checklistId === b.checklistId);
|
|
221
340
|
if (!refusal)
|
|
222
|
-
return [` ▶ ${b.subagent} — ${this.why(b)}
|
|
341
|
+
return [` ▶ ${b.subagent} — ${this.why(b)}`, ...this.docLine(b)];
|
|
223
342
|
return [
|
|
224
343
|
` ⛔ ${b.subagent} — ALREADY REVIEWED THIS BRANCH AND REFUSED. It will refuse again on unchanged code.`,
|
|
225
344
|
` ${refusal.message}`,
|
|
@@ -227,6 +346,18 @@ let ReviewReport = class ReviewReport {
|
|
|
227
346
|
' write a fresh verdict:',
|
|
228
347
|
];
|
|
229
348
|
}
|
|
349
|
+
/**
|
|
350
|
+
* The checklist's guidance doc, for OPTIONAL reviewers only.
|
|
351
|
+
*
|
|
352
|
+
* "4 file(s) matched" plus a broad glob does not tell a human what the review would actually look AT,
|
|
353
|
+
* and they are being asked to decide exactly that. Omitted for required reviewers: there is no decision
|
|
354
|
+
* to inform there — the reviewer runs either way, and the doc is already in its instructions file.
|
|
355
|
+
*/
|
|
356
|
+
docLine(b) {
|
|
357
|
+
if (b.required || b.docPath === '')
|
|
358
|
+
return [];
|
|
359
|
+
return [` reviews against: ${b.docPath}`];
|
|
360
|
+
}
|
|
230
361
|
// Why this one is in scope. A patternless checklist is NOT "matched" — it always runs, over the whole
|
|
231
362
|
// diff, and saying so is what tells a repo its checklist is firing on docs-only PRs by design.
|
|
232
363
|
why(b) {
|