@webpieces/pr-gate 0.4.497 → 0.4.499
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -3
- package/src/dashboard/dashboard.d.ts +15 -0
- package/src/dashboard/dashboard.js +25 -2
- package/src/dashboard/dashboard.js.map +1 -1
- package/src/scripts/commands/cleanup-command.d.ts +42 -3
- package/src/scripts/commands/cleanup-command.js +69 -11
- package/src/scripts/commands/cleanup-command.js.map +1 -1
- package/src/scripts/commands/finish-upsert-pr-command.d.ts +55 -5
- package/src/scripts/commands/finish-upsert-pr-command.js +162 -30
- package/src/scripts/commands/finish-upsert-pr-command.js.map +1 -1
- package/src/scripts/commands/land-pr-command.d.ts +17 -2
- package/src/scripts/commands/land-pr-command.js +32 -3
- package/src/scripts/commands/land-pr-command.js.map +1 -1
- package/src/scripts/commands/review-upsert-pr-command.d.ts +88 -0
- package/src/scripts/commands/review-upsert-pr-command.js +244 -0
- package/src/scripts/commands/review-upsert-pr-command.js.map +1 -0
- package/src/scripts/commands/start-upsert-pr-command.d.ts +10 -15
- package/src/scripts/commands/start-upsert-pr-command.js +41 -47
- package/src/scripts/commands/start-upsert-pr-command.js.map +1 -1
- package/src/scripts/commands/worktree-cleanup.d.ts +45 -0
- package/src/scripts/commands/worktree-cleanup.js +130 -0
- package/src/scripts/commands/worktree-cleanup.js.map +1 -0
- package/src/scripts/pr-gate-app.d.ts +9 -5
- package/src/scripts/pr-gate-app.js +12 -8
- package/src/scripts/pr-gate-app.js.map +1 -1
- package/src/scripts/workflow/checklist-scanner.d.ts +27 -11
- package/src/scripts/workflow/checklist-scanner.js +37 -15
- package/src/scripts/workflow/checklist-scanner.js.map +1 -1
- package/src/scripts/workflow/diff-basis.d.ts +53 -0
- package/src/scripts/workflow/diff-basis.js +113 -0
- package/src/scripts/workflow/diff-basis.js.map +1 -0
- package/src/scripts/workflow/diff-materializer.d.ts +85 -0
- package/src/scripts/workflow/diff-materializer.js +253 -0
- package/src/scripts/workflow/diff-materializer.js.map +1 -0
- package/src/scripts/workflow/git-findForkPoint.d.ts +1 -1
- package/src/scripts/workflow/git-findForkPoint.js +1 -1
- package/src/scripts/workflow/git-findForkPoint.js.map +1 -1
- package/src/scripts/workflow/pr-context-writer.d.ts +16 -10
- package/src/scripts/workflow/pr-context-writer.js +20 -14
- package/src/scripts/workflow/pr-context-writer.js.map +1 -1
- package/src/scripts/workflow/review-stage-receipt.d.ts +36 -0
- package/src/scripts/workflow/review-stage-receipt.js +82 -0
- package/src/scripts/workflow/review-stage-receipt.js.map +1 -0
- package/src/scripts/workflow/reviewer-briefing-builder.d.ts +36 -0
- package/src/scripts/workflow/reviewer-briefing-builder.js +104 -0
- package/src/scripts/workflow/reviewer-briefing-builder.js.map +1 -0
- package/src/scripts/{wp-checklist.js → wp-review-upsert-pr.js} +3 -3
- package/src/scripts/wp-review-upsert-pr.js.map +1 -0
- package/src/scripts/commands/checklist-command.d.ts +0 -27
- package/src/scripts/commands/checklist-command.js +0 -97
- package/src/scripts/commands/checklist-command.js.map +0 -1
- package/src/scripts/wp-checklist.js.map +0 -1
- /package/src/scripts/{wp-checklist.d.ts → wp-review-upsert-pr.d.ts} +0 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.WorktreeCleanupSection = 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 SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n';
|
|
8
|
+
/**
|
|
9
|
+
* The WORKTREE half of `wp-cleanup` — the verdicts, the reap and the human-facing text.
|
|
10
|
+
*
|
|
11
|
+
* Split out of CleanupCommand rather than bolted onto it because the two halves answer different
|
|
12
|
+
* questions: a branch is a ref, a worktree is a DIRECTORY OF FILES, and the second one needs its own
|
|
13
|
+
* report (paths, not just names), its own restore command (`git worktree add`, not `git checkout -b`)
|
|
14
|
+
* and its own spared vocabulary (locked, detached, "you are standing in it"). Keeping them in one class
|
|
15
|
+
* meant every string had to hedge about which kind of thing it was talking about.
|
|
16
|
+
*
|
|
17
|
+
* WHY worktrees are reaped BEFORE branches in wp-cleanup: a worktree HOLDS its branch, so that branch
|
|
18
|
+
* is spared as `in-use` with "remove that worktree before deleting the branch". Reaping the worktree
|
|
19
|
+
* first is what makes the branch reapable — the reap takes the branch with it, and the branch pass that
|
|
20
|
+
* follows recomputes its verdicts against the post-removal truth. Run the other way round, every
|
|
21
|
+
* worktree-held branch survives forever, which is exactly the deadlock this change exists to break.
|
|
22
|
+
*/
|
|
23
|
+
let WorktreeCleanupSection = class WorktreeCleanupSection {
|
|
24
|
+
mergedBranches;
|
|
25
|
+
reaper;
|
|
26
|
+
constructor(mergedBranches, reaper) {
|
|
27
|
+
this.mergedBranches = mergedBranches;
|
|
28
|
+
this.reaper = reaper;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* FRESH verdicts — never the cache on disk. Same rule the branch half follows: the cached file is
|
|
32
|
+
* deliberately allowed to go stale, which is fine for BLOCKING a `git worktree add` and never fine
|
|
33
|
+
* for removing a directory, since the tree may have gained uncommitted work since it was written.
|
|
34
|
+
*/
|
|
35
|
+
verdicts(repoRoot) {
|
|
36
|
+
return this.mergedBranches.computeMergedBranches(repoRoot).worktrees;
|
|
37
|
+
}
|
|
38
|
+
provablyDead(verdicts) {
|
|
39
|
+
return verdicts.filter((tree) => tree.deletable);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The spared worktrees a human can meaningfully rule on, grouped most-safe first — the same
|
|
43
|
+
* classification order the branch prompt uses, because it is literally the same verdict on the
|
|
44
|
+
* branch the worktree holds.
|
|
45
|
+
*
|
|
46
|
+
* Deliberately excluded: LOCKED (a human already said do not touch), CURRENT (removing your own cwd
|
|
47
|
+
* is not a thing to offer), DETACHED (no branch, so nothing to archive and nothing to judge) and
|
|
48
|
+
* PRUNABLE (already provably dead — it is in the auto-reap list, not this one).
|
|
49
|
+
*/
|
|
50
|
+
promptable(verdicts) {
|
|
51
|
+
const out = [];
|
|
52
|
+
for (const classification of rules_config_1.PROMPTABLE_CLASSIFICATIONS) {
|
|
53
|
+
for (const tree of verdicts) {
|
|
54
|
+
if (!tree.deletable && tree.classification === classification)
|
|
55
|
+
out.push(tree);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
reap(repoRoot, verb, targets, retention) {
|
|
61
|
+
return this.reaper.reapWorktrees(repoRoot, process.cwd(), verb, targets, retention);
|
|
62
|
+
}
|
|
63
|
+
report(result) {
|
|
64
|
+
if (result.reaped.length === 0 && result.failed.length === 0)
|
|
65
|
+
return '';
|
|
66
|
+
let out = '\n' + SEP + `🌲 Removed ${String(result.reaped.length)} dead worktree(s)\n` + SEP + '\n';
|
|
67
|
+
for (const entry of result.reaped)
|
|
68
|
+
out += this.reapedLine(entry);
|
|
69
|
+
if (result.failed.length > 0) {
|
|
70
|
+
out += `\n⚠️ ${String(result.failed.length)} worktree(s) could not be removed:\n`;
|
|
71
|
+
for (const entry of result.failed)
|
|
72
|
+
out += ` ✗ ${entry.path} — ${entry.error}\n`;
|
|
73
|
+
}
|
|
74
|
+
// Printed on success too: removing a worktree deletes real files, and a human who cannot see
|
|
75
|
+
// how to undo that has to take it on trust — which is precisely what nobody should have to do.
|
|
76
|
+
out += '\nEvery removal is logged in .webpieces/hooks/branch-mutations.log (phase REAP_WORKTREE)\n'
|
|
77
|
+
+ 'with the `recover=` command that brings back both the directory and its branch.\n';
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
reapedLine(entry) {
|
|
81
|
+
const branch = entry.branch !== '' ? ` [${entry.branch}]` : ' [detached]';
|
|
82
|
+
// The restore command is printed inline for the same reason the branch half prints the archive
|
|
83
|
+
// tag: it is the one thing that makes this reversible without going and digging in a log.
|
|
84
|
+
const restore = `\n restore: ${this.reaper.restoreCommand(entry)}`;
|
|
85
|
+
// A directory that went while its branch survived is a real half-state and must not read as done.
|
|
86
|
+
const partial = entry.branch !== '' && !entry.branchDeleted
|
|
87
|
+
? `\n ⚠️ the branch '${entry.branch}' was NOT deleted — git refused it`
|
|
88
|
+
: '';
|
|
89
|
+
return ` ✓ ${entry.path}${branch} — ${entry.reason}${restore}${partial}\n`;
|
|
90
|
+
}
|
|
91
|
+
/** The spared worktrees, with WHY — including the ones nobody will ever be asked about. */
|
|
92
|
+
sparedBlock(verdicts, removed) {
|
|
93
|
+
const gone = new Set(removed.map((tree) => tree.path));
|
|
94
|
+
const spared = verdicts.filter((tree) => !tree.deletable && !gone.has(tree.path)
|
|
95
|
+
&& this.isMechanical(tree.classification));
|
|
96
|
+
if (spared.length === 0)
|
|
97
|
+
return '';
|
|
98
|
+
let out = '\nWorktrees deliberately left alone:\n';
|
|
99
|
+
for (const tree of spared)
|
|
100
|
+
out += ` · ${tree.path} — ${tree.reason}\n`;
|
|
101
|
+
return out;
|
|
102
|
+
}
|
|
103
|
+
isMechanical(classification) {
|
|
104
|
+
return classification === rules_config_1.CLASSIFICATION_LOCKED
|
|
105
|
+
|| classification === rules_config_1.CLASSIFICATION_CURRENT
|
|
106
|
+
|| classification === rules_config_1.CLASSIFICATION_DETACHED
|
|
107
|
+
|| classification === rules_config_1.CLASSIFICATION_PRUNABLE;
|
|
108
|
+
}
|
|
109
|
+
// The table a human answers: path, branch, and the same reason the branch prompt would show, since
|
|
110
|
+
// the verdict IS the branch's verdict.
|
|
111
|
+
promptBlock(promptable) {
|
|
112
|
+
let out = '\n' + SEP
|
|
113
|
+
+ `🤔 ${String(promptable.length)} worktree(s) are probably dead — your call\n` + SEP + '\n'
|
|
114
|
+
+ 'Removing one deletes its DIRECTORY and its branch. The branch is archived as a tag first,\n'
|
|
115
|
+
+ 'so both come back with one `git worktree add -b …` — but uncommitted or untracked files in\n'
|
|
116
|
+
+ 'that directory are NOT archived, and git will refuse the removal if any exist.\n\n';
|
|
117
|
+
for (let i = 0; i < promptable.length; i += 1) {
|
|
118
|
+
const tree = promptable[i];
|
|
119
|
+
out += ` [${String(i + 1)}] ${tree.path}\n [${tree.branch}] — ${tree.reason}\n`;
|
|
120
|
+
}
|
|
121
|
+
return out;
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
exports.WorktreeCleanupSection = WorktreeCleanupSection;
|
|
125
|
+
exports.WorktreeCleanupSection = WorktreeCleanupSection = tslib_1.__decorate([
|
|
126
|
+
(0, inversify_1.injectable)(inversify_1.bindingScopeValues.Singleton),
|
|
127
|
+
tslib_1.__metadata("design:paramtypes", [rules_config_1.MergedBranchesService,
|
|
128
|
+
rules_config_1.WorktreeReaper])
|
|
129
|
+
], WorktreeCleanupSection);
|
|
130
|
+
//# sourceMappingURL=worktree-cleanup.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"worktree-cleanup.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/commands/worktree-cleanup.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n MutationVerb,\n DeletableWorktree,\n MergedBranchesService,\n ReapedWorktree,\n WorktreeReapResult,\n WorktreeReaper,\n CLASSIFICATION_LOCKED,\n CLASSIFICATION_CURRENT,\n CLASSIFICATION_DETACHED,\n CLASSIFICATION_PRUNABLE,\n PROMPTABLE_CLASSIFICATIONS,\n} from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n/**\n * The WORKTREE half of `wp-cleanup` — the verdicts, the reap and the human-facing text.\n *\n * Split out of CleanupCommand rather than bolted onto it because the two halves answer different\n * questions: a branch is a ref, a worktree is a DIRECTORY OF FILES, and the second one needs its own\n * report (paths, not just names), its own restore command (`git worktree add`, not `git checkout -b`)\n * and its own spared vocabulary (locked, detached, \"you are standing in it\"). Keeping them in one class\n * meant every string had to hedge about which kind of thing it was talking about.\n *\n * WHY worktrees are reaped BEFORE branches in wp-cleanup: a worktree HOLDS its branch, so that branch\n * is spared as `in-use` with \"remove that worktree before deleting the branch\". Reaping the worktree\n * first is what makes the branch reapable — the reap takes the branch with it, and the branch pass that\n * follows recomputes its verdicts against the post-removal truth. Run the other way round, every\n * worktree-held branch survives forever, which is exactly the deadlock this change exists to break.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class WorktreeCleanupSection {\n constructor(\n private readonly mergedBranches: MergedBranchesService,\n private readonly reaper: WorktreeReaper,\n ) {}\n\n /**\n * FRESH verdicts — never the cache on disk. Same rule the branch half follows: the cached file is\n * deliberately allowed to go stale, which is fine for BLOCKING a `git worktree add` and never fine\n * for removing a directory, since the tree may have gained uncommitted work since it was written.\n */\n verdicts(repoRoot: string): DeletableWorktree[] {\n return this.mergedBranches.computeMergedBranches(repoRoot).worktrees;\n }\n\n provablyDead(verdicts: DeletableWorktree[]): DeletableWorktree[] {\n return verdicts.filter((tree: DeletableWorktree): boolean => tree.deletable);\n }\n\n /**\n * The spared worktrees a human can meaningfully rule on, grouped most-safe first — the same\n * classification order the branch prompt uses, because it is literally the same verdict on the\n * branch the worktree holds.\n *\n * Deliberately excluded: LOCKED (a human already said do not touch), CURRENT (removing your own cwd\n * is not a thing to offer), DETACHED (no branch, so nothing to archive and nothing to judge) and\n * PRUNABLE (already provably dead — it is in the auto-reap list, not this one).\n */\n promptable(verdicts: DeletableWorktree[]): DeletableWorktree[] {\n const out: DeletableWorktree[] = [];\n for (const classification of PROMPTABLE_CLASSIFICATIONS) {\n for (const tree of verdicts) {\n if (!tree.deletable && tree.classification === classification) out.push(tree);\n }\n }\n return out;\n }\n\n reap(\n repoRoot: string,\n verb: MutationVerb,\n targets: DeletableWorktree[],\n retention: string,\n ): WorktreeReapResult {\n return this.reaper.reapWorktrees(repoRoot, process.cwd(), verb, targets, retention);\n }\n\n report(result: WorktreeReapResult): string {\n if (result.reaped.length === 0 && result.failed.length === 0) return '';\n\n let out = '\\n' + SEP + `🌲 Removed ${String(result.reaped.length)} dead worktree(s)\\n` + SEP + '\\n';\n for (const entry of result.reaped) out += this.reapedLine(entry);\n\n if (result.failed.length > 0) {\n out += `\\n⚠️ ${String(result.failed.length)} worktree(s) could not be removed:\\n`;\n for (const entry of result.failed) out += ` ✗ ${entry.path} — ${entry.error}\\n`;\n }\n // Printed on success too: removing a worktree deletes real files, and a human who cannot see\n // how to undo that has to take it on trust — which is precisely what nobody should have to do.\n out += '\\nEvery removal is logged in .webpieces/hooks/branch-mutations.log (phase REAP_WORKTREE)\\n'\n + 'with the `recover=` command that brings back both the directory and its branch.\\n';\n return out;\n }\n\n private reapedLine(entry: ReapedWorktree): string {\n const branch = entry.branch !== '' ? ` [${entry.branch}]` : ' [detached]';\n // The restore command is printed inline for the same reason the branch half prints the archive\n // tag: it is the one thing that makes this reversible without going and digging in a log.\n const restore = `\\n restore: ${this.reaper.restoreCommand(entry)}`;\n // A directory that went while its branch survived is a real half-state and must not read as done.\n const partial = entry.branch !== '' && !entry.branchDeleted\n ? `\\n ⚠️ the branch '${entry.branch}' was NOT deleted — git refused it`\n : '';\n return ` ✓ ${entry.path}${branch} — ${entry.reason}${restore}${partial}\\n`;\n }\n\n /** The spared worktrees, with WHY — including the ones nobody will ever be asked about. */\n sparedBlock(verdicts: DeletableWorktree[], removed: DeletableWorktree[]): string {\n const gone = new Set(removed.map((tree: DeletableWorktree): string => tree.path));\n const spared = verdicts.filter(\n (tree: DeletableWorktree): boolean => !tree.deletable && !gone.has(tree.path)\n && this.isMechanical(tree.classification));\n if (spared.length === 0) return '';\n let out = '\\nWorktrees deliberately left alone:\\n';\n for (const tree of spared) out += ` · ${tree.path} — ${tree.reason}\\n`;\n return out;\n }\n\n private isMechanical(classification: string): boolean {\n return classification === CLASSIFICATION_LOCKED\n || classification === CLASSIFICATION_CURRENT\n || classification === CLASSIFICATION_DETACHED\n || classification === CLASSIFICATION_PRUNABLE;\n }\n\n // The table a human answers: path, branch, and the same reason the branch prompt would show, since\n // the verdict IS the branch's verdict.\n promptBlock(promptable: DeletableWorktree[]): string {\n let out = '\\n' + SEP\n + `🤔 ${String(promptable.length)} worktree(s) are probably dead — your call\\n` + SEP + '\\n'\n + 'Removing one deletes its DIRECTORY and its branch. The branch is archived as a tag first,\\n'\n + 'so both come back with one `git worktree add -b …` — but uncommitted or untracked files in\\n'\n + 'that directory are NOT archived, and git will refuse the removal if any exist.\\n\\n';\n for (let i = 0; i < promptable.length; i += 1) {\n const tree = promptable[i];\n out += ` [${String(i + 1)}] ${tree.path}\\n [${tree.branch}] — ${tree.reason}\\n`;\n }\n return out;\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 {
|
|
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
|
|
24
|
-
constructor(startUpdateCommand: StartUpdateCommand, finishUpdateCommand: FinishUpdateCommand, startUpsertPrCommand: StartUpsertPrCommand, finishUpsertPrCommand: FinishUpsertPrCommand, cleanupCommand: CleanupCommand, landPrCommand: LandPrCommand, checkPrCommand: CheckPrCommand,
|
|
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
|
-
/**
|
|
38
|
-
|
|
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
|
|
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
|
-
|
|
30
|
-
constructor(startUpdateCommand, finishUpdateCommand, startUpsertPrCommand, finishUpsertPrCommand, cleanupCommand, landPrCommand, checkPrCommand,
|
|
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.
|
|
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
|
-
/**
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
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,
|
|
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 {
|
|
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-
|
|
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
|
-
|
|
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-
|
|
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.
|
|
48
|
-
*
|
|
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
|
|
67
|
+
private readonly diffBasisResolver;
|
|
57
68
|
private readonly prContextWriter;
|
|
58
69
|
private readonly reviewJsonService;
|
|
59
|
-
constructor(aiBranchName: AiBranchName, checklistDetector: ChecklistDetector, diffScope: DiffScope,
|
|
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.
|
|
70
|
-
* load-bearing
|
|
71
|
-
*
|
|
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
|
|
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-
|
|
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-
|
|
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.
|
|
74
|
-
*
|
|
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
|
-
|
|
95
|
+
diffBasisResolver;
|
|
83
96
|
prContextWriter;
|
|
84
97
|
reviewJsonService;
|
|
85
|
-
constructor(aiBranchName, checklistDetector, diffScope,
|
|
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.
|
|
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
|
-
|
|
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,
|
|
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.
|
|
117
|
-
* load-bearing
|
|
118
|
-
*
|
|
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
|
-
|
|
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
|
+
}
|