@webpieces/pr-gate 0.4.719 → 0.4.721
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 +4 -6
- package/src/dashboard/checklist-comment-renderer.js +2 -4
- package/src/dashboard/checklist-comment-renderer.js.map +1 -1
- package/src/dashboard/dashboard.js +0 -2
- package/src/dashboard/dashboard.js.map +1 -1
- package/src/scripts/commands/finish-upsert-pr-command.js +9 -10
- package/src/scripts/commands/finish-upsert-pr-command.js.map +1 -1
- package/src/scripts/commands/review-upsert-pr-command.d.ts +2 -2
- package/src/scripts/commands/review-upsert-pr-command.js +5 -5
- package/src/scripts/commands/review-upsert-pr-command.js.map +1 -1
- package/src/scripts/pr-gate-app.d.ts +2 -13
- package/src/scripts/pr-gate-app.js +1 -20
- package/src/scripts/pr-gate-app.js.map +1 -1
- package/src/scripts/workflow/checklist-scanner.d.ts +21 -24
- package/src/scripts/workflow/checklist-scanner.js +35 -40
- package/src/scripts/workflow/checklist-scanner.js.map +1 -1
- package/src/scripts/workflow/review-report.d.ts +3 -3
- package/src/scripts/workflow/review-report.js +3 -3
- package/src/scripts/workflow/review-report.js.map +1 -1
- package/src/scripts/workflow/reviewer-verdict-gate.js +2 -2
- package/src/scripts/workflow/reviewer-verdict-gate.js.map +1 -1
- package/src/scripts/commands/authorize-command.d.ts +0 -82
- package/src/scripts/commands/authorize-command.js +0 -205
- package/src/scripts/commands/authorize-command.js.map +0 -1
- package/src/scripts/commands/check-auth-command.d.ts +0 -33
- package/src/scripts/commands/check-auth-command.js +0 -99
- package/src/scripts/commands/check-auth-command.js.map +0 -1
- package/src/scripts/workflow/authorization-context-resolver.d.ts +0 -63
- package/src/scripts/workflow/authorization-context-resolver.js +0 -107
- package/src/scripts/workflow/authorization-context-resolver.js.map +0 -1
- package/src/scripts/wp-authorize.d.ts +0 -2
- package/src/scripts/wp-authorize.js +0 -22
- package/src/scripts/wp-authorize.js.map +0 -1
- package/src/scripts/wp-check-auth.d.ts +0 -2
- package/src/scripts/wp-check-auth.js +0 -16
- package/src/scripts/wp-check-auth.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pr-gate-app.js","sourceRoot":"","sources":["../../../../../../packages/tooling/pr-gate/src/scripts/pr-gate-app.ts"],"names":[],"mappings":"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","sourcesContent":["import { DocumentDesign } from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { StartUpdateCommand } from './commands/start-update-command';\nimport { FinishUpdateCommand } from './commands/finish-update-command';\nimport { StartUpsertPrCommand } from './commands/start-upsert-pr-command';\nimport { FinishUpsertPrCommand } from './commands/finish-upsert-pr-command';\nimport { CleanupCommand } from './commands/cleanup-command';\nimport { CleanupOptions } from './commands/cleanup-options';\nimport { CheckoutCleanMainCommand } from './commands/checkout-clean-main-command';\nimport { LandPrCommand } from './commands/land-pr-command';\nimport { CheckPrCommand } from './commands/check-pr-command';\nimport { AuthorizeCommand } from './commands/authorize-command';\nimport { CheckAuthCommand } from './commands/check-auth-command';\nimport { ReviewUpsertPrCommand, ReviewUpsertPrOptions } from './commands/review-upsert-pr-command';\nimport { ReapWorktreeCommand } from './commands/reap-worktree-command';\nimport { BuildCommand, BuildOptions } from './commands/build-command';\nimport { PushDevCommand, PushDevOptions } from './commands/push-dev-command';\nimport { FinishPushDevCommand, FinishPushDevOptions } from './commands/finish-push-dev-command';\nimport { PushDevStateStore } from './workflow/push-dev-state';\nimport { CliArgSet, RepoRootFinder } from '@webpieces/rules-config';\n\n/**\n * The pr-gate application root. `container.get(PrGateApp)` resolves the entire workflow DAG (the command\n * classes → the injected git/merge/dashboard services). `@DocumentDesign` marks it the\n * top-of-DAG the DI-design analyzer roots on, so `role:app` pr-gate draws its design. Each `bin/*`\n * entry resolves THIS and calls the matching command method.\n */\n@DocumentDesign()\n@injectable(bindingScopeValues.Singleton)\nexport class PrGateApp {\n constructor(\n private readonly startUpdateCommand: StartUpdateCommand,\n private readonly finishUpdateCommand: FinishUpdateCommand,\n private readonly startUpsertPrCommand: StartUpsertPrCommand,\n private readonly finishUpsertPrCommand: FinishUpsertPrCommand,\n private readonly cleanupCommand: CleanupCommand,\n private readonly checkoutCleanMainCommand: CheckoutCleanMainCommand,\n private readonly landPrCommand: LandPrCommand,\n private readonly checkPrCommand: CheckPrCommand,\n private readonly authorizeCommand: AuthorizeCommand,\n private readonly checkAuthCommand: CheckAuthCommand,\n private readonly reviewUpsertPrCommand: ReviewUpsertPrCommand,\n private readonly reapWorktreeCommand: ReapWorktreeCommand,\n private readonly buildCommand: BuildCommand,\n private readonly pushDevCommand: PushDevCommand,\n private readonly finishPushDevCommand: FinishPushDevCommand,\n private readonly pushDevStateStore: PushDevStateStore,\n private readonly repoRootFinder: RepoRootFinder,\n ) {}\n\n /**\n * Refuse `command` while a `wp-push-dev --resolve` is half-finished.\n *\n * Enforced HERE, at the one place every bin funnels through, rather than in each command: a resolve\n * parks the checkout on a throwaway branch, so every command below would act on a branch that is not\n * the one it thinks it is. Putting the check in nine constructors is nine chances to forget it in the\n * tenth. PushDevStateStore owns the blocked list AND renders the hint from it, so the two cannot drift.\n */\n private assertNoResolveInProgress(command: string): void {\n if (!this.pushDevStateStore.isBlockedDuringResolve(command)) return;\n this.pushDevStateStore.assertIdle(this.repoRootFinder.resolveRepoRoot(process.cwd()), command);\n }\n\n /** `wp-push-dev`: publish a DISPOSABLE copy of this branch for the shared dev environment. No PR, no build. */\n pushDev(opts: PushDevOptions = new PushDevOptions()): Promise<void> {\n return this.pushDevCommand.run(opts);\n }\n\n /** `wp-finish-push-dev`: commit a resolved dev composition and publish it (conflict path only). */\n finishPushDev(opts: FinishPushDevOptions = new FinishPushDevOptions()): Promise<void> {\n return this.finishPushDevCommand.run(opts);\n }\n\n /**\n * INTERNAL (`wp-reap-worktree.js`, no `bin` entry): remove ONE named worktree and its branch.\n * `wp-land-pr` spawns it with cwd = the primary clone so the tree it just landed from can be\n * reaped by a process that is not standing in it.\n */\n reapWorktree(args: string[]): Promise<void> {\n return this.reapWorktreeCommand.run(args);\n }\n\n /**\n * `wp-build`: run this repo's ONE configured build (`commands.pr-gate.buildCommand`) — the same\n * command, resolved by the same resolver, that the PR gate's build stage runs. Not blocked during a\n * `wp-push-dev --resolve`: it mutates nothing and reads no branch state.\n */\n build(opts: BuildOptions): Promise<void> {\n return this.buildCommand.run(opts);\n }\n\n /** `wp-start-update`: 3-point squash-update from main (no PR). */\n startUpdate(): Promise<void> {\n this.assertNoResolveInProgress('wp-start-update');\n return this.startUpdateCommand.run();\n }\n\n /** `wp-finish-update`: validate + finalize a resolved 3-point merge (no PR). */\n finishUpdate(): Promise<void> {\n this.assertNoResolveInProgress('wp-finish-update');\n return this.finishUpdateCommand.run();\n }\n\n /** `wp-start-upsert-pr`: update from main (3-point merge), hand off review.json. No build gate, no push. */\n startUpsertPr(): Promise<void> {\n this.assertNoResolveInProgress('wp-start-upsert-pr');\n return this.startUpsertPrCommand.run();\n }\n\n /** `wp-finish-upsert-pr`: finalize merge, authoritative build gate, dashboard, create/update PR. */\n finishUpsertPr(): Promise<void> {\n this.assertNoResolveInProgress('wp-finish-upsert-pr');\n return this.finishUpsertPrCommand.run();\n }\n\n /**\n * `wp-cleanup`: reap what is provably merged AND every zero-commit husk, then report the rest with\n * the exact flags that take it. `options` carries what argv said — see CleanupOptions.\n */\n cleanup(options: CleanupOptions): Promise<void> {\n this.assertNoResolveInProgress('wp-cleanup');\n return this.cleanupCommand.run(options);\n }\n\n /**\n * `wp-checkout-clean-main`: go to main, fast-forward it, reap dead worktrees and branches, and sweep\n * the orphan directories a project move leaves behind. Replaces `git checkout main && git pull\n * origin main` outright — see CheckoutCleanMainCommand for why the old pair must stop being accepted\n * rather than surviving beside this.\n */\n checkoutCleanMain(): Promise<void> {\n this.assertNoResolveInProgress('wp-checkout-clean-main');\n return this.checkoutCleanMainCommand.run();\n }\n\n /** `wp-land-pr`: squash-merge this branch's PR into main with the compact commit body. */\n landPr(): Promise<void> {\n this.assertNoResolveInProgress('wp-land-pr');\n return this.landPrCommand.run();\n }\n\n /**\n * `wp-review-upsert-pr`: STAGE ② — validate the 3-point merge, run the build gate, extract this\n * branch's diff, and brief the reviewer subagents. Unlike the report-only command it replaces, this CAN\n * fail — before any reviewer is spawned, so a broken branch costs no reviewer tokens.\n */\n reviewUpsertPr(opts: ReviewUpsertPrOptions = new ReviewUpsertPrOptions()): Promise<void> {\n this.assertNoResolveInProgress('wp-review-upsert-pr');\n return this.reviewUpsertPrCommand.run(opts);\n }\n\n /**\n * `wp-authorize`: HUMAN ONLY — sign an authorization allowing one review checklist to be overridden.\n * Reads from /dev/tty, so an agent cannot drive it. See AuthorizeCommand for why that is the mechanism.\n */\n authorize(args: CliArgSet): void {\n this.authorizeCommand.run(args);\n }\n\n /** `wp-check-auth`: read-only verification of those authorizations — the half agents run. */\n checkAuth(args: CliArgSet): void {\n this.checkAuthCommand.run(args);\n }\n\n /** `wp-check-pr`: READ-ONLY CI check — verify the PR body carries a valid HMAC gate token for its head sha. */\n checkPr(): Promise<void> {\n return this.checkPrCommand.run();\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"pr-gate-app.js","sourceRoot":"","sources":["../../../../../../packages/tooling/pr-gate/src/scripts/pr-gate-app.ts"],"names":[],"mappings":"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","sourcesContent":["import { DocumentDesign } from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { StartUpdateCommand } from './commands/start-update-command';\nimport { FinishUpdateCommand } from './commands/finish-update-command';\nimport { StartUpsertPrCommand } from './commands/start-upsert-pr-command';\nimport { FinishUpsertPrCommand } from './commands/finish-upsert-pr-command';\nimport { CleanupCommand } from './commands/cleanup-command';\nimport { CleanupOptions } from './commands/cleanup-options';\nimport { CheckoutCleanMainCommand } from './commands/checkout-clean-main-command';\nimport { LandPrCommand } from './commands/land-pr-command';\nimport { CheckPrCommand } from './commands/check-pr-command';\nimport { ReviewUpsertPrCommand, ReviewUpsertPrOptions } from './commands/review-upsert-pr-command';\nimport { ReapWorktreeCommand } from './commands/reap-worktree-command';\nimport { BuildCommand, BuildOptions } from './commands/build-command';\nimport { PushDevCommand, PushDevOptions } from './commands/push-dev-command';\nimport { FinishPushDevCommand, FinishPushDevOptions } from './commands/finish-push-dev-command';\nimport { PushDevStateStore } from './workflow/push-dev-state';\nimport { RepoRootFinder } from '@webpieces/rules-config';\n\n/**\n * The pr-gate application root. `container.get(PrGateApp)` resolves the entire workflow DAG (the command\n * classes → the injected git/merge/dashboard services). `@DocumentDesign` marks it the\n * top-of-DAG the DI-design analyzer roots on, so `role:app` pr-gate draws its design. Each `bin/*`\n * entry resolves THIS and calls the matching command method.\n */\n@DocumentDesign()\n@injectable(bindingScopeValues.Singleton)\nexport class PrGateApp {\n constructor(\n private readonly startUpdateCommand: StartUpdateCommand,\n private readonly finishUpdateCommand: FinishUpdateCommand,\n private readonly startUpsertPrCommand: StartUpsertPrCommand,\n private readonly finishUpsertPrCommand: FinishUpsertPrCommand,\n private readonly cleanupCommand: CleanupCommand,\n private readonly checkoutCleanMainCommand: CheckoutCleanMainCommand,\n private readonly landPrCommand: LandPrCommand,\n private readonly checkPrCommand: CheckPrCommand,\n private readonly reviewUpsertPrCommand: ReviewUpsertPrCommand,\n private readonly reapWorktreeCommand: ReapWorktreeCommand,\n private readonly buildCommand: BuildCommand,\n private readonly pushDevCommand: PushDevCommand,\n private readonly finishPushDevCommand: FinishPushDevCommand,\n private readonly pushDevStateStore: PushDevStateStore,\n private readonly repoRootFinder: RepoRootFinder,\n ) {}\n\n /**\n * Refuse `command` while a `wp-push-dev --resolve` is half-finished.\n *\n * Enforced HERE, at the one place every bin funnels through, rather than in each command: a resolve\n * parks the checkout on a throwaway branch, so every command below would act on a branch that is not\n * the one it thinks it is. Putting the check in nine constructors is nine chances to forget it in the\n * tenth. PushDevStateStore owns the blocked list AND renders the hint from it, so the two cannot drift.\n */\n private assertNoResolveInProgress(command: string): void {\n if (!this.pushDevStateStore.isBlockedDuringResolve(command)) return;\n this.pushDevStateStore.assertIdle(this.repoRootFinder.resolveRepoRoot(process.cwd()), command);\n }\n\n /** `wp-push-dev`: publish a DISPOSABLE copy of this branch for the shared dev environment. No PR, no build. */\n pushDev(opts: PushDevOptions = new PushDevOptions()): Promise<void> {\n return this.pushDevCommand.run(opts);\n }\n\n /** `wp-finish-push-dev`: commit a resolved dev composition and publish it (conflict path only). */\n finishPushDev(opts: FinishPushDevOptions = new FinishPushDevOptions()): Promise<void> {\n return this.finishPushDevCommand.run(opts);\n }\n\n /**\n * INTERNAL (`wp-reap-worktree.js`, no `bin` entry): remove ONE named worktree and its branch.\n * `wp-land-pr` spawns it with cwd = the primary clone so the tree it just landed from can be\n * reaped by a process that is not standing in it.\n */\n reapWorktree(args: string[]): Promise<void> {\n return this.reapWorktreeCommand.run(args);\n }\n\n /**\n * `wp-build`: run this repo's ONE configured build (`commands.pr-gate.buildCommand`) — the same\n * command, resolved by the same resolver, that the PR gate's build stage runs. Not blocked during a\n * `wp-push-dev --resolve`: it mutates nothing and reads no branch state.\n */\n build(opts: BuildOptions): Promise<void> {\n return this.buildCommand.run(opts);\n }\n\n /** `wp-start-update`: 3-point squash-update from main (no PR). */\n startUpdate(): Promise<void> {\n this.assertNoResolveInProgress('wp-start-update');\n return this.startUpdateCommand.run();\n }\n\n /** `wp-finish-update`: validate + finalize a resolved 3-point merge (no PR). */\n finishUpdate(): Promise<void> {\n this.assertNoResolveInProgress('wp-finish-update');\n return this.finishUpdateCommand.run();\n }\n\n /** `wp-start-upsert-pr`: update from main (3-point merge), hand off review.json. No build gate, no push. */\n startUpsertPr(): Promise<void> {\n this.assertNoResolveInProgress('wp-start-upsert-pr');\n return this.startUpsertPrCommand.run();\n }\n\n /** `wp-finish-upsert-pr`: finalize merge, authoritative build gate, dashboard, create/update PR. */\n finishUpsertPr(): Promise<void> {\n this.assertNoResolveInProgress('wp-finish-upsert-pr');\n return this.finishUpsertPrCommand.run();\n }\n\n /**\n * `wp-cleanup`: reap what is provably merged AND every zero-commit husk, then report the rest with\n * the exact flags that take it. `options` carries what argv said — see CleanupOptions.\n */\n cleanup(options: CleanupOptions): Promise<void> {\n this.assertNoResolveInProgress('wp-cleanup');\n return this.cleanupCommand.run(options);\n }\n\n /**\n * `wp-checkout-clean-main`: go to main, fast-forward it, reap dead worktrees and branches, and sweep\n * the orphan directories a project move leaves behind. Replaces `git checkout main && git pull\n * origin main` outright — see CheckoutCleanMainCommand for why the old pair must stop being accepted\n * rather than surviving beside this.\n */\n checkoutCleanMain(): Promise<void> {\n this.assertNoResolveInProgress('wp-checkout-clean-main');\n return this.checkoutCleanMainCommand.run();\n }\n\n /** `wp-land-pr`: squash-merge this branch's PR into main with the compact commit body. */\n landPr(): Promise<void> {\n this.assertNoResolveInProgress('wp-land-pr');\n return this.landPrCommand.run();\n }\n\n /**\n * `wp-review-upsert-pr`: STAGE ② — validate the 3-point merge, run the build gate, extract this\n * branch's diff, and brief the reviewer subagents. Unlike the report-only command it replaces, this CAN\n * fail — before any reviewer is spawned, so a broken branch costs no reviewer tokens.\n */\n reviewUpsertPr(opts: ReviewUpsertPrOptions = new ReviewUpsertPrOptions()): Promise<void> {\n this.assertNoResolveInProgress('wp-review-upsert-pr');\n return this.reviewUpsertPrCommand.run(opts);\n }\n\n /** `wp-check-pr`: READ-ONLY CI check — verify the PR body carries a valid HMAC gate token for its head sha. */\n checkPr(): Promise<void> {\n return this.checkPrCommand.run();\n }\n}\n"]}
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ChecklistDefinition, ChecklistResult, ChecklistReviewContext, DiffScope, RequiredChecklist, ReviewJsonService } from '@webpieces/rules-config';
|
|
2
2
|
import { AiBranchName } from './git-readAiBranchName';
|
|
3
3
|
import { ChecklistDetector, ChecklistRoster } from './checklist-detector';
|
|
4
4
|
import { DiffBasis, DiffBasisResolver } from './diff-basis';
|
|
5
5
|
import { PrContextWriter } from './pr-context-writer';
|
|
6
|
-
import { ReviewChangedFiles } from './authorization-context-resolver';
|
|
7
6
|
/** How a caller wants the scan filtered. Data-only (per CLAUDE.md). */
|
|
8
7
|
export declare class ChecklistScanOptions {
|
|
9
8
|
/**
|
|
@@ -73,17 +72,7 @@ export declare class ChecklistScan {
|
|
|
73
72
|
* passed. An optional checklist that ran and went RED is absent from here and stays in `outstanding`.
|
|
74
73
|
*/
|
|
75
74
|
optionalNotRun: RequiredChecklist[];
|
|
76
|
-
|
|
77
|
-
* The human authorizations that VERIFY for this branch right now — the only thing that turns a
|
|
78
|
-
* reviewer's `override` into a shipping verdict.
|
|
79
|
-
*
|
|
80
|
-
* Carried on the scan for the same reason `results` is: it is an input to every verdict, and a caller
|
|
81
|
-
* that re-resolved it would be verifying against a second reading of the fork point and changed-file
|
|
82
|
-
* set. One resolution, one answer, or the command that gates and the command that reports disagree
|
|
83
|
-
* about whether the human said yes.
|
|
84
|
-
*/
|
|
85
|
-
authorized: AuthorizedOverrides;
|
|
86
|
-
constructor(defined: ChecklistDefinition[], applicable: RequiredChecklist[], reviewed: RequiredChecklist[], outstanding: RequiredChecklist[], context: ChecklistReviewContext, reviewPath: string, forkPoint: string, roster: ChecklistRoster, formatErrors: string[], basis: DiffBasis | undefined, changedFiles: string[] | undefined, results: ChecklistResult[] | undefined, optionalNotRun: RequiredChecklist[] | undefined, authorized: AuthorizedOverrides);
|
|
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[]);
|
|
87
76
|
}
|
|
88
77
|
/**
|
|
89
78
|
* The ONE computation of which reviewer subagents a branch owes, shared by `wp-review-upsert-pr` (which lists) and
|
|
@@ -95,9 +84,10 @@ export declare class ChecklistScan {
|
|
|
95
84
|
* 1. **The base is the FORK POINT of main, computed directly** — never `DiffScope.resolveBase`, which
|
|
96
85
|
* overlays `NX_BASE`/`NX_HEAD` from the environment. That made review coverage depend on an env var.
|
|
97
86
|
* It now arrives via {@link DiffBasisResolver}, which injects ForkPoint; same sha, one resolution.
|
|
98
|
-
* 2. **UNCOMMITTED work counts
|
|
99
|
-
*
|
|
100
|
-
*
|
|
87
|
+
* 2. **UNCOMMITTED work counts.** `getChangedFiles` is called with NO head, which is the branch of it that
|
|
88
|
+
* diffs base → WORKING TREE and unions in untracked files. Passing a head would diff commit-to-commit and
|
|
89
|
+
* silently miss staged, unstaged and untracked changes — so a checklist matching only uncommitted files
|
|
90
|
+
* would never fire and its reviewer would never be listed.
|
|
101
91
|
* 3. **The range and the command it prints are the SAME basis.** Property 2 used to be true of the file set
|
|
102
92
|
* only: reviewers were handed `git diff <base> HEAD`, which on a dirty tree covers a different range and
|
|
103
93
|
* prints nothing. Both now derive from one {@link DiffBasis}, carried on the scan so a materializing
|
|
@@ -108,23 +98,19 @@ export declare class ChecklistScan {
|
|
|
108
98
|
export declare class ChecklistScanner {
|
|
109
99
|
private readonly aiBranchName;
|
|
110
100
|
private readonly checklistDetector;
|
|
111
|
-
private readonly
|
|
101
|
+
private readonly diffScope;
|
|
112
102
|
private readonly diffBasisResolver;
|
|
113
103
|
private readonly prContextWriter;
|
|
114
104
|
private readonly reviewJsonService;
|
|
115
|
-
|
|
116
|
-
constructor(aiBranchName: AiBranchName, checklistDetector: ChecklistDetector, reviewChangedFiles: ReviewChangedFiles, diffBasisResolver: DiffBasisResolver, prContextWriter: PrContextWriter, reviewJsonService: ReviewJsonService, humanAuthorization: HumanAuthorizationService);
|
|
105
|
+
constructor(aiBranchName: AiBranchName, checklistDetector: ChecklistDetector, diffScope: DiffScope, diffBasisResolver: DiffBasisResolver, prContextWriter: PrContextWriter, reviewJsonService: ReviewJsonService);
|
|
117
106
|
/**
|
|
118
107
|
* `defined` is the caller's ALREADY-VALIDATED `prGate.checklists`. The scanner deliberately does not load
|
|
119
108
|
* config itself: `loadAndValidate` is the one gate on the checklist set (it rejects a non-array
|
|
120
109
|
* `checklists`, including the removed `{ doc }` manifest shape, and verifies every guidance doc and
|
|
121
110
|
* reviewer-agent file exists), both callers already run it for other fields, and keeping the read out of
|
|
122
|
-
* here leaves this a function of its inputs rather than of the filesystem.
|
|
123
|
-
* way and for the same reason — and as a POSITIONAL parameter rather than a field on
|
|
124
|
-
* {@link ChecklistScanOptions}, so it cannot be omitted. Omitted, every human authorization on the
|
|
125
|
-
* branch would be silently ignored, which looks exactly like a human who never approved anything.
|
|
111
|
+
* here leaves this a function of its inputs rather than of the filesystem.
|
|
126
112
|
*/
|
|
127
|
-
scan(repoRoot: string, defined: ChecklistDefinition[],
|
|
113
|
+
scan(repoRoot: string, defined: ChecklistDefinition[], opts: ChecklistScanOptions): ChecklistScan;
|
|
128
114
|
/**
|
|
129
115
|
* What `wp-finish-upsert-pr` actually REFUSES on: everything still owing a verdict, minus the optional
|
|
130
116
|
* checklists nobody ran.
|
|
@@ -135,4 +121,15 @@ export declare class ChecklistScanner {
|
|
|
135
121
|
* exactly the divergence this class's docstring exists to prevent.
|
|
136
122
|
*/
|
|
137
123
|
private blocking;
|
|
124
|
+
/**
|
|
125
|
+
* Every file changed since the fork point, INCLUDING uncommitted and untracked ones. Two non-default
|
|
126
|
+
* options, both load-bearing:
|
|
127
|
+
*
|
|
128
|
+
* `tsOnly:false` — the default drops every *.sql / Dockerfile / .env* file a checklist most wants
|
|
129
|
+
* to key on, which would silently shrink the set a reviewer is pointed at.
|
|
130
|
+
* `includeDeletions:true` — the default is `--diff-filter=d`, so a DELETED file is invisible. A PR that
|
|
131
|
+
* deletes a migration, an auth check or a terraform rule changed exactly what a
|
|
132
|
+
* checklist exists to catch, and under the default no checklist fires at all.
|
|
133
|
+
*/
|
|
134
|
+
private changedFiles;
|
|
138
135
|
}
|
|
@@ -8,7 +8,6 @@ const git_readAiBranchName_1 = require("./git-readAiBranchName");
|
|
|
8
8
|
const checklist_detector_1 = require("./checklist-detector");
|
|
9
9
|
const diff_basis_1 = require("./diff-basis");
|
|
10
10
|
const pr_context_writer_1 = require("./pr-context-writer");
|
|
11
|
-
const authorization_context_resolver_1 = require("./authorization-context-resolver");
|
|
12
11
|
/** How a caller wants the scan filtered. Data-only (per CLAUDE.md). */
|
|
13
12
|
class ChecklistScanOptions {
|
|
14
13
|
/**
|
|
@@ -88,25 +87,11 @@ class ChecklistScan {
|
|
|
88
87
|
* passed. An optional checklist that ran and went RED is absent from here and stays in `outstanding`.
|
|
89
88
|
*/
|
|
90
89
|
optionalNotRun;
|
|
91
|
-
/**
|
|
92
|
-
* The human authorizations that VERIFY for this branch right now — the only thing that turns a
|
|
93
|
-
* reviewer's `override` into a shipping verdict.
|
|
94
|
-
*
|
|
95
|
-
* Carried on the scan for the same reason `results` is: it is an input to every verdict, and a caller
|
|
96
|
-
* that re-resolved it would be verifying against a second reading of the fork point and changed-file
|
|
97
|
-
* set. One resolution, one answer, or the command that gates and the command that reports disagree
|
|
98
|
-
* about whether the human said yes.
|
|
99
|
-
*/
|
|
100
|
-
authorized;
|
|
101
90
|
// eslint-disable-next-line @typescript-eslint/max-params
|
|
102
91
|
constructor(defined, applicable, reviewed, outstanding, context, reviewPath, forkPoint, roster, formatErrors, basis = new diff_basis_1.DiffBasis(), changedFiles = [],
|
|
103
92
|
// Defaulted so a caller that only cares about the X/N/Z counts (and every existing test construction)
|
|
104
93
|
// stays a one-liner; the scanner itself always passes the real set.
|
|
105
|
-
results = [], optionalNotRun = []
|
|
106
|
-
// REQUIRED, with no default. An empty grant fails closed, but a construction that silently got one
|
|
107
|
-
// by omission would report "no human authorized this" for a branch where somebody did — the exact
|
|
108
|
-
// stall this whole feature exists to end, arrived at by forgetting an argument.
|
|
109
|
-
authorized) {
|
|
94
|
+
results = [], optionalNotRun = []) {
|
|
110
95
|
this.defined = defined;
|
|
111
96
|
this.applicable = applicable;
|
|
112
97
|
this.reviewed = reviewed;
|
|
@@ -120,7 +105,6 @@ class ChecklistScan {
|
|
|
120
105
|
this.changedFiles = changedFiles;
|
|
121
106
|
this.results = results;
|
|
122
107
|
this.optionalNotRun = optionalNotRun;
|
|
123
|
-
this.authorized = authorized;
|
|
124
108
|
}
|
|
125
109
|
}
|
|
126
110
|
exports.ChecklistScan = ChecklistScan;
|
|
@@ -134,9 +118,10 @@ exports.ChecklistScan = ChecklistScan;
|
|
|
134
118
|
* 1. **The base is the FORK POINT of main, computed directly** — never `DiffScope.resolveBase`, which
|
|
135
119
|
* overlays `NX_BASE`/`NX_HEAD` from the environment. That made review coverage depend on an env var.
|
|
136
120
|
* It now arrives via {@link DiffBasisResolver}, which injects ForkPoint; same sha, one resolution.
|
|
137
|
-
* 2. **UNCOMMITTED work counts
|
|
138
|
-
*
|
|
139
|
-
*
|
|
121
|
+
* 2. **UNCOMMITTED work counts.** `getChangedFiles` is called with NO head, which is the branch of it that
|
|
122
|
+
* diffs base → WORKING TREE and unions in untracked files. Passing a head would diff commit-to-commit and
|
|
123
|
+
* silently miss staged, unstaged and untracked changes — so a checklist matching only uncommitted files
|
|
124
|
+
* would never fire and its reviewer would never be listed.
|
|
140
125
|
* 3. **The range and the command it prints are the SAME basis.** Property 2 used to be true of the file set
|
|
141
126
|
* only: reviewers were handed `git diff <base> HEAD`, which on a dirty tree covers a different range and
|
|
142
127
|
* prints nothing. Both now derive from one {@link DiffBasis}, carried on the scan so a materializing
|
|
@@ -147,31 +132,26 @@ exports.ChecklistScan = ChecklistScan;
|
|
|
147
132
|
let ChecklistScanner = class ChecklistScanner {
|
|
148
133
|
aiBranchName;
|
|
149
134
|
checklistDetector;
|
|
150
|
-
|
|
135
|
+
diffScope;
|
|
151
136
|
diffBasisResolver;
|
|
152
137
|
prContextWriter;
|
|
153
138
|
reviewJsonService;
|
|
154
|
-
|
|
155
|
-
constructor(aiBranchName, checklistDetector, reviewChangedFiles, diffBasisResolver, prContextWriter, reviewJsonService, humanAuthorization) {
|
|
139
|
+
constructor(aiBranchName, checklistDetector, diffScope, diffBasisResolver, prContextWriter, reviewJsonService) {
|
|
156
140
|
this.aiBranchName = aiBranchName;
|
|
157
141
|
this.checklistDetector = checklistDetector;
|
|
158
|
-
this.
|
|
142
|
+
this.diffScope = diffScope;
|
|
159
143
|
this.diffBasisResolver = diffBasisResolver;
|
|
160
144
|
this.prContextWriter = prContextWriter;
|
|
161
145
|
this.reviewJsonService = reviewJsonService;
|
|
162
|
-
this.humanAuthorization = humanAuthorization;
|
|
163
146
|
}
|
|
164
147
|
/**
|
|
165
148
|
* `defined` is the caller's ALREADY-VALIDATED `prGate.checklists`. The scanner deliberately does not load
|
|
166
149
|
* config itself: `loadAndValidate` is the one gate on the checklist set (it rejects a non-array
|
|
167
150
|
* `checklists`, including the removed `{ doc }` manifest shape, and verifies every guidance doc and
|
|
168
151
|
* reviewer-agent file exists), both callers already run it for other fields, and keeping the read out of
|
|
169
|
-
* here leaves this a function of its inputs rather than of the filesystem.
|
|
170
|
-
* way and for the same reason — and as a POSITIONAL parameter rather than a field on
|
|
171
|
-
* {@link ChecklistScanOptions}, so it cannot be omitted. Omitted, every human authorization on the
|
|
172
|
-
* branch would be silently ignored, which looks exactly like a human who never approved anything.
|
|
152
|
+
* here leaves this a function of its inputs rather than of the filesystem.
|
|
173
153
|
*/
|
|
174
|
-
scan(repoRoot, defined,
|
|
154
|
+
scan(repoRoot, defined, opts) {
|
|
175
155
|
const featureName = this.aiBranchName.getFeatureName();
|
|
176
156
|
const reviewPath = (0, rules_config_1.reviewJsonPath)(repoRoot, featureName);
|
|
177
157
|
// ONE basis for the file set, the reproduce command and any downstream materialization. The fork
|
|
@@ -180,23 +160,20 @@ let ChecklistScanner = class ChecklistScanner {
|
|
|
180
160
|
const base = basis.base;
|
|
181
161
|
// ONE changed-file computation feeds both the roster (all X) and the applicable set (N). `detect` is
|
|
182
162
|
// pure and defined as the roster minus its empty entries, so the two cannot disagree about a match.
|
|
183
|
-
const changedFiles = this.
|
|
163
|
+
const changedFiles = this.changedFiles(repoRoot, base);
|
|
184
164
|
const roster = new checklist_detector_1.ChecklistRoster(this.checklistDetector.roster(defined, changedFiles), changedFiles.length, base !== '');
|
|
185
165
|
const applicable = this.checklistDetector.toRequired(this.checklistDetector.detect(defined, changedFiles));
|
|
186
166
|
const results = this.reviewJsonService.loadChecklistResults(reviewPath, applicable);
|
|
187
|
-
|
|
188
|
-
// approval's scope is checked against THIS diff, not against a second reading of it.
|
|
189
|
-
const authorized = this.humanAuthorization.verifiedFor(repoRoot, new rules_config_1.AuthorizationContext(featureName, base, changedFiles), gateSalt);
|
|
190
|
-
const stillOwed = this.reviewJsonService.pendingChecklists(applicable, results, authorized);
|
|
167
|
+
const stillOwed = this.reviewJsonService.pendingChecklists(applicable, results);
|
|
191
168
|
const owedIds = new Set(stillOwed.map((r) => r.id));
|
|
192
169
|
// NOT `!owedIds.has(...)`-with-the-optional-exemption-folded-in: an optional checklist nobody ran is
|
|
193
170
|
// neither reviewed nor blocking, and calling it "reviewed" would put a ✓ on the dashboard for a review
|
|
194
171
|
// that never happened.
|
|
195
172
|
const reviewed = applicable.filter((r) => !owedIds.has(r.id));
|
|
196
|
-
const optionalNotRun = this.reviewJsonService.optionalWithoutVerdict(applicable, results
|
|
173
|
+
const optionalNotRun = this.reviewJsonService.optionalWithoutVerdict(applicable, results);
|
|
197
174
|
return new ChecklistScan(defined, applicable, reviewed, opts.filterAlreadyReviewed ? this.blocking(stillOwed, optionalNotRun) : applicable, opts.contextStage === ''
|
|
198
175
|
? this.prContextWriter.contextFor(repoRoot, featureName, basis)
|
|
199
|
-
: this.prContextWriter.ensure(repoRoot, featureName, basis, opts.contextStage, changedFiles), reviewPath, base, roster, this.reviewJsonService.checklistFormatErrors(applicable, results
|
|
176
|
+
: this.prContextWriter.ensure(repoRoot, featureName, basis, opts.contextStage, changedFiles), reviewPath, base, roster, this.reviewJsonService.checklistFormatErrors(applicable, results), basis, changedFiles, results, optionalNotRun);
|
|
200
177
|
}
|
|
201
178
|
/**
|
|
202
179
|
* What `wp-finish-upsert-pr` actually REFUSES on: everything still owing a verdict, minus the optional
|
|
@@ -211,16 +188,34 @@ let ChecklistScanner = class ChecklistScanner {
|
|
|
211
188
|
const skipped = new Set(optionalNotRun.map((r) => r.id));
|
|
212
189
|
return stillOwed.filter((r) => !skipped.has(r.id));
|
|
213
190
|
}
|
|
191
|
+
/**
|
|
192
|
+
* Every file changed since the fork point, INCLUDING uncommitted and untracked ones. Two non-default
|
|
193
|
+
* options, both load-bearing:
|
|
194
|
+
*
|
|
195
|
+
* `tsOnly:false` — the default drops every *.sql / Dockerfile / .env* file a checklist most wants
|
|
196
|
+
* to key on, which would silently shrink the set a reviewer is pointed at.
|
|
197
|
+
* `includeDeletions:true` — the default is `--diff-filter=d`, so a DELETED file is invisible. A PR that
|
|
198
|
+
* deletes a migration, an auth check or a terraform rule changed exactly what a
|
|
199
|
+
* checklist exists to catch, and under the default no checklist fires at all.
|
|
200
|
+
*/
|
|
201
|
+
changedFiles(repoRoot, base) {
|
|
202
|
+
if (base === '')
|
|
203
|
+
return [];
|
|
204
|
+
const opts = new rules_config_1.ChangedFilesOptions();
|
|
205
|
+
opts.tsOnly = false;
|
|
206
|
+
opts.includeDeletions = true;
|
|
207
|
+
// No head argument — see the class comment. This is what includes the working tree.
|
|
208
|
+
return this.diffScope.getChangedFiles(repoRoot, base, undefined, opts);
|
|
209
|
+
}
|
|
214
210
|
};
|
|
215
211
|
exports.ChecklistScanner = ChecklistScanner;
|
|
216
212
|
exports.ChecklistScanner = ChecklistScanner = tslib_1.__decorate([
|
|
217
213
|
(0, inversify_1.injectable)(inversify_1.bindingScopeValues.Singleton),
|
|
218
214
|
tslib_1.__metadata("design:paramtypes", [git_readAiBranchName_1.AiBranchName,
|
|
219
215
|
checklist_detector_1.ChecklistDetector,
|
|
220
|
-
|
|
216
|
+
rules_config_1.DiffScope,
|
|
221
217
|
diff_basis_1.DiffBasisResolver,
|
|
222
218
|
pr_context_writer_1.PrContextWriter,
|
|
223
|
-
rules_config_1.ReviewJsonService
|
|
224
|
-
rules_config_1.HumanAuthorizationService])
|
|
219
|
+
rules_config_1.ReviewJsonService])
|
|
225
220
|
], ChecklistScanner);
|
|
226
221
|
//# sourceMappingURL=checklist-scanner.js.map
|
|
@@ -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 AuthorizationContext, AuthorizedOverrides, ChecklistDefinition, ChecklistResult,\n ChecklistReviewContext, HumanAuthorizationService, RequiredChecklist, ReviewJsonService,\n 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';\nimport { ReviewChangedFiles } from './authorization-context-resolver';\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 * The human authorizations that VERIFY for this branch right now — the only thing that turns a\n * reviewer's `override` into a shipping verdict.\n *\n * Carried on the scan for the same reason `results` is: it is an input to every verdict, and a caller\n * that re-resolved it would be verifying against a second reading of the fork point and changed-file\n * set. One resolution, one answer, or the command that gates and the command that reports disagree\n * about whether the human said yes.\n */\n authorized: AuthorizedOverrides;\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 // REQUIRED, with no default. An empty grant fails closed, but a construction that silently got one\n // by omission would report \"no human authorized this\" for a branch where somebody did — the exact\n // stall this whole feature exists to end, arrived at by forgetting an argument.\n authorized: AuthorizedOverrides,\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 this.authorized = authorized;\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**, and the set comes from {@link ReviewChangedFiles} — the one place that\n * computation lives, shared with the authorization commands so a checklist's scope and an approval's\n * scope are literally the same file set rather than two implementations of it.\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 reviewChangedFiles: ReviewChangedFiles,\n private readonly diffBasisResolver: DiffBasisResolver,\n private readonly prContextWriter: PrContextWriter,\n private readonly reviewJsonService: ReviewJsonService,\n private readonly humanAuthorization: HumanAuthorizationService,\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. `gateSalt` travels the same\n * way and for the same reason — and as a POSITIONAL parameter rather than a field on\n * {@link ChecklistScanOptions}, so it cannot be omitted. Omitted, every human authorization on the\n * branch would be silently ignored, which looks exactly like a human who never approved anything.\n */\n scan(\n repoRoot: string, defined: ChecklistDefinition[], gateSalt: string, opts: ChecklistScanOptions,\n ): 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.reviewChangedFiles.since(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 // Resolved ONCE, from the same fork point and changed-file set the matching ran against — the\n // approval's scope is checked against THIS diff, not against a second reading of it.\n const authorized = this.humanAuthorization.verifiedFor(\n repoRoot, new AuthorizationContext(featureName, base, changedFiles), gateSalt);\n const stillOwed = this.reviewJsonService.pendingChecklists(applicable, results, authorized);\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, authorized);\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, authorized),\n basis,\n changedFiles,\n results,\n optionalNotRun,\n authorized,\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"]}
|
|
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"]}
|
|
@@ -9,9 +9,9 @@ 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
|
|
13
|
-
* which is only
|
|
14
|
-
* finish's act, on the refusal it is actually acting on.
|
|
12
|
+
* anything. `refusalError` called with no archive path says "fix it, then re-run; or set an override in
|
|
13
|
+
* review-<id>.json", which is only true while that file is still live — and at stage ② it is. Retiring a
|
|
14
|
+
* verdict is finish's act, on the refusal it is actually acting on.
|
|
15
15
|
*/
|
|
16
16
|
export declare class RefusedReviewer {
|
|
17
17
|
checklistId: string;
|
|
@@ -15,9 +15,9 @@ 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
|
|
19
|
-
* which is only
|
|
20
|
-
* finish's act, on the refusal it is actually acting on.
|
|
18
|
+
* anything. `refusalError` called with no archive path says "fix it, then re-run; or set an override in
|
|
19
|
+
* review-<id>.json", which is only true while that file is still live — and at stage ② it is. Retiring a
|
|
20
|
+
* verdict is finish's act, on the refusal it is actually acting on.
|
|
21
21
|
*/
|
|
22
22
|
class RefusedReviewer {
|
|
23
23
|
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 points the reader at the LIVE review-<id>.json,\n * which is only correct while that file is still there — and at stage ② it is. Retiring a verdict is\n * 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":"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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"]}
|