@webpieces/pr-gate 0.4.706 → 0.4.707

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 (36) hide show
  1. package/package.json +5 -3
  2. package/src/dashboard/checklist-comment-renderer.js +4 -2
  3. package/src/dashboard/checklist-comment-renderer.js.map +1 -1
  4. package/src/dashboard/dashboard.js +2 -0
  5. package/src/dashboard/dashboard.js.map +1 -1
  6. package/src/scripts/commands/authorize-command.d.ts +82 -0
  7. package/src/scripts/commands/authorize-command.js +205 -0
  8. package/src/scripts/commands/authorize-command.js.map +1 -0
  9. package/src/scripts/commands/check-auth-command.d.ts +33 -0
  10. package/src/scripts/commands/check-auth-command.js +99 -0
  11. package/src/scripts/commands/check-auth-command.js.map +1 -0
  12. package/src/scripts/commands/finish-upsert-pr-command.js +10 -9
  13. package/src/scripts/commands/finish-upsert-pr-command.js.map +1 -1
  14. package/src/scripts/commands/review-upsert-pr-command.d.ts +2 -2
  15. package/src/scripts/commands/review-upsert-pr-command.js +5 -5
  16. package/src/scripts/commands/review-upsert-pr-command.js.map +1 -1
  17. package/src/scripts/pr-gate-app.d.ts +13 -2
  18. package/src/scripts/pr-gate-app.js +20 -1
  19. package/src/scripts/pr-gate-app.js.map +1 -1
  20. package/src/scripts/workflow/authorization-context-resolver.d.ts +63 -0
  21. package/src/scripts/workflow/authorization-context-resolver.js +107 -0
  22. package/src/scripts/workflow/authorization-context-resolver.js.map +1 -0
  23. package/src/scripts/workflow/checklist-scanner.d.ts +24 -21
  24. package/src/scripts/workflow/checklist-scanner.js +40 -35
  25. package/src/scripts/workflow/checklist-scanner.js.map +1 -1
  26. package/src/scripts/workflow/review-report.d.ts +3 -3
  27. package/src/scripts/workflow/review-report.js +3 -3
  28. package/src/scripts/workflow/review-report.js.map +1 -1
  29. package/src/scripts/workflow/reviewer-verdict-gate.js +2 -2
  30. package/src/scripts/workflow/reviewer-verdict-gate.js.map +1 -1
  31. package/src/scripts/wp-authorize.d.ts +2 -0
  32. package/src/scripts/wp-authorize.js +22 -0
  33. package/src/scripts/wp-authorize.js.map +1 -0
  34. package/src/scripts/wp-check-auth.d.ts +2 -0
  35. package/src/scripts/wp-check-auth.js +16 -0
  36. package/src/scripts/wp-check-auth.js.map +1 -0
