@webpieces/pr-gate 0.4.498 → 0.4.500

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.
Files changed (44) hide show
  1. package/package.json +3 -3
  2. package/src/dashboard/dashboard.d.ts +15 -0
  3. package/src/dashboard/dashboard.js +25 -2
  4. package/src/dashboard/dashboard.js.map +1 -1
  5. package/src/scripts/commands/finish-upsert-pr-command.d.ts +55 -5
  6. package/src/scripts/commands/finish-upsert-pr-command.js +162 -30
  7. package/src/scripts/commands/finish-upsert-pr-command.js.map +1 -1
  8. package/src/scripts/commands/review-upsert-pr-command.d.ts +88 -0
  9. package/src/scripts/commands/review-upsert-pr-command.js +244 -0
  10. package/src/scripts/commands/review-upsert-pr-command.js.map +1 -0
  11. package/src/scripts/commands/start-upsert-pr-command.d.ts +10 -15
  12. package/src/scripts/commands/start-upsert-pr-command.js +41 -47
  13. package/src/scripts/commands/start-upsert-pr-command.js.map +1 -1
  14. package/src/scripts/pr-gate-app.d.ts +9 -5
  15. package/src/scripts/pr-gate-app.js +12 -8
  16. package/src/scripts/pr-gate-app.js.map +1 -1
  17. package/src/scripts/workflow/checklist-scanner.d.ts +27 -11
  18. package/src/scripts/workflow/checklist-scanner.js +37 -15
  19. package/src/scripts/workflow/checklist-scanner.js.map +1 -1
  20. package/src/scripts/workflow/diff-basis.d.ts +53 -0
  21. package/src/scripts/workflow/diff-basis.js +113 -0
  22. package/src/scripts/workflow/diff-basis.js.map +1 -0
  23. package/src/scripts/workflow/diff-materializer.d.ts +85 -0
  24. package/src/scripts/workflow/diff-materializer.js +253 -0
  25. package/src/scripts/workflow/diff-materializer.js.map +1 -0
  26. package/src/scripts/workflow/git-findForkPoint.d.ts +1 -1
  27. package/src/scripts/workflow/git-findForkPoint.js +1 -1
  28. package/src/scripts/workflow/git-findForkPoint.js.map +1 -1
  29. package/src/scripts/workflow/pr-context-writer.d.ts +16 -10
  30. package/src/scripts/workflow/pr-context-writer.js +20 -14
  31. package/src/scripts/workflow/pr-context-writer.js.map +1 -1
  32. package/src/scripts/workflow/review-stage-receipt.d.ts +36 -0
  33. package/src/scripts/workflow/review-stage-receipt.js +82 -0
  34. package/src/scripts/workflow/review-stage-receipt.js.map +1 -0
  35. package/src/scripts/workflow/reviewer-briefing-builder.d.ts +36 -0
  36. package/src/scripts/workflow/reviewer-briefing-builder.js +104 -0
  37. package/src/scripts/workflow/reviewer-briefing-builder.js.map +1 -0
  38. package/src/scripts/{wp-checklist.js → wp-review-upsert-pr.js} +3 -3
  39. package/src/scripts/wp-review-upsert-pr.js.map +1 -0
  40. package/src/scripts/commands/checklist-command.d.ts +0 -27
  41. package/src/scripts/commands/checklist-command.js +0 -97
  42. package/src/scripts/commands/checklist-command.js.map +0 -1
  43. package/src/scripts/wp-checklist.js.map +0 -1
  44. /package/src/scripts/{wp-checklist.d.ts → wp-review-upsert-pr.d.ts} +0 -0
