@webpieces/pr-gate 0.4.566 → 0.4.568
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/scripts/commands/push-dev-command.d.ts +3 -1
- package/src/scripts/commands/push-dev-command.js +10 -3
- package/src/scripts/commands/push-dev-command.js.map +1 -1
- package/src/scripts/workflow/dev-deploy-refs.d.ts +8 -0
- package/src/scripts/workflow/dev-deploy-refs.js +10 -0
- package/src/scripts/workflow/dev-deploy-refs.js.map +1 -1
- package/src/scripts/workflow/dev-deploy-watch-hints.d.ts +35 -0
- package/src/scripts/workflow/dev-deploy-watch-hints.js +76 -0
- package/src/scripts/workflow/dev-deploy-watch-hints.js.map +1 -0
- package/src/scripts/workflow/dev-resolve-runner.d.ts +3 -1
- package/src/scripts/workflow/dev-resolve-runner.js +11 -3
- package/src/scripts/workflow/dev-resolve-runner.js.map +1 -1
- package/src/scripts/workflow/review-report.d.ts +24 -0
- package/src/scripts/workflow/review-report.js +37 -7
- package/src/scripts/workflow/review-report.js.map +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webpieces/pr-gate",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.568",
|
|
4
4
|
"description": "Gated PR system: 3-point squash-merge, merge validation gate, and red/yellow/green PR dashboard. Standalone scripts, no Nx dependency required.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"directory": "packages/tooling/pr-gate"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@webpieces/rules-config": "0.4.
|
|
31
|
+
"@webpieces/rules-config": "0.4.568",
|
|
32
32
|
"@inversifyjs/binding-decorators": "1.1.5",
|
|
33
33
|
"inversify": "7.10.4",
|
|
34
34
|
"reflect-metadata": "0.2.2"
|
|
@@ -3,6 +3,7 @@ import { GitExec } from '../workflow/git-exec';
|
|
|
3
3
|
import { DevDeployRefs } from '../workflow/dev-deploy-refs';
|
|
4
4
|
import { PushDevStateStore } from '../workflow/push-dev-state';
|
|
5
5
|
import { DevResolveRunner } from '../workflow/dev-resolve-runner';
|
|
6
|
+
import { DevDeployWatchHints } from '../workflow/dev-deploy-watch-hints';
|
|
6
7
|
/**
|
|
7
8
|
* Which of `wp-push-dev`'s modes was asked for. Data-only (per CLAUDE.md) with plain assigned fields
|
|
8
9
|
* rather than a seven-argument constructor — the bin sets what argv carried and nothing else.
|
|
@@ -45,7 +46,8 @@ export declare class PushDevCommand {
|
|
|
45
46
|
private readonly refs;
|
|
46
47
|
private readonly store;
|
|
47
48
|
private readonly runner;
|
|
48
|
-
|
|
49
|
+
private readonly watchHints;
|
|
50
|
+
constructor(repoRootFinder: RepoRootFinder, gitExec: GitExec, refs: DevDeployRefs, store: PushDevStateStore, runner: DevResolveRunner, watchHints: DevDeployWatchHints);
|
|
49
51
|
run(opts: PushDevOptions): Promise<void>;
|
|
50
52
|
/** `--list`: exactly the live copies, straight from the remote — never a cached local view. */
|
|
51
53
|
private list;
|
|
@@ -8,6 +8,7 @@ const git_exec_1 = require("../workflow/git-exec");
|
|
|
8
8
|
const dev_deploy_refs_1 = require("../workflow/dev-deploy-refs");
|
|
9
9
|
const push_dev_state_1 = require("../workflow/push-dev-state");
|
|
10
10
|
const dev_resolve_runner_1 = require("../workflow/dev-resolve-runner");
|
|
11
|
+
const dev_deploy_watch_hints_1 = require("../workflow/dev-deploy-watch-hints");
|
|
11
12
|
const SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n';
|
|
12
13
|
/**
|
|
13
14
|
* Which of `wp-push-dev`'s modes was asked for. Data-only (per CLAUDE.md) with plain assigned fields
|
|
@@ -52,12 +53,14 @@ let PushDevCommand = class PushDevCommand {
|
|
|
52
53
|
refs;
|
|
53
54
|
store;
|
|
54
55
|
runner;
|
|
55
|
-
|
|
56
|
+
watchHints;
|
|
57
|
+
constructor(repoRootFinder, gitExec, refs, store, runner, watchHints) {
|
|
56
58
|
this.repoRootFinder = repoRootFinder;
|
|
57
59
|
this.gitExec = gitExec;
|
|
58
60
|
this.refs = refs;
|
|
59
61
|
this.store = store;
|
|
60
62
|
this.runner = runner;
|
|
63
|
+
this.watchHints = watchHints;
|
|
61
64
|
}
|
|
62
65
|
async run(opts) {
|
|
63
66
|
const repoRoot = this.repoRootFinder.resolveRepoRoot(process.cwd());
|
|
@@ -138,11 +141,14 @@ let PushDevCommand = class PushDevCommand {
|
|
|
138
141
|
}
|
|
139
142
|
this.gitExec.runGitChecked(['-C', repoRoot, 'push', ...this.pushSafety(targetRef, remoteSha, opts.force),
|
|
140
143
|
'origin', `HEAD:refs/heads/${targetRef}`], `Failed to publish the dev copy ${targetRef}`);
|
|
144
|
+
// Read AFTER the push, so the sha in the watch hints is provably the one that is now published.
|
|
145
|
+
const published = this.refs.headSha(repoRoot);
|
|
141
146
|
process.stdout.write('\n' + SEP + `✅ Published ${targetRef}\n` + SEP + '\n'
|
|
142
147
|
+ `\`${branch}\` is now in the pool your CI composes \`${cfg.devBranch}\` from — it will be picked up on\n`
|
|
143
148
|
+ 'the next composition run. No PR was opened and nothing landed on main.\n\n'
|
|
144
149
|
+ ` ${rules_config_1.WP_PUSH_DEV} --list ← what else is in the pool\n`
|
|
145
|
-
+ ` ${rules_config_1.WP_PUSH_DEV} --remove ← take this branch back out\n`
|
|
150
|
+
+ ` ${rules_config_1.WP_PUSH_DEV} --remove ← take this branch back out\n`
|
|
151
|
+
+ this.watchHints.render(cfg, targetRef, published));
|
|
146
152
|
}
|
|
147
153
|
/**
|
|
148
154
|
* `--rebase-resolution`: keep the published resolution, replay it on top of the new commits.
|
|
@@ -244,6 +250,7 @@ exports.PushDevCommand = PushDevCommand = tslib_1.__decorate([
|
|
|
244
250
|
git_exec_1.GitExec,
|
|
245
251
|
dev_deploy_refs_1.DevDeployRefs,
|
|
246
252
|
push_dev_state_1.PushDevStateStore,
|
|
247
|
-
dev_resolve_runner_1.DevResolveRunner
|
|
253
|
+
dev_resolve_runner_1.DevResolveRunner,
|
|
254
|
+
dev_deploy_watch_hints_1.DevDeployWatchHints])
|
|
248
255
|
], PushDevCommand);
|
|
249
256
|
//# sourceMappingURL=push-dev-command.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"push-dev-command.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/commands/push-dev-command.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n CliExitError,\n DevDeployConfig,\n RepoRootFinder,\n WP_FINISH_PUSH_DEV,\n WP_PUSH_DEV,\n writeTemplate,\n} from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\n\nimport { GitExec } from '../workflow/git-exec';\nimport { DevCopy, DevDeployRefs } from '../workflow/dev-deploy-refs';\nimport { PushDevState, PushDevStateStore } from '../workflow/push-dev-state';\nimport { DevResolveRunner } from '../workflow/dev-resolve-runner';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n/**\n * Which of `wp-push-dev`'s modes was asked for. Data-only (per CLAUDE.md) with plain assigned fields\n * rather than a seven-argument constructor — the bin sets what argv carried and nothing else.\n */\nexport class PushDevOptions {\n /** Delete the remote copy — the rollback. */\n remove = false;\n /** List the live copies and stop. The one mode that needs no branch preconditions. */\n list = false;\n /** Discard a conflict resolution already published on the copy, and overwrite it. */\n force = false;\n /** Replay that resolution on top of the new local commits instead of discarding it. */\n rebaseResolution = false;\n /** Compose the other copies onto this one so the shared environment can build them together. */\n resolve = false;\n /** With `--resolve`: merge ONLY this developer's branch. Empty ⇒ every other copy. */\n resolveTarget = '';\n}\n\n/**\n * `wp-push-dev` — publish a DISPOSABLE copy of this branch so a shared dev environment can build it,\n * without landing anything on `main` and without opening a PR.\n *\n * WHY THIS COMMAND HAS TO EXIST AT ALL: `pr-creation-or-push-guard` blocks every `git push`, no\n * exceptions, by design. Its own fix-hint offers \"ask a human to run the push\" for out-of-band cases —\n * which is the right answer for a genuine exception and the wrong one for a routine daily action. Without\n * a gated command there is simply no legal way for an AI to get a branch onto a dev server, so the\n * feature was undoable rather than merely awkward.\n *\n * WHY THERE IS NO BUILD, deliberately unlike `wp-start-upsert-pr`: the shared environment's CI builds the\n * COMPOSED tree (main + every listed branch). A local build here proves the branch compiles against\n * `main` alone, which is not the question — it would be both slower and reassuring about the wrong thing.\n * The gate that matters runs on the composition, in CI, where it belongs.\n *\n * WHAT IT NEVER DOES: touch the feature branch. See DevDeployRefs for the invariant and why the copy\n * exists to hold conflict resolutions that must not reach the PR.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class PushDevCommand {\n constructor(\n private readonly repoRootFinder: RepoRootFinder,\n private readonly gitExec: GitExec,\n private readonly refs: DevDeployRefs,\n private readonly store: PushDevStateStore,\n private readonly runner: DevResolveRunner,\n ) {}\n\n async run(opts: PushDevOptions): Promise<void> {\n const repoRoot = this.repoRootFinder.resolveRepoRoot(process.cwd());\n // Refresh the AI-facing workflow doc so it is present + current for any failure message to cite —\n // same reason every other wp-* command opens this way.\n writeTemplate(repoRoot, 'webpieces.git-workflow.md');\n const cfg = this.refs.config(repoRoot);\n\n // A resolve already in flight owns the checkout; starting a second one would strand the first.\n this.assertNoResolveInProgress(repoRoot);\n\n if (opts.list) {\n this.list(repoRoot, cfg);\n return Promise.resolve();\n }\n\n const branch = this.refs.currentBranch(repoRoot);\n this.refs.assertPushable(cfg, branch);\n const targetRef = cfg.copyRefFor(branch);\n\n if (opts.remove) {\n this.remove(repoRoot, cfg, branch, targetRef);\n return Promise.resolve();\n }\n // Every remaining mode commits or merges, so the tree must be the AI's committed work and nothing\n // else. The tooling never commits for you.\n this.gitExec.assertCleanTree(repoRoot);\n\n if (opts.resolve) {\n this.startResolve(repoRoot, cfg, branch, targetRef, opts.resolveTarget);\n return Promise.resolve();\n }\n this.publish(repoRoot, cfg, branch, targetRef, opts);\n return Promise.resolve();\n }\n\n /** `--list`: exactly the live copies, straight from the remote — never a cached local view. */\n private list(repoRoot: string, cfg: DevDeployConfig): void {\n const copies = this.refs.liveCopies(repoRoot, cfg);\n if (copies.length === 0) {\n process.stdout.write(\n `\\nNo dev copies published — nothing matches \\`${cfg.copyRefGlob()}\\` on origin.\\n`\n + `Publish this branch's with: ${WP_PUSH_DEV}\\n`);\n return;\n }\n let out = '\\n' + SEP + `📋 ${String(copies.length)} dev cop${copies.length === 1 ? 'y' : 'ies'} on origin\\n` + SEP + '\\n';\n for (const copy of copies) out += ` ${copy.sha.slice(0, 8)} ${copy.ref}\\n`;\n out += `\\nYour CI composes \\`${cfg.devBranch}\\` from origin/main plus every ref above, on every run.\\n`\n + `Remove yours with: ${WP_PUSH_DEV} --remove\\n`;\n process.stdout.write(out);\n }\n\n /** `--remove`: delete the remote copy. This IS the rollback — nothing else has to be undone. */\n private remove(repoRoot: string, cfg: DevDeployConfig, branch: string, targetRef: string): void {\n if (this.refs.remoteSha(repoRoot, targetRef) === '') {\n process.stdout.write(\n `\\nNothing to remove — \\`${targetRef}\\` is not published.\\n`\n + `List what IS published with: ${WP_PUSH_DEV} --list\\n`);\n return;\n }\n this.gitExec.runGitChecked(['-C', repoRoot, 'push', 'origin', '--delete', `refs/heads/${targetRef}`],\n `Failed to delete the dev copy ${targetRef}`);\n process.stdout.write(\n '\\n' + SEP + `🗑️ Removed ${targetRef}\\n` + SEP + '\\n'\n + `\\`${branch}\\` will drop out of \\`${cfg.devBranch}\\` on its next composition run. Your branch and any\\n`\n + 'PR on it are untouched.\\n');\n }\n\n /**\n * The ordinary path: publish HEAD as `<namespace>/<branch>`.\n *\n * THE CLOBBER GUARD is the whole of the interesting logic here. If the remote copy holds commits the\n * local branch does not, somebody (quite possibly this developer, days ago) resolved a real conflict\n * over there. A silent force-push destroys work that cost a human a genuine merge resolution, so the\n * default is refusal with two named ways forward — never a \"use --force\" one-liner, which is how a\n * warning becomes a reflex.\n */\n private publish(repoRoot: string, cfg: DevDeployConfig, branch: string, targetRef: string, opts: PushDevOptions): void {\n const remoteSha = this.refs.remoteSha(repoRoot, targetRef);\n if (remoteSha !== '') {\n this.refs.fetchCopies(repoRoot, [targetRef]);\n const ahead = this.refs.commitsOnlyOnRemote(repoRoot, targetRef);\n if (ahead > 0 && opts.rebaseResolution) {\n this.rebaseResolution(repoRoot, branch, targetRef);\n return;\n }\n if (ahead > 0 && !opts.force) this.refuseClobber(cfg, branch, targetRef, ahead);\n }\n this.gitExec.runGitChecked(\n ['-C', repoRoot, 'push', ...this.pushSafety(targetRef, remoteSha, opts.force),\n 'origin', `HEAD:refs/heads/${targetRef}`],\n `Failed to publish the dev copy ${targetRef}`);\n process.stdout.write(\n '\\n' + SEP + `✅ Published ${targetRef}\\n` + SEP + '\\n'\n + `\\`${branch}\\` is now in the pool your CI composes \\`${cfg.devBranch}\\` from — it will be picked up on\\n`\n + 'the next composition run. No PR was opened and nothing landed on main.\\n\\n'\n + ` ${WP_PUSH_DEV} --list ← what else is in the pool\\n`\n + ` ${WP_PUSH_DEV} --remove ← take this branch back out\\n`);\n }\n\n /**\n * `--rebase-resolution`: keep the published resolution, replay it on top of the new commits.\n *\n * Implemented as a resolve whose BASE is the local HEAD and whose queue is the copy itself, so a\n * replay that conflicts again lands in exactly the same halt/`wp-finish-push-dev` state machine as\n * any other conflict, rather than inventing a second way to be stuck.\n */\n private rebaseResolution(repoRoot: string, branch: string, targetRef: string): void {\n const state = new PushDevState(branch, this.runner.tmpBranchFor(branch), targetRef, [targetRef]);\n process.stdout.write(\n '\\n' + SEP + `♻️ Replaying the published resolution onto \\`${branch}\\`\\n` + SEP + '\\n'\n + `Merging \\`${targetRef}\\` (which holds the resolution) into your new commits on a throwaway branch.\\n`\n + `Your feature branch \\`${branch}\\` is not touched.\\n\\n`);\n this.runner.start(repoRoot, state, 'HEAD', [targetRef]);\n }\n\n /**\n * `--resolve`: compose the other copies onto this one, so what is published is what CI will build.\n *\n * With no argument the queue is EVERY other copy, in the same sorted order CI composes them — the\n * local resolution is then a rehearsal of the real composition, not a guess at it. With an argument\n * (the branch a CI conflict message names) it is just that one.\n */\n private startResolve(repoRoot: string, cfg: DevDeployConfig, branch: string, targetRef: string, only: string): void {\n if (this.refs.remoteSha(repoRoot, targetRef) === '') {\n throw new CliExitError(2,\n '\\n' + SEP + `⛔ \\`${targetRef}\\` is not published yet\\n` + SEP + '\\n'\n + 'There is nothing to compose against. Publish your copy first, then resolve:\\n'\n + ` ${WP_PUSH_DEV}\\n ${WP_PUSH_DEV} --resolve\\n`);\n }\n const queue = this.resolveQueue(repoRoot, cfg, targetRef, only);\n if (queue.length === 0) {\n process.stdout.write(\n `\\nNothing to compose — \\`${targetRef}\\` is the only copy in \\`${cfg.copyRefGlob()}\\`.\\n`\n + `Your CI will merge it onto origin/main by itself; there is no other branch to conflict with.\\n`);\n return;\n }\n const state = new PushDevState(branch, this.runner.tmpBranchFor(branch), targetRef, queue);\n process.stdout.write(this.runner.announce(cfg, state));\n this.runner.start(repoRoot, state, `refs/remotes/origin/${targetRef}`, [targetRef, ...queue]);\n }\n\n // The refs to merge: one named copy, or every copy that is not mine. A named branch is accepted with\n // or without the namespace prefix, because the CI message that names it may print either.\n private resolveQueue(repoRoot: string, cfg: DevDeployConfig, targetRef: string, only: string): string[] {\n const copies = this.refs.liveCopies(repoRoot, cfg);\n if (only === '') {\n return copies.filter((c: DevCopy): boolean => c.ref !== targetRef).map((c: DevCopy): string => c.ref);\n }\n const wanted = only.startsWith(`${cfg.branchNamespace}/`) ? only : cfg.copyRefFor(only);\n if (wanted === targetRef) {\n throw new CliExitError(2, `\\n⛔ \\`${only}\\` is your own copy — there is nothing to compose it with.\\n`);\n }\n if (!copies.some((c: DevCopy): boolean => c.ref === wanted)) {\n throw new CliExitError(2,\n '\\n' + SEP + `⛔ No published copy \\`${wanted}\\`\\n` + SEP + '\\n'\n + `Live copies (${WP_PUSH_DEV} --list):\\n`\n + copies.map((c: DevCopy): string => ` ${c.ref}`).join('\\n') + '\\n');\n }\n return [wanted];\n }\n\n /**\n * `--force-with-lease` pinned to the sha we just inspected, so the clobber guard's verdict cannot go\n * stale between the check and the push. Plain `--force` only when the caller explicitly asked to\n * discard, and no force at all for a brand-new copy.\n */\n private pushSafety(targetRef: string, remoteSha: string, force: boolean): string[] {\n if (remoteSha === '') return [];\n if (force) return ['--force'];\n return [`--force-with-lease=refs/heads/${targetRef}:${remoteSha}`];\n }\n\n private refuseClobber(cfg: DevDeployConfig, branch: string, targetRef: string, ahead: number): never {\n throw new CliExitError(2,\n '\\n' + SEP + `⛔ \\`${targetRef}\\` holds ${String(ahead)} commit(s) your branch does not\\n` + SEP + '\\n'\n + 'That is what a CONFLICT RESOLUTION looks like: somebody composed this copy against another\\n'\n + `developer's branch so \\`${cfg.devBranch}\\` could build, and that work exists ONLY on the copy —\\n`\n + 'by design, because it must never reach your feature branch or your PR.\\n\\n'\n + 'Publishing now would force-push over it and destroy it, so this command refuses. Pick one:\\n\\n'\n + ` ${WP_PUSH_DEV} --rebase-resolution\\n`\n + ' Replay that resolution on top of your new commits. Keeps the work; may ask you to\\n'\n + ' re-resolve the parts your new commits actually changed.\\n\\n'\n + ` ${WP_PUSH_DEV} --force\\n`\n + ' Discard it and publish your branch as-is. The composition breaks again until someone\\n'\n + ` re-resolves it with \\`${WP_PUSH_DEV} --resolve\\`.\\n\\n`\n + `See what is on the copy: git log --oneline ${branch}..refs/remotes/origin/${targetRef}\\n`);\n }\n\n // A half-finished resolve owns the checkout — it is parked on a throwaway branch, so \"the current\n // branch\" is not the feature branch this command would otherwise act on.\n private assertNoResolveInProgress(repoRoot: string): void {\n const state = this.store.read(repoRoot);\n if (state === null) return;\n throw new CliExitError(2,\n '\\n' + SEP + '⛔ A dev-deploy resolve is already in progress\\n' + SEP + '\\n'\n + `You are on the throwaway branch \\`${state.tmpBranch}\\`, mid-composition. Finish it or bail out:\\n`\n + ` ${WP_FINISH_PUSH_DEV} ← commit the resolution, resume the queue, publish\\n`\n + ` ${WP_FINISH_PUSH_DEV} --abort ← throw it away and go back to \\`${state.originalBranch}\\`\\n`);\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"push-dev-command.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/commands/push-dev-command.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n CliExitError,\n DevDeployConfig,\n RepoRootFinder,\n WP_FINISH_PUSH_DEV,\n WP_PUSH_DEV,\n writeTemplate,\n} from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\n\nimport { GitExec } from '../workflow/git-exec';\nimport { DevCopy, DevDeployRefs } from '../workflow/dev-deploy-refs';\nimport { PushDevState, PushDevStateStore } from '../workflow/push-dev-state';\nimport { DevResolveRunner } from '../workflow/dev-resolve-runner';\nimport { DevDeployWatchHints } from '../workflow/dev-deploy-watch-hints';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n/**\n * Which of `wp-push-dev`'s modes was asked for. Data-only (per CLAUDE.md) with plain assigned fields\n * rather than a seven-argument constructor — the bin sets what argv carried and nothing else.\n */\nexport class PushDevOptions {\n /** Delete the remote copy — the rollback. */\n remove = false;\n /** List the live copies and stop. The one mode that needs no branch preconditions. */\n list = false;\n /** Discard a conflict resolution already published on the copy, and overwrite it. */\n force = false;\n /** Replay that resolution on top of the new local commits instead of discarding it. */\n rebaseResolution = false;\n /** Compose the other copies onto this one so the shared environment can build them together. */\n resolve = false;\n /** With `--resolve`: merge ONLY this developer's branch. Empty ⇒ every other copy. */\n resolveTarget = '';\n}\n\n/**\n * `wp-push-dev` — publish a DISPOSABLE copy of this branch so a shared dev environment can build it,\n * without landing anything on `main` and without opening a PR.\n *\n * WHY THIS COMMAND HAS TO EXIST AT ALL: `pr-creation-or-push-guard` blocks every `git push`, no\n * exceptions, by design. Its own fix-hint offers \"ask a human to run the push\" for out-of-band cases —\n * which is the right answer for a genuine exception and the wrong one for a routine daily action. Without\n * a gated command there is simply no legal way for an AI to get a branch onto a dev server, so the\n * feature was undoable rather than merely awkward.\n *\n * WHY THERE IS NO BUILD, deliberately unlike `wp-start-upsert-pr`: the shared environment's CI builds the\n * COMPOSED tree (main + every listed branch). A local build here proves the branch compiles against\n * `main` alone, which is not the question — it would be both slower and reassuring about the wrong thing.\n * The gate that matters runs on the composition, in CI, where it belongs.\n *\n * WHAT IT NEVER DOES: touch the feature branch. See DevDeployRefs for the invariant and why the copy\n * exists to hold conflict resolutions that must not reach the PR.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class PushDevCommand {\n constructor(\n private readonly repoRootFinder: RepoRootFinder,\n private readonly gitExec: GitExec,\n private readonly refs: DevDeployRefs,\n private readonly store: PushDevStateStore,\n private readonly runner: DevResolveRunner,\n private readonly watchHints: DevDeployWatchHints,\n ) {}\n\n async run(opts: PushDevOptions): Promise<void> {\n const repoRoot = this.repoRootFinder.resolveRepoRoot(process.cwd());\n // Refresh the AI-facing workflow doc so it is present + current for any failure message to cite —\n // same reason every other wp-* command opens this way.\n writeTemplate(repoRoot, 'webpieces.git-workflow.md');\n const cfg = this.refs.config(repoRoot);\n\n // A resolve already in flight owns the checkout; starting a second one would strand the first.\n this.assertNoResolveInProgress(repoRoot);\n\n if (opts.list) {\n this.list(repoRoot, cfg);\n return Promise.resolve();\n }\n\n const branch = this.refs.currentBranch(repoRoot);\n this.refs.assertPushable(cfg, branch);\n const targetRef = cfg.copyRefFor(branch);\n\n if (opts.remove) {\n this.remove(repoRoot, cfg, branch, targetRef);\n return Promise.resolve();\n }\n // Every remaining mode commits or merges, so the tree must be the AI's committed work and nothing\n // else. The tooling never commits for you.\n this.gitExec.assertCleanTree(repoRoot);\n\n if (opts.resolve) {\n this.startResolve(repoRoot, cfg, branch, targetRef, opts.resolveTarget);\n return Promise.resolve();\n }\n this.publish(repoRoot, cfg, branch, targetRef, opts);\n return Promise.resolve();\n }\n\n /** `--list`: exactly the live copies, straight from the remote — never a cached local view. */\n private list(repoRoot: string, cfg: DevDeployConfig): void {\n const copies = this.refs.liveCopies(repoRoot, cfg);\n if (copies.length === 0) {\n process.stdout.write(\n `\\nNo dev copies published — nothing matches \\`${cfg.copyRefGlob()}\\` on origin.\\n`\n + `Publish this branch's with: ${WP_PUSH_DEV}\\n`);\n return;\n }\n let out = '\\n' + SEP + `📋 ${String(copies.length)} dev cop${copies.length === 1 ? 'y' : 'ies'} on origin\\n` + SEP + '\\n';\n for (const copy of copies) out += ` ${copy.sha.slice(0, 8)} ${copy.ref}\\n`;\n out += `\\nYour CI composes \\`${cfg.devBranch}\\` from origin/main plus every ref above, on every run.\\n`\n + `Remove yours with: ${WP_PUSH_DEV} --remove\\n`;\n process.stdout.write(out);\n }\n\n /** `--remove`: delete the remote copy. This IS the rollback — nothing else has to be undone. */\n private remove(repoRoot: string, cfg: DevDeployConfig, branch: string, targetRef: string): void {\n if (this.refs.remoteSha(repoRoot, targetRef) === '') {\n process.stdout.write(\n `\\nNothing to remove — \\`${targetRef}\\` is not published.\\n`\n + `List what IS published with: ${WP_PUSH_DEV} --list\\n`);\n return;\n }\n this.gitExec.runGitChecked(['-C', repoRoot, 'push', 'origin', '--delete', `refs/heads/${targetRef}`],\n `Failed to delete the dev copy ${targetRef}`);\n process.stdout.write(\n '\\n' + SEP + `🗑️ Removed ${targetRef}\\n` + SEP + '\\n'\n + `\\`${branch}\\` will drop out of \\`${cfg.devBranch}\\` on its next composition run. Your branch and any\\n`\n + 'PR on it are untouched.\\n');\n }\n\n /**\n * The ordinary path: publish HEAD as `<namespace>/<branch>`.\n *\n * THE CLOBBER GUARD is the whole of the interesting logic here. If the remote copy holds commits the\n * local branch does not, somebody (quite possibly this developer, days ago) resolved a real conflict\n * over there. A silent force-push destroys work that cost a human a genuine merge resolution, so the\n * default is refusal with two named ways forward — never a \"use --force\" one-liner, which is how a\n * warning becomes a reflex.\n */\n private publish(repoRoot: string, cfg: DevDeployConfig, branch: string, targetRef: string, opts: PushDevOptions): void {\n const remoteSha = this.refs.remoteSha(repoRoot, targetRef);\n if (remoteSha !== '') {\n this.refs.fetchCopies(repoRoot, [targetRef]);\n const ahead = this.refs.commitsOnlyOnRemote(repoRoot, targetRef);\n if (ahead > 0 && opts.rebaseResolution) {\n this.rebaseResolution(repoRoot, branch, targetRef);\n return;\n }\n if (ahead > 0 && !opts.force) this.refuseClobber(cfg, branch, targetRef, ahead);\n }\n this.gitExec.runGitChecked(\n ['-C', repoRoot, 'push', ...this.pushSafety(targetRef, remoteSha, opts.force),\n 'origin', `HEAD:refs/heads/${targetRef}`],\n `Failed to publish the dev copy ${targetRef}`);\n // Read AFTER the push, so the sha in the watch hints is provably the one that is now published.\n const published = this.refs.headSha(repoRoot);\n process.stdout.write(\n '\\n' + SEP + `✅ Published ${targetRef}\\n` + SEP + '\\n'\n + `\\`${branch}\\` is now in the pool your CI composes \\`${cfg.devBranch}\\` from — it will be picked up on\\n`\n + 'the next composition run. No PR was opened and nothing landed on main.\\n\\n'\n + ` ${WP_PUSH_DEV} --list ← what else is in the pool\\n`\n + ` ${WP_PUSH_DEV} --remove ← take this branch back out\\n`\n + this.watchHints.render(cfg, targetRef, published));\n }\n\n /**\n * `--rebase-resolution`: keep the published resolution, replay it on top of the new commits.\n *\n * Implemented as a resolve whose BASE is the local HEAD and whose queue is the copy itself, so a\n * replay that conflicts again lands in exactly the same halt/`wp-finish-push-dev` state machine as\n * any other conflict, rather than inventing a second way to be stuck.\n */\n private rebaseResolution(repoRoot: string, branch: string, targetRef: string): void {\n const state = new PushDevState(branch, this.runner.tmpBranchFor(branch), targetRef, [targetRef]);\n process.stdout.write(\n '\\n' + SEP + `♻️ Replaying the published resolution onto \\`${branch}\\`\\n` + SEP + '\\n'\n + `Merging \\`${targetRef}\\` (which holds the resolution) into your new commits on a throwaway branch.\\n`\n + `Your feature branch \\`${branch}\\` is not touched.\\n\\n`);\n this.runner.start(repoRoot, state, 'HEAD', [targetRef]);\n }\n\n /**\n * `--resolve`: compose the other copies onto this one, so what is published is what CI will build.\n *\n * With no argument the queue is EVERY other copy, in the same sorted order CI composes them — the\n * local resolution is then a rehearsal of the real composition, not a guess at it. With an argument\n * (the branch a CI conflict message names) it is just that one.\n */\n private startResolve(repoRoot: string, cfg: DevDeployConfig, branch: string, targetRef: string, only: string): void {\n if (this.refs.remoteSha(repoRoot, targetRef) === '') {\n throw new CliExitError(2,\n '\\n' + SEP + `⛔ \\`${targetRef}\\` is not published yet\\n` + SEP + '\\n'\n + 'There is nothing to compose against. Publish your copy first, then resolve:\\n'\n + ` ${WP_PUSH_DEV}\\n ${WP_PUSH_DEV} --resolve\\n`);\n }\n const queue = this.resolveQueue(repoRoot, cfg, targetRef, only);\n if (queue.length === 0) {\n process.stdout.write(\n `\\nNothing to compose — \\`${targetRef}\\` is the only copy in \\`${cfg.copyRefGlob()}\\`.\\n`\n + `Your CI will merge it onto origin/main by itself; there is no other branch to conflict with.\\n`);\n return;\n }\n const state = new PushDevState(branch, this.runner.tmpBranchFor(branch), targetRef, queue);\n process.stdout.write(this.runner.announce(cfg, state));\n this.runner.start(repoRoot, state, `refs/remotes/origin/${targetRef}`, [targetRef, ...queue]);\n }\n\n // The refs to merge: one named copy, or every copy that is not mine. A named branch is accepted with\n // or without the namespace prefix, because the CI message that names it may print either.\n private resolveQueue(repoRoot: string, cfg: DevDeployConfig, targetRef: string, only: string): string[] {\n const copies = this.refs.liveCopies(repoRoot, cfg);\n if (only === '') {\n return copies.filter((c: DevCopy): boolean => c.ref !== targetRef).map((c: DevCopy): string => c.ref);\n }\n const wanted = only.startsWith(`${cfg.branchNamespace}/`) ? only : cfg.copyRefFor(only);\n if (wanted === targetRef) {\n throw new CliExitError(2, `\\n⛔ \\`${only}\\` is your own copy — there is nothing to compose it with.\\n`);\n }\n if (!copies.some((c: DevCopy): boolean => c.ref === wanted)) {\n throw new CliExitError(2,\n '\\n' + SEP + `⛔ No published copy \\`${wanted}\\`\\n` + SEP + '\\n'\n + `Live copies (${WP_PUSH_DEV} --list):\\n`\n + copies.map((c: DevCopy): string => ` ${c.ref}`).join('\\n') + '\\n');\n }\n return [wanted];\n }\n\n /**\n * `--force-with-lease` pinned to the sha we just inspected, so the clobber guard's verdict cannot go\n * stale between the check and the push. Plain `--force` only when the caller explicitly asked to\n * discard, and no force at all for a brand-new copy.\n */\n private pushSafety(targetRef: string, remoteSha: string, force: boolean): string[] {\n if (remoteSha === '') return [];\n if (force) return ['--force'];\n return [`--force-with-lease=refs/heads/${targetRef}:${remoteSha}`];\n }\n\n private refuseClobber(cfg: DevDeployConfig, branch: string, targetRef: string, ahead: number): never {\n throw new CliExitError(2,\n '\\n' + SEP + `⛔ \\`${targetRef}\\` holds ${String(ahead)} commit(s) your branch does not\\n` + SEP + '\\n'\n + 'That is what a CONFLICT RESOLUTION looks like: somebody composed this copy against another\\n'\n + `developer's branch so \\`${cfg.devBranch}\\` could build, and that work exists ONLY on the copy —\\n`\n + 'by design, because it must never reach your feature branch or your PR.\\n\\n'\n + 'Publishing now would force-push over it and destroy it, so this command refuses. Pick one:\\n\\n'\n + ` ${WP_PUSH_DEV} --rebase-resolution\\n`\n + ' Replay that resolution on top of your new commits. Keeps the work; may ask you to\\n'\n + ' re-resolve the parts your new commits actually changed.\\n\\n'\n + ` ${WP_PUSH_DEV} --force\\n`\n + ' Discard it and publish your branch as-is. The composition breaks again until someone\\n'\n + ` re-resolves it with \\`${WP_PUSH_DEV} --resolve\\`.\\n\\n`\n + `See what is on the copy: git log --oneline ${branch}..refs/remotes/origin/${targetRef}\\n`);\n }\n\n // A half-finished resolve owns the checkout — it is parked on a throwaway branch, so \"the current\n // branch\" is not the feature branch this command would otherwise act on.\n private assertNoResolveInProgress(repoRoot: string): void {\n const state = this.store.read(repoRoot);\n if (state === null) return;\n throw new CliExitError(2,\n '\\n' + SEP + '⛔ A dev-deploy resolve is already in progress\\n' + SEP + '\\n'\n + `You are on the throwaway branch \\`${state.tmpBranch}\\`, mid-composition. Finish it or bail out:\\n`\n + ` ${WP_FINISH_PUSH_DEV} ← commit the resolution, resume the queue, publish\\n`\n + ` ${WP_FINISH_PUSH_DEV} --abort ← throw it away and go back to \\`${state.originalBranch}\\`\\n`);\n }\n}\n"]}
|
|
@@ -52,6 +52,14 @@ export declare class DevDeployRefs {
|
|
|
52
52
|
* result, which would make the local resolution a guess about CI rather than a rehearsal of it.
|
|
53
53
|
*/
|
|
54
54
|
liveCopies(repoRoot: string, cfg: DevDeployConfig): DevCopy[];
|
|
55
|
+
/**
|
|
56
|
+
* The sha of a local ref (default HEAD).
|
|
57
|
+
*
|
|
58
|
+
* Read AFTER a push so the watch hints quote the sha that is provably published. Quoting the copy
|
|
59
|
+
* REF instead would be a moving target — it advances the next time anybody publishes — and the whole
|
|
60
|
+
* value of the "did my code land in dev" check is that it is a claim about one specific commit.
|
|
61
|
+
*/
|
|
62
|
+
headSha(repoRoot: string, ref?: string): string;
|
|
55
63
|
/** The remote sha of one copy, or '' when that copy does not exist yet. */
|
|
56
64
|
remoteSha(repoRoot: string, ref: string): string;
|
|
57
65
|
/**
|
|
@@ -106,6 +106,16 @@ let DevDeployRefs = class DevDeployRefs {
|
|
|
106
106
|
}
|
|
107
107
|
return copies.sort((a, b) => (a.ref < b.ref ? -1 : a.ref > b.ref ? 1 : 0));
|
|
108
108
|
}
|
|
109
|
+
/**
|
|
110
|
+
* The sha of a local ref (default HEAD).
|
|
111
|
+
*
|
|
112
|
+
* Read AFTER a push so the watch hints quote the sha that is provably published. Quoting the copy
|
|
113
|
+
* REF instead would be a moving target — it advances the next time anybody publishes — and the whole
|
|
114
|
+
* value of the "did my code land in dev" check is that it is a claim about one specific commit.
|
|
115
|
+
*/
|
|
116
|
+
headSha(repoRoot, ref = 'HEAD') {
|
|
117
|
+
return this.gitExec.gitQuery(['-C', repoRoot, 'rev-parse', ref], repoRoot, `Failed to read the sha of ${ref} (git rev-parse).`);
|
|
118
|
+
}
|
|
109
119
|
/** The remote sha of one copy, or '' when that copy does not exist yet. */
|
|
110
120
|
remoteSha(repoRoot, ref) {
|
|
111
121
|
const out = this.gitExec.gitQuery(['ls-remote', '--heads', 'origin', ref], repoRoot, `Failed to look up origin/${ref} (git ls-remote).`);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dev-deploy-refs.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/dev-deploy-refs.ts"],"names":[],"mappings":"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","sourcesContent":["import { CliExitError, DevDeployConfig, loadAndValidate, WP_PUSH_DEV } from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\n\nimport { GitExec } from './git-exec';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\nconst MAIN = 'main';\nconst DETACHED = 'HEAD';\n\n/**\n * One live copy in the dev namespace: the remote ref and the sha it points at. Data-only per CLAUDE.md —\n * `--list` prints it and the clobber guard compares against `sha`, and neither should be re-parsing\n * `ls-remote` output.\n */\nexport class DevCopy {\n /** The full ref name WITHOUT `refs/heads/`, e.g. `dev-include/dean/ONE-2275`. */\n ref: string;\n sha: string;\n /** The feature branch this is a copy OF — `ref` with the namespace prefix stripped. */\n sourceBranch: string;\n\n constructor(ref: string, sha: string, sourceBranch: string) {\n this.ref = ref;\n this.sha = sha;\n this.sourceBranch = sourceBranch;\n }\n}\n\n/**\n * Ref derivation and preconditions for the dev-deploy flow — the half of `wp-push-dev` that decides WHICH\n * refs are in play, kept out of the commands so both `wp-push-dev` and `wp-finish-push-dev` answer those\n * questions identically.\n *\n * THE INVARIANT THIS FILE EXISTS TO PROTECT: the feature branch is the PR head, so it must never acquire\n * another developer's commits — landing that PR would ship their unreviewed work to production. Every\n * write this flow performs therefore targets either the `<namespace>/<feature>` copy or a local throwaway\n * branch, and NOTHING here ever moves `<feature>` itself. That is why a conflict resolution needs the copy\n * to live in: it is the only place the resolution can be recorded without contaminating the PR.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class DevDeployRefs {\n constructor(private readonly gitExec: GitExec) {}\n\n /** The `pr-gate.devDeploy` block — `dev-include` / `dev` unless the consumer overrode it. */\n config(repoRoot: string): DevDeployConfig {\n return loadAndValidate(repoRoot).prGate.devDeploy;\n }\n\n /** The checked-out branch name, or `'HEAD'` on a detached HEAD (which {@link assertPushable} refuses). */\n currentBranch(repoRoot: string): string {\n return this.gitExec.gitQuery(['rev-parse', '--abbrev-ref', 'HEAD'], repoRoot,\n 'Failed to read the current branch (git rev-parse --abbrev-ref HEAD).');\n }\n\n /**\n * Refuse every branch that must never be the SOURCE of a dev copy, before anything is fetched or\n * pushed.\n *\n * `main` and the composed dev branch are refused for the same reason from opposite ends: `main` is the\n * trunk (whatever is on it already deploys, so copying it is meaningless), and the dev branch is a\n * BUILD ARTIFACT that CI recomputes from `origin/main` on every run — copying it would publish a\n * composition of everyone's work under one developer's name, and it would be wiped on the next CI run\n * anyway. A branch already inside the namespace is refused because publishing a copy of a copy nests\n * the prefix (`dev-include/dev-include/…`) and nothing would ever clean it up.\n */\n assertPushable(cfg: DevDeployConfig, branch: string): void {\n if (branch === DETACHED || branch === '') {\n throw new CliExitError(2, this.refusal(\n 'Detached HEAD',\n 'There is no branch name to derive the dev copy from. Check out your feature branch first:\\n'\n + ' git checkout <your-feature-branch>\\n'));\n }\n if (branch === MAIN) {\n throw new CliExitError(2, this.refusal(\n `Refusing to publish a dev copy of \\`${MAIN}\\``,\n `\\`${MAIN}\\` is the trunk — whatever is on it already deploys, so a dev copy of it means nothing.\\n`\n + 'Check out the feature branch you actually want on the dev server and re-run.\\n'));\n }\n if (branch === cfg.devBranch) {\n throw new CliExitError(2, this.refusal(\n `Refusing to publish a dev copy of \\`${cfg.devBranch}\\``,\n `\\`${cfg.devBranch}\\` is a BUILD ARTIFACT, not a source branch: your CI resets it to origin/${MAIN},\\n`\n + `merges every \\`${cfg.copyRefGlob()}\\` ref into it, and force-pushes — every run. Copying it would\\n`\n + \"publish a composition of everybody's work under your branch's name, and the next CI run would\\n\"\n + 'wipe it. Check out your own feature branch and re-run.\\n'));\n }\n if (branch === cfg.branchNamespace || branch.startsWith(`${cfg.branchNamespace}/`)) {\n throw new CliExitError(2, this.refusal(\n `Refusing to publish a dev copy of a dev copy (\\`${branch}\\`)`,\n `\\`${cfg.branchNamespace}/\\` holds the disposable copies this command WRITES; you are standing on one.\\n`\n + `Publishing from here would nest the prefix (\\`${cfg.branchNamespace}/${branch}\\`) and nothing ever\\n`\n + 'cleans that up. Check out the real feature branch and re-run.\\n'));\n }\n }\n\n /**\n * Every live copy in the namespace, sorted by ref name.\n *\n * The sort is not cosmetic: `--resolve` with no argument queues these in exactly this order, and the\n * consumer's CI composes `dev` in the same order, so what a developer resolves locally is the\n * composition CI will actually build. A different order can produce a different (still valid) merge\n * result, which would make the local resolution a guess about CI rather than a rehearsal of it.\n */\n liveCopies(repoRoot: string, cfg: DevDeployConfig): DevCopy[] {\n const out = this.gitExec.gitQuery(['ls-remote', '--heads', 'origin', cfg.copyRefGlob()], repoRoot,\n `Failed to list the dev copies (git ls-remote --heads origin '${cfg.copyRefGlob()}').`);\n const copies: DevCopy[] = [];\n for (const line of out.split('\\n')) {\n if (line.trim() === '') continue;\n const fields = line.split(/\\s+/);\n const sha = fields[0];\n const ref = fields[1];\n if (sha === undefined || ref === undefined || !ref.startsWith('refs/heads/')) continue;\n const shortRef = ref.slice('refs/heads/'.length);\n copies.push(new DevCopy(shortRef, sha, shortRef.slice(cfg.branchNamespace.length + 1)));\n }\n return copies.sort((a: DevCopy, b: DevCopy): number => (a.ref < b.ref ? -1 : a.ref > b.ref ? 1 : 0));\n }\n\n /** The remote sha of one copy, or '' when that copy does not exist yet. */\n remoteSha(repoRoot: string, ref: string): string {\n const out = this.gitExec.gitQuery(['ls-remote', '--heads', 'origin', ref], repoRoot,\n `Failed to look up origin/${ref} (git ls-remote).`);\n const line = out.split('\\n').find((l: string): boolean => l.trim() !== '');\n if (line === undefined) return '';\n return line.split(/\\s+/)[0] ?? '';\n }\n\n /**\n * Fetch the named copy refs into `refs/remotes/origin/<ref>` so the local merges below run against\n * fresh state. A no-op for an empty list, and a hard failure otherwise: a resolve queued against\n * stale refs rehearses a composition CI will not build.\n */\n fetchCopies(repoRoot: string, refs: string[]): void {\n if (refs.length === 0) return;\n const specs = refs.map((r: string): string => `+refs/heads/${r}:refs/remotes/origin/${r}`);\n this.gitExec.runGitChecked(['-C', repoRoot, 'fetch', 'origin', ...specs],\n 'Failed to fetch the dev copy refs from origin');\n }\n\n /** How many commits `origin/<ref>` has that `HEAD` does not — the clobber signal. 0 = safe to publish. */\n commitsOnlyOnRemote(repoRoot: string, ref: string): number {\n const out = this.gitExec.gitQuery(['rev-list', '--count', `HEAD..refs/remotes/origin/${ref}`], repoRoot,\n `Failed to compare HEAD against origin/${ref} (git rev-list --count).`);\n const count = Number(out);\n return Number.isInteger(count) ? count : 0;\n }\n\n private refusal(headline: string, body: string): string {\n return '\\n' + SEP + `⛔ ${headline}\\n` + SEP + '\\n' + body + `\\nRun \\`${WP_PUSH_DEV} --help\\` for the full command.\\n`;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"dev-deploy-refs.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/dev-deploy-refs.ts"],"names":[],"mappings":"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","sourcesContent":["import { CliExitError, DevDeployConfig, loadAndValidate, WP_PUSH_DEV } from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\n\nimport { GitExec } from './git-exec';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\nconst MAIN = 'main';\nconst DETACHED = 'HEAD';\n\n/**\n * One live copy in the dev namespace: the remote ref and the sha it points at. Data-only per CLAUDE.md —\n * `--list` prints it and the clobber guard compares against `sha`, and neither should be re-parsing\n * `ls-remote` output.\n */\nexport class DevCopy {\n /** The full ref name WITHOUT `refs/heads/`, e.g. `dev-include/dean/ONE-2275`. */\n ref: string;\n sha: string;\n /** The feature branch this is a copy OF — `ref` with the namespace prefix stripped. */\n sourceBranch: string;\n\n constructor(ref: string, sha: string, sourceBranch: string) {\n this.ref = ref;\n this.sha = sha;\n this.sourceBranch = sourceBranch;\n }\n}\n\n/**\n * Ref derivation and preconditions for the dev-deploy flow — the half of `wp-push-dev` that decides WHICH\n * refs are in play, kept out of the commands so both `wp-push-dev` and `wp-finish-push-dev` answer those\n * questions identically.\n *\n * THE INVARIANT THIS FILE EXISTS TO PROTECT: the feature branch is the PR head, so it must never acquire\n * another developer's commits — landing that PR would ship their unreviewed work to production. Every\n * write this flow performs therefore targets either the `<namespace>/<feature>` copy or a local throwaway\n * branch, and NOTHING here ever moves `<feature>` itself. That is why a conflict resolution needs the copy\n * to live in: it is the only place the resolution can be recorded without contaminating the PR.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class DevDeployRefs {\n constructor(private readonly gitExec: GitExec) {}\n\n /** The `pr-gate.devDeploy` block — `dev-include` / `dev` unless the consumer overrode it. */\n config(repoRoot: string): DevDeployConfig {\n return loadAndValidate(repoRoot).prGate.devDeploy;\n }\n\n /** The checked-out branch name, or `'HEAD'` on a detached HEAD (which {@link assertPushable} refuses). */\n currentBranch(repoRoot: string): string {\n return this.gitExec.gitQuery(['rev-parse', '--abbrev-ref', 'HEAD'], repoRoot,\n 'Failed to read the current branch (git rev-parse --abbrev-ref HEAD).');\n }\n\n /**\n * Refuse every branch that must never be the SOURCE of a dev copy, before anything is fetched or\n * pushed.\n *\n * `main` and the composed dev branch are refused for the same reason from opposite ends: `main` is the\n * trunk (whatever is on it already deploys, so copying it is meaningless), and the dev branch is a\n * BUILD ARTIFACT that CI recomputes from `origin/main` on every run — copying it would publish a\n * composition of everyone's work under one developer's name, and it would be wiped on the next CI run\n * anyway. A branch already inside the namespace is refused because publishing a copy of a copy nests\n * the prefix (`dev-include/dev-include/…`) and nothing would ever clean it up.\n */\n assertPushable(cfg: DevDeployConfig, branch: string): void {\n if (branch === DETACHED || branch === '') {\n throw new CliExitError(2, this.refusal(\n 'Detached HEAD',\n 'There is no branch name to derive the dev copy from. Check out your feature branch first:\\n'\n + ' git checkout <your-feature-branch>\\n'));\n }\n if (branch === MAIN) {\n throw new CliExitError(2, this.refusal(\n `Refusing to publish a dev copy of \\`${MAIN}\\``,\n `\\`${MAIN}\\` is the trunk — whatever is on it already deploys, so a dev copy of it means nothing.\\n`\n + 'Check out the feature branch you actually want on the dev server and re-run.\\n'));\n }\n if (branch === cfg.devBranch) {\n throw new CliExitError(2, this.refusal(\n `Refusing to publish a dev copy of \\`${cfg.devBranch}\\``,\n `\\`${cfg.devBranch}\\` is a BUILD ARTIFACT, not a source branch: your CI resets it to origin/${MAIN},\\n`\n + `merges every \\`${cfg.copyRefGlob()}\\` ref into it, and force-pushes — every run. Copying it would\\n`\n + \"publish a composition of everybody's work under your branch's name, and the next CI run would\\n\"\n + 'wipe it. Check out your own feature branch and re-run.\\n'));\n }\n if (branch === cfg.branchNamespace || branch.startsWith(`${cfg.branchNamespace}/`)) {\n throw new CliExitError(2, this.refusal(\n `Refusing to publish a dev copy of a dev copy (\\`${branch}\\`)`,\n `\\`${cfg.branchNamespace}/\\` holds the disposable copies this command WRITES; you are standing on one.\\n`\n + `Publishing from here would nest the prefix (\\`${cfg.branchNamespace}/${branch}\\`) and nothing ever\\n`\n + 'cleans that up. Check out the real feature branch and re-run.\\n'));\n }\n }\n\n /**\n * Every live copy in the namespace, sorted by ref name.\n *\n * The sort is not cosmetic: `--resolve` with no argument queues these in exactly this order, and the\n * consumer's CI composes `dev` in the same order, so what a developer resolves locally is the\n * composition CI will actually build. A different order can produce a different (still valid) merge\n * result, which would make the local resolution a guess about CI rather than a rehearsal of it.\n */\n liveCopies(repoRoot: string, cfg: DevDeployConfig): DevCopy[] {\n const out = this.gitExec.gitQuery(['ls-remote', '--heads', 'origin', cfg.copyRefGlob()], repoRoot,\n `Failed to list the dev copies (git ls-remote --heads origin '${cfg.copyRefGlob()}').`);\n const copies: DevCopy[] = [];\n for (const line of out.split('\\n')) {\n if (line.trim() === '') continue;\n const fields = line.split(/\\s+/);\n const sha = fields[0];\n const ref = fields[1];\n if (sha === undefined || ref === undefined || !ref.startsWith('refs/heads/')) continue;\n const shortRef = ref.slice('refs/heads/'.length);\n copies.push(new DevCopy(shortRef, sha, shortRef.slice(cfg.branchNamespace.length + 1)));\n }\n return copies.sort((a: DevCopy, b: DevCopy): number => (a.ref < b.ref ? -1 : a.ref > b.ref ? 1 : 0));\n }\n\n /**\n * The sha of a local ref (default HEAD).\n *\n * Read AFTER a push so the watch hints quote the sha that is provably published. Quoting the copy\n * REF instead would be a moving target — it advances the next time anybody publishes — and the whole\n * value of the \"did my code land in dev\" check is that it is a claim about one specific commit.\n */\n headSha(repoRoot: string, ref = 'HEAD'): string {\n return this.gitExec.gitQuery(['-C', repoRoot, 'rev-parse', ref], repoRoot,\n `Failed to read the sha of ${ref} (git rev-parse).`);\n }\n\n /** The remote sha of one copy, or '' when that copy does not exist yet. */\n remoteSha(repoRoot: string, ref: string): string {\n const out = this.gitExec.gitQuery(['ls-remote', '--heads', 'origin', ref], repoRoot,\n `Failed to look up origin/${ref} (git ls-remote).`);\n const line = out.split('\\n').find((l: string): boolean => l.trim() !== '');\n if (line === undefined) return '';\n return line.split(/\\s+/)[0] ?? '';\n }\n\n /**\n * Fetch the named copy refs into `refs/remotes/origin/<ref>` so the local merges below run against\n * fresh state. A no-op for an empty list, and a hard failure otherwise: a resolve queued against\n * stale refs rehearses a composition CI will not build.\n */\n fetchCopies(repoRoot: string, refs: string[]): void {\n if (refs.length === 0) return;\n const specs = refs.map((r: string): string => `+refs/heads/${r}:refs/remotes/origin/${r}`);\n this.gitExec.runGitChecked(['-C', repoRoot, 'fetch', 'origin', ...specs],\n 'Failed to fetch the dev copy refs from origin');\n }\n\n /** How many commits `origin/<ref>` has that `HEAD` does not — the clobber signal. 0 = safe to publish. */\n commitsOnlyOnRemote(repoRoot: string, ref: string): number {\n const out = this.gitExec.gitQuery(['rev-list', '--count', `HEAD..refs/remotes/origin/${ref}`], repoRoot,\n `Failed to compare HEAD against origin/${ref} (git rev-list --count).`);\n const count = Number(out);\n return Number.isInteger(count) ? count : 0;\n }\n\n private refusal(headline: string, body: string): string {\n return '\\n' + SEP + `⛔ ${headline}\\n` + SEP + '\\n' + body + `\\nRun \\`${WP_PUSH_DEV} --help\\` for the full command.\\n`;\n }\n}\n"]}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { DevDeployConfig } from '@webpieces/rules-config';
|
|
2
|
+
/**
|
|
3
|
+
* The "now go watch it deploy" block printed after a copy is published.
|
|
4
|
+
*
|
|
5
|
+
* WHY THIS IS TEXT AND NOT A FEATURE: webpieces cannot know the consumer's CD. The composed branch might
|
|
6
|
+
* be built by GitHub Actions, by Jenkins, by a cron job on a box — so polling for "the run" would mean
|
|
7
|
+
* either a config key naming their workflow, or a heuristic that is wrong for somebody. Handing the AI
|
|
8
|
+
* the exact commands costs nothing, degrades gracefully (an empty `gh run list` is itself an answer), and
|
|
9
|
+
* keeps webpieces out of the business of modelling someone else's pipeline.
|
|
10
|
+
*
|
|
11
|
+
* The commands are pre-substituted with the real sha and refs — an AI given a template with blanks to
|
|
12
|
+
* fill in gets one of them wrong eventually, and a wrong `git merge-base` argument answers the question
|
|
13
|
+
* confidently and incorrectly.
|
|
14
|
+
*
|
|
15
|
+
* All of them are READ-ONLY and none is blocked: `git fetch`/`merge-base` are queries, and
|
|
16
|
+
* `gh run list|view|watch` is on the guards' allowed list.
|
|
17
|
+
*/
|
|
18
|
+
export declare class DevDeployWatchHints {
|
|
19
|
+
/**
|
|
20
|
+
* @param sha the commit that was just published — the ONLY thing that makes step 1 a proof rather
|
|
21
|
+
* than a guess, since `<copyRef>` itself moves the next time anyone publishes.
|
|
22
|
+
*/
|
|
23
|
+
render(cfg: DevDeployConfig, copyRef: string, sha: string): string;
|
|
24
|
+
/**
|
|
25
|
+
* Step 1 — pure git, works with ANY CI, and is the only claim provably about YOUR code.
|
|
26
|
+
*
|
|
27
|
+
* `<devBranch>` is recomposed from `origin/main` plus every copy on each CI run, so your published
|
|
28
|
+
* sha is an ancestor of it if and only if the composition included you.
|
|
29
|
+
*/
|
|
30
|
+
private landedSection;
|
|
31
|
+
/** Step 2 — GitHub Actions, if that is where this repo's dev deploy lives. Empty output is an answer. */
|
|
32
|
+
private runSection;
|
|
33
|
+
/** The trap worth one sentence: a green run on a SHARED branch is not a statement about your code. */
|
|
34
|
+
private orderingWarning;
|
|
35
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DevDeployWatchHints = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const inversify_1 = require("inversify");
|
|
6
|
+
const SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n';
|
|
7
|
+
/**
|
|
8
|
+
* The "now go watch it deploy" block printed after a copy is published.
|
|
9
|
+
*
|
|
10
|
+
* WHY THIS IS TEXT AND NOT A FEATURE: webpieces cannot know the consumer's CD. The composed branch might
|
|
11
|
+
* be built by GitHub Actions, by Jenkins, by a cron job on a box — so polling for "the run" would mean
|
|
12
|
+
* either a config key naming their workflow, or a heuristic that is wrong for somebody. Handing the AI
|
|
13
|
+
* the exact commands costs nothing, degrades gracefully (an empty `gh run list` is itself an answer), and
|
|
14
|
+
* keeps webpieces out of the business of modelling someone else's pipeline.
|
|
15
|
+
*
|
|
16
|
+
* The commands are pre-substituted with the real sha and refs — an AI given a template with blanks to
|
|
17
|
+
* fill in gets one of them wrong eventually, and a wrong `git merge-base` argument answers the question
|
|
18
|
+
* confidently and incorrectly.
|
|
19
|
+
*
|
|
20
|
+
* All of them are READ-ONLY and none is blocked: `git fetch`/`merge-base` are queries, and
|
|
21
|
+
* `gh run list|view|watch` is on the guards' allowed list.
|
|
22
|
+
*/
|
|
23
|
+
let DevDeployWatchHints = class DevDeployWatchHints {
|
|
24
|
+
/**
|
|
25
|
+
* @param sha the commit that was just published — the ONLY thing that makes step 1 a proof rather
|
|
26
|
+
* than a guess, since `<copyRef>` itself moves the next time anyone publishes.
|
|
27
|
+
*/
|
|
28
|
+
render(cfg, copyRef, sha) {
|
|
29
|
+
return '\n' + SEP + '👀 Is it on the dev server yet? — two questions, in this order\n' + SEP + '\n'
|
|
30
|
+
+ this.landedSection(cfg, sha)
|
|
31
|
+
+ '\n'
|
|
32
|
+
+ this.runSection(cfg, copyRef)
|
|
33
|
+
+ '\n'
|
|
34
|
+
+ this.orderingWarning(cfg);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Step 1 — pure git, works with ANY CI, and is the only claim provably about YOUR code.
|
|
38
|
+
*
|
|
39
|
+
* `<devBranch>` is recomposed from `origin/main` plus every copy on each CI run, so your published
|
|
40
|
+
* sha is an ancestor of it if and only if the composition included you.
|
|
41
|
+
*/
|
|
42
|
+
landedSection(cfg, sha) {
|
|
43
|
+
return `1. Did YOUR code land in \`${cfg.devBranch}\`? (pure git — works whatever your CI is)\n`
|
|
44
|
+
+ ` git fetch origin ${cfg.devBranch}\n`
|
|
45
|
+
+ ` git merge-base --is-ancestor ${sha} origin/${cfg.devBranch} && echo IN || echo "not yet"\n`
|
|
46
|
+
+ ` \`${cfg.devBranch}\` is rebuilt from origin/main + every \`${cfg.copyRefGlob()}\` ref on each run, so\n`
|
|
47
|
+
+ ' ancestry IS inclusion. Re-run after the next composition if it says "not yet".\n';
|
|
48
|
+
}
|
|
49
|
+
/** Step 2 — GitHub Actions, if that is where this repo's dev deploy lives. Empty output is an answer. */
|
|
50
|
+
runSection(cfg, copyRef) {
|
|
51
|
+
const rows = [
|
|
52
|
+
[`gh run list --branch ${copyRef} --limit 5`, 'the run your push triggered, if any'],
|
|
53
|
+
[`gh run list --branch ${cfg.devBranch} --limit 5`, 'the composition / deploy run'],
|
|
54
|
+
['gh run watch <run-id>', 'follow one to completion'],
|
|
55
|
+
['gh run view <run-id> --log-failed', 'read why it failed'],
|
|
56
|
+
];
|
|
57
|
+
// Aligned off the longest command, because the ref names are consumer-configurable and any
|
|
58
|
+
// hand-counted padding would go crooked the moment somebody renames the namespace.
|
|
59
|
+
const width = Math.max(...rows.map((row) => row[0].length));
|
|
60
|
+
return '2. Is the CI/CD run finished? (GitHub Actions — all read-only, none of it is blocked)\n'
|
|
61
|
+
+ rows.map((row) => ` ${row[0].padEnd(width)} ← ${row[1]}\n`).join('')
|
|
62
|
+
+ ' Empty output is not a bug: it means this repo deploys from somewhere other than Actions.\n'
|
|
63
|
+
+ ' Ask the human where the dev deploy runs, and do not guess.\n';
|
|
64
|
+
}
|
|
65
|
+
/** The trap worth one sentence: a green run on a SHARED branch is not a statement about your code. */
|
|
66
|
+
orderingWarning(cfg) {
|
|
67
|
+
return `A green run on \`${cfg.devBranch}\` is NOT proof your change deployed — that branch carries everyone's\n`
|
|
68
|
+
+ 'copies, and the composition may have excluded yours. Question 1 is the one that answers for YOUR\n'
|
|
69
|
+
+ 'code; question 2 only tells you whether the machinery finished.\n';
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
exports.DevDeployWatchHints = DevDeployWatchHints;
|
|
73
|
+
exports.DevDeployWatchHints = DevDeployWatchHints = tslib_1.__decorate([
|
|
74
|
+
(0, inversify_1.injectable)(inversify_1.bindingScopeValues.Singleton)
|
|
75
|
+
], DevDeployWatchHints);
|
|
76
|
+
//# sourceMappingURL=dev-deploy-watch-hints.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dev-deploy-watch-hints.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/dev-deploy-watch-hints.ts"],"names":[],"mappings":";;;;AACA,yCAA2D;AAE3D,MAAM,GAAG,GAAG,0DAA0D,CAAC;AAEvE;;;;;;;;;;;;;;;GAeG;AAEI,IAAM,mBAAmB,GAAzB,MAAM,mBAAmB;IAC5B;;;OAGG;IACH,MAAM,CAAC,GAAoB,EAAE,OAAe,EAAE,GAAW;QACrD,OAAO,IAAI,GAAG,GAAG,GAAG,kEAAkE,GAAG,GAAG,GAAG,IAAI;cAC7F,IAAI,CAAC,aAAa,CAAC,GAAG,EAAE,GAAG,CAAC;cAC5B,IAAI;cACJ,IAAI,CAAC,UAAU,CAAC,GAAG,EAAE,OAAO,CAAC;cAC7B,IAAI;cACJ,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,CAAC;IACpC,CAAC;IAED;;;;;OAKG;IACK,aAAa,CAAC,GAAoB,EAAE,GAAW;QACnD,OAAO,8BAA8B,GAAG,CAAC,SAAS,+CAA+C;cAC3F,yBAAyB,GAAG,CAAC,SAAS,IAAI;cAC1C,qCAAqC,GAAG,WAAW,GAAG,CAAC,SAAS,iCAAiC;cACjG,QAAQ,GAAG,CAAC,SAAS,4CAA4C,GAAG,CAAC,WAAW,EAAE,0BAA0B;cAC5G,qFAAqF,CAAC;IAChG,CAAC;IAED,yGAAyG;IACjG,UAAU,CAAC,GAAoB,EAAE,OAAe;QACpD,MAAM,IAAI,GAAG;YACT,CAAC,wBAAwB,OAAO,YAAY,EAAE,qCAAqC,CAAC;YACpF,CAAC,wBAAwB,GAAG,CAAC,SAAS,YAAY,EAAE,8BAA8B,CAAC;YACnF,CAAC,uBAAuB,EAAE,0BAA0B,CAAC;YACrD,CAAC,mCAAmC,EAAE,oBAAoB,CAAC;SAC9D,CAAC;QACF,2FAA2F;QAC3F,mFAAmF;QACnF,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,GAAa,EAAU,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC;QAC9E,OAAO,0FAA0F;cAC3F,IAAI,CAAC,GAAG,CAAC,CAAC,GAAa,EAAU,EAAE,CAAC,QAAQ,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,QAAQ,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC;cAC5F,+FAA+F;cAC/F,iEAAiE,CAAC;IAC5E,CAAC;IAED,sGAAsG;IAC9F,eAAe,CAAC,GAAoB;QACxC,OAAO,oBAAoB,GAAG,CAAC,SAAS,yEAAyE;cAC3G,oGAAoG;cACpG,mEAAmE,CAAC;IAC9E,CAAC;CACJ,CAAA;AAnDY,kDAAmB;8BAAnB,mBAAmB;IAD/B,IAAA,sBAAU,EAAC,8BAAkB,CAAC,SAAS,CAAC;GAC5B,mBAAmB,CAmD/B","sourcesContent":["import { DevDeployConfig } from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n/**\n * The \"now go watch it deploy\" block printed after a copy is published.\n *\n * WHY THIS IS TEXT AND NOT A FEATURE: webpieces cannot know the consumer's CD. The composed branch might\n * be built by GitHub Actions, by Jenkins, by a cron job on a box — so polling for \"the run\" would mean\n * either a config key naming their workflow, or a heuristic that is wrong for somebody. Handing the AI\n * the exact commands costs nothing, degrades gracefully (an empty `gh run list` is itself an answer), and\n * keeps webpieces out of the business of modelling someone else's pipeline.\n *\n * The commands are pre-substituted with the real sha and refs — an AI given a template with blanks to\n * fill in gets one of them wrong eventually, and a wrong `git merge-base` argument answers the question\n * confidently and incorrectly.\n *\n * All of them are READ-ONLY and none is blocked: `git fetch`/`merge-base` are queries, and\n * `gh run list|view|watch` is on the guards' allowed list.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class DevDeployWatchHints {\n /**\n * @param sha the commit that was just published — the ONLY thing that makes step 1 a proof rather\n * than a guess, since `<copyRef>` itself moves the next time anyone publishes.\n */\n render(cfg: DevDeployConfig, copyRef: string, sha: string): string {\n return '\\n' + SEP + '👀 Is it on the dev server yet? — two questions, in this order\\n' + SEP + '\\n'\n + this.landedSection(cfg, sha)\n + '\\n'\n + this.runSection(cfg, copyRef)\n + '\\n'\n + this.orderingWarning(cfg);\n }\n\n /**\n * Step 1 — pure git, works with ANY CI, and is the only claim provably about YOUR code.\n *\n * `<devBranch>` is recomposed from `origin/main` plus every copy on each CI run, so your published\n * sha is an ancestor of it if and only if the composition included you.\n */\n private landedSection(cfg: DevDeployConfig, sha: string): string {\n return `1. Did YOUR code land in \\`${cfg.devBranch}\\`? (pure git — works whatever your CI is)\\n`\n + ` git fetch origin ${cfg.devBranch}\\n`\n + ` git merge-base --is-ancestor ${sha} origin/${cfg.devBranch} && echo IN || echo \"not yet\"\\n`\n + ` \\`${cfg.devBranch}\\` is rebuilt from origin/main + every \\`${cfg.copyRefGlob()}\\` ref on each run, so\\n`\n + ' ancestry IS inclusion. Re-run after the next composition if it says \"not yet\".\\n';\n }\n\n /** Step 2 — GitHub Actions, if that is where this repo's dev deploy lives. Empty output is an answer. */\n private runSection(cfg: DevDeployConfig, copyRef: string): string {\n const rows = [\n [`gh run list --branch ${copyRef} --limit 5`, 'the run your push triggered, if any'],\n [`gh run list --branch ${cfg.devBranch} --limit 5`, 'the composition / deploy run'],\n ['gh run watch <run-id>', 'follow one to completion'],\n ['gh run view <run-id> --log-failed', 'read why it failed'],\n ];\n // Aligned off the longest command, because the ref names are consumer-configurable and any\n // hand-counted padding would go crooked the moment somebody renames the namespace.\n const width = Math.max(...rows.map((row: string[]): number => row[0].length));\n return '2. Is the CI/CD run finished? (GitHub Actions — all read-only, none of it is blocked)\\n'\n + rows.map((row: string[]): string => ` ${row[0].padEnd(width)} ← ${row[1]}\\n`).join('')\n + ' Empty output is not a bug: it means this repo deploys from somewhere other than Actions.\\n'\n + ' Ask the human where the dev deploy runs, and do not guess.\\n';\n }\n\n /** The trap worth one sentence: a green run on a SHARED branch is not a statement about your code. */\n private orderingWarning(cfg: DevDeployConfig): string {\n return `A green run on \\`${cfg.devBranch}\\` is NOT proof your change deployed — that branch carries everyone's\\n`\n + 'copies, and the composition may have excluded yours. Question 1 is the one that answers for YOUR\\n'\n + 'code; question 2 only tells you whether the machinery finished.\\n';\n }\n}\n"]}
|
|
@@ -2,6 +2,7 @@ import { DevDeployConfig } from '@webpieces/rules-config';
|
|
|
2
2
|
import { GitExec } from './git-exec';
|
|
3
3
|
import { DevDeployRefs } from './dev-deploy-refs';
|
|
4
4
|
import { PushDevState, PushDevStateStore } from './push-dev-state';
|
|
5
|
+
import { DevDeployWatchHints } from './dev-deploy-watch-hints';
|
|
5
6
|
/**
|
|
6
7
|
* The queue-draining engine behind `wp-push-dev --resolve` and `wp-finish-push-dev`.
|
|
7
8
|
*
|
|
@@ -16,7 +17,8 @@ export declare class DevResolveRunner {
|
|
|
16
17
|
private readonly gitExec;
|
|
17
18
|
private readonly refs;
|
|
18
19
|
private readonly store;
|
|
19
|
-
|
|
20
|
+
private readonly watchHints;
|
|
21
|
+
constructor(gitExec: GitExec, refs: DevDeployRefs, store: PushDevStateStore, watchHints: DevDeployWatchHints);
|
|
20
22
|
/**
|
|
21
23
|
* Start a resolve: park on a throwaway branch at the current copy, then drain the queue.
|
|
22
24
|
*
|
|
@@ -7,6 +7,7 @@ const inversify_1 = require("inversify");
|
|
|
7
7
|
const git_exec_1 = require("./git-exec");
|
|
8
8
|
const dev_deploy_refs_1 = require("./dev-deploy-refs");
|
|
9
9
|
const push_dev_state_1 = require("./push-dev-state");
|
|
10
|
+
const dev_deploy_watch_hints_1 = require("./dev-deploy-watch-hints");
|
|
10
11
|
const SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n';
|
|
11
12
|
/**
|
|
12
13
|
* The queue-draining engine behind `wp-push-dev --resolve` and `wp-finish-push-dev`.
|
|
@@ -22,10 +23,12 @@ let DevResolveRunner = class DevResolveRunner {
|
|
|
22
23
|
gitExec;
|
|
23
24
|
refs;
|
|
24
25
|
store;
|
|
25
|
-
|
|
26
|
+
watchHints;
|
|
27
|
+
constructor(gitExec, refs, store, watchHints) {
|
|
26
28
|
this.gitExec = gitExec;
|
|
27
29
|
this.refs = refs;
|
|
28
30
|
this.store = store;
|
|
31
|
+
this.watchHints = watchHints;
|
|
29
32
|
}
|
|
30
33
|
/**
|
|
31
34
|
* Start a resolve: park on a throwaway branch at the current copy, then drain the queue.
|
|
@@ -125,13 +128,17 @@ let DevResolveRunner = class DevResolveRunner {
|
|
|
125
128
|
/** Queue empty: force-push the composed result over the copy, then restore the original checkout. */
|
|
126
129
|
publish(repoRoot, state) {
|
|
127
130
|
this.gitExec.runGitChecked(['-C', repoRoot, 'push', '--force', 'origin', `${state.tmpBranch}:refs/heads/${state.targetRef}`], `Failed to publish the dev copy ${state.targetRef}`);
|
|
131
|
+
// Captured BEFORE the tmp branch is deleted — it is the composed sha the watch hints are about,
|
|
132
|
+
// and after the checkout below HEAD is the feature branch, which is a different commit entirely.
|
|
133
|
+
const published = this.refs.headSha(repoRoot, state.tmpBranch);
|
|
128
134
|
this.gitExec.runGitChecked(['-C', repoRoot, 'checkout', state.originalBranch], `Failed to return to ${state.originalBranch}`);
|
|
129
135
|
this.gitExec.tryGit(['-C', repoRoot, 'branch', '-D', state.tmpBranch], repoRoot);
|
|
130
136
|
this.store.clear(repoRoot);
|
|
131
137
|
process.stdout.write('\n' + SEP + `✅ Published ${state.targetRef} with the resolutions\n` + SEP + '\n'
|
|
132
138
|
+ `You are back on \`${state.originalBranch}\`, which was never modified — verify with:\n`
|
|
133
139
|
+ ` git log --oneline ${state.originalBranch}\n`
|
|
134
|
-
+ 'Your CI will pick the copy up on its next composition run.\n'
|
|
140
|
+
+ 'Your CI will pick the copy up on its next composition run.\n'
|
|
141
|
+
+ this.watchHints.render(this.refs.config(repoRoot), state.targetRef, published));
|
|
135
142
|
}
|
|
136
143
|
/** Conflicted (unmerged-in-index) paths. Empty once every one has been resolved and `git add`ed. */
|
|
137
144
|
unmergedPaths(repoRoot) {
|
|
@@ -175,6 +182,7 @@ exports.DevResolveRunner = DevResolveRunner = tslib_1.__decorate([
|
|
|
175
182
|
(0, inversify_1.injectable)(inversify_1.bindingScopeValues.Singleton),
|
|
176
183
|
tslib_1.__metadata("design:paramtypes", [git_exec_1.GitExec,
|
|
177
184
|
dev_deploy_refs_1.DevDeployRefs,
|
|
178
|
-
push_dev_state_1.PushDevStateStore
|
|
185
|
+
push_dev_state_1.PushDevStateStore,
|
|
186
|
+
dev_deploy_watch_hints_1.DevDeployWatchHints])
|
|
179
187
|
], DevResolveRunner);
|
|
180
188
|
//# sourceMappingURL=dev-resolve-runner.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dev-resolve-runner.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/dev-resolve-runner.ts"],"names":[],"mappings":"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","sourcesContent":["import { CliExitError, DevDeployConfig, WP_FINISH_PUSH_DEV } from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\n\nimport { GitExec } from './git-exec';\nimport { DevDeployRefs } from './dev-deploy-refs';\nimport { PushDevState, PushDevStateStore } from './push-dev-state';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n/**\n * The queue-draining engine behind `wp-push-dev --resolve` and `wp-finish-push-dev`.\n *\n * ONE class owns every git WRITE in the resolve flow, and that is a hard requirement rather than tidiness:\n * `redirect-how-to-merge-main` blocks `git merge` in every form INCLUDING `--continue`, and\n * `pr-creation-or-push-guard` blocks every `git push`. So an AI cannot be told \"now run `git commit`\" or\n * \"`git merge --continue`\" — those instructions are unrunnable by the only party reading them. The command\n * owns the merges, the commit and the push (as child processes the hooks never see); the AI's entire job is\n * editing the conflicted files and `git add`-ing them, neither of which is blocked.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class DevResolveRunner {\n constructor(\n private readonly gitExec: GitExec,\n private readonly refs: DevDeployRefs,\n private readonly store: PushDevStateStore,\n ) {}\n\n /**\n * Start a resolve: park on a throwaway branch at the current copy, then drain the queue.\n *\n * The checkout is `-B <tmp> <base>` and NEVER the feature branch, which is the cardinal constraint made\n * mechanical — every merge below lands on a branch that is deleted at the end, so no other developer's\n * commits can reach the PR head even if the resolve is abandoned halfway.\n *\n * `base` differs between the two callers and the difference IS the semantics. `--resolve` starts at the\n * published copy and merges the other copies in (compose what CI will build). `--rebase-resolution`\n * starts at the LOCAL HEAD and merges the published copy in (replay an existing resolution on top of\n * the new commits, instead of force-pushing over it).\n *\n * `fetchRefs` is passed separately from the queue because `--rebase-resolution` merges a ref that is\n * also its push destination, so \"everything to fetch\" and \"everything to merge\" are not the same list.\n */\n start(repoRoot: string, state: PushDevState, base: string, fetchRefs: string[]): void {\n this.refs.fetchCopies(repoRoot, fetchRefs);\n this.store.write(repoRoot, state);\n this.gitExec.runGitChecked(\n ['-C', repoRoot, 'checkout', '-B', state.tmpBranch, base],\n `Failed to create the throwaway resolve branch ${state.tmpBranch}`);\n this.drain(repoRoot, state);\n }\n\n /**\n * Resume after the human/AI has resolved the halted merge: commit it, then keep draining.\n *\n * REFETCHES the remaining queue first. A resolution session can run long, and queuing against refs\n * fetched an hour ago rehearses a composition CI will not build — the one freshness gap the sibling\n * update flow is still known to have (only its stage ① ever fetches).\n */\n resume(repoRoot: string, state: PushDevState): void {\n this.assertNoUnmergedPaths(repoRoot, state);\n this.gitExec.assertNoUntracked(repoRoot);\n this.gitExec.runGitChecked(['-C', repoRoot, 'add', '-u'], 'Failed to stage the resolved files (git add -u)');\n this.gitExec.runGitChecked(['-C', repoRoot, 'commit', '--no-edit'],\n `Failed to commit the resolution of ${state.current}`);\n process.stdout.write(`✅ Committed the resolution of ${state.current}.\\n`);\n state.current = '';\n this.refs.fetchCopies(repoRoot, state.queue);\n this.drain(repoRoot, state);\n }\n\n /**\n * Throw the whole resolve away: undo any halted merge, go back to the original branch, delete the\n * throwaway branch and the state file. Nothing published, nothing on the feature branch.\n */\n abort(repoRoot: string, state: PushDevState): void {\n // Allowed to fail: there is no merge to abort when the resolve halted between merges, and that is\n // not an error condition — the rest of the teardown must still run.\n this.gitExec.tryGit(['-C', repoRoot, 'merge', '--abort'], repoRoot);\n this.gitExec.runGitChecked(['-C', repoRoot, 'checkout', state.originalBranch],\n `Failed to return to ${state.originalBranch}`);\n this.gitExec.tryGit(['-C', repoRoot, 'branch', '-D', state.tmpBranch], repoRoot);\n this.store.clear(repoRoot);\n process.stdout.write(\n '\\n' + SEP + '🛑 Dev-deploy resolve aborted\\n' + SEP + '\\n'\n + `You are back on \\`${state.originalBranch}\\`; \\`${state.tmpBranch}\\` and the state file are gone.\\n`\n + `The remote copy \\`${state.targetRef}\\` is untouched, and so is your feature branch.\\n`);\n }\n\n /**\n * Merge queued refs until one conflicts (HALT, hand the tree over) or the queue empties (PUBLISH).\n *\n * A LATER ref can conflict too, which is why this is a loop resumed from a state file rather than a\n * single pass: `wp-finish-push-dev` can legitimately stop again on the next ref in the queue.\n */\n private drain(repoRoot: string, state: PushDevState): void {\n while (state.queue.length > 0) {\n const ref = state.queue[0];\n state.queue = state.queue.slice(1);\n const outcome = this.gitExec.tryGit(\n ['-C', repoRoot, 'merge', '--no-ff', '--no-edit', '-m',\n `Dev-deploy: merge ${ref} into ${state.targetRef}`, `refs/remotes/origin/${ref}`],\n repoRoot);\n if (outcome.ok) {\n process.stdout.write(` ✓ merged ${ref}\\n`);\n continue;\n }\n state.current = ref;\n this.store.write(repoRoot, state);\n this.halt(repoRoot, state);\n return;\n }\n this.publish(repoRoot, state);\n }\n\n /** Stop with the conflicted files listed and the ONE command that continues. Exit 2 = \"your turn\". */\n private halt(repoRoot: string, state: PushDevState): never {\n const conflicted = this.unmergedPaths(repoRoot);\n const remaining = state.queue.length > 0\n ? `\\nStill queued after this one: ${state.queue.join(', ')}\\n`\n + `\\`${WP_FINISH_PUSH_DEV}\\` resumes the queue, so it may stop again on a later ref.\\n`\n : '';\n throw new CliExitError(2,\n '\\n' + SEP + `⚠️ CONFLICT merging ${state.current}\\n` + SEP + '\\n'\n + `You are on the throwaway branch \\`${state.tmpBranch}\\` — NOT \\`${state.originalBranch}\\`. Nothing here\\n`\n + 'can reach your feature branch or your PR, which is exactly why the resolution happens over here.\\n\\n'\n + 'Resolve these files (edit until no conflict markers remain, then `git add` each one):\\n'\n + conflicted.map((f: string): string => ` ${f}`).join('\\n') + '\\n'\n + remaining\n + '\\nThen run ONE of:\\n'\n + ` ${WP_FINISH_PUSH_DEV} ← commit, resume the queue, publish the copy\\n`\n + ` ${WP_FINISH_PUSH_DEV} --abort ← throw it away and go back to \\`${state.originalBranch}\\`\\n\\n`\n + 'Do NOT run `git merge --continue`, `git commit` or `git push` yourself — they are blocked, and\\n'\n + 'the finish command above does all three for you.\\n');\n }\n\n /** Queue empty: force-push the composed result over the copy, then restore the original checkout. */\n private publish(repoRoot: string, state: PushDevState): void {\n this.gitExec.runGitChecked(\n ['-C', repoRoot, 'push', '--force', 'origin', `${state.tmpBranch}:refs/heads/${state.targetRef}`],\n `Failed to publish the dev copy ${state.targetRef}`);\n this.gitExec.runGitChecked(['-C', repoRoot, 'checkout', state.originalBranch],\n `Failed to return to ${state.originalBranch}`);\n this.gitExec.tryGit(['-C', repoRoot, 'branch', '-D', state.tmpBranch], repoRoot);\n this.store.clear(repoRoot);\n process.stdout.write(\n '\\n' + SEP + `✅ Published ${state.targetRef} with the resolutions\\n` + SEP + '\\n'\n + `You are back on \\`${state.originalBranch}\\`, which was never modified — verify with:\\n`\n + ` git log --oneline ${state.originalBranch}\\n`\n + 'Your CI will pick the copy up on its next composition run.\\n');\n }\n\n /** Conflicted (unmerged-in-index) paths. Empty once every one has been resolved and `git add`ed. */\n private unmergedPaths(repoRoot: string): string[] {\n const out = this.gitExec.gitQuery(['-C', repoRoot, 'diff', '--name-only', '--diff-filter=U'], repoRoot,\n 'Failed to list the conflicted files (git diff --name-only --diff-filter=U).');\n return out.split('\\n').filter((l: string): boolean => l.trim() !== '');\n }\n\n // The gate on `wp-finish-push-dev`: refuse (and re-print the list) while anything is still unmerged.\n private assertNoUnmergedPaths(repoRoot: string, state: PushDevState): void {\n const conflicted = this.unmergedPaths(repoRoot);\n if (conflicted.length === 0) return;\n throw new CliExitError(2,\n '\\n' + SEP + '⛔ Conflicts are NOT resolved yet\\n' + SEP + '\\n'\n + `${WP_FINISH_PUSH_DEV} commits the resolution — it cannot write one for you. These files are still\\n`\n + 'unmerged (edit until no conflict markers remain, then `git add` each one):\\n'\n + conflicted.map((f: string): string => ` ${f}`).join('\\n') + '\\n\\n'\n + `Merging: ${state.current === '' ? '(none)' : state.current}\\n`\n + `Give up instead with: ${WP_FINISH_PUSH_DEV} --abort\\n`);\n }\n\n /**\n * The name of the throwaway branch for a resolve of `branch`.\n *\n * Suffixed rather than prefixed, matching the existing `<feature>Squash` / `<feature>PreMerge<n>`\n * convention — and deliberately NOT inside the dev namespace, which holds REMOTE copies only.\n */\n tmpBranchFor(branch: string): string {\n return `${branch}DevResolve`;\n }\n\n /** The banner `--resolve` prints before the first merge, so the queue is visible before it halts. */\n announce(cfg: DevDeployConfig, state: PushDevState): string {\n return '\\n' + SEP + `🔀 Resolving the dev composition for ${state.targetRef}\\n` + SEP + '\\n'\n + `Merging ${String(state.queue.length)} other cop${state.queue.length === 1 ? 'y' : 'ies'} from `\n + `\\`${cfg.copyRefGlob()}\\`, in the order your CI composes\\n\\`${cfg.devBranch}\\`, onto the throwaway branch `\n + `\\`${state.tmpBranch}\\`:\\n`\n + state.queue.map((r: string): string => ` ${r}`).join('\\n') + '\\n\\n'\n + `Your feature branch \\`${state.originalBranch}\\` is not touched by any of this.\\n`\n + `A clean run publishes the copy itself — ${WP_FINISH_PUSH_DEV} is only needed if a merge stops.\\n\\n`;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"dev-resolve-runner.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/dev-resolve-runner.ts"],"names":[],"mappings":"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","sourcesContent":["import { CliExitError, DevDeployConfig, WP_FINISH_PUSH_DEV } from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\n\nimport { GitExec } from './git-exec';\nimport { DevDeployRefs } from './dev-deploy-refs';\nimport { PushDevState, PushDevStateStore } from './push-dev-state';\nimport { DevDeployWatchHints } from './dev-deploy-watch-hints';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n/**\n * The queue-draining engine behind `wp-push-dev --resolve` and `wp-finish-push-dev`.\n *\n * ONE class owns every git WRITE in the resolve flow, and that is a hard requirement rather than tidiness:\n * `redirect-how-to-merge-main` blocks `git merge` in every form INCLUDING `--continue`, and\n * `pr-creation-or-push-guard` blocks every `git push`. So an AI cannot be told \"now run `git commit`\" or\n * \"`git merge --continue`\" — those instructions are unrunnable by the only party reading them. The command\n * owns the merges, the commit and the push (as child processes the hooks never see); the AI's entire job is\n * editing the conflicted files and `git add`-ing them, neither of which is blocked.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class DevResolveRunner {\n constructor(\n private readonly gitExec: GitExec,\n private readonly refs: DevDeployRefs,\n private readonly store: PushDevStateStore,\n private readonly watchHints: DevDeployWatchHints,\n ) {}\n\n /**\n * Start a resolve: park on a throwaway branch at the current copy, then drain the queue.\n *\n * The checkout is `-B <tmp> <base>` and NEVER the feature branch, which is the cardinal constraint made\n * mechanical — every merge below lands on a branch that is deleted at the end, so no other developer's\n * commits can reach the PR head even if the resolve is abandoned halfway.\n *\n * `base` differs between the two callers and the difference IS the semantics. `--resolve` starts at the\n * published copy and merges the other copies in (compose what CI will build). `--rebase-resolution`\n * starts at the LOCAL HEAD and merges the published copy in (replay an existing resolution on top of\n * the new commits, instead of force-pushing over it).\n *\n * `fetchRefs` is passed separately from the queue because `--rebase-resolution` merges a ref that is\n * also its push destination, so \"everything to fetch\" and \"everything to merge\" are not the same list.\n */\n start(repoRoot: string, state: PushDevState, base: string, fetchRefs: string[]): void {\n this.refs.fetchCopies(repoRoot, fetchRefs);\n this.store.write(repoRoot, state);\n this.gitExec.runGitChecked(\n ['-C', repoRoot, 'checkout', '-B', state.tmpBranch, base],\n `Failed to create the throwaway resolve branch ${state.tmpBranch}`);\n this.drain(repoRoot, state);\n }\n\n /**\n * Resume after the human/AI has resolved the halted merge: commit it, then keep draining.\n *\n * REFETCHES the remaining queue first. A resolution session can run long, and queuing against refs\n * fetched an hour ago rehearses a composition CI will not build — the one freshness gap the sibling\n * update flow is still known to have (only its stage ① ever fetches).\n */\n resume(repoRoot: string, state: PushDevState): void {\n this.assertNoUnmergedPaths(repoRoot, state);\n this.gitExec.assertNoUntracked(repoRoot);\n this.gitExec.runGitChecked(['-C', repoRoot, 'add', '-u'], 'Failed to stage the resolved files (git add -u)');\n this.gitExec.runGitChecked(['-C', repoRoot, 'commit', '--no-edit'],\n `Failed to commit the resolution of ${state.current}`);\n process.stdout.write(`✅ Committed the resolution of ${state.current}.\\n`);\n state.current = '';\n this.refs.fetchCopies(repoRoot, state.queue);\n this.drain(repoRoot, state);\n }\n\n /**\n * Throw the whole resolve away: undo any halted merge, go back to the original branch, delete the\n * throwaway branch and the state file. Nothing published, nothing on the feature branch.\n */\n abort(repoRoot: string, state: PushDevState): void {\n // Allowed to fail: there is no merge to abort when the resolve halted between merges, and that is\n // not an error condition — the rest of the teardown must still run.\n this.gitExec.tryGit(['-C', repoRoot, 'merge', '--abort'], repoRoot);\n this.gitExec.runGitChecked(['-C', repoRoot, 'checkout', state.originalBranch],\n `Failed to return to ${state.originalBranch}`);\n this.gitExec.tryGit(['-C', repoRoot, 'branch', '-D', state.tmpBranch], repoRoot);\n this.store.clear(repoRoot);\n process.stdout.write(\n '\\n' + SEP + '🛑 Dev-deploy resolve aborted\\n' + SEP + '\\n'\n + `You are back on \\`${state.originalBranch}\\`; \\`${state.tmpBranch}\\` and the state file are gone.\\n`\n + `The remote copy \\`${state.targetRef}\\` is untouched, and so is your feature branch.\\n`);\n }\n\n /**\n * Merge queued refs until one conflicts (HALT, hand the tree over) or the queue empties (PUBLISH).\n *\n * A LATER ref can conflict too, which is why this is a loop resumed from a state file rather than a\n * single pass: `wp-finish-push-dev` can legitimately stop again on the next ref in the queue.\n */\n private drain(repoRoot: string, state: PushDevState): void {\n while (state.queue.length > 0) {\n const ref = state.queue[0];\n state.queue = state.queue.slice(1);\n const outcome = this.gitExec.tryGit(\n ['-C', repoRoot, 'merge', '--no-ff', '--no-edit', '-m',\n `Dev-deploy: merge ${ref} into ${state.targetRef}`, `refs/remotes/origin/${ref}`],\n repoRoot);\n if (outcome.ok) {\n process.stdout.write(` ✓ merged ${ref}\\n`);\n continue;\n }\n state.current = ref;\n this.store.write(repoRoot, state);\n this.halt(repoRoot, state);\n return;\n }\n this.publish(repoRoot, state);\n }\n\n /** Stop with the conflicted files listed and the ONE command that continues. Exit 2 = \"your turn\". */\n private halt(repoRoot: string, state: PushDevState): never {\n const conflicted = this.unmergedPaths(repoRoot);\n const remaining = state.queue.length > 0\n ? `\\nStill queued after this one: ${state.queue.join(', ')}\\n`\n + `\\`${WP_FINISH_PUSH_DEV}\\` resumes the queue, so it may stop again on a later ref.\\n`\n : '';\n throw new CliExitError(2,\n '\\n' + SEP + `⚠️ CONFLICT merging ${state.current}\\n` + SEP + '\\n'\n + `You are on the throwaway branch \\`${state.tmpBranch}\\` — NOT \\`${state.originalBranch}\\`. Nothing here\\n`\n + 'can reach your feature branch or your PR, which is exactly why the resolution happens over here.\\n\\n'\n + 'Resolve these files (edit until no conflict markers remain, then `git add` each one):\\n'\n + conflicted.map((f: string): string => ` ${f}`).join('\\n') + '\\n'\n + remaining\n + '\\nThen run ONE of:\\n'\n + ` ${WP_FINISH_PUSH_DEV} ← commit, resume the queue, publish the copy\\n`\n + ` ${WP_FINISH_PUSH_DEV} --abort ← throw it away and go back to \\`${state.originalBranch}\\`\\n\\n`\n + 'Do NOT run `git merge --continue`, `git commit` or `git push` yourself — they are blocked, and\\n'\n + 'the finish command above does all three for you.\\n');\n }\n\n /** Queue empty: force-push the composed result over the copy, then restore the original checkout. */\n private publish(repoRoot: string, state: PushDevState): void {\n this.gitExec.runGitChecked(\n ['-C', repoRoot, 'push', '--force', 'origin', `${state.tmpBranch}:refs/heads/${state.targetRef}`],\n `Failed to publish the dev copy ${state.targetRef}`);\n // Captured BEFORE the tmp branch is deleted — it is the composed sha the watch hints are about,\n // and after the checkout below HEAD is the feature branch, which is a different commit entirely.\n const published = this.refs.headSha(repoRoot, state.tmpBranch);\n this.gitExec.runGitChecked(['-C', repoRoot, 'checkout', state.originalBranch],\n `Failed to return to ${state.originalBranch}`);\n this.gitExec.tryGit(['-C', repoRoot, 'branch', '-D', state.tmpBranch], repoRoot);\n this.store.clear(repoRoot);\n process.stdout.write(\n '\\n' + SEP + `✅ Published ${state.targetRef} with the resolutions\\n` + SEP + '\\n'\n + `You are back on \\`${state.originalBranch}\\`, which was never modified — verify with:\\n`\n + ` git log --oneline ${state.originalBranch}\\n`\n + 'Your CI will pick the copy up on its next composition run.\\n'\n + this.watchHints.render(this.refs.config(repoRoot), state.targetRef, published));\n }\n\n /** Conflicted (unmerged-in-index) paths. Empty once every one has been resolved and `git add`ed. */\n private unmergedPaths(repoRoot: string): string[] {\n const out = this.gitExec.gitQuery(['-C', repoRoot, 'diff', '--name-only', '--diff-filter=U'], repoRoot,\n 'Failed to list the conflicted files (git diff --name-only --diff-filter=U).');\n return out.split('\\n').filter((l: string): boolean => l.trim() !== '');\n }\n\n // The gate on `wp-finish-push-dev`: refuse (and re-print the list) while anything is still unmerged.\n private assertNoUnmergedPaths(repoRoot: string, state: PushDevState): void {\n const conflicted = this.unmergedPaths(repoRoot);\n if (conflicted.length === 0) return;\n throw new CliExitError(2,\n '\\n' + SEP + '⛔ Conflicts are NOT resolved yet\\n' + SEP + '\\n'\n + `${WP_FINISH_PUSH_DEV} commits the resolution — it cannot write one for you. These files are still\\n`\n + 'unmerged (edit until no conflict markers remain, then `git add` each one):\\n'\n + conflicted.map((f: string): string => ` ${f}`).join('\\n') + '\\n\\n'\n + `Merging: ${state.current === '' ? '(none)' : state.current}\\n`\n + `Give up instead with: ${WP_FINISH_PUSH_DEV} --abort\\n`);\n }\n\n /**\n * The name of the throwaway branch for a resolve of `branch`.\n *\n * Suffixed rather than prefixed, matching the existing `<feature>Squash` / `<feature>PreMerge<n>`\n * convention — and deliberately NOT inside the dev namespace, which holds REMOTE copies only.\n */\n tmpBranchFor(branch: string): string {\n return `${branch}DevResolve`;\n }\n\n /** The banner `--resolve` prints before the first merge, so the queue is visible before it halts. */\n announce(cfg: DevDeployConfig, state: PushDevState): string {\n return '\\n' + SEP + `🔀 Resolving the dev composition for ${state.targetRef}\\n` + SEP + '\\n'\n + `Merging ${String(state.queue.length)} other cop${state.queue.length === 1 ? 'y' : 'ies'} from `\n + `\\`${cfg.copyRefGlob()}\\`, in the order your CI composes\\n\\`${cfg.devBranch}\\`, onto the throwaway branch `\n + `\\`${state.tmpBranch}\\`:\\n`\n + state.queue.map((r: string): string => ` ${r}`).join('\\n') + '\\n\\n'\n + `Your feature branch \\`${state.originalBranch}\\` is not touched by any of this.\\n`\n + `A clean run publishes the copy itself — ${WP_FINISH_PUSH_DEV} is only needed if a merge stops.\\n\\n`;\n }\n}\n"]}
|
|
@@ -91,7 +91,31 @@ export declare class ReviewReport {
|
|
|
91
91
|
* catch "wait, not THAT one" is to see which ones went unreviewed.
|
|
92
92
|
*/
|
|
93
93
|
private skippedLines;
|
|
94
|
+
/**
|
|
95
|
+
* The all-clear, then the RULE that makes it actionable.
|
|
96
|
+
*
|
|
97
|
+
* "nothing to spawn" on its own is a description of the current state, and an agent that has just been
|
|
98
|
+
* told a state — rather than a rule — treats re-spawning as a judgement call it is entitled to make. It
|
|
99
|
+
* then makes it, reasoning (correctly, on the facts) that the carried-forward verdicts judged an earlier
|
|
100
|
+
* tree. Reviews here are once per branch BY CONSTRUCTION: a passing review-<id>.json satisfies its
|
|
101
|
+
* checklist for the branch's whole life, and `wp-finish-upsert-pr` never archives one the way it archives
|
|
102
|
+
* review.json. So the reuse is deliberate — it is what keeps post-PR iteration from re-paying for every
|
|
103
|
+
* matched reviewer — and the output has to say so, because the alternative reading is the expensive one.
|
|
104
|
+
*
|
|
105
|
+
* The overwrite warning is not decoration. A re-spawned reviewer writes to the SAME verdict path, so a
|
|
106
|
+
* gratuitous re-run does not merely cost a subagent — it destroys the verdict that was already banked.
|
|
107
|
+
*
|
|
108
|
+
* It must NOT claim everything was reviewed when optional reviews were skipped — that is the one sentence
|
|
109
|
+
* that would turn a deliberate skip into a false record of a review that happened.
|
|
110
|
+
*/
|
|
94
111
|
private allClear;
|
|
112
|
+
/**
|
|
113
|
+
* The once-per-branch rule, stated as a prohibition rather than left to be inferred from a ✅.
|
|
114
|
+
*
|
|
115
|
+
* Printed whenever verdicts are being reused — which is every iteration of a PR after the first, i.e. the
|
|
116
|
+
* majority of stage-② runs on any branch that gets review feedback.
|
|
117
|
+
*/
|
|
118
|
+
private oncePerBranchRule;
|
|
95
119
|
/**
|
|
96
120
|
* The ONE instruction block, and the last thing the stage prints. Numbered rather than prose-linked
|
|
97
121
|
* ("Then… Finally…") so that skipping step 1 is visibly skipping a step, and worded so no earlier line
|
|
@@ -123,8 +123,11 @@ let ReviewReport = class ReviewReport {
|
|
|
123
123
|
if (input.applicableCount === 0)
|
|
124
124
|
return '\n' + this.checklistNotice.build(input.definedCount);
|
|
125
125
|
const lines = [];
|
|
126
|
+
// The prohibition rides on the REUSE line itself, not only in the all-clear below, because the
|
|
127
|
+
// all-clear is not printed when anything is still owed — and "some reviewers are reused, others
|
|
128
|
+
// must be spawned" is exactly the shape in which an agent re-spawns the reused ones too.
|
|
126
129
|
for (const r of input.reviewed) {
|
|
127
|
-
lines.push(` ✓ ${r.subagent} — already reviewed on this branch
|
|
130
|
+
lines.push(` ✓ ${r.subagent} — already reviewed on this branch; verdict STANDS, do NOT re-spawn (review-${r.id}.json)`);
|
|
128
131
|
}
|
|
129
132
|
// A verdict file that EXISTS but is unreadable as a verdict is called out here. Without it this
|
|
130
133
|
// reports the checklist as simply owed, and the AI re-runs a reviewer that already ran instead of
|
|
@@ -154,13 +157,40 @@ let ReviewReport = class ReviewReport {
|
|
|
154
157
|
' Not blocking. Drop the flag and re-run this command to offer them after all.',
|
|
155
158
|
];
|
|
156
159
|
}
|
|
157
|
-
|
|
158
|
-
|
|
160
|
+
/**
|
|
161
|
+
* The all-clear, then the RULE that makes it actionable.
|
|
162
|
+
*
|
|
163
|
+
* "nothing to spawn" on its own is a description of the current state, and an agent that has just been
|
|
164
|
+
* told a state — rather than a rule — treats re-spawning as a judgement call it is entitled to make. It
|
|
165
|
+
* then makes it, reasoning (correctly, on the facts) that the carried-forward verdicts judged an earlier
|
|
166
|
+
* tree. Reviews here are once per branch BY CONSTRUCTION: a passing review-<id>.json satisfies its
|
|
167
|
+
* checklist for the branch's whole life, and `wp-finish-upsert-pr` never archives one the way it archives
|
|
168
|
+
* review.json. So the reuse is deliberate — it is what keeps post-PR iteration from re-paying for every
|
|
169
|
+
* matched reviewer — and the output has to say so, because the alternative reading is the expensive one.
|
|
170
|
+
*
|
|
171
|
+
* The overwrite warning is not decoration. A re-spawned reviewer writes to the SAME verdict path, so a
|
|
172
|
+
* gratuitous re-run does not merely cost a subagent — it destroys the verdict that was already banked.
|
|
173
|
+
*
|
|
174
|
+
* It must NOT claim everything was reviewed when optional reviews were skipped — that is the one sentence
|
|
175
|
+
* that would turn a deliberate skip into a false record of a review that happened.
|
|
176
|
+
*/
|
|
159
177
|
allClear(input) {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
return
|
|
178
|
+
const headline = input.skipOptional && this.optionalOwed(input).length > 0
|
|
179
|
+
? '✅ Nothing left to spawn — every REQUIRED checklist is reviewed (optional ones skipped above).'
|
|
180
|
+
: '✅ Every checklist that applies is already reviewed — nothing to spawn.';
|
|
181
|
+
return headline + '\n' + this.oncePerBranchRule();
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* The once-per-branch rule, stated as a prohibition rather than left to be inferred from a ✅.
|
|
185
|
+
*
|
|
186
|
+
* Printed whenever verdicts are being reused — which is every iteration of a PR after the first, i.e. the
|
|
187
|
+
* majority of stage-② runs on any branch that gets review feedback.
|
|
188
|
+
*/
|
|
189
|
+
oncePerBranchRule() {
|
|
190
|
+
return (' Reviews here are ONCE PER BRANCH: a passing verdict carries forward to every later iteration\n' +
|
|
191
|
+
' of this PR, deliberately, so post-PR edits cost no reviewer tokens. Do NOT re-spawn a reviewer\n' +
|
|
192
|
+
' listed above to "re-check" the newer code — it burns a full subagent run AND overwrites the\n' +
|
|
193
|
+
' verdict file it already wrote. The only reviewers you may spawn are ones a STEP below names.');
|
|
164
194
|
}
|
|
165
195
|
/**
|
|
166
196
|
* The ONE instruction block, and the last thing the stage prints. Numbered rather than prose-linked
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"review-report.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/review-report.ts"],"names":[],"mappings":"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","sourcesContent":["import { injectable, bindingScopeValues } from 'inversify';\nimport { reviewJsonSchemaHint, RequiredChecklist, ReviewerBriefing, ReviewerInstructionsService } from '@webpieces/rules-config';\nimport { ChecklistNotice } from './checklist-notice';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n/**\n * One reviewer that ALREADY ANSWERED on this branch and refused, with the refusal rendered by\n * `ReviewJsonService.refusalError` — the ONE wording, shared with `wp-finish-upsert-pr`. Data-only.\n *\n * Stage ② carries these because it had the same defect finish did: a refused checklist has no passing\n * verdict, so it is \"owed\", so it got an ordinary spawn block identical to a reviewer that never ran. An\n * agent obeys that block, the reviewer re-reads unchanged code, refuses again — the loop, one stage earlier.\n *\n * The message is rendered by the COMMAND rather than in here for one reason: stage ② must not archive\n * anything. `refusalError` called with no archive path says \"fix it, then re-run; or set an override in\n * review-<id>.json\", which is only true while that file is still live — and at stage ② it is. Retiring a\n * verdict is finish's act, on the refusal it is actually acting on.\n */\nexport class RefusedReviewer {\n checklistId: string;\n message: string;\n\n constructor(checklistId: string, message: string) {\n this.checklistId = checklistId;\n this.message = message;\n }\n}\n\n/**\n * Everything the closing block of `wp-review-upsert-pr` needs. Data-only, and a class rather than an\n * object literal per CLAUDE.md. The three identifying paths are constructor args; the rest are optional\n * facts about the scan that default to \"nothing\", so a repo with no checklists constructs it in one line.\n */\nexport class ReviewReportInput {\n repoRoot: string;\n featureName: string;\n reviewPath: string; // the branch's review.json — the file the AI must write next\n definedCount: number; // how many checklists pr-gate.checklists defines\n applicableCount: number; // how many of them apply to THIS diff (0 ⇒ the notice, not spawn blocks)\n reviewed: RequiredChecklist[]; // already have a passing verdict on this branch\n formatErrors: string[]; // verdict files that exist but cannot be read as verdicts\n briefings: ReviewerBriefing[]; // one per applicable checklist, already written to disk\n refused: RefusedReviewer[]; // of the owed ones, those that already ran and said no (see RefusedReviewer)\n /**\n * `--no-optional` was passed: the human has already said to submit without the optional reviews, so the\n * block that offers them is replaced by a one-line statement that they were skipped. It never suppresses\n * a REQUIRED reviewer, and it is not a gate — nothing about what blocks the PR changes.\n */\n skipOptional: boolean;\n\n constructor(repoRoot: string, featureName: string, reviewPath: string) {\n this.repoRoot = repoRoot;\n this.featureName = featureName;\n this.reviewPath = reviewPath;\n this.definedCount = 0;\n this.applicableCount = 0;\n this.reviewed = [];\n this.formatErrors = [];\n this.briefings = [];\n this.refused = [];\n this.skipOptional = false;\n }\n}\n\n/**\n * Renders the closing block of `wp-review-upsert-pr` — the checklist verdict, then EXACTLY ONE \"what to do\n * next\".\n *\n * Extracted from the command so it can be asserted on as a rendered string, because the ordering IS the\n * contract. The bug it was extracted to fix: the zero-checklist notice ended with \"Carry on and run: pnpm\n * wp-finish-upsert-pr\", and the block printed directly beneath it said to write review.json first and\n * finish afterwards. Two next-steps, in the wrong order, and an agent that follows instructions literally —\n * which is the entire reason this command prints them — took the first one and opened a PR with no review.\n * That is precisely the failure the three-stage flow exists to prevent (reported on PR #519).\n *\n * The invariants, enforced by review-report.spec.ts:\n * 1. `wp-finish-upsert-pr` is named as a thing to run EXACTLY ONCE in the whole block.\n * 2. The review.json instruction comes BEFORE it — and BEFORE the spawn blocks (see nextSteps).\n * 3. With zero checklists the all-clear precedes any configuration guidance.\n * 4. REQUIRED reviewers are spawned unasked; OPTIONAL ones are only ever OFFERED, in one batched\n * question. The two never share a step, because one instruction says \"do it\" and the other says\n * \"ask first\", and an agent reading a merged list will act on the stronger of the two.\n *\n * Pure string building, no I/O. `@injectable(bindingScopeValues.Singleton)` so it is injected by type.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class ReviewReport {\n constructor(\n private readonly checklistNotice: ChecklistNotice,\n private readonly reviewerInstructions: ReviewerInstructionsService,\n ) {}\n\n render(input: ReviewReportInput): string {\n return '\\n' + SEP + this.header(input) + SEP\n + this.scanVerdict(input)\n + this.nextSteps(input);\n }\n\n /**\n * Name what this block is actually about. A repo with reviewers owed is being told to SPAWN; a repo with\n * none is not, and promising subagents it does not have is the same kind of noise as explaining checklist\n * configuration to a repo that configured none. Keyed on what is actually ACTIONABLE rather than on the\n * applicable count: every applicable checklist already having a verdict means nothing to spawn, and so\n * does a branch whose only outstanding reviews are optional ones the human already waved off.\n *\n * \"spawn\" and \"ask about\" are separate headings because they are separate obligations. A branch owing\n * only optional reviews has nothing the AI may do unilaterally, and a heading that says SPAWN is the\n * single line most likely to make it do exactly that.\n */\n private header(input: ReviewReportInput): string {\n if (this.requiredOwed(input).length > 0) return '② Review, spawn subagent reviewers, then finish\\n';\n if (this.offerableOwed(input).length > 0) return '② Review, offer the optional reviewers, then finish\\n';\n return '② Review, then finish\\n';\n }\n\n /**\n * What the SCAN found: either \"nothing applies here\" or the already-reviewed / unreadable-verdict lines.\n * Verdicts, not instructions — every instruction lives in nextSteps() below, so no line up here can be\n * mistaken for the next action.\n */\n private scanVerdict(input: ReviewReportInput): string {\n if (input.applicableCount === 0) return '\\n' + this.checklistNotice.build(input.definedCount);\n const lines: string[] = [];\n for (const r of input.reviewed) {\n lines.push(` ✓ ${r.subagent} — already reviewed on this branch (reusing its review-${r.id}.json)`);\n }\n // A verdict file that EXISTS but is unreadable as a verdict is called out here. Without it this\n // reports the checklist as simply owed, and the AI re-runs a reviewer that already ran instead of\n // correcting the file sitting right there.\n for (const e of input.formatErrors) lines.push(` ⛔ ${e}`);\n lines.push(...this.skippedLines(input));\n if (this.actionableOwed(input).length === 0) lines.push('', this.allClear(input));\n if (lines.length === 0) return '';\n return '\\n' + lines.join('\\n') + '\\n';\n }\n\n /**\n * `--no-optional`, stated as a VERDICT rather than left silent. Named individually, not just counted: the\n * whole reason the human is allowed to skip these is that they know this diff, and the only way they can\n * catch \"wait, not THAT one\" is to see which ones went unreviewed.\n */\n private skippedLines(input: ReviewReportInput): string[] {\n const skipped = this.optionalOwed(input);\n if (!input.skipOptional || skipped.length === 0) return [];\n return [\n '',\n ` ⏭️ ${skipped.length} OPTIONAL checklist(s) matched this diff and were SKIPPED (--no-optional):`,\n ...skipped.map((b: ReviewerBriefing): string => ` ${b.subagent} — ${this.why(b)}`),\n ' Not blocking. Drop the flag and re-run this command to offer them after all.',\n ];\n }\n\n // The all-clear. It must NOT claim everything was reviewed when optional reviews were skipped — that is\n // the one sentence that would turn a deliberate skip into a false record of a review that happened.\n private allClear(input: ReviewReportInput): string {\n if (input.skipOptional && this.optionalOwed(input).length > 0) {\n return '✅ Nothing left to spawn — every REQUIRED checklist is reviewed (optional ones skipped above).';\n }\n return '✅ Every checklist that applies is already reviewed — nothing to spawn.';\n }\n\n /**\n * The ONE instruction block, and the last thing the stage prints. Numbered rather than prose-linked\n * (\"Then… Finally…\") so that skipping step 1 is visibly skipping a step, and worded so no earlier line\n * can be mistaken for the real next action.\n *\n * review.json is STEP 1 and the spawn blocks are STEP 2 — that ORDER is the contract, not a preference.\n * This block used to print the spawn blocks first and then say to write review.json \"WHILE any reviewer\n * subagents above are still running\", which does not merely permit spawning first, it instructs it.\n * Harmless for a reviewer that only reads the diff; wrong for one that judges the PR's stated INTENT —\n * its title, summary or risk level — because review.json is the only place that intent lives. Such a\n * reviewer either finds no file (a false RED and a wasted reviewer run) or, on a second run of this\n * stage on the same branch, finds the PREVIOUS run's file and validates a title that no longer exists —\n * a false GREEN, with nothing in the output saying which of the two happened. A consuming repo had to\n * write itself a rule telling its agents to DISOBEY this block to work around it.\n *\n * What the old ordering bought was overlap on a single local file write, not a subagent round-trip.\n *\n * The schema hint is rendered by ReviewJsonService — the single renderer — so the shape printed here\n * can never drift from the shape `wp-finish-upsert-pr` validates.\n */\n private nextSteps(input: ReviewReportInput): string {\n const required = this.requiredOwed(input);\n const offerable = this.offerableOwed(input);\n // Numbered by what is actually PRINTED, so the numbers a reader sees are 1..n with no gaps: write\n // review.json, then a spawn step only if anything must run, then an offer step only if anything may.\n let step = 1;\n const write = this.writeReviewStep(input.reviewPath, step++);\n const spawn = required.length === 0 ? '' : this.spawnStep(input, required, step++);\n const offer = offerable.length === 0 ? '' : this.offerStep(input, offerable, step++);\n return '\\n' + SEP\n + `▶ NEXT — ${step} steps, in this order. Step 1 is NOT optional:\\n` + SEP + '\\n'\n + write + spawn + offer + this.finishStep(step, required.length + offerable.length > 0);\n }\n\n private writeReviewStep(reviewPath: string, step: number): string {\n return (\n `STEP ${step} — review your own changes, then write the review file. Write it FIRST — BEFORE you spawn\\n` +\n ' anything below. finish REFUSES without it, and a reviewer subagent may READ it: a\\n' +\n ' checklist that judges the PR title, summary or risk level reads exactly this file, so\\n' +\n ' writing it afterwards races that reviewer into seeing nothing — or, on a re-run of this\\n' +\n ' stage, into judging the PREVIOUS run\\'s review of code that has since changed.\\n\\n' +\n reviewJsonSchemaHint(reviewPath) + '\\n\\n'\n );\n }\n\n /**\n * The REQUIRED reviewers — one copy-paste block each, and nothing at all when none is owed. The prompt is\n * deliberately a POINTER and nothing else: the generated instructions file is the contract, so anything\n * restated here is a second copy that can go stale — which is exactly how a removed `success` field\n * outlived its own removal in print.\n *\n * These are spawned WITHOUT asking. They are the checklists the repo declared `required: true`, which is\n * the repo saying the decision was already made; putting them to the human again would re-open a question\n * the config exists to settle.\n */\n private spawnStep(input: ReviewReportInput, owed: readonly ReviewerBriefing[], step: number): string {\n const lines: string[] = [\n `STEP ${step} — only once that file is written, spawn these ${owed.length} REQUIRED reviewer subagent(s) — a`,\n ' SEPARATE one each. They block the PR, so do NOT ask whether to run them. You may NOT',\n ' review your own work, and you may NOT write a reviewer\\'s verdict file on its behalf.',\n '',\n ];\n lines.push(...this.refusedWarning(input, owed));\n for (const b of owed) lines.push(...this.oneSpawnBlock(input, b));\n lines.push('');\n return lines.join('\\n');\n }\n\n /**\n * STEP n — the OPTIONAL reviewers: listed, never spawned unasked.\n *\n * This is the whole point of `required: false`. A one-line bug fix in a repo whose checklists key on a\n * glob as broad as every TypeScript file otherwise pays for a dozen subagent reviews, and the only party\n * who can judge whether this particular diff is worth them is the human looking at it.\n *\n * ONE batched multi-select question, explicitly. Asked one at a time, a human answering \"no\" nine times\n * is being worn down rather than consulted, and by the third question the cheap thing is to say yes to\n * everything — which is the state this feature exists to leave. The \"None\" option has to be spelled out\n * too: an agent that offers a list without an explicit way to decline it has not really offered a choice.\n *\n * The blocking consequence is stated because it is the one non-obvious part of the contract: `required`\n * governs whether a reviewer must RUN, not whether its answer counts. Choosing to run one and then\n * shrugging off a red verdict would make the whole exercise theater.\n */\n private offerStep(input: ReviewReportInput, offerable: readonly ReviewerBriefing[], step: number): string {\n const lines: string[] = [\n `STEP ${step} — these ${offerable.length} OPTIONAL review checklist(s) matched this diff. They do NOT block the`,\n ' PR, and you may NOT decide for the human whether to run them.',\n '',\n ' ASK THE HUMAN, in ONE multi-select question listing all of them plus an explicit',\n ' \"None — required only\" choice. Do not ask one question per reviewer. Then spawn ONLY',\n ' what they picked, the same way as any other reviewer.',\n '',\n ' If they pick none, that is a complete answer: go straight to the final step. If they',\n ' told you up front to submit without reviews, re-run this stage as',\n ' `pnpm wp-review-upsert-pr --no-optional` and this step disappears.',\n '',\n ' NOTE: whichever ones you DO run, their verdicts count in full — a red verdict from an',\n ' optional reviewer blocks the PR exactly like a required one.',\n '',\n ];\n lines.push(...this.refusedWarning(input, offerable));\n for (const b of offerable) lines.push(...this.oneSpawnBlock(input, b));\n lines.push('');\n return lines.join('\\n');\n }\n\n // Said up front, not only beside the block: an agent that has decided to spawn everything listed here\n // needs to know BEFORE it starts that one of these entries is not a spawn-shaped task. Scoped to the\n // group being printed — a refusal among the REQUIRED reviewers is not a caveat on the optional list.\n private refusedWarning(input: ReviewReportInput, group: readonly ReviewerBriefing[]): string[] {\n const ids = new Set(group.map((b: ReviewerBriefing): string => b.checklistId));\n const n = input.refused.filter((r: RefusedReviewer): boolean => ids.has(r.checklistId)).length;\n if (n === 0) return [];\n return [\n ` ${n} of them already ANSWERED and refused (marked ⛔ below). Do not spawn`,\n ' those against unchanged code — fix what they found first; the fix is the prerequisite.',\n '',\n ];\n }\n\n private finishStep(stepNumber: number, anyReviewers: boolean): string {\n // \"every reviewer you ran\" rather than \"every reviewer above\": with an optional list the human may\n // legitimately have run none of them, and a precondition naming reviewers that were declined reads as\n // an unmeetable one.\n const precondition = anyReviewers\n ? 'once every reviewer you ran has written its verdict file'\n : 'once that file exists';\n return (\n `STEP ${stepNumber} — only ${precondition}, run: pnpm wp-finish-upsert-pr\\n` +\n ' (The build gate is already green for this commit — finish reuses it unless HEAD moves.)\\n\\n'\n );\n }\n\n // The briefings with no passing verdict yet — the ONE definition of \"owed\", shared by the header, the\n // scan verdict and the step numbering, so they cannot disagree about whether there is anything to spawn.\n private owedReviewers(input: ReviewReportInput): ReviewerBriefing[] {\n const reviewedIds = new Set(input.reviewed.map((r: RequiredChecklist): string => r.id));\n return input.briefings.filter((b: ReviewerBriefing): boolean => !reviewedIds.has(b.checklistId));\n }\n\n // Owed AND blocking — spawned without asking.\n private requiredOwed(input: ReviewReportInput): ReviewerBriefing[] {\n return this.owedReviewers(input).filter((b: ReviewerBriefing): boolean => b.required);\n }\n\n // Owed and optional. Still listed under `--no-optional` (as a skip verdict), just never as a step.\n private optionalOwed(input: ReviewReportInput): ReviewerBriefing[] {\n return this.owedReviewers(input).filter((b: ReviewerBriefing): boolean => !b.required);\n }\n\n // The optional ones the human is actually to be ASKED about — none, once they have already answered.\n private offerableOwed(input: ReviewReportInput): ReviewerBriefing[] {\n return input.skipOptional ? [] : this.optionalOwed(input);\n }\n\n // Everything the AI still has to act on. Distinct from `owedReviewers`: a skipped optional checklist is\n // owed a verdict it will never get, and treating it as pending work is what would print a spawn\n // instruction for a review the human just declined.\n private actionableOwed(input: ReviewReportInput): ReviewerBriefing[] {\n return [...this.requiredOwed(input), ...this.offerableOwed(input)];\n }\n\n /**\n * One reviewer's block. A reviewer that already REFUSED gets the SAME spawn coordinates but a different\n * lead-in, because the action before spawning is different: its own words are printed, and the spawn is\n * explicitly conditioned on having fixed the finding first.\n *\n * It keeps its spawn block rather than being dropped from the list, because the reviewer genuinely does\n * still owe a fresh verdict — dropping it would leave nothing anywhere saying how to get one. What must\n * not happen is a bare \"spawn this\" that reads identically to a reviewer that never ran, which is the\n * loop this exists to break.\n */\n private oneSpawnBlock(input: ReviewReportInput, b: ReviewerBriefing): string[] {\n const instructionsFile = this.reviewerInstructions.pathFor(input.repoRoot, input.featureName, b.subagent);\n return [\n ...this.leadIn(input, b),\n ` subagent_type: ${b.subagent}`,\n ' prompt: Read your instructions file FIRST and follow it exactly:',\n ` ${instructionsFile}`,\n '',\n ];\n }\n\n // The lines above the spawn coordinates: normally just why this reviewer is in scope; for one that\n // already refused, its verdict verbatim plus the order the two actions must happen in.\n private leadIn(input: ReviewReportInput, b: ReviewerBriefing): string[] {\n const refusal = input.refused.find((r: RefusedReviewer): boolean => r.checklistId === b.checklistId);\n if (!refusal) return [` ▶ ${b.subagent} — ${this.why(b)}`, ...this.docLine(b)];\n return [\n ` ⛔ ${b.subagent} — ALREADY REVIEWED THIS BRANCH AND REFUSED. It will refuse again on unchanged code.`,\n ` ${refusal.message}`,\n ' FIX THE FINDING FIRST (or record a human-authored override). ONLY THEN spawn it again, to',\n ' write a fresh verdict:',\n ];\n }\n\n /**\n * The checklist's guidance doc, for OPTIONAL reviewers only.\n *\n * \"4 file(s) matched\" plus a broad glob does not tell a human what the review would actually look AT,\n * and they are being asked to decide exactly that. Omitted for required reviewers: there is no decision\n * to inform there — the reviewer runs either way, and the doc is already in its instructions file.\n */\n private docLine(b: ReviewerBriefing): string[] {\n if (b.required || b.docPath === '') return [];\n return [` reviews against: ${b.docPath}`];\n }\n\n // Why this one is in scope. A patternless checklist is NOT \"matched\" — it always runs, over the whole\n // diff, and saying so is what tells a repo its checklist is firing on docs-only PRs by design.\n private why(b: ReviewerBriefing): string {\n if (b.matchedPatterns.length === 0) {\n return `ALWAYS RUNS (no \"patterns\" configured), whole diff in scope — ${b.myFiles.length} file(s)`;\n }\n return `${b.myFiles.length} file(s) matched ${b.matchedPatterns.map((p: string): string => `\"${p}\"`).join(', ')}`;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"review-report.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/review-report.ts"],"names":[],"mappings":";;;;AAAA,yCAA2D;AAC3D,0DAAiI;AACjI,yDAAqD;AAErD,MAAM,GAAG,GAAG,0DAA0D,CAAC;AAEvE;;;;;;;;;;;;GAYG;AACH,MAAa,eAAe;IACxB,WAAW,CAAS;IACpB,OAAO,CAAS;IAEhB,YAAY,WAAmB,EAAE,OAAe;QAC5C,IAAI,CAAC,WAAW,GAAG,WAAW,CAAC;QAC/B,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;IAC3B,CAAC;CACJ;AARD,0CAQC;AAED;;;;GAIG;AACH,MAAa,iBAAiB;IAC1B,QAAQ,CAAS;IACjB,WAAW,CAAS;IACpB,UAAU,CAAS,CAAa,6DAA6D;IAC7F,YAAY,CAAS,CAAW,iDAAiD;IACjF,eAAe,CAAS,CAAQ,yEAAyE;IACzG,QAAQ,CAAsB,CAAE,gDAAgD;IAChF,YAAY,CAAW,CAAS,0DAA0D;IAC1F,SAAS,CAAqB,CAAE,wDAAwD;IACxF,OAAO,CAAoB,CAAK,6EAA6E;IAC7G;;;;OAIG;IACH,YAAY,CAAU;IAEtB,YAAY,QAAgB,EAAE,WAAmB,EAAE,UAAkB;QACjE,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,WAAW,GAAG,WAAW,CAAC;QAC/B,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;QAC7B,IAAI,CAAC,YAAY,GAAG,CAAC,CAAC;QACtB,IAAI,CAAC,eAAe,GAAG,CAAC,CAAC;QACzB,IAAI,CAAC,QAAQ,GAAG,EAAE,CAAC;QACnB,IAAI,CAAC,YAAY,GAAG,EAAE,CAAC;QACvB,IAAI,CAAC,SAAS,GAAG,EAAE,CAAC;QACpB,IAAI,CAAC,OAAO,GAAG,EAAE,CAAC;QAClB,IAAI,CAAC,YAAY,GAAG,KAAK,CAAC;IAC9B,CAAC;CACJ;AA7BD,8CA6BC;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AAEI,IAAM,YAAY,GAAlB,MAAM,YAAY;IAEA;IACA;IAFrB,YACqB,eAAgC,EAChC,oBAAiD;QADjD,oBAAe,GAAf,eAAe,CAAiB;QAChC,yBAAoB,GAApB,oBAAoB,CAA6B;IACnE,CAAC;IAEJ,MAAM,CAAC,KAAwB;QAC3B,OAAO,IAAI,GAAG,GAAG,GAAG,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,GAAG;cACtC,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC;cACvB,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;IAChC,CAAC;IAED;;;;;;;;;;OAUG;IACK,MAAM,CAAC,KAAwB;QACnC,IAAI,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,MAAM,GAAG,CAAC;YAAE,OAAO,mDAAmD,CAAC;QACpG,IAAI,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC,MAAM,GAAG,CAAC;YAAE,OAAO,uDAAuD,CAAC;QACzG,OAAO,yBAAyB,CAAC;IACrC,CAAC;IAED;;;;OAIG;IACK,WAAW,CAAC,KAAwB;QACxC,IAAI,KAAK,CAAC,eAAe,KAAK,CAAC;YAAE,OAAO,IAAI,GAAG,IAAI,CAAC,eAAe,CAAC,KAAK,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC;QAC9F,MAAM,KAAK,GAAa,EAAE,CAAC;QAC3B,+FAA+F;QAC/F,gGAAgG;QAChG,yFAAyF;QACzF,KAAK,MAAM,CAAC,IAAI,KAAK,CAAC,QAAQ,EAAE,CAAC;YAC7B,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,QAAQ,+EAA+E,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC;QAC7H,CAAC;QACD,gGAAgG;QAChG,kGAAkG;QAClG,2CAA2C;QAC3C,KAAK,MAAM,CAAC,IAAI,KAAK,CAAC,YAAY;YAAE,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;QAC3D,KAAK,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,CAAC;QACxC,IAAI,IAAI,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC,MAAM,KAAK,CAAC;YAAE,KAAK,CAAC,IAAI,CAAC,EAAE,EAAE,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC;QAClF,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,EAAE,CAAC;QAClC,OAAO,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC1C,CAAC;IAED;;;;OAIG;IACK,YAAY,CAAC,KAAwB;QACzC,MAAM,OAAO,GAAG,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC;QACzC,IAAI,CAAC,KAAK,CAAC,YAAY,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,EAAE,CAAC;QAC3D,OAAO;YACH,EAAE;YACF,SAAS,OAAO,CAAC,MAAM,4EAA4E;YACnG,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,CAAmB,EAAU,EAAE,CAAC,UAAU,CAAC,CAAC,QAAQ,MAAM,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;YACxF,oFAAoF;SACvF,CAAC;IACN,CAAC;IAED;;;;;;;;;;;;;;;;OAgBG;IACK,QAAQ,CAAC,KAAwB;QACrC,MAAM,QAAQ,GAAG,KAAK,CAAC,YAAY,IAAI,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,MAAM,GAAG,CAAC;YACtE,CAAC,CAAC,+FAA+F;YACjG,CAAC,CAAC,wEAAwE,CAAC;QAC/E,OAAO,QAAQ,GAAG,IAAI,GAAG,IAAI,CAAC,iBAAiB,EAAE,CAAC;IACtD,CAAC;IAED;;;;;OAKG;IACK,iBAAiB;QACrB,OAAO,CACH,mGAAmG;YACnG,qGAAqG;YACrG,kGAAkG;YAClG,iGAAiG,CACpG,CAAC;IACN,CAAC;IAED;;;;;;;;;;;;;;;;;;;OAmBG;IACK,SAAS,CAAC,KAAwB;QACtC,MAAM,QAAQ,GAAG,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC;QAC1C,MAAM,SAAS,GAAG,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;QAC5C,kGAAkG;QAClG,qGAAqG;QACrG,IAAI,IAAI,GAAG,CAAC,CAAC;QACb,MAAM,KAAK,GAAG,IAAI,CAAC,eAAe,CAAC,KAAK,CAAC,UAAU,EAAE,IAAI,EAAE,CAAC,CAAC;QAC7D,MAAM,KAAK,GAAG,QAAQ,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;QACnF,MAAM,KAAK,GAAG,SAAS,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACrF,OAAO,IAAI,GAAG,GAAG;cACX,YAAY,IAAI,kDAAkD,GAAG,GAAG,GAAG,IAAI;cAC/E,KAAK,GAAG,KAAK,GAAG,KAAK,GAAG,IAAI,CAAC,UAAU,CAAC,IAAI,EAAE,QAAQ,CAAC,MAAM,GAAG,SAAS,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IAChG,CAAC;IAEO,eAAe,CAAC,UAAkB,EAAE,IAAY;QACpD,OAAO,CACH,QAAQ,IAAI,6FAA6F;YACzG,8FAA8F;YAC9F,kGAAkG;YAClG,oGAAoG;YACpG,6FAA6F;YAC7F,IAAA,mCAAoB,EAAC,UAAU,CAAC,GAAG,MAAM,CAC5C,CAAC;IACN,CAAC;IAED;;;;;;;;;OASG;IACK,SAAS,CAAC,KAAwB,EAAE,IAAiC,EAAE,IAAY;QACvF,MAAM,KAAK,GAAa;YACpB,QAAQ,IAAI,kDAAkD,IAAI,CAAC,MAAM,oCAAoC;YAC7G,+FAA+F;YAC/F,gGAAgG;YAChG,EAAE;SACL,CAAC;QACF,KAAK,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,cAAc,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC,CAAC;QAChD,KAAK,MAAM,CAAC,IAAI,IAAI;YAAE,KAAK,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,aAAa,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC;QAClE,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACf,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC5B,CAAC;IAED;;;;;;;;;;;;;;;OAeG;IACK,SAAS,CAAC,KAAwB,EAAE,SAAsC,EAAE,IAAY;QAC5F,MAAM,KAAK,GAAa;YACpB,QAAQ,IAAI,YAAY,SAAS,CAAC,MAAM,wEAAwE;YAChH,wEAAwE;YACxE,EAAE;YACF,2FAA2F;YAC3F,+FAA+F;YAC/F,gEAAgE;YAChE,EAAE;YACF,+FAA+F;YAC/F,4EAA4E;YAC5E,6EAA6E;YAC7E,EAAE;YACF,gGAAgG;YAChG,uEAAuE;YACvE,EAAE;SACL,CAAC;QACF,KAAK,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,cAAc,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC,CAAC;QACrD,KAAK,MAAM,CAAC,IAAI,SAAS;YAAE,KAAK,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,aAAa,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC;QACvE,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACf,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC5B,CAAC;IAED,sGAAsG;IACtG,qGAAqG;IACrG,qGAAqG;IAC7F,cAAc,CAAC,KAAwB,EAAE,KAAkC;QAC/E,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAmB,EAAU,EAAE,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC;QAC/E,MAAM,CAAC,GAAG,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAkB,EAAW,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,MAAM,CAAC;QAC/F,IAAI,CAAC,KAAK,CAAC;YAAE,OAAO,EAAE,CAAC;QACvB,OAAO;YACH,YAAY,CAAC,sEAAsE;YACnF,iGAAiG;YACjG,EAAE;SACL,CAAC;IACN,CAAC;IAEO,UAAU,CAAC,UAAkB,EAAE,YAAqB;QACxD,mGAAmG;QACnG,sGAAsG;QACtG,qBAAqB;QACrB,MAAM,YAAY,GAAG,YAAY;YAC7B,CAAC,CAAC,0DAA0D;YAC5D,CAAC,CAAC,uBAAuB,CAAC;QAC9B,OAAO,CACH,QAAQ,UAAU,WAAW,YAAY,oCAAoC;YAC7E,sGAAsG,CACzG,CAAC;IACN,CAAC;IAED,sGAAsG;IACtG,yGAAyG;IACjG,aAAa,CAAC,KAAwB;QAC1C,MAAM,WAAW,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAoB,EAAU,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;QACxF,OAAO,KAAK,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAmB,EAAW,EAAE,CAAC,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC;IACrG,CAAC;IAED,8CAA8C;IACtC,YAAY,CAAC,KAAwB;QACzC,OAAO,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC,CAAmB,EAAW,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC;IAC1F,CAAC;IAED,mGAAmG;IAC3F,YAAY,CAAC,KAAwB;QACzC,OAAO,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC,CAAmB,EAAW,EAAE,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC;IAC3F,CAAC;IAED,qGAAqG;IAC7F,aAAa,CAAC,KAAwB;QAC1C,OAAO,KAAK,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC;IAC9D,CAAC;IAED,wGAAwG;IACxG,gGAAgG;IAChG,oDAAoD;IAC5C,cAAc,CAAC,KAAwB;QAC3C,OAAO,CAAC,GAAG,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC,EAAE,GAAG,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC,CAAC;IACvE,CAAC;IAED;;;;;;;;;OASG;IACK,aAAa,CAAC,KAAwB,EAAE,CAAmB;QAC/D,MAAM,gBAAgB,GAAG,IAAI,CAAC,oBAAoB,CAAC,OAAO,CAAC,KAAK,CAAC,QAAQ,EAAE,KAAK,CAAC,WAAW,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC;QAC1G,OAAO;YACH,GAAG,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC;YACxB,wBAAwB,CAAC,CAAC,QAAQ,EAAE;YACpC,+EAA+E;YAC/E,wBAAwB,gBAAgB,EAAE;YAC1C,EAAE;SACL,CAAC;IACN,CAAC;IAED,mGAAmG;IACnG,uFAAuF;IAC/E,MAAM,CAAC,KAAwB,EAAE,CAAmB;QACxD,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAkB,EAAW,EAAE,CAAC,CAAC,CAAC,WAAW,KAAK,CAAC,CAAC,WAAW,CAAC,CAAC;QACrG,IAAI,CAAC,OAAO;YAAE,OAAO,CAAC,OAAO,CAAC,CAAC,QAAQ,MAAM,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,GAAG,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;QAChF,OAAO;YACH,OAAO,CAAC,CAAC,QAAQ,sFAAsF;YACvG,SAAS,OAAO,CAAC,OAAO,EAAE;YAC1B,iGAAiG;YACjG,8BAA8B;SACjC,CAAC;IACN,CAAC;IAED;;;;;;OAMG;IACK,OAAO,CAAC,CAAmB;QAC/B,IAAI,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC,OAAO,KAAK,EAAE;YAAE,OAAO,EAAE,CAAC;QAC9C,OAAO,CAAC,0BAA0B,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC;IACnD,CAAC;IAED,sGAAsG;IACtG,+FAA+F;IACvF,GAAG,CAAC,CAAmB;QAC3B,IAAI,CAAC,CAAC,eAAe,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACjC,OAAO,iEAAiE,CAAC,CAAC,OAAO,CAAC,MAAM,UAAU,CAAC;QACvG,CAAC;QACD,OAAO,GAAG,CAAC,CAAC,OAAO,CAAC,MAAM,oBAAoB,CAAC,CAAC,eAAe,CAAC,GAAG,CAAC,CAAC,CAAS,EAAU,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;IACtH,CAAC;CACJ,CAAA;AArUY,oCAAY;uBAAZ,YAAY;IADxB,IAAA,sBAAU,EAAC,8BAAkB,CAAC,SAAS,CAAC;6CAGC,kCAAe;QACV,0CAA2B;GAH7D,YAAY,CAqUxB","sourcesContent":["import { injectable, bindingScopeValues } from 'inversify';\nimport { reviewJsonSchemaHint, RequiredChecklist, ReviewerBriefing, ReviewerInstructionsService } from '@webpieces/rules-config';\nimport { ChecklistNotice } from './checklist-notice';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n/**\n * One reviewer that ALREADY ANSWERED on this branch and refused, with the refusal rendered by\n * `ReviewJsonService.refusalError` — the ONE wording, shared with `wp-finish-upsert-pr`. Data-only.\n *\n * Stage ② carries these because it had the same defect finish did: a refused checklist has no passing\n * verdict, so it is \"owed\", so it got an ordinary spawn block identical to a reviewer that never ran. An\n * agent obeys that block, the reviewer re-reads unchanged code, refuses again — the loop, one stage earlier.\n *\n * The message is rendered by the COMMAND rather than in here for one reason: stage ② must not archive\n * anything. `refusalError` called with no archive path says \"fix it, then re-run; or set an override in\n * review-<id>.json\", which is only true while that file is still live — and at stage ② it is. Retiring a\n * verdict is finish's act, on the refusal it is actually acting on.\n */\nexport class RefusedReviewer {\n checklistId: string;\n message: string;\n\n constructor(checklistId: string, message: string) {\n this.checklistId = checklistId;\n this.message = message;\n }\n}\n\n/**\n * Everything the closing block of `wp-review-upsert-pr` needs. Data-only, and a class rather than an\n * object literal per CLAUDE.md. The three identifying paths are constructor args; the rest are optional\n * facts about the scan that default to \"nothing\", so a repo with no checklists constructs it in one line.\n */\nexport class ReviewReportInput {\n repoRoot: string;\n featureName: string;\n reviewPath: string; // the branch's review.json — the file the AI must write next\n definedCount: number; // how many checklists pr-gate.checklists defines\n applicableCount: number; // how many of them apply to THIS diff (0 ⇒ the notice, not spawn blocks)\n reviewed: RequiredChecklist[]; // already have a passing verdict on this branch\n formatErrors: string[]; // verdict files that exist but cannot be read as verdicts\n briefings: ReviewerBriefing[]; // one per applicable checklist, already written to disk\n refused: RefusedReviewer[]; // of the owed ones, those that already ran and said no (see RefusedReviewer)\n /**\n * `--no-optional` was passed: the human has already said to submit without the optional reviews, so the\n * block that offers them is replaced by a one-line statement that they were skipped. It never suppresses\n * a REQUIRED reviewer, and it is not a gate — nothing about what blocks the PR changes.\n */\n skipOptional: boolean;\n\n constructor(repoRoot: string, featureName: string, reviewPath: string) {\n this.repoRoot = repoRoot;\n this.featureName = featureName;\n this.reviewPath = reviewPath;\n this.definedCount = 0;\n this.applicableCount = 0;\n this.reviewed = [];\n this.formatErrors = [];\n this.briefings = [];\n this.refused = [];\n this.skipOptional = false;\n }\n}\n\n/**\n * Renders the closing block of `wp-review-upsert-pr` — the checklist verdict, then EXACTLY ONE \"what to do\n * next\".\n *\n * Extracted from the command so it can be asserted on as a rendered string, because the ordering IS the\n * contract. The bug it was extracted to fix: the zero-checklist notice ended with \"Carry on and run: pnpm\n * wp-finish-upsert-pr\", and the block printed directly beneath it said to write review.json first and\n * finish afterwards. Two next-steps, in the wrong order, and an agent that follows instructions literally —\n * which is the entire reason this command prints them — took the first one and opened a PR with no review.\n * That is precisely the failure the three-stage flow exists to prevent (reported on PR #519).\n *\n * The invariants, enforced by review-report.spec.ts:\n * 1. `wp-finish-upsert-pr` is named as a thing to run EXACTLY ONCE in the whole block.\n * 2. The review.json instruction comes BEFORE it — and BEFORE the spawn blocks (see nextSteps).\n * 3. With zero checklists the all-clear precedes any configuration guidance.\n * 4. REQUIRED reviewers are spawned unasked; OPTIONAL ones are only ever OFFERED, in one batched\n * question. The two never share a step, because one instruction says \"do it\" and the other says\n * \"ask first\", and an agent reading a merged list will act on the stronger of the two.\n *\n * Pure string building, no I/O. `@injectable(bindingScopeValues.Singleton)` so it is injected by type.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class ReviewReport {\n constructor(\n private readonly checklistNotice: ChecklistNotice,\n private readonly reviewerInstructions: ReviewerInstructionsService,\n ) {}\n\n render(input: ReviewReportInput): string {\n return '\\n' + SEP + this.header(input) + SEP\n + this.scanVerdict(input)\n + this.nextSteps(input);\n }\n\n /**\n * Name what this block is actually about. A repo with reviewers owed is being told to SPAWN; a repo with\n * none is not, and promising subagents it does not have is the same kind of noise as explaining checklist\n * configuration to a repo that configured none. Keyed on what is actually ACTIONABLE rather than on the\n * applicable count: every applicable checklist already having a verdict means nothing to spawn, and so\n * does a branch whose only outstanding reviews are optional ones the human already waved off.\n *\n * \"spawn\" and \"ask about\" are separate headings because they are separate obligations. A branch owing\n * only optional reviews has nothing the AI may do unilaterally, and a heading that says SPAWN is the\n * single line most likely to make it do exactly that.\n */\n private header(input: ReviewReportInput): string {\n if (this.requiredOwed(input).length > 0) return '② Review, spawn subagent reviewers, then finish\\n';\n if (this.offerableOwed(input).length > 0) return '② Review, offer the optional reviewers, then finish\\n';\n return '② Review, then finish\\n';\n }\n\n /**\n * What the SCAN found: either \"nothing applies here\" or the already-reviewed / unreadable-verdict lines.\n * Verdicts, not instructions — every instruction lives in nextSteps() below, so no line up here can be\n * mistaken for the next action.\n */\n private scanVerdict(input: ReviewReportInput): string {\n if (input.applicableCount === 0) return '\\n' + this.checklistNotice.build(input.definedCount);\n const lines: string[] = [];\n // The prohibition rides on the REUSE line itself, not only in the all-clear below, because the\n // all-clear is not printed when anything is still owed — and \"some reviewers are reused, others\n // must be spawned\" is exactly the shape in which an agent re-spawns the reused ones too.\n for (const r of input.reviewed) {\n lines.push(` ✓ ${r.subagent} — already reviewed on this branch; verdict STANDS, do NOT re-spawn (review-${r.id}.json)`);\n }\n // A verdict file that EXISTS but is unreadable as a verdict is called out here. Without it this\n // reports the checklist as simply owed, and the AI re-runs a reviewer that already ran instead of\n // correcting the file sitting right there.\n for (const e of input.formatErrors) lines.push(` ⛔ ${e}`);\n lines.push(...this.skippedLines(input));\n if (this.actionableOwed(input).length === 0) lines.push('', this.allClear(input));\n if (lines.length === 0) return '';\n return '\\n' + lines.join('\\n') + '\\n';\n }\n\n /**\n * `--no-optional`, stated as a VERDICT rather than left silent. Named individually, not just counted: the\n * whole reason the human is allowed to skip these is that they know this diff, and the only way they can\n * catch \"wait, not THAT one\" is to see which ones went unreviewed.\n */\n private skippedLines(input: ReviewReportInput): string[] {\n const skipped = this.optionalOwed(input);\n if (!input.skipOptional || skipped.length === 0) return [];\n return [\n '',\n ` ⏭️ ${skipped.length} OPTIONAL checklist(s) matched this diff and were SKIPPED (--no-optional):`,\n ...skipped.map((b: ReviewerBriefing): string => ` ${b.subagent} — ${this.why(b)}`),\n ' Not blocking. Drop the flag and re-run this command to offer them after all.',\n ];\n }\n\n /**\n * The all-clear, then the RULE that makes it actionable.\n *\n * \"nothing to spawn\" on its own is a description of the current state, and an agent that has just been\n * told a state — rather than a rule — treats re-spawning as a judgement call it is entitled to make. It\n * then makes it, reasoning (correctly, on the facts) that the carried-forward verdicts judged an earlier\n * tree. Reviews here are once per branch BY CONSTRUCTION: a passing review-<id>.json satisfies its\n * checklist for the branch's whole life, and `wp-finish-upsert-pr` never archives one the way it archives\n * review.json. So the reuse is deliberate — it is what keeps post-PR iteration from re-paying for every\n * matched reviewer — and the output has to say so, because the alternative reading is the expensive one.\n *\n * The overwrite warning is not decoration. A re-spawned reviewer writes to the SAME verdict path, so a\n * gratuitous re-run does not merely cost a subagent — it destroys the verdict that was already banked.\n *\n * It must NOT claim everything was reviewed when optional reviews were skipped — that is the one sentence\n * that would turn a deliberate skip into a false record of a review that happened.\n */\n private allClear(input: ReviewReportInput): string {\n const headline = input.skipOptional && this.optionalOwed(input).length > 0\n ? '✅ Nothing left to spawn — every REQUIRED checklist is reviewed (optional ones skipped above).'\n : '✅ Every checklist that applies is already reviewed — nothing to spawn.';\n return headline + '\\n' + this.oncePerBranchRule();\n }\n\n /**\n * The once-per-branch rule, stated as a prohibition rather than left to be inferred from a ✅.\n *\n * Printed whenever verdicts are being reused — which is every iteration of a PR after the first, i.e. the\n * majority of stage-② runs on any branch that gets review feedback.\n */\n private oncePerBranchRule(): string {\n return (\n ' Reviews here are ONCE PER BRANCH: a passing verdict carries forward to every later iteration\\n' +\n ' of this PR, deliberately, so post-PR edits cost no reviewer tokens. Do NOT re-spawn a reviewer\\n' +\n ' listed above to \"re-check\" the newer code — it burns a full subagent run AND overwrites the\\n' +\n ' verdict file it already wrote. The only reviewers you may spawn are ones a STEP below names.'\n );\n }\n\n /**\n * The ONE instruction block, and the last thing the stage prints. Numbered rather than prose-linked\n * (\"Then… Finally…\") so that skipping step 1 is visibly skipping a step, and worded so no earlier line\n * can be mistaken for the real next action.\n *\n * review.json is STEP 1 and the spawn blocks are STEP 2 — that ORDER is the contract, not a preference.\n * This block used to print the spawn blocks first and then say to write review.json \"WHILE any reviewer\n * subagents above are still running\", which does not merely permit spawning first, it instructs it.\n * Harmless for a reviewer that only reads the diff; wrong for one that judges the PR's stated INTENT —\n * its title, summary or risk level — because review.json is the only place that intent lives. Such a\n * reviewer either finds no file (a false RED and a wasted reviewer run) or, on a second run of this\n * stage on the same branch, finds the PREVIOUS run's file and validates a title that no longer exists —\n * a false GREEN, with nothing in the output saying which of the two happened. A consuming repo had to\n * write itself a rule telling its agents to DISOBEY this block to work around it.\n *\n * What the old ordering bought was overlap on a single local file write, not a subagent round-trip.\n *\n * The schema hint is rendered by ReviewJsonService — the single renderer — so the shape printed here\n * can never drift from the shape `wp-finish-upsert-pr` validates.\n */\n private nextSteps(input: ReviewReportInput): string {\n const required = this.requiredOwed(input);\n const offerable = this.offerableOwed(input);\n // Numbered by what is actually PRINTED, so the numbers a reader sees are 1..n with no gaps: write\n // review.json, then a spawn step only if anything must run, then an offer step only if anything may.\n let step = 1;\n const write = this.writeReviewStep(input.reviewPath, step++);\n const spawn = required.length === 0 ? '' : this.spawnStep(input, required, step++);\n const offer = offerable.length === 0 ? '' : this.offerStep(input, offerable, step++);\n return '\\n' + SEP\n + `▶ NEXT — ${step} steps, in this order. Step 1 is NOT optional:\\n` + SEP + '\\n'\n + write + spawn + offer + this.finishStep(step, required.length + offerable.length > 0);\n }\n\n private writeReviewStep(reviewPath: string, step: number): string {\n return (\n `STEP ${step} — review your own changes, then write the review file. Write it FIRST — BEFORE you spawn\\n` +\n ' anything below. finish REFUSES without it, and a reviewer subagent may READ it: a\\n' +\n ' checklist that judges the PR title, summary or risk level reads exactly this file, so\\n' +\n ' writing it afterwards races that reviewer into seeing nothing — or, on a re-run of this\\n' +\n ' stage, into judging the PREVIOUS run\\'s review of code that has since changed.\\n\\n' +\n reviewJsonSchemaHint(reviewPath) + '\\n\\n'\n );\n }\n\n /**\n * The REQUIRED reviewers — one copy-paste block each, and nothing at all when none is owed. The prompt is\n * deliberately a POINTER and nothing else: the generated instructions file is the contract, so anything\n * restated here is a second copy that can go stale — which is exactly how a removed `success` field\n * outlived its own removal in print.\n *\n * These are spawned WITHOUT asking. They are the checklists the repo declared `required: true`, which is\n * the repo saying the decision was already made; putting them to the human again would re-open a question\n * the config exists to settle.\n */\n private spawnStep(input: ReviewReportInput, owed: readonly ReviewerBriefing[], step: number): string {\n const lines: string[] = [\n `STEP ${step} — only once that file is written, spawn these ${owed.length} REQUIRED reviewer subagent(s) — a`,\n ' SEPARATE one each. They block the PR, so do NOT ask whether to run them. You may NOT',\n ' review your own work, and you may NOT write a reviewer\\'s verdict file on its behalf.',\n '',\n ];\n lines.push(...this.refusedWarning(input, owed));\n for (const b of owed) lines.push(...this.oneSpawnBlock(input, b));\n lines.push('');\n return lines.join('\\n');\n }\n\n /**\n * STEP n — the OPTIONAL reviewers: listed, never spawned unasked.\n *\n * This is the whole point of `required: false`. A one-line bug fix in a repo whose checklists key on a\n * glob as broad as every TypeScript file otherwise pays for a dozen subagent reviews, and the only party\n * who can judge whether this particular diff is worth them is the human looking at it.\n *\n * ONE batched multi-select question, explicitly. Asked one at a time, a human answering \"no\" nine times\n * is being worn down rather than consulted, and by the third question the cheap thing is to say yes to\n * everything — which is the state this feature exists to leave. The \"None\" option has to be spelled out\n * too: an agent that offers a list without an explicit way to decline it has not really offered a choice.\n *\n * The blocking consequence is stated because it is the one non-obvious part of the contract: `required`\n * governs whether a reviewer must RUN, not whether its answer counts. Choosing to run one and then\n * shrugging off a red verdict would make the whole exercise theater.\n */\n private offerStep(input: ReviewReportInput, offerable: readonly ReviewerBriefing[], step: number): string {\n const lines: string[] = [\n `STEP ${step} — these ${offerable.length} OPTIONAL review checklist(s) matched this diff. They do NOT block the`,\n ' PR, and you may NOT decide for the human whether to run them.',\n '',\n ' ASK THE HUMAN, in ONE multi-select question listing all of them plus an explicit',\n ' \"None — required only\" choice. Do not ask one question per reviewer. Then spawn ONLY',\n ' what they picked, the same way as any other reviewer.',\n '',\n ' If they pick none, that is a complete answer: go straight to the final step. If they',\n ' told you up front to submit without reviews, re-run this stage as',\n ' `pnpm wp-review-upsert-pr --no-optional` and this step disappears.',\n '',\n ' NOTE: whichever ones you DO run, their verdicts count in full — a red verdict from an',\n ' optional reviewer blocks the PR exactly like a required one.',\n '',\n ];\n lines.push(...this.refusedWarning(input, offerable));\n for (const b of offerable) lines.push(...this.oneSpawnBlock(input, b));\n lines.push('');\n return lines.join('\\n');\n }\n\n // Said up front, not only beside the block: an agent that has decided to spawn everything listed here\n // needs to know BEFORE it starts that one of these entries is not a spawn-shaped task. Scoped to the\n // group being printed — a refusal among the REQUIRED reviewers is not a caveat on the optional list.\n private refusedWarning(input: ReviewReportInput, group: readonly ReviewerBriefing[]): string[] {\n const ids = new Set(group.map((b: ReviewerBriefing): string => b.checklistId));\n const n = input.refused.filter((r: RefusedReviewer): boolean => ids.has(r.checklistId)).length;\n if (n === 0) return [];\n return [\n ` ${n} of them already ANSWERED and refused (marked ⛔ below). Do not spawn`,\n ' those against unchanged code — fix what they found first; the fix is the prerequisite.',\n '',\n ];\n }\n\n private finishStep(stepNumber: number, anyReviewers: boolean): string {\n // \"every reviewer you ran\" rather than \"every reviewer above\": with an optional list the human may\n // legitimately have run none of them, and a precondition naming reviewers that were declined reads as\n // an unmeetable one.\n const precondition = anyReviewers\n ? 'once every reviewer you ran has written its verdict file'\n : 'once that file exists';\n return (\n `STEP ${stepNumber} — only ${precondition}, run: pnpm wp-finish-upsert-pr\\n` +\n ' (The build gate is already green for this commit — finish reuses it unless HEAD moves.)\\n\\n'\n );\n }\n\n // The briefings with no passing verdict yet — the ONE definition of \"owed\", shared by the header, the\n // scan verdict and the step numbering, so they cannot disagree about whether there is anything to spawn.\n private owedReviewers(input: ReviewReportInput): ReviewerBriefing[] {\n const reviewedIds = new Set(input.reviewed.map((r: RequiredChecklist): string => r.id));\n return input.briefings.filter((b: ReviewerBriefing): boolean => !reviewedIds.has(b.checklistId));\n }\n\n // Owed AND blocking — spawned without asking.\n private requiredOwed(input: ReviewReportInput): ReviewerBriefing[] {\n return this.owedReviewers(input).filter((b: ReviewerBriefing): boolean => b.required);\n }\n\n // Owed and optional. Still listed under `--no-optional` (as a skip verdict), just never as a step.\n private optionalOwed(input: ReviewReportInput): ReviewerBriefing[] {\n return this.owedReviewers(input).filter((b: ReviewerBriefing): boolean => !b.required);\n }\n\n // The optional ones the human is actually to be ASKED about — none, once they have already answered.\n private offerableOwed(input: ReviewReportInput): ReviewerBriefing[] {\n return input.skipOptional ? [] : this.optionalOwed(input);\n }\n\n // Everything the AI still has to act on. Distinct from `owedReviewers`: a skipped optional checklist is\n // owed a verdict it will never get, and treating it as pending work is what would print a spawn\n // instruction for a review the human just declined.\n private actionableOwed(input: ReviewReportInput): ReviewerBriefing[] {\n return [...this.requiredOwed(input), ...this.offerableOwed(input)];\n }\n\n /**\n * One reviewer's block. A reviewer that already REFUSED gets the SAME spawn coordinates but a different\n * lead-in, because the action before spawning is different: its own words are printed, and the spawn is\n * explicitly conditioned on having fixed the finding first.\n *\n * It keeps its spawn block rather than being dropped from the list, because the reviewer genuinely does\n * still owe a fresh verdict — dropping it would leave nothing anywhere saying how to get one. What must\n * not happen is a bare \"spawn this\" that reads identically to a reviewer that never ran, which is the\n * loop this exists to break.\n */\n private oneSpawnBlock(input: ReviewReportInput, b: ReviewerBriefing): string[] {\n const instructionsFile = this.reviewerInstructions.pathFor(input.repoRoot, input.featureName, b.subagent);\n return [\n ...this.leadIn(input, b),\n ` subagent_type: ${b.subagent}`,\n ' prompt: Read your instructions file FIRST and follow it exactly:',\n ` ${instructionsFile}`,\n '',\n ];\n }\n\n // The lines above the spawn coordinates: normally just why this reviewer is in scope; for one that\n // already refused, its verdict verbatim plus the order the two actions must happen in.\n private leadIn(input: ReviewReportInput, b: ReviewerBriefing): string[] {\n const refusal = input.refused.find((r: RefusedReviewer): boolean => r.checklistId === b.checklistId);\n if (!refusal) return [` ▶ ${b.subagent} — ${this.why(b)}`, ...this.docLine(b)];\n return [\n ` ⛔ ${b.subagent} — ALREADY REVIEWED THIS BRANCH AND REFUSED. It will refuse again on unchanged code.`,\n ` ${refusal.message}`,\n ' FIX THE FINDING FIRST (or record a human-authored override). ONLY THEN spawn it again, to',\n ' write a fresh verdict:',\n ];\n }\n\n /**\n * The checklist's guidance doc, for OPTIONAL reviewers only.\n *\n * \"4 file(s) matched\" plus a broad glob does not tell a human what the review would actually look AT,\n * and they are being asked to decide exactly that. Omitted for required reviewers: there is no decision\n * to inform there — the reviewer runs either way, and the doc is already in its instructions file.\n */\n private docLine(b: ReviewerBriefing): string[] {\n if (b.required || b.docPath === '') return [];\n return [` reviews against: ${b.docPath}`];\n }\n\n // Why this one is in scope. A patternless checklist is NOT \"matched\" — it always runs, over the whole\n // diff, and saying so is what tells a repo its checklist is firing on docs-only PRs by design.\n private why(b: ReviewerBriefing): string {\n if (b.matchedPatterns.length === 0) {\n return `ALWAYS RUNS (no \"patterns\" configured), whole diff in scope — ${b.myFiles.length} file(s)`;\n }\n return `${b.myFiles.length} file(s) matched ${b.matchedPatterns.map((p: string): string => `\"${p}\"`).join(', ')}`;\n }\n}\n"]}
|