@@ -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;AAE5D,wFAAkF;AAClF,gEAA2D;AAC3D,kEAA6D;AAC7D,kFAAmG;AACnG,4EAAuE;AACvE,4DAAsE;AACtE,kEAA6E;AAC7E,gFAAgG;AAChG,8DAA8D;AAC9D,0DAAyD;AAEzD;;;;;GAKG;AAGI,IAAM,SAAS,GAAf,MAAM,SAAS;IAEG;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IAfrB,YACqB,kBAAsC,EACtC,mBAAwC,EACxC,oBAA0C,EAC1C,qBAA4C,EAC5C,cAA8B,EAC9B,wBAAkD,EAClD,aAA4B,EAC5B,cAA8B,EAC9B,qBAA4C,EAC5C,mBAAwC,EACxC,YAA0B,EAC1B,cAA8B,EAC9B,oBAA0C,EAC1C,iBAAoC,EACpC,cAA8B;QAd9B,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,6BAAwB,GAAxB,wBAAwB,CAA0B;QAClD,kBAAa,GAAb,aAAa,CAAe;QAC5B,mBAAc,GAAd,cAAc,CAAgB;QAC9B,0BAAqB,GAArB,qBAAqB,CAAuB;QAC5C,wBAAmB,GAAnB,mBAAmB,CAAqB;QACxC,iBAAY,GAAZ,YAAY,CAAc;QAC1B,mBAAc,GAAd,cAAc,CAAgB;QAC9B,yBAAoB,GAApB,oBAAoB,CAAsB;QAC1C,sBAAiB,GAAjB,iBAAiB,CAAmB;QACpC,mBAAc,GAAd,cAAc,CAAgB;IAChD,CAAC;IAEJ;;;;;;;OAOG;IACK,yBAAyB,CAAC,OAAe;QAC7C,IAAI,CAAC,IAAI,CAAC,iBAAiB,CAAC,sBAAsB,CAAC,OAAO,CAAC;YAAE,OAAO;QACpE,IAAI,CAAC,iBAAiB,CAAC,UAAU,CAAC,IAAI,CAAC,cAAc,CAAC,eAAe,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,EAAE,OAAO,CAAC,CAAC;IACnG,CAAC;IAED,+GAA+G;IAC/G,OAAO,CAAC,OAAuB,IAAI,iCAAc,EAAE;QAC/C,OAAO,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACzC,CAAC;IAED,mGAAmG;IACnG,aAAa,CAAC,OAA6B,IAAI,8CAAoB,EAAE;QACjE,OAAO,IAAI,CAAC,oBAAoB,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAC/C,CAAC;IAED;;;;OAIG;IACH,YAAY,CAAC,IAAc;QACvB,OAAO,IAAI,CAAC,mBAAmB,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAC9C,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,IAAkB;QACpB,OAAO,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACvC,CAAC;IAED,kEAAkE;IAClE,WAAW;QACP,IAAI,CAAC,yBAAyB,CAAC,iBAAiB,CAAC,CAAC;QAClD,OAAO,IAAI,CAAC,kBAAkB,CAAC,GAAG,EAAE,CAAC;IACzC,CAAC;IAED,gFAAgF;IAChF,YAAY;QACR,IAAI,CAAC,yBAAyB,CAAC,kBAAkB,CAAC,CAAC;QACnD,OAAO,IAAI,CAAC,mBAAmB,CAAC,GAAG,EAAE,CAAC;IAC1C,CAAC;IAED,4GAA4G;IAC5G,aAAa;QACT,IAAI,CAAC,yBAAyB,CAAC,oBAAoB,CAAC,CAAC;QACrD,OAAO,IAAI,CAAC,oBAAoB,CAAC,GAAG,EAAE,CAAC;IAC3C,CAAC;IAED,oGAAoG;IACpG,cAAc;QACV,IAAI,CAAC,yBAAyB,CAAC,qBAAqB,CAAC,CAAC;QACtD,OAAO,IAAI,CAAC,qBAAqB,CAAC,GAAG,EAAE,CAAC;IAC5C,CAAC;IAED;;;OAGG;IACH,OAAO,CAAC,OAAuB;QAC3B,IAAI,CAAC,yBAAyB,CAAC,YAAY,CAAC,CAAC;QAC7C,OAAO,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;IAC5C,CAAC;IAED;;;;;OAKG;IACH,iBAAiB;QACb,IAAI,CAAC,yBAAyB,CAAC,wBAAwB,CAAC,CAAC;QACzD,OAAO,IAAI,CAAC,wBAAwB,CAAC,GAAG,EAAE,CAAC;IAC/C,CAAC;IAED,0FAA0F;IAC1F,MAAM;QACF,IAAI,CAAC,yBAAyB,CAAC,YAAY,CAAC,CAAC;QAC7C,OAAO,IAAI,CAAC,aAAa,CAAC,GAAG,EAAE,CAAC;IACpC,CAAC;IAED;;;;OAIG;IACH,cAAc,CAAC,OAA8B,IAAI,gDAAqB,EAAE;QACpE,IAAI,CAAC,yBAAyB,CAAC,qBAAqB,CAAC,CAAC;QACtD,OAAO,IAAI,CAAC,qBAAqB,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAChD,CAAC;IAED,+GAA+G;IAC/G,OAAO;QACH,OAAO,IAAI,CAAC,cAAc,CAAC,GAAG,EAAE,CAAC;IACrC,CAAC;CACJ,CAAA;AA5HY,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;QACJ,sDAAwB;QACnC,+BAAa;QACZ,iCAAc;QACP,gDAAqB;QACvB,2CAAmB;QAC1B,4BAAY;QACV,iCAAc;QACR,8CAAoB;QACvB,kCAAiB;QACpB,6BAAc;GAhB1C,SAAS,CA4HrB","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
+ {"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"]}
@@ -0,0 +1,63 @@
1
+ import { AuthorizationContext, DiffScope } from '@webpieces/rules-config';
2
+ import { AiBranchName } from './git-readAiBranchName';
3
+ import { DiffBasisResolver } from './diff-basis';
4
+ /**
5
+ * THE changed-file set the review system judges a branch by — extracted so the checklist scanner and the
6
+ * authorization commands cannot answer it two different ways.
7
+ *
8
+ * Two non-default options, both load-bearing, and both the reason this is worth a class rather than a
9
+ * repeated three-liner:
10
+ *
11
+ * `tsOnly:false` — the default drops every *.sql / Dockerfile / .env* file, silently shrinking the
12
+ * set a reviewer is pointed at — and, here, the set an approval's scope is
13
+ * checked against, which would let terraform-adjacent files slip inside a
14
+ * "terraform only" grant.
15
+ * `includeDeletions:true` — the default is `--diff-filter=d`, so a DELETED file is invisible. A PR that
16
+ * deletes a migration or an auth check changed exactly what a checklist exists
17
+ * to catch.
18
+ *
19
+ * `@injectable(bindingScopeValues.Singleton)` so it is injected by type and drawn in the DI design.
20
+ */
21
+ export declare class ReviewChangedFiles {
22
+ private readonly diffScope;
23
+ constructor(diffScope: DiffScope);
24
+ /**
25
+ * Every file changed since `base`, INCLUDING uncommitted and untracked ones. [] when there is no base.
26
+ *
27
+ * No head argument — that is the branch of `getChangedFiles` which diffs base → WORKING TREE and unions
28
+ * in untracked files. Passing a head would diff commit-to-commit and miss staged, unstaged and
29
+ * untracked work, so an approval could be verified against a scope the working tree has already grown
30
+ * out of.
31
+ */
32
+ since(repoRoot: string, base: string): string[];
33
+ }
34
+ /**
35
+ * Gathers the ONE branch state a human authorization is minted against and verified against: the stable
36
+ * feature name, the fork point, and the changed-file set.
37
+ *
38
+ * Why the FEATURE name and not `git branch --show-current`: the 3-point merge parks the checkout on a
39
+ * transient `<feature>Squash` branch, and an approval keyed to the raw branch name would stop verifying
40
+ * for exactly as long as the flow that needs it is running. `AiBranchName.getFeatureName()` is already the
41
+ * stable identity every other per-branch artifact is keyed by, so an approval lives and dies with the same
42
+ * unit of work as the review it authorizes.
43
+ *
44
+ * `@injectable(bindingScopeValues.Singleton)` so it is injected by type and drawn in the DI design.
45
+ */
46
+ export declare class AuthorizationContextResolver {
47
+ private readonly aiBranchName;
48
+ private readonly diffBasisResolver;
49
+ private readonly changedFiles;
50
+ constructor(aiBranchName: AiBranchName, diffBasisResolver: DiffBasisResolver, changedFiles: ReviewChangedFiles);
51
+ resolve(repoRoot: string): AuthorizationContext;
52
+ /**
53
+ * The scope globs PROPOSED for a fresh approval, derived from what the diff touches today: one
54
+ * `<dir>/**` per distinct directory, collapsed to the first two path segments so a deep tree yields a
55
+ * readable handful rather than one glob per folder.
56
+ *
57
+ * Proposed, never imposed — `wp-authorize` prints these and lets the human replace them. The derived
58
+ * default matters anyway, because the alternative to a good default is a human who types `**` to get
59
+ * past the prompt, and `**` is the widening-by-absence this whole mechanism exists to make impossible
60
+ * to reach by accident.
61
+ */
62
+ proposeScopePaths(changedFiles: readonly string[]): string[];
63
+ }
@@ -0,0 +1,107 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AuthorizationContextResolver = exports.ReviewChangedFiles = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const rules_config_1 = require("@webpieces/rules-config");
6
+ const inversify_1 = require("inversify");
7
+ const git_readAiBranchName_1 = require("./git-readAiBranchName");
8
+ const diff_basis_1 = require("./diff-basis");
9
+ /**
10
+ * THE changed-file set the review system judges a branch by — extracted so the checklist scanner and the
11
+ * authorization commands cannot answer it two different ways.
12
+ *
13
+ * Two non-default options, both load-bearing, and both the reason this is worth a class rather than a
14
+ * repeated three-liner:
15
+ *
16
+ * `tsOnly:false` — the default drops every *.sql / Dockerfile / .env* file, silently shrinking the
17
+ * set a reviewer is pointed at — and, here, the set an approval's scope is
18
+ * checked against, which would let terraform-adjacent files slip inside a
19
+ * "terraform only" grant.
20
+ * `includeDeletions:true` — the default is `--diff-filter=d`, so a DELETED file is invisible. A PR that
21
+ * deletes a migration or an auth check changed exactly what a checklist exists
22
+ * to catch.
23
+ *
24
+ * `@injectable(bindingScopeValues.Singleton)` so it is injected by type and drawn in the DI design.
25
+ */
26
+ let ReviewChangedFiles = class ReviewChangedFiles {
27
+ diffScope;
28
+ constructor(diffScope) {
29
+ this.diffScope = diffScope;
30
+ }
31
+ /**
32
+ * Every file changed since `base`, INCLUDING uncommitted and untracked ones. [] when there is no base.
33
+ *
34
+ * No head argument — that is the branch of `getChangedFiles` which diffs base → WORKING TREE and unions
35
+ * in untracked files. Passing a head would diff commit-to-commit and miss staged, unstaged and
36
+ * untracked work, so an approval could be verified against a scope the working tree has already grown
37
+ * out of.
38
+ */
39
+ since(repoRoot, base) {
40
+ if (base === '')
41
+ return [];
42
+ const opts = new rules_config_1.ChangedFilesOptions();
43
+ opts.tsOnly = false;
44
+ opts.includeDeletions = true;
45
+ return this.diffScope.getChangedFiles(repoRoot, base, undefined, opts);
46
+ }
47
+ };
48
+ exports.ReviewChangedFiles = ReviewChangedFiles;
49
+ exports.ReviewChangedFiles = ReviewChangedFiles = tslib_1.__decorate([
50
+ (0, inversify_1.injectable)(inversify_1.bindingScopeValues.Singleton),
51
+ tslib_1.__metadata("design:paramtypes", [rules_config_1.DiffScope])
52
+ ], ReviewChangedFiles);
53
+ /**
54
+ * Gathers the ONE branch state a human authorization is minted against and verified against: the stable
55
+ * feature name, the fork point, and the changed-file set.
56
+ *
57
+ * Why the FEATURE name and not `git branch --show-current`: the 3-point merge parks the checkout on a
58
+ * transient `<feature>Squash` branch, and an approval keyed to the raw branch name would stop verifying
59
+ * for exactly as long as the flow that needs it is running. `AiBranchName.getFeatureName()` is already the
60
+ * stable identity every other per-branch artifact is keyed by, so an approval lives and dies with the same
61
+ * unit of work as the review it authorizes.
62
+ *
63
+ * `@injectable(bindingScopeValues.Singleton)` so it is injected by type and drawn in the DI design.
64
+ */
65
+ let AuthorizationContextResolver = class AuthorizationContextResolver {
66
+ aiBranchName;
67
+ diffBasisResolver;
68
+ changedFiles;
69
+ constructor(aiBranchName, diffBasisResolver, changedFiles) {
70
+ this.aiBranchName = aiBranchName;
71
+ this.diffBasisResolver = diffBasisResolver;
72
+ this.changedFiles = changedFiles;
73
+ }
74
+ resolve(repoRoot) {
75
+ const base = this.diffBasisResolver.resolve(repoRoot).base;
76
+ return new rules_config_1.AuthorizationContext(this.aiBranchName.getFeatureName(), base, this.changedFiles.since(repoRoot, base));
77
+ }
78
+ /**
79
+ * The scope globs PROPOSED for a fresh approval, derived from what the diff touches today: one
80
+ * `<dir>/**` per distinct directory, collapsed to the first two path segments so a deep tree yields a
81
+ * readable handful rather than one glob per folder.
82
+ *
83
+ * Proposed, never imposed — `wp-authorize` prints these and lets the human replace them. The derived
84
+ * default matters anyway, because the alternative to a good default is a human who types `**` to get
85
+ * past the prompt, and `**` is the widening-by-absence this whole mechanism exists to make impossible
86
+ * to reach by accident.
87
+ */
88
+ proposeScopePaths(changedFiles) {
89
+ const globs = new Set();
90
+ for (const file of changedFiles) {
91
+ const parts = file.split('/');
92
+ if (parts.length === 1)
93
+ globs.add(file);
94
+ else
95
+ globs.add(`${parts.slice(0, Math.min(2, parts.length - 1)).join('/')}/**`);
96
+ }
97
+ return [...globs].sort();
98
+ }
99
+ };
100
+ exports.AuthorizationContextResolver = AuthorizationContextResolver;
101
+ exports.AuthorizationContextResolver = AuthorizationContextResolver = tslib_1.__decorate([
102
+ (0, inversify_1.injectable)(inversify_1.bindingScopeValues.Singleton),
103
+ tslib_1.__metadata("design:paramtypes", [git_readAiBranchName_1.AiBranchName,
104
+ diff_basis_1.DiffBasisResolver,
105
+ ReviewChangedFiles])
106
+ ], AuthorizationContextResolver);
107
+ //# sourceMappingURL=authorization-context-resolver.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"authorization-context-resolver.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/authorization-context-resolver.ts"],"names":[],"mappings":";;;;AAAA,0DAA+F;AAC/F,yCAA2D;AAC3D,iEAAsD;AACtD,6CAAiD;AAEjD;;;;;;;;;;;;;;;;GAgBG;AAEI,IAAM,kBAAkB,GAAxB,MAAM,kBAAkB;IACE;IAA7B,YAA6B,SAAoB;QAApB,cAAS,GAAT,SAAS,CAAW;IAAG,CAAC;IAErD;;;;;;;OAOG;IACH,KAAK,CAAC,QAAgB,EAAE,IAAY;QAChC,IAAI,IAAI,KAAK,EAAE;YAAE,OAAO,EAAE,CAAC;QAC3B,MAAM,IAAI,GAAG,IAAI,kCAAmB,EAAE,CAAC;QACvC,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC;QACpB,IAAI,CAAC,gBAAgB,GAAG,IAAI,CAAC;QAC7B,OAAO,IAAI,CAAC,SAAS,CAAC,eAAe,CAAC,QAAQ,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,CAAC,CAAC;IAC3E,CAAC;CACJ,CAAA;AAlBY,gDAAkB;6BAAlB,kBAAkB;IAD9B,IAAA,sBAAU,EAAC,8BAAkB,CAAC,SAAS,CAAC;6CAEG,wBAAS;GADxC,kBAAkB,CAkB9B;AAED;;;;;;;;;;;GAWG;AAEI,IAAM,4BAA4B,GAAlC,MAAM,4BAA4B;IAEhB;IACA;IACA;IAHrB,YACqB,YAA0B,EAC1B,iBAAoC,EACpC,YAAgC;QAFhC,iBAAY,GAAZ,YAAY,CAAc;QAC1B,sBAAiB,GAAjB,iBAAiB,CAAmB;QACpC,iBAAY,GAAZ,YAAY,CAAoB;IAClD,CAAC;IAEJ,OAAO,CAAC,QAAgB;QACpB,MAAM,IAAI,GAAG,IAAI,CAAC,iBAAiB,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC;QAC3D,OAAO,IAAI,mCAAoB,CAC3B,IAAI,CAAC,YAAY,CAAC,cAAc,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC,CAAC;IAC3F,CAAC;IAED;;;;;;;;;OASG;IACH,iBAAiB,CAAC,YAA+B;QAC7C,MAAM,KAAK,GAAG,IAAI,GAAG,EAAU,CAAC;QAChC,KAAK,MAAM,IAAI,IAAI,YAAY,EAAE,CAAC;YAC9B,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;YAC9B,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;gBAAE,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;;gBACnC,KAAK,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QACpF,CAAC;QACD,OAAO,CAAC,GAAG,KAAK,CAAC,CAAC,IAAI,EAAE,CAAC;IAC7B,CAAC;CACJ,CAAA;AAhCY,oEAA4B;uCAA5B,4BAA4B;IADxC,IAAA,sBAAU,EAAC,8BAAkB,CAAC,SAAS,CAAC;6CAGF,mCAAY;QACP,8BAAiB;QACtB,kBAAkB;GAJ5C,4BAA4B,CAgCxC","sourcesContent":["import { AuthorizationContext, ChangedFilesOptions, DiffScope } from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { AiBranchName } from './git-readAiBranchName';\nimport { DiffBasisResolver } from './diff-basis';\n\n/**\n * THE changed-file set the review system judges a branch by — extracted so the checklist scanner and the\n * authorization commands cannot answer it two different ways.\n *\n * Two non-default options, both load-bearing, and both the reason this is worth a class rather than a\n * repeated three-liner:\n *\n * `tsOnly:false` — the default drops every *.sql / Dockerfile / .env* file, silently shrinking the\n * set a reviewer is pointed at — and, here, the set an approval's scope is\n * checked against, which would let terraform-adjacent files slip inside a\n * \"terraform only\" grant.\n * `includeDeletions:true` — the default is `--diff-filter=d`, so a DELETED file is invisible. A PR that\n * deletes a migration or an auth check changed exactly what a checklist exists\n * to catch.\n *\n * `@injectable(bindingScopeValues.Singleton)` so it is injected by type and drawn in the DI design.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class ReviewChangedFiles {\n constructor(private readonly diffScope: DiffScope) {}\n\n /**\n * Every file changed since `base`, INCLUDING uncommitted and untracked ones. [] when there is no base.\n *\n * No head argument — that is the branch of `getChangedFiles` which diffs base → WORKING TREE and unions\n * in untracked files. Passing a head would diff commit-to-commit and miss staged, unstaged and\n * untracked work, so an approval could be verified against a scope the working tree has already grown\n * out of.\n */\n since(repoRoot: string, base: string): string[] {\n if (base === '') return [];\n const opts = new ChangedFilesOptions();\n opts.tsOnly = false;\n opts.includeDeletions = true;\n return this.diffScope.getChangedFiles(repoRoot, base, undefined, opts);\n }\n}\n\n/**\n * Gathers the ONE branch state a human authorization is minted against and verified against: the stable\n * feature name, the fork point, and the changed-file set.\n *\n * Why the FEATURE name and not `git branch --show-current`: the 3-point merge parks the checkout on a\n * transient `<feature>Squash` branch, and an approval keyed to the raw branch name would stop verifying\n * for exactly as long as the flow that needs it is running. `AiBranchName.getFeatureName()` is already the\n * stable identity every other per-branch artifact is keyed by, so an approval lives and dies with the same\n * unit of work as the review it authorizes.\n *\n * `@injectable(bindingScopeValues.Singleton)` so it is injected by type and drawn in the DI design.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class AuthorizationContextResolver {\n constructor(\n private readonly aiBranchName: AiBranchName,\n private readonly diffBasisResolver: DiffBasisResolver,\n private readonly changedFiles: ReviewChangedFiles,\n ) {}\n\n resolve(repoRoot: string): AuthorizationContext {\n const base = this.diffBasisResolver.resolve(repoRoot).base;\n return new AuthorizationContext(\n this.aiBranchName.getFeatureName(), base, this.changedFiles.since(repoRoot, base));\n }\n\n /**\n * The scope globs PROPOSED for a fresh approval, derived from what the diff touches today: one\n * `<dir>/**` per distinct directory, collapsed to the first two path segments so a deep tree yields a\n * readable handful rather than one glob per folder.\n *\n * Proposed, never imposed — `wp-authorize` prints these and lets the human replace them. The derived\n * default matters anyway, because the alternative to a good default is a human who types `**` to get\n * past the prompt, and `**` is the widening-by-absence this whole mechanism exists to make impossible\n * to reach by accident.\n */\n proposeScopePaths(changedFiles: readonly string[]): string[] {\n const globs = new Set<string>();\n for (const file of changedFiles) {\n const parts = file.split('/');\n if (parts.length === 1) globs.add(file);\n else globs.add(`${parts.slice(0, Math.min(2, parts.length - 1)).join('/')}/**`);\n }\n return [...globs].sort();\n }\n}\n"]}
@@ -1,8 +1,9 @@
1
- import { ChecklistDefinition, ChecklistResult, ChecklistReviewContext, DiffScope, RequiredChecklist, ReviewJsonService } from '@webpieces/rules-config';
1
+ import { AuthorizedOverrides, ChecklistDefinition, ChecklistResult, ChecklistReviewContext, HumanAuthorizationService, 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';
6
7
  /** How a caller wants the scan filtered. Data-only (per CLAUDE.md). */
7
8
  export declare class ChecklistScanOptions {
8
9
  /**
@@ -72,7 +73,17 @@ export declare class ChecklistScan {
72
73
  * passed. An optional checklist that ran and went RED is absent from here and stays in `outstanding`.
73
74
  */
74
75
  optionalNotRun: RequiredChecklist[];
75
- constructor(defined: ChecklistDefinition[], applicable: RequiredChecklist[], reviewed: RequiredChecklist[], outstanding: RequiredChecklist[], context: ChecklistReviewContext, reviewPath: string, forkPoint: string, roster: ChecklistRoster, formatErrors: string[], basis?: DiffBasis, changedFiles?: string[], results?: ChecklistResult[], optionalNotRun?: RequiredChecklist[]);
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);
76
87
  }