@@ -1 +1 @@
1
- {"version":3,"file":"start-upsert-pr-command.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/commands/start-upsert-pr-command.ts"],"names":[],"mappings":"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","sourcesContent":["import { execSync } from 'child_process';\nimport { loadAndValidate, reviewJsonPath, reviewJsonSchemaHint, writeTemplate, writeTemplateIfMissing, CliExitError, RepoRootFinder, ChecklistReviewContext } from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { AiBranchName } from '../workflow/git-readAiBranchName';\nimport { BranchNaming } from '../workflow/branch-naming';\nimport { GitExec } from '../workflow/git-exec';\nimport { ForkPoint } from '../workflow/git-findForkPoint';\nimport { PrContextWriter } from '../workflow/pr-context-writer';\nimport { RunUpdate } from '../workflow/run-update';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n// START of the AI-first PR flow: the deterministic setup — update from main — then hand the AI\n// instructions to run `wp-checklist`, WRITE review.json, and run `wp-finish-upsert-pr` (which reads it\n// and posts the PR). It runs NO build gate: start's job is the update / 3-point merge, and the SINGLE\n// build gate is authoritative and lives in wp-finish-upsert-pr (so `pr-gate.buildCommand` is a\n// finish-only knob). This command NEVER creates/updates a PR and NEVER pushes: all `gh` posting and the\n// ONE push live in finish, behind review.json + the checklists + the authoritative build gate.\n@injectable(bindingScopeValues.Singleton)\nexport class StartUpsertPrCommand {\n constructor(\n private readonly repoRootFinder: RepoRootFinder,\n private readonly aiBranchName: AiBranchName,\n private readonly branchNaming: BranchNaming,\n private readonly gitExec: GitExec,\n private readonly runUpdate: RunUpdate,\n private readonly forkPoint: ForkPoint,\n private readonly prContextWriter: PrContextWriter,\n ) {}\n\n async run(): Promise<void> {\n const repoRoot = this.repoRootFinder.resolveRepoRoot(process.cwd());\n // Refresh the AI-facing workflow doc so it's present + current for any failure message to cite.\n writeTemplate(repoRoot, 'webpieces.git-workflow.md');\n // When this repo has opted into server-side enforcement (a committed gateSalt), scaffold the CI\n // workflow into the gitignored instruct-ai dir (never .github directly — that would dirty the tree\n // before the clean-tree check) and tell the human to copy + require it. IfMissing so it is written\n // once and never clobbers a customized copy.\n this.scaffoldCiWorkflow(repoRoot);\n\n // Precondition: a fully-committed tree. This flow squash-updates the branch and builds it — the\n // tooling must not commit your work for you, and building a dirty working tree would let an\n // uncommitted change build green over a different commit than the one that ships. Fail if dirty.\n this.gitExec.assertCleanTree(repoRoot);\n\n // Nothing here pushes. This command reviews; wp-finish-upsert-pr pushes ONCE, after review.json,\n // every BLOCK checklist, and the authoritative build gate — so no unreviewed commit reaches the\n // remote, and there is no early `synchronize` firing against a PR body with a stale gate token.\n await this.updateBranchFromMain(repoRoot);\n\n // No build gate here — start only updates from main / drives the 3-point merge. The one build\n // gate is authoritative and runs in wp-finish-upsert-pr, after review.json + every checklist.\n this.handOffToReview(repoRoot);\n }\n\n /**\n * Hand the AI its next step: run `wp-checklist`, write review.json, then run finish (which posts the PR).\n *\n * This USED to compute the matched checklists and print every reviewer's full instructions inline — twice,\n * in two blocks that could drift. That detail now lives in ONE command, `wp-checklist`, which wp-finish\n * validates against; here we only point at it. Keeping this section short is the point: it is the block\n * the AI must actually act on, and burying the three steps in forty lines of reviewer detail is how a\n * step gets skipped.\n */\n private handOffToReview(repoRoot: string): void {\n const reviewPath = reviewJsonPath(repoRoot, this.aiBranchName.getFeatureName());\n // Persist the review-format + process instructions where any failure message can cite them.\n writeTemplate(repoRoot, 'webpieces.review-checklists.md');\n // Persist the PR diff context (base sha + the full changed-file set) — wp-checklist and every reviewer\n // subagent read it, so it must exist before either runs.\n const context = this.prContextWriter.ensure(repoRoot, this.aiBranchName.getFeatureName(), this.forkPoint.resolveForkPoint(repoRoot));\n process.stdout.write('\\n' + SEP + '② Review the PR, then finish\\n' + SEP + '\\n');\n process.stdout.write(\n `Branch is updated (nothing pushed yet — finish does the one push, behind the build gate).\\n` +\n `${this.contextLines(context)}\\n` +\n `${this.checklistPointer(repoRoot)}\\n` +\n `Then review your own changes and\\n` +\n `${reviewJsonSchemaHint(reviewPath)}\\n\\n` +\n `Finally run: pnpm wp-finish-upsert-pr\\n` +\n `(It re-validates the build, re-checks every checklist, renders the dashboard, and creates/updates the PR.)\\n\\n`,\n );\n }\n\n /**\n * Step 1 of the review: find out what review this diff owes. A repo with checklists gets pointed at\n * `wp-checklist`; a repo with none is told plainly that it has none, because printing \"run wp-checklist\"\n * forever at a repo that will never have one is noise, and silence reads as \"a checklist passed\".\n */\n private checklistPointer(repoRoot: string): string {\n if (loadAndValidate(repoRoot).prGate.checklists.length === 0) {\n return '📋 Review checklists: NONE CONFIGURED for this repo — nothing to run, and that is fine.\\n';\n }\n return (\n '📋 FIRST run: pnpm wp-checklist\\n' +\n ' It validates this repo\\'s checklist patterns against your diff and prints WHICH reviewer\\n' +\n ' subagents you must spawn, plus the exact review-<id>.json each must write. Do that BEFORE\\n' +\n ' finishing — wp-finish-upsert-pr refuses to open the PR until every one of them has run.\\n'\n );\n }\n\n // The diff facts, stated where the instruction to use them is — not in a separate block above it.\n private contextLines(context: ChecklistReviewContext): string {\n if (context.baseSha.trim() === '') return '';\n return (\n `Your diff: git diff ${context.baseSha} HEAD\\n` +\n `Full changed-file set + base/head sha: ${context.prContextPath}\\n`\n );\n }\n\n // Scaffold the server-side CI check when (and only when) this repo set a gateSalt. Written to the\n // gitignored instruct-ai dir so it never dirties the tree; the human copies it to .github/workflows\n // and marks it required (webpieces can't set branch protection). No-op for repos with no gateSalt.\n private scaffoldCiWorkflow(repoRoot: string): void {\n if (loadAndValidate(repoRoot).prGate.gateSalt.trim() === '') return;\n writeTemplateIfMissing(repoRoot, 'webpieces-pr-gate.yml');\n process.stdout.write(\n `\\nℹ️ Server-side gate enforcement is ON (gateSalt set). If you have not already:\\n` +\n ` • copy .webpieces/instruct-ai/webpieces-pr-gate.yml → .github/workflows/ and commit it\\n` +\n ` • mark the \"webpieces-pr-gate\" check REQUIRED in branch protection (repo admin only)\\n`,\n );\n }\n\n // Bring the branch up to date with main via the shared 3-point engine (in-process). On conflict the\n // merge process doc it writes names `wp-finish-upsert-pr` as the finish command.\n private async updateBranchFromMain(repoRoot: string): Promise<void> {\n process.stdout.write('\\n' + SEP + '① Updating branch from main\\n' + SEP + '\\n');\n // pushRemote=false — finish owns the single push (see MergeEndOptions).\n const outcome = await this.runUpdate.runUpdateFromMain(repoRoot, 'wp-start-upsert-pr', 'wp-finish-upsert-pr', false);\n if (outcome === 'conflict' || outcome === 'unvalidatedResume') {\n throw new CliExitError(2,\n '\\n⏸️ Conflicts — resolve them, then run pnpm wp-finish-upsert-pr (it validates the merge AND finishes the PR).',\n );\n }\n }\n}\n"]}
1
+ {"version":3,"file":"start-upsert-pr-command.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/commands/start-upsert-pr-command.ts"],"names":[],"mappings":"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","sourcesContent":["import { execSync } from 'child_process';\nimport { loadAndValidate, writeTemplate, writeTemplateIfMissing, CliExitError, RepoRootFinder, ChecklistReviewContext } from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { AiBranchName } from '../workflow/git-readAiBranchName';\nimport { BranchNaming } from '../workflow/branch-naming';\nimport { DiffBasisResolver } from '../workflow/diff-basis';\nimport { GitExec } from '../workflow/git-exec';\nimport { PrContextWriter } from '../workflow/pr-context-writer';\nimport { RunUpdate } from '../workflow/run-update';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n// STAGE ① of the AI-first PR flow: the deterministic setup — update from main / drive the 3-point merge —\n// then hand the AI ONE next command, `wp-review-upsert-pr`.\n//\n// It runs NO build gate, and that is still deliberate, but the reason has changed. It is not that the build\n// belongs only at the end: it is that at THIS point a conflicted 3-point merge may still be unresolved, so\n// there is nothing worth building yet. The gate moved FORWARD, not away — stage ② finalizes the merge and\n// then builds, so reviewers never judge a branch that does not compile, and wp-finish-upsert-pr skips the\n// rebuild when HEAD has not moved since. `pr-gate.buildCommand` is therefore no longer a finish-only knob.\n//\n// This command NEVER creates/updates a PR and NEVER pushes: all `gh` posting and the ONE push live in\n// finish, behind review.json + the checklists + the build gate.\n@injectable(bindingScopeValues.Singleton)\nexport class StartUpsertPrCommand {\n constructor(\n private readonly repoRootFinder: RepoRootFinder,\n private readonly aiBranchName: AiBranchName,\n private readonly branchNaming: BranchNaming,\n private readonly gitExec: GitExec,\n private readonly runUpdate: RunUpdate,\n private readonly diffBasisResolver: DiffBasisResolver,\n private readonly prContextWriter: PrContextWriter,\n ) {}\n\n async run(): Promise<void> {\n const repoRoot = this.repoRootFinder.resolveRepoRoot(process.cwd());\n // Refresh the AI-facing workflow doc so it's present + current for any failure message to cite.\n writeTemplate(repoRoot, 'webpieces.git-workflow.md');\n // When this repo has opted into server-side enforcement (a committed gateSalt), scaffold the CI\n // workflow into the gitignored instruct-ai dir (never .github directly — that would dirty the tree\n // before the clean-tree check) and tell the human to copy + require it. IfMissing so it is written\n // once and never clobbers a customized copy.\n this.scaffoldCiWorkflow(repoRoot);\n\n // Precondition: a fully-committed tree. This flow squash-updates the branch and builds it — the\n // tooling must not commit your work for you, and building a dirty working tree would let an\n // uncommitted change build green over a different commit than the one that ships. Fail if dirty.\n this.gitExec.assertCleanTree(repoRoot);\n\n // Nothing here pushes. This command reviews; wp-finish-upsert-pr pushes ONCE, after review.json,\n // every BLOCK checklist, and the authoritative build gate — so no unreviewed commit reaches the\n // remote, and there is no early `synchronize` firing against a PR body with a stale gate token.\n await this.updateBranchFromMain(repoRoot);\n\n // No build gate here — a conflicted merge may still be unresolved, so there is nothing worth\n // building. Stage ② finalizes the merge FIRST, then builds; see the class comment.\n this.handOffToReview(repoRoot);\n }\n\n /**\n * Hand the AI its ONE next step: `wp-review-upsert-pr`.\n *\n * This prints exactly one command on purpose. It used to print three (run the checklist, write\n * review.json, then finish), and a three-item list is a list with a step to skip. Stage ② now owns all\n * of it — it validates the 3-point merge, builds, materializes the diff, briefs the reviewers, AND\n * prints the review.json schema — so there is nothing here to enumerate. Notably review.json is NOT\n * requested here any more: asking for a written review before the branch is known to compile invites\n * one that describes code that does not build.\n */\n private handOffToReview(repoRoot: string): void {\n // Persist the review-format + process instructions where any failure message can cite them.\n writeTemplate(repoRoot, 'webpieces.review-checklists.md');\n // Persist the PR diff context (base sha + the full changed-file set) so it exists even if the AI\n // stops here. Stage ② rewrites it with the materialized-diff dir once it has one.\n const context = this.prContextWriter.ensure(\n repoRoot, this.aiBranchName.getFeatureName(), this.diffBasisResolver.resolve(repoRoot));\n process.stdout.write('\\n' + SEP + '② Review the PR, then finish\\n' + SEP + '\\n');\n process.stdout.write(\n `Branch is updated (nothing pushed yet — finish does the one push, behind the build gate).\\n` +\n `${this.contextLines(context)}\\n` +\n `▶ NEXT run: pnpm wp-review-upsert-pr\\n` +\n ` It validates the 3-point merge, runs the build gate, extracts this branch's diff for the\\n` +\n ` reviewers, and prints what to spawn plus the review.json schema. Everything else waits on it —\\n` +\n ` wp-finish-upsert-pr refuses to open a PR until it has run.\\n\\n`,\n );\n }\n\n // The diff facts, stated where the instruction to use them is — not in a separate block above it. The\n // command comes from the resolved basis; hand-writing `<base> HEAD` here is empty on a dirty tree.\n private contextLines(context: ChecklistReviewContext): string {\n if (context.baseSha.trim() === '') return '';\n const cmd = context.fileDiffCommand.replace(' -- <file>', '');\n return (\n `${cmd === '' ? '' : `Your diff: ${cmd}\\n`}` +\n `Full changed-file set + base/head sha: ${context.prContextPath}\\n`\n );\n }\n\n // Scaffold the server-side CI check when (and only when) this repo set a gateSalt. Written to the\n // gitignored instruct-ai dir so it never dirties the tree; the human copies it to .github/workflows\n // and marks it required (webpieces can't set branch protection). No-op for repos with no gateSalt.\n private scaffoldCiWorkflow(repoRoot: string): void {\n if (loadAndValidate(repoRoot).prGate.gateSalt.trim() === '') return;\n writeTemplateIfMissing(repoRoot, 'webpieces-pr-gate.yml');\n process.stdout.write(\n `\\nℹ️ Server-side gate enforcement is ON (gateSalt set). If you have not already:\\n` +\n ` • copy .webpieces/instruct-ai/webpieces-pr-gate.yml → .github/workflows/ and commit it\\n` +\n ` • mark the \"webpieces-pr-gate\" check REQUIRED in branch protection (repo admin only)\\n`,\n );\n }\n\n // Bring the branch up to date with main via the shared 3-point engine (in-process). On conflict the\n // merge process doc it writes names `wp-review-upsert-pr` as the finish command — that is the command\n // that now validates and commits a conflict resolution, so it is what the doc must send the AI to.\n private async updateBranchFromMain(repoRoot: string): Promise<void> {\n process.stdout.write('\\n' + SEP + '① Updating branch from main\\n' + SEP + '\\n');\n // pushRemote=false — finish owns the single push (see MergeEndOptions).\n const outcome = await this.runUpdate.runUpdateFromMain(repoRoot, 'wp-start-upsert-pr', 'wp-review-upsert-pr', false);\n if (outcome === 'conflict' || outcome === 'unvalidatedResume') {\n throw new CliExitError(2,\n '\\n⏸️ Conflicts — resolve them, then run pnpm wp-review-upsert-pr (it validates the merge, builds it,\\n' +\n ' and only then briefs the reviewers).',\n );\n }\n }\n}\n"]}
@@ -5,7 +5,7 @@ import { FinishUpsertPrCommand } from './commands/finish-upsert-pr-command';
5
5
  import { CleanupCommand } from './commands/cleanup-command';