77
88
  /**
78
89
  * The ONE computation of which reviewer subagents a branch owes, shared by `wp-review-upsert-pr` (which lists) and
@@ -84,10 +95,9 @@ export declare class ChecklistScan {
84
95
  * 1. **The base is the FORK POINT of main, computed directly** — never `DiffScope.resolveBase`, which
85
96
  * overlays `NX_BASE`/`NX_HEAD` from the environment. That made review coverage depend on an env var.
86
97
  * It now arrives via {@link DiffBasisResolver}, which injects ForkPoint; same sha, one resolution.
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.
98
+ * 2. **UNCOMMITTED work counts**, and the set comes from {@link ReviewChangedFiles} the one place that
99
+ * computation lives, shared with the authorization commands so a checklist's scope and an approval's
100
+ * scope are literally the same file set rather than two implementations of it.
91
101
  * 3. **The range and the command it prints are the SAME basis.** Property 2 used to be true of the file set
92
102
  * only: reviewers were handed `git diff <base> HEAD`, which on a dirty tree covers a different range and
93
103
  * prints nothing. Both now derive from one {@link DiffBasis}, carried on the scan so a materializing
@@ -98,19 +108,23 @@ export declare class ChecklistScan {
98
108
  export declare class ChecklistScanner {
99
109
  private readonly aiBranchName;
100
110
  private readonly checklistDetector;
101
- private readonly diffScope;
111
+ private readonly reviewChangedFiles;
102
112
  private readonly diffBasisResolver;
103
113
  private readonly prContextWriter;
104
114
  private readonly reviewJsonService;
105
- constructor(aiBranchName: AiBranchName, checklistDetector: ChecklistDetector, diffScope: DiffScope, diffBasisResolver: DiffBasisResolver, prContextWriter: PrContextWriter, reviewJsonService: ReviewJsonService);
115
+ private readonly humanAuthorization;
116
+ constructor(aiBranchName: AiBranchName, checklistDetector: ChecklistDetector, reviewChangedFiles: ReviewChangedFiles, diffBasisResolver: DiffBasisResolver, prContextWriter: PrContextWriter, reviewJsonService: ReviewJsonService, humanAuthorization: HumanAuthorizationService);
106
117
  /**
107
118
  * `defined` is the caller's ALREADY-VALIDATED `prGate.checklists`. The scanner deliberately does not load
108
119
  * config itself: `loadAndValidate` is the one gate on the checklist set (it rejects a non-array
109
120
  * `checklists`, including the removed `{ doc }` manifest shape, and verifies every guidance doc and
110
121
  * reviewer-agent file exists), both callers already run it for other fields, and keeping the read out of
111
- * here leaves this a function of its inputs rather than of the filesystem.
122
+ * here leaves this a function of its inputs rather than of the filesystem. `gateSalt` travels the same
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.
112
126
  */