6
6
  import { LandPrCommand } from './commands/land-pr-command';
7
7
  import { CheckPrCommand } from './commands/check-pr-command';
8
- import { ChecklistCommand } from './commands/checklist-command';
8
+ import { ReviewUpsertPrCommand } from './commands/review-upsert-pr-command';
9
9
  /**
10
10
  * The pr-gate application root. `container.get(PrGateApp)` resolves the entire workflow DAG (the command
11
11
  * classes → the injected git/merge/dashboard services). `@DocumentDesign` marks it the
@@ -20,8 +20,8 @@ export declare class PrGateApp {
20
20
  private readonly cleanupCommand;
21
21
  private readonly landPrCommand;
22
22
  private readonly checkPrCommand;
23
- private readonly checklistCommand;
24
- constructor(startUpdateCommand: StartUpdateCommand, finishUpdateCommand: FinishUpdateCommand, startUpsertPrCommand: StartUpsertPrCommand, finishUpsertPrCommand: FinishUpsertPrCommand, cleanupCommand: CleanupCommand, landPrCommand: LandPrCommand, checkPrCommand: CheckPrCommand, checklistCommand: ChecklistCommand);
23
+ private readonly reviewUpsertPrCommand;
24
+ constructor(startUpdateCommand: StartUpdateCommand, finishUpdateCommand: FinishUpdateCommand, startUpsertPrCommand: StartUpsertPrCommand, finishUpsertPrCommand: FinishUpsertPrCommand, cleanupCommand: CleanupCommand, landPrCommand: LandPrCommand, checkPrCommand: CheckPrCommand, reviewUpsertPrCommand: ReviewUpsertPrCommand);
25
25
  /** `wp-start-update`: 3-point squash-update from main (no PR). */
26
26
  startUpdate(): Promise<void>;
27
27
  /** `wp-finish-update`: validate + finalize a resolved 3-point merge (no PR). */
@@ -34,8 +34,12 @@ export declare class PrGateApp {
34
34
  cleanup(): Promise<void>;
35
35
  /** `wp-land-pr`: squash-merge this branch's PR into main with the compact commit body. */
36
36
  landPr(): Promise<void>;
37
- /** `wp-checklist`: READ-ONLY — which reviewer subagents this diff still owes, and what to tell them. */
38
- checklist(): Promise<void>;
37
+ /**
38
+ * `wp-review-upsert-pr`: STAGE ② — validate the 3-point merge, run the build gate, extract this
39
+ * branch's diff, and brief the reviewer subagents. Unlike the report-only command it replaces, this CAN
40
+ * fail — before any reviewer is spawned, so a broken branch costs no reviewer tokens.
41
+ */
42
+ reviewUpsertPr(): Promise<void>;
39
43
  /** `wp-check-pr`: READ-ONLY CI check — verify the PR body carries a valid HMAC gate token for its head sha. */
40
44
  checkPr(): Promise<void>;
41
45
  }