113
- scan(repoRoot: string, defined: ChecklistDefinition[], opts: ChecklistScanOptions): ChecklistScan;
127
+ scan(repoRoot: string, defined: ChecklistDefinition[], gateSalt: string, opts: ChecklistScanOptions): ChecklistScan;
114
128
  /**
115
129
  * What `wp-finish-upsert-pr` actually REFUSES on: everything still owing a verdict, minus the optional
116
130
  * checklists nobody ran.
@@ -121,15 +135,4 @@ export declare class ChecklistScanner {
121
135
  * exactly the divergence this class's docstring exists to prevent.
122
136
  */
123
137
  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;
135
138
  }
@@ -8,6 +8,7 @@ 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");
11
12
  /** How a caller wants the scan filtered. Data-only (per CLAUDE.md). */
12
13
  class ChecklistScanOptions {
13
14
  /**
@@ -87,11 +88,25 @@ class ChecklistScan {
87
88
  * passed. An optional checklist that ran and went RED is absent from here and stays in `outstanding`.
88
89
  */
89
90
  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;
90
101
  // eslint-disable-next-line @typescript-eslint/max-params
91
102
  constructor(defined, applicable, reviewed, outstanding, context, reviewPath, forkPoint, roster, formatErrors, basis = new diff_basis_1.DiffBasis(), changedFiles = [],
92
103
  // Defaulted so a caller that only cares about the X/N/Z counts (and every existing test construction)
93
104
  // stays a one-liner; the scanner itself always passes the real set.
94
- results = [], optionalNotRun = []) {
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) {
95
110
  this.defined = defined;
96
111
  this.applicable = applicable;
97
112
  this.reviewed = reviewed;
@@ -105,6 +120,7 @@ class ChecklistScan {
105
120
  this.changedFiles = changedFiles;
106
121
  this.results = results;
107
122
  this.optionalNotRun = optionalNotRun;
123
+ this.authorized = authorized;
108
124
  }
109
125
  }
110
126
  exports.ChecklistScan = ChecklistScan;
@@ -118,10 +134,9 @@ exports.ChecklistScan = ChecklistScan;
118
134
  * 1. **The base is the FORK POINT of main, computed directly** — never `DiffScope.resolveBase`, which
119
135
  * overlays `NX_BASE`/`NX_HEAD` from the environment. That made review coverage depend on an env var.
120
136
  * It now arrives via {@link DiffBasisResolver}, which injects ForkPoint; same sha, one resolution.
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.
137
+ * 2. **UNCOMMITTED work counts**, and the set comes from {@link ReviewChangedFiles} the one place that
138
+ * computation lives, shared with the authorization commands so a checklist's scope and an approval's
139
+ * scope are literally the same file set rather than two implementations of it.
125
140
  * 3. **The range and the command it prints are the SAME basis.** Property 2 used to be true of the file set
126
141
  * only: reviewers were handed `git diff <base> HEAD`, which on a dirty tree covers a different range and
127
142
  * prints nothing. Both now derive from one {@link DiffBasis}, carried on the scan so a materializing
@@ -132,26 +147,31 @@ exports.ChecklistScan = ChecklistScan;
132
147
  let ChecklistScanner = class ChecklistScanner {
133
148
  aiBranchName;
134
149
  checklistDetector;
135
- diffScope;
150
+ reviewChangedFiles;
136
151
  diffBasisResolver;
137
152
  prContextWriter;
138
153
  reviewJsonService;
139
- constructor(aiBranchName, checklistDetector, diffScope, diffBasisResolver, prContextWriter, reviewJsonService) {
154
+ humanAuthorization;
155
+ constructor(aiBranchName, checklistDetector, reviewChangedFiles, diffBasisResolver, prContextWriter, reviewJsonService, humanAuthorization) {
140
156
  this.aiBranchName = aiBranchName;
141
157
  this.checklistDetector = checklistDetector;
142
- this.diffScope = diffScope;
158
+ this.reviewChangedFiles = reviewChangedFiles;
143
159
  this.diffBasisResolver = diffBasisResolver;
144
160
  this.prContextWriter = prContextWriter;
145
161
  this.reviewJsonService = reviewJsonService;
162
+ this.humanAuthorization = humanAuthorization;
146
163
  }
147
164
  /**
148
165
  * `defined` is the caller's ALREADY-VALIDATED `prGate.checklists`. The scanner deliberately does not load
149
166
  * config itself: `loadAndValidate` is the one gate on the checklist set (it rejects a non-array
150
167
  * `checklists`, including the removed `{ doc }` manifest shape, and verifies every guidance doc and
151
168
  * reviewer-agent file exists), both callers already run it for other fields, and keeping the read out of
152
- * here leaves this a function of its inputs rather than of the filesystem.
169
+ * here leaves this a function of its inputs rather than of the filesystem. `gateSalt` travels the same
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.
153
173
  */
154
- scan(repoRoot, defined, opts) {
174
+ scan(repoRoot, defined, gateSalt, opts) {
155
175
  const featureName = this.aiBranchName.getFeatureName();
156
176
  const reviewPath = (0, rules_config_1.reviewJsonPath)(repoRoot, featureName);
157
177
  // ONE basis for the file set, the reproduce command and any downstream materialization. The fork
@@ -160,20 +180,23 @@ let ChecklistScanner = class ChecklistScanner {
160
180
  const base = basis.base;
161
181
  // ONE changed-file computation feeds both the roster (all X) and the applicable set (N). `detect` is
162
182
  // pure and defined as the roster minus its empty entries, so the two cannot disagree about a match.
163
- const changedFiles = this.changedFiles(repoRoot, base);
183
+ const changedFiles = this.reviewChangedFiles.since(repoRoot, base);
164
184
  const roster = new checklist_detector_1.ChecklistRoster(this.checklistDetector.roster(defined, changedFiles), changedFiles.length, base !== '');
165
185
  const applicable = this.checklistDetector.toRequired(this.checklistDetector.detect(defined, changedFiles));
166
186
  const results = this.reviewJsonService.loadChecklistResults(reviewPath, applicable);
167
- const stillOwed = this.reviewJsonService.pendingChecklists(applicable, results);
187
+ // Resolved ONCE, from the same fork point and changed-file set the matching ran against — the
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);
168
191
  const owedIds = new Set(stillOwed.map((r) => r.id));
169
192
  // NOT `!owedIds.has(...)`-with-the-optional-exemption-folded-in: an optional checklist nobody ran is
170
193
  // neither reviewed nor blocking, and calling it "reviewed" would put a ✓ on the dashboard for a review
171
194
  // that never happened.
172
195
  const reviewed = applicable.filter((r) => !owedIds.has(r.id));
173
- const optionalNotRun = this.reviewJsonService.optionalWithoutVerdict(applicable, results);
196
+ const optionalNotRun = this.reviewJsonService.optionalWithoutVerdict(applicable, results, authorized);
174
197
  return new ChecklistScan(defined, applicable, reviewed, opts.filterAlreadyReviewed ? this.blocking(stillOwed, optionalNotRun) : applicable, opts.contextStage === ''
175
198
  ? this.prContextWriter.contextFor(repoRoot, featureName, basis)
176
- : this.prContextWriter.ensure(repoRoot, featureName, basis, opts.contextStage, changedFiles), reviewPath, base, roster, this.reviewJsonService.checklistFormatErrors(applicable, results), basis, changedFiles, results, optionalNotRun);
199
+ : this.prContextWriter.ensure(repoRoot, featureName, basis, opts.contextStage, changedFiles), reviewPath, base, roster, this.reviewJsonService.checklistFormatErrors(applicable, results, authorized), basis, changedFiles, results, optionalNotRun, authorized);
177
200
  }
178
201
  /**
179
202
  * What `wp-finish-upsert-pr` actually REFUSES on: everything still owing a verdict, minus the optional
@@ -188,34 +211,16 @@ let ChecklistScanner = class ChecklistScanner {
188
211
  const skipped = new Set(optionalNotRun.map((r) => r.id));
189
212
  return stillOwed.filter((r) => !skipped.has(r.id));
190
213
  }
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
- }
210
214
  };
211
215
  exports.ChecklistScanner = ChecklistScanner;
212
216
  exports.ChecklistScanner = ChecklistScanner = tslib_1.__decorate([
213
217
  (0, inversify_1.injectable)(inversify_1.bindingScopeValues.Singleton),
214
218
  tslib_1.__metadata("design:paramtypes", [git_readAiBranchName_1.AiBranchName,
215
219
  checklist_detector_1.ChecklistDetector,
216
- rules_config_1.DiffScope,
220
+ authorization_context_resolver_1.ReviewChangedFiles,
217
221
  diff_basis_1.DiffBasisResolver,
218
222
  pr_context_writer_1.PrContextWriter,
219
- rules_config_1.ReviewJsonService])
223
+ rules_config_1.ReviewJsonService,
224
+ rules_config_1.HumanAuthorizationService])
220
225
  ], ChecklistScanner);
221
226
  //# 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 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"]}
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"]}
@@ -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 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.
12
+ * anything. `refusalError` called with no archive path points the reader at the LIVE review-<id>.json,
13
+ * which is only correct while that file is still there — and at stage ② it is. Retiring a verdict is
14
+ * 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 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.
18
+ * anything. `refusalError` called with no archive path points the reader at the LIVE review-<id>.json,
19
+ * which is only correct while that file is still there — and at stage ② it is. Retiring a verdict is
20
+ * finish's act, on the refusal it is actually acting on.
21
21
  */
22
22
  class RefusedReviewer {
23
23
  checklistId;