@@ -11,7 +11,7 @@ const finish_upsert_pr_command_1 = require("./commands/finish-upsert-pr-command"
11
11
  const cleanup_command_1 = require("./commands/cleanup-command");
12
12
  const land_pr_command_1 = require("./commands/land-pr-command");
13
13
  const check_pr_command_1 = require("./commands/check-pr-command");
14
- const checklist_command_1 = require("./commands/checklist-command");
14
+ const review_upsert_pr_command_1 = require("./commands/review-upsert-pr-command");
15
15
  /**
16
16
  * The pr-gate application root. `container.get(PrGateApp)` resolves the entire workflow DAG (the command
17
17
  * classes → the injected git/merge/dashboard services). `@DocumentDesign` marks it the
@@ -26,8 +26,8 @@ let PrGateApp = class PrGateApp {
26
26
  cleanupCommand;
27
27
  landPrCommand;
28
28
  checkPrCommand;
29
- checklistCommand;
30
- constructor(startUpdateCommand, finishUpdateCommand, startUpsertPrCommand, finishUpsertPrCommand, cleanupCommand, landPrCommand, checkPrCommand, checklistCommand) {
29
+ reviewUpsertPrCommand;
30
+ constructor(startUpdateCommand, finishUpdateCommand, startUpsertPrCommand, finishUpsertPrCommand, cleanupCommand, landPrCommand, checkPrCommand, reviewUpsertPrCommand) {
31
31
  this.startUpdateCommand = startUpdateCommand;
32
32
  this.finishUpdateCommand = finishUpdateCommand;
33
33
  this.startUpsertPrCommand = startUpsertPrCommand;
@@ -35,7 +35,7 @@ let PrGateApp = class PrGateApp {
35
35
  this.cleanupCommand = cleanupCommand;
36
36
  this.landPrCommand = landPrCommand;
37
37
  this.checkPrCommand = checkPrCommand;
38
- this.checklistCommand = checklistCommand;
38
+ this.reviewUpsertPrCommand = reviewUpsertPrCommand;
39
39
  }
40
40
  /** `wp-start-update`: 3-point squash-update from main (no PR). */
41
41
  startUpdate() {
@@ -61,9 +61,13 @@ let PrGateApp = class PrGateApp {
61
61
  landPr() {
62
62
  return this.landPrCommand.run();
63
63
  }
64
- /** `wp-checklist`: READ-ONLY — which reviewer subagents this diff still owes, and what to tell them. */
65
- checklist() {
66
- return this.checklistCommand.run();
64
+ /**
65
+ * `wp-review-upsert-pr`: STAGE ② — validate the 3-point merge, run the build gate, extract this
66
+ * branch's diff, and brief the reviewer subagents. Unlike the report-only command it replaces, this CAN
67
+ * fail — before any reviewer is spawned, so a broken branch costs no reviewer tokens.
68
+ */
69
+ reviewUpsertPr() {
70
+ return this.reviewUpsertPrCommand.run();
67
71
  }
68
72
  /** `wp-check-pr`: READ-ONLY CI check — verify the PR body carries a valid HMAC gate token for its head sha. */
69
73
  checkPr() {
@@ -81,6 +85,6 @@ exports.PrGateApp = PrGateApp = tslib_1.__decorate([
81
85
  cleanup_command_1.CleanupCommand,
82
86
  land_pr_command_1.LandPrCommand,
83
87
  check_pr_command_1.CheckPrCommand,
84
- checklist_command_1.ChecklistCommand])
88
+ review_upsert_pr_command_1.ReviewUpsertPrCommand])
85
89
  ], PrGateApp);
86
90
  //# sourceMappingURL=pr-gate-app.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"pr-gate-app.js","sourceRoot":"","sources":["../../../../../../packages/tooling/pr-gate/src/scripts/pr-gate-app.ts"],"names":[],"mappings":";;;;AAAA,0DAAyD;AACzD,yCAA2D;AAC3D,0EAAqE;AACrE,4EAAuE;AACvE,gFAA0E;AAC1E,kFAA4E;AAC5E,gEAA4D;AAC5D,gEAA2D;AAC3D,kEAA6D;AAC7D,oEAAgE;AAEhE;;;;;GAKG;AAGI,IAAM,SAAS,GAAf,MAAM,SAAS;IAEG;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IARrB,YACqB,kBAAsC,EACtC,mBAAwC,EACxC,oBAA0C,EAC1C,qBAA4C,EAC5C,cAA8B,EAC9B,aAA4B,EAC5B,cAA8B,EAC9B,gBAAkC;QAPlC,uBAAkB,GAAlB,kBAAkB,CAAoB;QACtC,wBAAmB,GAAnB,mBAAmB,CAAqB;QACxC,yBAAoB,GAApB,oBAAoB,CAAsB;QAC1C,0BAAqB,GAArB,qBAAqB,CAAuB;QAC5C,mBAAc,GAAd,cAAc,CAAgB;QAC9B,kBAAa,GAAb,aAAa,CAAe;QAC5B,mBAAc,GAAd,cAAc,CAAgB;QAC9B,qBAAgB,GAAhB,gBAAgB,CAAkB;IACpD,CAAC;IAEJ,kEAAkE;IAClE,WAAW;QACP,OAAO,IAAI,CAAC,kBAAkB,CAAC,GAAG,EAAE,CAAC;IACzC,CAAC;IAED,gFAAgF;IAChF,YAAY;QACR,OAAO,IAAI,CAAC,mBAAmB,CAAC,GAAG,EAAE,CAAC;IAC1C,CAAC;IAED,4GAA4G;IAC5G,aAAa;QACT,OAAO,IAAI,CAAC,oBAAoB,CAAC,GAAG,EAAE,CAAC;IAC3C,CAAC;IAED,oGAAoG;IACpG,cAAc;QACV,OAAO,IAAI,CAAC,qBAAqB,CAAC,GAAG,EAAE,CAAC;IAC5C,CAAC;IAED,gGAAgG;IAChG,OAAO;QACH,OAAO,IAAI,CAAC,cAAc,CAAC,GAAG,EAAE,CAAC;IACrC,CAAC;IAED,0FAA0F;IAC1F,MAAM;QACF,OAAO,IAAI,CAAC,aAAa,CAAC,GAAG,EAAE,CAAC;IACpC,CAAC;IAED,wGAAwG;IACxG,SAAS;QACL,OAAO,IAAI,CAAC,gBAAgB,CAAC,GAAG,EAAE,CAAC;IACvC,CAAC;IAED,+GAA+G;IAC/G,OAAO;QACH,OAAO,IAAI,CAAC,cAAc,CAAC,GAAG,EAAE,CAAC;IACrC,CAAC;CACJ,CAAA;AAnDY,8BAAS;oBAAT,SAAS;IAFrB,IAAA,6BAAc,GAAE;IAChB,IAAA,sBAAU,EAAC,8BAAkB,CAAC,SAAS,CAAC;6CAGI,yCAAkB;QACjB,2CAAmB;QAClB,8CAAoB;QACnB,gDAAqB;QAC5B,gCAAc;QACf,+BAAa;QACZ,iCAAc;QACZ,oCAAgB;GAT9C,SAAS,CAmDrB","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 { LandPrCommand } from './commands/land-pr-command';\nimport { CheckPrCommand } from './commands/check-pr-command';\nimport { ChecklistCommand } from './commands/checklist-command';\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 landPrCommand: LandPrCommand,\n private readonly checkPrCommand: CheckPrCommand,\n private readonly checklistCommand: ChecklistCommand,\n ) {}\n\n /** `wp-start-update`: 3-point squash-update from main (no PR). */\n startUpdate(): Promise<void> {\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 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 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 return this.finishUpsertPrCommand.run();\n }\n\n /** `wp-cleanup`: delete local branches whose PR is already merged (or that hold no commits). */\n cleanup(): Promise<void> {\n return this.cleanupCommand.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 return this.landPrCommand.run();\n }\n\n /** `wp-checklist`: READ-ONLYwhich reviewer subagents this diff still owes, and what to tell them. */\n checklist(): Promise<void> {\n return this.checklistCommand.run();\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":";;;;AAAA,0DAAyD;AACzD,yCAA2D;AAC3D,0EAAqE;AACrE,4EAAuE;AACvE,gFAA0E;AAC1E,kFAA4E;AAC5E,gEAA4D;AAC5D,gEAA2D;AAC3D,kEAA6D;AAC7D,kFAA4E;AAE5E;;;;;GAKG;AAGI,IAAM,SAAS,GAAf,MAAM,SAAS;IAEG;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IARrB,YACqB,kBAAsC,EACtC,mBAAwC,EACxC,oBAA0C,EAC1C,qBAA4C,EAC5C,cAA8B,EAC9B,aAA4B,EAC5B,cAA8B,EAC9B,qBAA4C;QAP5C,uBAAkB,GAAlB,kBAAkB,CAAoB;QACtC,wBAAmB,GAAnB,mBAAmB,CAAqB;QACxC,yBAAoB,GAApB,oBAAoB,CAAsB;QAC1C,0BAAqB,GAArB,qBAAqB,CAAuB;QAC5C,mBAAc,GAAd,cAAc,CAAgB;QAC9B,kBAAa,GAAb,aAAa,CAAe;QAC5B,mBAAc,GAAd,cAAc,CAAgB;QAC9B,0BAAqB,GAArB,qBAAqB,CAAuB;IAC9D,CAAC;IAEJ,kEAAkE;IAClE,WAAW;QACP,OAAO,IAAI,CAAC,kBAAkB,CAAC,GAAG,EAAE,CAAC;IACzC,CAAC;IAED,gFAAgF;IAChF,YAAY;QACR,OAAO,IAAI,CAAC,mBAAmB,CAAC,GAAG,EAAE,CAAC;IAC1C,CAAC;IAED,4GAA4G;IAC5G,aAAa;QACT,OAAO,IAAI,CAAC,oBAAoB,CAAC,GAAG,EAAE,CAAC;IAC3C,CAAC;IAED,oGAAoG;IACpG,cAAc;QACV,OAAO,IAAI,CAAC,qBAAqB,CAAC,GAAG,EAAE,CAAC;IAC5C,CAAC;IAED,gGAAgG;IAChG,OAAO;QACH,OAAO,IAAI,CAAC,cAAc,CAAC,GAAG,EAAE,CAAC;IACrC,CAAC;IAED,0FAA0F;IAC1F,MAAM;QACF,OAAO,IAAI,CAAC,aAAa,CAAC,GAAG,EAAE,CAAC;IACpC,CAAC;IAED;;;;OAIG;IACH,cAAc;QACV,OAAO,IAAI,CAAC,qBAAqB,CAAC,GAAG,EAAE,CAAC;IAC5C,CAAC;IAED,+GAA+G;IAC/G,OAAO;QACH,OAAO,IAAI,CAAC,cAAc,CAAC,GAAG,EAAE,CAAC;IACrC,CAAC;CACJ,CAAA;AAvDY,8BAAS;oBAAT,SAAS;IAFrB,IAAA,6BAAc,GAAE;IAChB,IAAA,sBAAU,EAAC,8BAAkB,CAAC,SAAS,CAAC;6CAGI,yCAAkB;QACjB,2CAAmB;QAClB,8CAAoB;QACnB,gDAAqB;QAC5B,gCAAc;QACf,+BAAa;QACZ,iCAAc;QACP,gDAAqB;GATxD,SAAS,CAuDrB","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 { LandPrCommand } from './commands/land-pr-command';\nimport { CheckPrCommand } from './commands/check-pr-command';\nimport { ReviewUpsertPrCommand } from './commands/review-upsert-pr-command';\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 landPrCommand: LandPrCommand,\n private readonly checkPrCommand: CheckPrCommand,\n private readonly reviewUpsertPrCommand: ReviewUpsertPrCommand,\n ) {}\n\n /** `wp-start-update`: 3-point squash-update from main (no PR). */\n startUpdate(): Promise<void> {\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 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 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 return this.finishUpsertPrCommand.run();\n }\n\n /** `wp-cleanup`: delete local branches whose PR is already merged (or that hold no commits). */\n cleanup(): Promise<void> {\n return this.cleanupCommand.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 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(): Promise<void> {\n return this.reviewUpsertPrCommand.run();\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,12 +1,12 @@
1
1
  import { ChecklistDefinition, ChecklistReviewContext, DiffScope, RequiredChecklist, ReviewJsonService } from '@webpieces/rules-config';
2
2
  import { AiBranchName } from './git-readAiBranchName';
3
3
  import { ChecklistDetector, ChecklistRoster } from './checklist-detector';
4
- import { ForkPoint } from './git-findForkPoint';
4
+ import { DiffBasis, DiffBasisResolver } from './diff-basis';
5
5
  import { PrContextWriter } from './pr-context-writer';
6
6
  /** How a caller wants the scan filtered. Data-only (per CLAUDE.md). */
7
7
  export declare class ChecklistScanOptions {
8
8
  /**
9
- * false — `outstanding` is every applicable checklist (what `wp-checklist` LISTS).
9
+ * false — `outstanding` is every applicable checklist (what `wp-review-upsert-pr` LISTS).
10
10
  * true — `outstanding` drops the ones already carrying a passing/overridden verdict, leaving only what
11
11
  * still owes review (what `wp-finish-upsert-pr` BLOCKS on).
12
12
  */
@@ -30,10 +30,17 @@ export declare class ChecklistScan {
30
30
  forkPoint: string;
31
31
  roster: ChecklistRoster;
32
32
  formatErrors: string[];
33
- constructor(defined: ChecklistDefinition[], applicable: RequiredChecklist[], reviewed: RequiredChecklist[], outstanding: RequiredChecklist[], context: ChecklistReviewContext, reviewPath: string, forkPoint: string, roster: ChecklistRoster, formatErrors: string[]);
33
+ /**
34
+ * The basis the matching ACTUALLY ran against. Carried out so a caller that materializes the diff
35
+ * (stage ②) reuses the identical range instead of resolving its own — two independent resolutions is
36
+ * exactly how the changed-file set and the printed `git diff` command came to disagree.
37
+ */
38
+ basis: DiffBasis;
39
+ changedFiles: string[];
40
+ constructor(defined: ChecklistDefinition[], applicable: RequiredChecklist[], reviewed: RequiredChecklist[], outstanding: RequiredChecklist[], context: ChecklistReviewContext, reviewPath: string, forkPoint: string, roster: ChecklistRoster, formatErrors: string[], basis?: DiffBasis, changedFiles?: string[]);
34
41
  }
35
42
  /**
36
- * The ONE computation of which reviewer subagents a branch owes, shared by `wp-checklist` (which lists) and
43
+ * The ONE computation of which reviewer subagents a branch owes, shared by `wp-review-upsert-pr` (which lists) and
37
44
  * `wp-finish-upsert-pr` (which blocks). They previously each assembled this from the same parts in slightly
38
45
  * different ways, and any divergence means the command that reports and the command that gates disagree.
39
46
  *
@@ -41,11 +48,15 @@ export declare class ChecklistScan {
41
48
  *
42
49
  * 1. **The base is the FORK POINT of main, computed directly** — never `DiffScope.resolveBase`, which
43
50
  * overlays `NX_BASE`/`NX_HEAD` from the environment. That made review coverage depend on an env var.
51
+ * It now arrives via {@link DiffBasisResolver}, which injects ForkPoint; same sha, one resolution.
44
52
  * 2. **UNCOMMITTED work counts.** `getChangedFiles` is called with NO head, which is the branch of it that
45
53
  * diffs base → WORKING TREE and unions in untracked files. Passing a head would diff commit-to-commit and
46
54
  * silently miss staged, unstaged and untracked changes — so a checklist matching only uncommitted files
47
- * would never fire and its reviewer would never be listed. `wp-checklist` is explicitly callable
48
- * mid-work, which is exactly when that matters.
55
+ * would never fire and its reviewer would never be listed.
56
+ * 3. **The range and the command it prints are the SAME basis.** Property 2 used to be true of the file set
57
+ * only: reviewers were handed `git diff <base> HEAD`, which on a dirty tree covers a different range and
58
+ * prints nothing. Both now derive from one {@link DiffBasis}, carried on the scan so a materializing
59
+ * caller cannot re-resolve and drift.
49
60
  *
50
61
  * `@injectable(bindingScopeValues.Singleton)` so it is injected by type and drawn in the DI design.
51
62
  */
@@ -53,10 +64,10 @@ export declare class ChecklistScanner {
53
64
  private readonly aiBranchName;
54
65
  private readonly checklistDetector;
55
66
  private readonly diffScope;
56
- private readonly forkPoint;
67
+ private readonly diffBasisResolver;
57
68
  private readonly prContextWriter;
58
69
  private readonly reviewJsonService;
59
- constructor(aiBranchName: AiBranchName, checklistDetector: ChecklistDetector, diffScope: DiffScope, forkPoint: ForkPoint, prContextWriter: PrContextWriter, reviewJsonService: ReviewJsonService);
70
+ constructor(aiBranchName: AiBranchName, checklistDetector: ChecklistDetector, diffScope: DiffScope, diffBasisResolver: DiffBasisResolver, prContextWriter: PrContextWriter, reviewJsonService: ReviewJsonService);
60
71
  /**
61
72
  * `defined` is the caller's ALREADY-VALIDATED `prGate.checklists`. The scanner deliberately does not load
62
73
  * config itself: `loadAndValidate` is the one gate on the checklist set (it rejects a non-array
@@ -66,9 +77,14 @@ export declare class ChecklistScanner {
66
77
  */
67
78
  scan(repoRoot: string, defined: ChecklistDefinition[], opts: ChecklistScanOptions): ChecklistScan;
68
79
  /**
69
- * Every file changed since the fork point, INCLUDING uncommitted and untracked ones. `tsOnly:false` is
70
- * load-bearing too: the default drops every *.sql / Dockerfile / .env* file a checklist most wants to
71
- * key on, which would silently shrink the set a reviewer is pointed at.
80
+ * Every file changed since the fork point, INCLUDING uncommitted and untracked ones. Two non-default
81
+ * options, both load-bearing:
82
+ *
83
+ * `tsOnly:false` — the default drops every *.sql / Dockerfile / .env* file a checklist most wants
84
+ * to key on, which would silently shrink the set a reviewer is pointed at.
85
+ * `includeDeletions:true` — the default is `--diff-filter=d`, so a DELETED file is invisible. A PR that
86
+ * deletes a migration, an auth check or a terraform rule changed exactly what a
87
+ * checklist exists to catch, and under the default no checklist fires at all.
72
88
  */
73
89
  private changedFiles;
74
90
  }
@@ -6,12 +6,12 @@ const rules_config_1 = require("@webpieces/rules-config");
6
6
  const inversify_1 = require("inversify");
7
7
  const git_readAiBranchName_1 = require("./git-readAiBranchName");
8
8
  const checklist_detector_1 = require("./checklist-detector");
9
- const git_findForkPoint_1 = require("./git-findForkPoint");
9
+ const diff_basis_1 = require("./diff-basis");
10
10
  const pr_context_writer_1 = require("./pr-context-writer");
11
11
  /** How a caller wants the scan filtered. Data-only (per CLAUDE.md). */
12
12
  class ChecklistScanOptions {
13
13
  /**
14
- * false — `outstanding` is every applicable checklist (what `wp-checklist` LISTS).
14
+ * false — `outstanding` is every applicable checklist (what `wp-review-upsert-pr` LISTS).
15
15
  * true — `outstanding` drops the ones already carrying a passing/overridden verdict, leaving only what
16
16
  * still owes review (what `wp-finish-upsert-pr` BLOCKS on).
17
17
  */
@@ -44,8 +44,15 @@ class ChecklistScan {
44
44
  // Carried on the SCAN because wp-finish-upsert-pr refuses on missing reviewers before it ever parses
45
45
  // review.json — a complaint raised only in there would never reach the AI.
46
46
  formatErrors;
47
+ /**
48
+ * The basis the matching ACTUALLY ran against. Carried out so a caller that materializes the diff
49
+ * (stage ②) reuses the identical range instead of resolving its own — two independent resolutions is
50
+ * exactly how the changed-file set and the printed `git diff` command came to disagree.
51
+ */
52
+ basis;
53
+ changedFiles; // the full changed-file set the matching ran against
47
54
  // eslint-disable-next-line @typescript-eslint/max-params
48
- constructor(defined, applicable, reviewed, outstanding, context, reviewPath, forkPoint, roster, formatErrors) {
55
+ constructor(defined, applicable, reviewed, outstanding, context, reviewPath, forkPoint, roster, formatErrors, basis = new diff_basis_1.DiffBasis(), changedFiles = []) {
49
56
  this.defined = defined;
50
57
  this.applicable = applicable;
51
58
  this.reviewed = reviewed;
@@ -55,11 +62,13 @@ class ChecklistScan {
55
62
  this.forkPoint = forkPoint;
56
63
  this.roster = roster;
57
64
  this.formatErrors = formatErrors;
65
+ this.basis = basis;
66
+ this.changedFiles = changedFiles;
58
67
  }
59
68
  }
60
69
  exports.ChecklistScan = ChecklistScan;
61
70
  /**
62
- * The ONE computation of which reviewer subagents a branch owes, shared by `wp-checklist` (which lists) and
71
+ * The ONE computation of which reviewer subagents a branch owes, shared by `wp-review-upsert-pr` (which lists) and
63
72
  * `wp-finish-upsert-pr` (which blocks). They previously each assembled this from the same parts in slightly
64
73
  * different ways, and any divergence means the command that reports and the command that gates disagree.
65
74
  *
@@ -67,11 +76,15 @@ exports.ChecklistScan = ChecklistScan;
67
76
  *
68
77
  * 1. **The base is the FORK POINT of main, computed directly** — never `DiffScope.resolveBase`, which
69
78
  * overlays `NX_BASE`/`NX_HEAD` from the environment. That made review coverage depend on an env var.
79
+ * It now arrives via {@link DiffBasisResolver}, which injects ForkPoint; same sha, one resolution.
70
80
  * 2. **UNCOMMITTED work counts.** `getChangedFiles` is called with NO head, which is the branch of it that
71
81
  * diffs base → WORKING TREE and unions in untracked files. Passing a head would diff commit-to-commit and
72
82
  * silently miss staged, unstaged and untracked changes — so a checklist matching only uncommitted files
73
- * would never fire and its reviewer would never be listed. `wp-checklist` is explicitly callable
74
- * mid-work, which is exactly when that matters.
83
+ * would never fire and its reviewer would never be listed.
84
+ * 3. **The range and the command it prints are the SAME basis.** Property 2 used to be true of the file set
85
+ * only: reviewers were handed `git diff <base> HEAD`, which on a dirty tree covers a different range and
86
+ * prints nothing. Both now derive from one {@link DiffBasis}, carried on the scan so a materializing
87
+ * caller cannot re-resolve and drift.
75
88
  *
76
89
  * `@injectable(bindingScopeValues.Singleton)` so it is injected by type and drawn in the DI design.
77
90
  */
@@ -79,14 +92,14 @@ let ChecklistScanner = class ChecklistScanner {
79
92
  aiBranchName;
80
93
  checklistDetector;
81
94
  diffScope;
82
- forkPoint;
95
+ diffBasisResolver;
83
96
  prContextWriter;
84
97
  reviewJsonService;
85
- constructor(aiBranchName, checklistDetector, diffScope, forkPoint, prContextWriter, reviewJsonService) {
98
+ constructor(aiBranchName, checklistDetector, diffScope, diffBasisResolver, prContextWriter, reviewJsonService) {
86
99
  this.aiBranchName = aiBranchName;
87
100
  this.checklistDetector = checklistDetector;
88
101
  this.diffScope = diffScope;
89
- this.forkPoint = forkPoint;
102
+ this.diffBasisResolver = diffBasisResolver;
90
103
  this.prContextWriter = prContextWriter;
91
104
  this.reviewJsonService = reviewJsonService;
92
105
  }
@@ -100,7 +113,10 @@ let ChecklistScanner = class ChecklistScanner {
100
113
  scan(repoRoot, defined, opts) {
101
114
  const featureName = this.aiBranchName.getFeatureName();
102
115
  const reviewPath = (0, rules_config_1.reviewJsonPath)(repoRoot, featureName);
103
- const base = this.forkPoint.resolveForkPoint(repoRoot);
116
+ // ONE basis for the file set, the reproduce command and any downstream materialization. The fork
117
+ // point still comes from ForkPoint (never DiffScope.resolveBase) — DiffBasisResolver injects it.
118
+ const basis = this.diffBasisResolver.resolve(repoRoot);
119
+ const base = basis.base;
104
120
  // ONE changed-file computation feeds both the roster (all X) and the applicable set (N). `detect` is
105
121
  // pure and defined as the roster minus its empty entries, so the two cannot disagree about a match.
106
122
  const changedFiles = this.changedFiles(repoRoot, base);
@@ -110,18 +126,24 @@ let ChecklistScanner = class ChecklistScanner {
110
126
  const stillOwed = this.reviewJsonService.pendingChecklists(applicable, results);
111
127
  const owedIds = new Set(stillOwed.map((r) => r.id));
112
128
  const reviewed = applicable.filter((r) => !owedIds.has(r.id));
113
- return new ChecklistScan(defined, applicable, reviewed, opts.filterAlreadyReviewed ? stillOwed : applicable, this.prContextWriter.ensure(repoRoot, featureName, base), reviewPath, base, roster, this.reviewJsonService.checklistFormatErrors(applicable, results));
129
+ return new ChecklistScan(defined, applicable, reviewed, opts.filterAlreadyReviewed ? stillOwed : applicable, this.prContextWriter.ensure(repoRoot, featureName, basis), reviewPath, base, roster, this.reviewJsonService.checklistFormatErrors(applicable, results), basis, changedFiles);
114
130
  }
115
131
  /**
116
- * Every file changed since the fork point, INCLUDING uncommitted and untracked ones. `tsOnly:false` is
117
- * load-bearing too: the default drops every *.sql / Dockerfile / .env* file a checklist most wants to
118
- * key on, which would silently shrink the set a reviewer is pointed at.
132
+ * Every file changed since the fork point, INCLUDING uncommitted and untracked ones. Two non-default
133
+ * options, both load-bearing:
134
+ *
135
+ * `tsOnly:false` — the default drops every *.sql / Dockerfile / .env* file a checklist most wants
136
+ * to key on, which would silently shrink the set a reviewer is pointed at.
137
+ * `includeDeletions:true` — the default is `--diff-filter=d`, so a DELETED file is invisible. A PR that
138
+ * deletes a migration, an auth check or a terraform rule changed exactly what a
139
+ * checklist exists to catch, and under the default no checklist fires at all.
119
140
  */
120
141
  changedFiles(repoRoot, base) {
121
142
  if (base === '')
122
143
  return [];
123
144
  const opts = new rules_config_1.ChangedFilesOptions();
124
145
  opts.tsOnly = false;
146
+ opts.includeDeletions = true;
125
147
  // No head argument — see the class comment. This is what includes the working tree.
126
148
  return this.diffScope.getChangedFiles(repoRoot, base, undefined, opts);
127
149
  }
@@ -132,7 +154,7 @@ exports.ChecklistScanner = ChecklistScanner = tslib_1.__decorate([
132
154
  tslib_1.__metadata("design:paramtypes", [git_readAiBranchName_1.AiBranchName,
133
155
  checklist_detector_1.ChecklistDetector,
134
156
  rules_config_1.DiffScope,
135
- git_findForkPoint_1.ForkPoint,
157
+ diff_basis_1.DiffBasisResolver,
136
158
  pr_context_writer_1.PrContextWriter,
137
159
  rules_config_1.ReviewJsonService])
138
160
  ], ChecklistScanner);
@@ -1 +1 @@
1
- {"version":3,"file":"checklist-scanner.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/checklist-scanner.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n ChangedFilesOptions, ChecklistDefinition, ChecklistReviewContext, DiffScope, RequiredChecklist,\n ReviewJsonService, reviewJsonPath,\n} from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { AiBranchName } from './git-readAiBranchName';\nimport { ChecklistDetector, ChecklistRoster } from './checklist-detector';\nimport { ForkPoint } from './git-findForkPoint';\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-checklist` 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 constructor(filterAlreadyReviewed = false) {\n this.filterAlreadyReviewed = filterAlreadyReviewed;\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 // 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 ) {\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 }\n}\n\n/**\n * The ONE computation of which reviewer subagents a branch owes, shared by `wp-checklist` (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 * 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. `wp-checklist` is explicitly callable\n * mid-work, which is exactly when that matters.\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 forkPoint: ForkPoint,\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 const base = this.forkPoint.resolveForkPoint(repoRoot);\n // ONE changed-file computation feeds both the roster (all X) and the applicable set (N). `detect` is\n // pure and defined as the roster minus its empty entries, so the two cannot disagree about a match.\n const changedFiles = this.changedFiles(repoRoot, base);\n const roster = new ChecklistRoster(\n this.checklistDetector.roster(defined, changedFiles), changedFiles.length, base !== '');\n const applicable = this.checklistDetector.toRequired(this.checklistDetector.detect(defined, changedFiles));\n const results = this.reviewJsonService.loadChecklistResults(reviewPath, applicable);\n const stillOwed = this.reviewJsonService.pendingChecklists(applicable, results);\n const owedIds = new Set(stillOwed.map((r: RequiredChecklist): string => r.id));\n const reviewed = applicable.filter((r: RequiredChecklist): boolean => !owedIds.has(r.id));\n return new ChecklistScan(\n defined,\n applicable,\n reviewed,\n opts.filterAlreadyReviewed ? stillOwed : applicable,\n this.prContextWriter.ensure(repoRoot, featureName, base),\n reviewPath,\n base,\n roster,\n this.reviewJsonService.checklistFormatErrors(applicable, results),\n );\n }\n\n /**\n * Every file changed since the fork point, INCLUDING uncommitted and untracked ones. `tsOnly:false` is\n * load-bearing too: the default drops every *.sql / Dockerfile / .env* file a checklist most wants to\n * key on, which would silently shrink the set a reviewer is pointed at.\n */\n private changedFiles(repoRoot: string, base: string): string[] {\n if (base === '') return [];\n const opts = new ChangedFilesOptions();\n opts.tsOnly = false;\n // No head argument — see the class comment. This is what includes the working tree.\n return this.diffScope.getChangedFiles(repoRoot, base, undefined, opts);\n }\n}\n"]}
1
+ {"version":3,"file":"checklist-scanner.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/checklist-scanner.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n ChangedFilesOptions, ChecklistDefinition, ChecklistReviewContext, DiffScope, RequiredChecklist,\n 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 constructor(filterAlreadyReviewed = false) {\n this.filterAlreadyReviewed = filterAlreadyReviewed;\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 // 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 ) {\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 }\n}\n\n/**\n * The ONE computation of which reviewer subagents a branch owes, shared by `wp-review-upsert-pr` (which lists) and\n * `wp-finish-upsert-pr` (which blocks). They previously each assembled this from the same parts in slightly\n * different ways, and any divergence means the command that reports and the command that gates disagree.\n *\n * Two properties are deliberate and load-bearing:\n *\n * 1. **The base is the FORK POINT of main, computed directly** — never `DiffScope.resolveBase`, which\n * overlays `NX_BASE`/`NX_HEAD` from the environment. That made review coverage depend on an env var.\n * It now arrives via {@link DiffBasisResolver}, which injects ForkPoint; same sha, one resolution.\n * 2. **UNCOMMITTED work counts.** `getChangedFiles` is called with NO head, which is the branch of it that\n * diffs base → WORKING TREE and unions in untracked files. Passing a head would diff commit-to-commit and\n * silently miss staged, unstaged and untracked changes — so a checklist matching only uncommitted files\n * would never fire and its reviewer would never be listed.\n * 3. **The range and the command it prints are the SAME basis.** Property 2 used to be true of the file set\n * only: reviewers were handed `git diff <base> HEAD`, which on a dirty tree covers a different range and\n * prints nothing. Both now derive from one {@link DiffBasis}, carried on the scan so a materializing\n * caller cannot re-resolve and drift.\n *\n * `@injectable(bindingScopeValues.Singleton)` so it is injected by type and drawn in the DI design.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class ChecklistScanner {\n constructor(\n private readonly aiBranchName: AiBranchName,\n private readonly checklistDetector: ChecklistDetector,\n private readonly diffScope: DiffScope,\n private readonly diffBasisResolver: DiffBasisResolver,\n private readonly prContextWriter: PrContextWriter,\n private readonly reviewJsonService: ReviewJsonService,\n ) {}\n\n /**\n * `defined` is the caller's ALREADY-VALIDATED `prGate.checklists`. The scanner deliberately does not load\n * config itself: `loadAndValidate` is the one gate on the checklist set (it rejects a non-array\n * `checklists`, including the removed `{ doc }` manifest shape, and verifies every guidance doc and\n * reviewer-agent file exists), both callers already run it for other fields, and keeping the read out of\n * here leaves this a function of its inputs rather than of the filesystem.\n */\n scan(repoRoot: string, defined: ChecklistDefinition[], opts: ChecklistScanOptions): ChecklistScan {\n const featureName = this.aiBranchName.getFeatureName();\n const reviewPath = reviewJsonPath(repoRoot, featureName);\n // ONE basis for the file set, the reproduce command and any downstream materialization. The fork\n // point still comes from ForkPoint (never DiffScope.resolveBase) — DiffBasisResolver injects it.\n const basis = this.diffBasisResolver.resolve(repoRoot);\n const base = basis.base;\n // ONE changed-file computation feeds both the roster (all X) and the applicable set (N). `detect` is\n // pure and defined as the roster minus its empty entries, so the two cannot disagree about a match.\n const changedFiles = this.changedFiles(repoRoot, base);\n const roster = new ChecklistRoster(\n this.checklistDetector.roster(defined, changedFiles), changedFiles.length, base !== '');\n const applicable = this.checklistDetector.toRequired(this.checklistDetector.detect(defined, changedFiles));\n const results = this.reviewJsonService.loadChecklistResults(reviewPath, applicable);\n const stillOwed = this.reviewJsonService.pendingChecklists(applicable, results);\n const owedIds = new Set(stillOwed.map((r: RequiredChecklist): string => r.id));\n const reviewed = applicable.filter((r: RequiredChecklist): boolean => !owedIds.has(r.id));\n return new ChecklistScan(\n defined,\n applicable,\n reviewed,\n opts.filterAlreadyReviewed ? stillOwed : applicable,\n this.prContextWriter.ensure(repoRoot, featureName, basis),\n reviewPath,\n base,\n roster,\n this.reviewJsonService.checklistFormatErrors(applicable, results),\n basis,\n changedFiles,\n );\n }\n\n /**\n * Every file changed since the fork point, INCLUDING uncommitted and untracked ones. Two non-default\n * options, both load-bearing:\n *\n * `tsOnly:false` — the default drops every *.sql / Dockerfile / .env* file a checklist most wants\n * to key on, which would silently shrink the set a reviewer is pointed at.\n * `includeDeletions:true` — the default is `--diff-filter=d`, so a DELETED file is invisible. A PR that\n * deletes a migration, an auth check or a terraform rule changed exactly what a\n * checklist exists to catch, and under the default no checklist fires at all.\n */\n private changedFiles(repoRoot: string, base: string): string[] {\n if (base === '') return [];\n const opts = new ChangedFilesOptions();\n opts.tsOnly = false;\n opts.includeDeletions = true;\n // No head argument — see the class comment. This is what includes the working tree.\n return this.diffScope.getChangedFiles(repoRoot, base, undefined, opts);\n }\n}\n"]}
@@ -0,0 +1,53 @@
1
+ import { ForkPoint } from './git-findForkPoint';
2
+ /**
3
+ * The ONE resolved answer to "what exactly is this branch's diff, and what command reproduces it?"
4
+ *
5
+ * It exists because the two halves of the review flow used to disagree. `ChecklistScanner` computes the
6
+ * changed-FILE set by calling `getChangedFiles` with NO head — base→WORKING TREE, unioning in untracked
7
+ * files — while `ChecklistInstructionsService` printed `git diff <base> HEAD -- <file>` to every
8
+ * reviewer. On a dirty tree those are different ranges, and the printed command returns NOTHING: a reviewer
9
+ * is handed a file list and a command that shows it nothing. That is not hypothetical — a reviewer subagent
10
+ * on monorepo-nx2 ran the printed command, got empty output, and had to guess its way to `git diff HEAD`.
11
+ *
12
+ * So the command is DERIVED from the same basis the file set is, and no caller writes one by hand.
13
+ *
14
+ * Data-only (per CLAUDE.md).
15
+ */
16
+ export declare class DiffBasis {
17
+ base: string;
18
+ headSha: string;
19
+ dirty: boolean;
20
+ dirtyFiles: string[];
21
+ diffCommand: string;
22
+ fileDiffCommand: string;
23
+ constructor(base?: string, headSha?: string, dirty?: boolean, dirtyFiles?: string[], diffCommand?: string, fileDiffCommand?: string);
24
+ get unresolved(): boolean;
25
+ }
26
+ /**
27
+ * Resolves {@link DiffBasis} for a repo. Injects {@link ForkPoint} so the base is the SAME fork-point
28
+ * computation the checklist matching uses — never `DiffScope.resolveBase`, which overlays NX_BASE/NX_HEAD
29
+ * from the environment and would make review coverage depend on an env var.
30
+ *
31
+ * `@injectable(bindingScopeValues.Singleton)` so it is injected by type and drawn in the DI design.
32
+ */
33
+ export declare class DiffBasisResolver {
34
+ private readonly forkPoint;
35
+ constructor(forkPoint: ForkPoint);
36
+ /**
37
+ * Resolve the basis. The command shape is the whole point:
38
+ *
39
+ * clean tree → `git diff <base> <headSha>` (commit-to-commit; reproducible later, unlike 'HEAD')
40
+ * dirty tree → `git diff <base>` (NO head — this is what includes the working tree)
41
+ *
42
+ * Omitting the head on a dirty tree is not a shortcut, it is the only form that shows uncommitted work,
43
+ * and supplying `HEAD` there is precisely the bug this class exists to make unrepresentable.
44
+ */
45
+ resolve(repoRoot: string): DiffBasis;
46
+ /**
47
+ * Every path that makes the tree dirty: tracked modifications/staged changes PLUS untracked files.
48
+ * Untracked must be included — `git status --porcelain` reports them as `??`, and they are exactly the
49
+ * files a brand-new migration or terraform rule arrives as, which is when a checklist most wants to fire.
50
+ */
51
+ private dirtyPaths;
52
+ private gitOut;
53
+ }
@@ -0,0 +1,113 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DiffBasisResolver = exports.DiffBasis = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const child_process_1 = require("child_process");
6
+ const inversify_1 = require("inversify");
7
+ const git_findForkPoint_1 = require("./git-findForkPoint");
8
+ /**
9
+ * The ONE resolved answer to "what exactly is this branch's diff, and what command reproduces it?"
10
+ *
11
+ * It exists because the two halves of the review flow used to disagree. `ChecklistScanner` computes the
12
+ * changed-FILE set by calling `getChangedFiles` with NO head — base→WORKING TREE, unioning in untracked
13
+ * files — while `ChecklistInstructionsService` printed `git diff <base> HEAD -- <file>` to every
14
+ * reviewer. On a dirty tree those are different ranges, and the printed command returns NOTHING: a reviewer
15
+ * is handed a file list and a command that shows it nothing. That is not hypothetical — a reviewer subagent
16
+ * on monorepo-nx2 ran the printed command, got empty output, and had to guess its way to `git diff HEAD`.
17
+ *
18
+ * So the command is DERIVED from the same basis the file set is, and no caller writes one by hand.
19
+ *
20
+ * Data-only (per CLAUDE.md).
21
+ */
22
+ class DiffBasis {
23
+ // The 3-point fork point of main. '' when neither origin/main nor main resolves.
24
+ base;
25
+ // The REAL sha, never the literal string 'HEAD'. A recorded 'HEAD' is worthless the moment a commit
26
+ // lands on top of it, and the stage receipt has to compare shas to know the tree moved under a review.
27
+ headSha;
28
+ // True when the diff includes staged/unstaged/untracked work, i.e. the range is base→working tree.
29
+ dirty;
30
+ // Exactly WHICH paths make it dirty, so a message can name them instead of asserting dirtiness.
31
+ dirtyFiles;
32
+ // The command that reproduces the WHOLE diff. Correct for both clean and dirty; see DiffBasisResolver.
33
+ diffCommand;
34
+ // The same command with a `-- <file>` tail — what every reviewer instruction prints.
35
+ fileDiffCommand;
36
+ // eslint-disable-next-line @typescript-eslint/max-params
37
+ constructor(base = '', headSha = '', dirty = false, dirtyFiles = [], diffCommand = '', fileDiffCommand = '') {
38
+ this.base = base;
39
+ this.headSha = headSha;
40
+ this.dirty = dirty;
41
+ this.dirtyFiles = dirtyFiles;
42
+ this.diffCommand = diffCommand;
43
+ this.fileDiffCommand = fileDiffCommand;
44
+ }
45
+ // True when there is no usable base — callers must SAY so rather than print a command that cannot work.
46
+ get unresolved() {
47
+ return this.base.trim() === '';
48
+ }
49
+ }
50
+ exports.DiffBasis = DiffBasis;
51
+ /**
52
+ * Resolves {@link DiffBasis} for a repo. Injects {@link ForkPoint} so the base is the SAME fork-point
53
+ * computation the checklist matching uses — never `DiffScope.resolveBase`, which overlays NX_BASE/NX_HEAD
54
+ * from the environment and would make review coverage depend on an env var.
55
+ *
56
+ * `@injectable(bindingScopeValues.Singleton)` so it is injected by type and drawn in the DI design.
57
+ */
58
+ let DiffBasisResolver = class DiffBasisResolver {
59
+ forkPoint;
60
+ constructor(forkPoint) {
61
+ this.forkPoint = forkPoint;
62
+ }
63
+ /**
64
+ * Resolve the basis. The command shape is the whole point:
65
+ *
66
+ * clean tree → `git diff <base> <headSha>` (commit-to-commit; reproducible later, unlike 'HEAD')
67
+ * dirty tree → `git diff <base>` (NO head — this is what includes the working tree)
68
+ *
69
+ * Omitting the head on a dirty tree is not a shortcut, it is the only form that shows uncommitted work,
70
+ * and supplying `HEAD` there is precisely the bug this class exists to make unrepresentable.
71
+ */
72
+ resolve(repoRoot) {
73
+ const base = this.forkPoint.resolveForkPoint(repoRoot);
74
+ const headSha = this.gitOut(repoRoot, ['rev-parse', 'HEAD']);
75
+ const dirtyFiles = this.dirtyPaths(repoRoot);
76
+ const dirty = dirtyFiles.length > 0;
77
+ if (base === '')
78
+ return new DiffBasis('', headSha, dirty, dirtyFiles);
79
+ const whole = dirty ? `git diff ${base}` : `git diff ${base} ${headSha}`;
80
+ return new DiffBasis(base, headSha, dirty, dirtyFiles, whole, `${whole} -- <file>`);
81
+ }
82
+ /**
83
+ * Every path that makes the tree dirty: tracked modifications/staged changes PLUS untracked files.
84
+ * Untracked must be included — `git status --porcelain` reports them as `??`, and they are exactly the
85
+ * files a brand-new migration or terraform rule arrives as, which is when a checklist most wants to fire.
86
+ */
87
+ dirtyPaths(repoRoot) {
88
+ const out = this.gitOut(repoRoot, ['status', '--porcelain', '--untracked-files=all']);
89
+ if (out === '')
90
+ return [];
91
+ const paths = [];
92
+ for (const line of out.split('\n')) {
93
+ // Porcelain v1: 2 status chars, a space, then the path. A rename reads `R old -> new`; the new
94
+ // name is the one that exists on disk, so it is the one a reviewer can open.
95
+ const raw = line.slice(3).trim();
96
+ if (raw === '')
97
+ continue;
98
+ const arrow = raw.indexOf(' -> ');
99
+ paths.push(arrow === -1 ? raw : raw.slice(arrow + 4));
100
+ }
101
+ return paths;
102
+ }
103
+ gitOut(repoRoot, args) {
104
+ const result = (0, child_process_1.spawnSync)('git', args, { cwd: repoRoot, encoding: 'utf8' });
105
+ return result.status === 0 ? (result.stdout ?? '').trim() : '';
106
+ }
107
+ };
108
+ exports.DiffBasisResolver = DiffBasisResolver;
109
+ exports.DiffBasisResolver = DiffBasisResolver = tslib_1.__decorate([
110
+ (0, inversify_1.injectable)(inversify_1.bindingScopeValues.Singleton),
111
+ tslib_1.__metadata("design:paramtypes", [git_findForkPoint_1.ForkPoint])
112
+ ], DiffBasisResolver);
113
+ //# sourceMappingURL=diff-basis.js.map