@webpieces/pr-gate 0.4.497 → 0.4.499
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -3
- package/src/dashboard/dashboard.d.ts +15 -0
- package/src/dashboard/dashboard.js +25 -2
- package/src/dashboard/dashboard.js.map +1 -1
- package/src/scripts/commands/cleanup-command.d.ts +42 -3
- package/src/scripts/commands/cleanup-command.js +69 -11
- package/src/scripts/commands/cleanup-command.js.map +1 -1
- package/src/scripts/commands/finish-upsert-pr-command.d.ts +55 -5
- package/src/scripts/commands/finish-upsert-pr-command.js +162 -30
- package/src/scripts/commands/finish-upsert-pr-command.js.map +1 -1
- package/src/scripts/commands/land-pr-command.d.ts +17 -2
- package/src/scripts/commands/land-pr-command.js +32 -3
- package/src/scripts/commands/land-pr-command.js.map +1 -1
- package/src/scripts/commands/review-upsert-pr-command.d.ts +88 -0
- package/src/scripts/commands/review-upsert-pr-command.js +244 -0
- package/src/scripts/commands/review-upsert-pr-command.js.map +1 -0
- package/src/scripts/commands/start-upsert-pr-command.d.ts +10 -15
- package/src/scripts/commands/start-upsert-pr-command.js +41 -47
- package/src/scripts/commands/start-upsert-pr-command.js.map +1 -1
- package/src/scripts/commands/worktree-cleanup.d.ts +45 -0
- package/src/scripts/commands/worktree-cleanup.js +130 -0
- package/src/scripts/commands/worktree-cleanup.js.map +1 -0
- package/src/scripts/pr-gate-app.d.ts +9 -5
- package/src/scripts/pr-gate-app.js +12 -8
- package/src/scripts/pr-gate-app.js.map +1 -1
- package/src/scripts/workflow/checklist-scanner.d.ts +27 -11
- package/src/scripts/workflow/checklist-scanner.js +37 -15
- package/src/scripts/workflow/checklist-scanner.js.map +1 -1
- package/src/scripts/workflow/diff-basis.d.ts +53 -0
- package/src/scripts/workflow/diff-basis.js +113 -0
- package/src/scripts/workflow/diff-basis.js.map +1 -0
- package/src/scripts/workflow/diff-materializer.d.ts +85 -0
- package/src/scripts/workflow/diff-materializer.js +253 -0
- package/src/scripts/workflow/diff-materializer.js.map +1 -0
- package/src/scripts/workflow/git-findForkPoint.d.ts +1 -1
- package/src/scripts/workflow/git-findForkPoint.js +1 -1
- package/src/scripts/workflow/git-findForkPoint.js.map +1 -1
- package/src/scripts/workflow/pr-context-writer.d.ts +16 -10
- package/src/scripts/workflow/pr-context-writer.js +20 -14
- package/src/scripts/workflow/pr-context-writer.js.map +1 -1
- package/src/scripts/workflow/review-stage-receipt.d.ts +36 -0
- package/src/scripts/workflow/review-stage-receipt.js +82 -0
- package/src/scripts/workflow/review-stage-receipt.js.map +1 -0
- package/src/scripts/workflow/reviewer-briefing-builder.d.ts +36 -0
- package/src/scripts/workflow/reviewer-briefing-builder.js +104 -0
- package/src/scripts/workflow/reviewer-briefing-builder.js.map +1 -0
- package/src/scripts/{wp-checklist.js → wp-review-upsert-pr.js} +3 -3
- package/src/scripts/wp-review-upsert-pr.js.map +1 -0
- package/src/scripts/commands/checklist-command.d.ts +0 -27
- package/src/scripts/commands/checklist-command.js +0 -97
- package/src/scripts/commands/checklist-command.js.map +0 -1
- package/src/scripts/wp-checklist.js.map +0 -1
- /package/src/scripts/{wp-checklist.d.ts → wp-review-upsert-pr.d.ts} +0 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DiffBasisResolver = exports.DiffBasis = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const child_process_1 = require("child_process");
|
|
6
|
+
const inversify_1 = require("inversify");
|
|
7
|
+
const git_findForkPoint_1 = require("./git-findForkPoint");
|
|
8
|
+
/**
|
|
9
|
+
* The ONE resolved answer to "what exactly is this branch's diff, and what command reproduces it?"
|
|
10
|
+
*
|
|
11
|
+
* It exists because the two halves of the review flow used to disagree. `ChecklistScanner` computes the
|
|
12
|
+
* changed-FILE set by calling `getChangedFiles` with NO head — base→WORKING TREE, unioning in untracked
|
|
13
|
+
* files — while `ChecklistInstructionsService` printed `git diff <base> HEAD -- <file>` to every
|
|
14
|
+
* reviewer. On a dirty tree those are different ranges, and the printed command returns NOTHING: a reviewer
|
|
15
|
+
* is handed a file list and a command that shows it nothing. That is not hypothetical — a reviewer subagent
|
|
16
|
+
* on monorepo-nx2 ran the printed command, got empty output, and had to guess its way to `git diff HEAD`.
|
|
17
|
+
*
|
|
18
|
+
* So the command is DERIVED from the same basis the file set is, and no caller writes one by hand.
|
|
19
|
+
*
|
|
20
|
+
* Data-only (per CLAUDE.md).
|
|
21
|
+
*/
|
|
22
|
+
class DiffBasis {
|
|
23
|
+
// The 3-point fork point of main. '' when neither origin/main nor main resolves.
|
|
24
|
+
base;
|
|
25
|
+
// The REAL sha, never the literal string 'HEAD'. A recorded 'HEAD' is worthless the moment a commit
|
|
26
|
+
// lands on top of it, and the stage receipt has to compare shas to know the tree moved under a review.
|
|
27
|
+
headSha;
|
|
28
|
+
// True when the diff includes staged/unstaged/untracked work, i.e. the range is base→working tree.
|
|
29
|
+
dirty;
|
|
30
|
+
// Exactly WHICH paths make it dirty, so a message can name them instead of asserting dirtiness.
|
|
31
|
+
dirtyFiles;
|
|
32
|
+
// The command that reproduces the WHOLE diff. Correct for both clean and dirty; see DiffBasisResolver.
|
|
33
|
+
diffCommand;
|
|
34
|
+
// The same command with a `-- <file>` tail — what every reviewer instruction prints.
|
|
35
|
+
fileDiffCommand;
|
|
36
|
+
// eslint-disable-next-line @typescript-eslint/max-params
|
|
37
|
+
constructor(base = '', headSha = '', dirty = false, dirtyFiles = [], diffCommand = '', fileDiffCommand = '') {
|
|
38
|
+
this.base = base;
|
|
39
|
+
this.headSha = headSha;
|
|
40
|
+
this.dirty = dirty;
|
|
41
|
+
this.dirtyFiles = dirtyFiles;
|
|
42
|
+
this.diffCommand = diffCommand;
|
|
43
|
+
this.fileDiffCommand = fileDiffCommand;
|
|
44
|
+
}
|
|
45
|
+
// True when there is no usable base — callers must SAY so rather than print a command that cannot work.
|
|
46
|
+
get unresolved() {
|
|
47
|
+
return this.base.trim() === '';
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
exports.DiffBasis = DiffBasis;
|
|
51
|
+
/**
|
|
52
|
+
* Resolves {@link DiffBasis} for a repo. Injects {@link ForkPoint} so the base is the SAME fork-point
|
|
53
|
+
* computation the checklist matching uses — never `DiffScope.resolveBase`, which overlays NX_BASE/NX_HEAD
|
|
54
|
+
* from the environment and would make review coverage depend on an env var.
|
|
55
|
+
*
|
|
56
|
+
* `@injectable(bindingScopeValues.Singleton)` so it is injected by type and drawn in the DI design.
|
|
57
|
+
*/
|
|
58
|
+
let DiffBasisResolver = class DiffBasisResolver {
|
|
59
|
+
forkPoint;
|
|
60
|
+
constructor(forkPoint) {
|
|
61
|
+
this.forkPoint = forkPoint;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Resolve the basis. The command shape is the whole point:
|
|
65
|
+
*
|
|
66
|
+
* clean tree → `git diff <base> <headSha>` (commit-to-commit; reproducible later, unlike 'HEAD')
|
|
67
|
+
* dirty tree → `git diff <base>` (NO head — this is what includes the working tree)
|
|
68
|
+
*
|
|
69
|
+
* Omitting the head on a dirty tree is not a shortcut, it is the only form that shows uncommitted work,
|
|
70
|
+
* and supplying `HEAD` there is precisely the bug this class exists to make unrepresentable.
|
|
71
|
+
*/
|
|
72
|
+
resolve(repoRoot) {
|
|
73
|
+
const base = this.forkPoint.resolveForkPoint(repoRoot);
|
|
74
|
+
const headSha = this.gitOut(repoRoot, ['rev-parse', 'HEAD']);
|
|
75
|
+
const dirtyFiles = this.dirtyPaths(repoRoot);
|
|
76
|
+
const dirty = dirtyFiles.length > 0;
|
|
77
|
+
if (base === '')
|
|
78
|
+
return new DiffBasis('', headSha, dirty, dirtyFiles);
|
|
79
|
+
const whole = dirty ? `git diff ${base}` : `git diff ${base} ${headSha}`;
|
|
80
|
+
return new DiffBasis(base, headSha, dirty, dirtyFiles, whole, `${whole} -- <file>`);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Every path that makes the tree dirty: tracked modifications/staged changes PLUS untracked files.
|
|
84
|
+
* Untracked must be included — `git status --porcelain` reports them as `??`, and they are exactly the
|
|
85
|
+
* files a brand-new migration or terraform rule arrives as, which is when a checklist most wants to fire.
|
|
86
|
+
*/
|
|
87
|
+
dirtyPaths(repoRoot) {
|
|
88
|
+
const out = this.gitOut(repoRoot, ['status', '--porcelain', '--untracked-files=all']);
|
|
89
|
+
if (out === '')
|
|
90
|
+
return [];
|
|
91
|
+
const paths = [];
|
|
92
|
+
for (const line of out.split('\n')) {
|
|
93
|
+
// Porcelain v1: 2 status chars, a space, then the path. A rename reads `R old -> new`; the new
|
|
94
|
+
// name is the one that exists on disk, so it is the one a reviewer can open.
|
|
95
|
+
const raw = line.slice(3).trim();
|
|
96
|
+
if (raw === '')
|
|
97
|
+
continue;
|
|
98
|
+
const arrow = raw.indexOf(' -> ');
|
|
99
|
+
paths.push(arrow === -1 ? raw : raw.slice(arrow + 4));
|
|
100
|
+
}
|
|
101
|
+
return paths;
|
|
102
|
+
}
|
|
103
|
+
gitOut(repoRoot, args) {
|
|
104
|
+
const result = (0, child_process_1.spawnSync)('git', args, { cwd: repoRoot, encoding: 'utf8' });
|
|
105
|
+
return result.status === 0 ? (result.stdout ?? '').trim() : '';
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
exports.DiffBasisResolver = DiffBasisResolver;
|
|
109
|
+
exports.DiffBasisResolver = DiffBasisResolver = tslib_1.__decorate([
|
|
110
|
+
(0, inversify_1.injectable)(inversify_1.bindingScopeValues.Singleton),
|
|
111
|
+
tslib_1.__metadata("design:paramtypes", [git_findForkPoint_1.ForkPoint])
|
|
112
|
+
], DiffBasisResolver);
|
|
113
|
+
//# sourceMappingURL=diff-basis.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"diff-basis.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/diff-basis.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawnSync } from 'child_process';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { ForkPoint } from './git-findForkPoint';\n\n/**\n * The ONE resolved answer to \"what exactly is this branch's diff, and what command reproduces it?\"\n *\n * It exists because the two halves of the review flow used to disagree. `ChecklistScanner` computes the\n * changed-FILE set by calling `getChangedFiles` with NO head — base→WORKING TREE, unioning in untracked\n * files — while `ChecklistInstructionsService` printed `git diff <base> HEAD -- <file>` to every\n * reviewer. On a dirty tree those are different ranges, and the printed command returns NOTHING: a reviewer\n * is handed a file list and a command that shows it nothing. That is not hypothetical — a reviewer subagent\n * on monorepo-nx2 ran the printed command, got empty output, and had to guess its way to `git diff HEAD`.\n *\n * So the command is DERIVED from the same basis the file set is, and no caller writes one by hand.\n *\n * Data-only (per CLAUDE.md).\n */\nexport class DiffBasis {\n // The 3-point fork point of main. '' when neither origin/main nor main resolves.\n base: string;\n // The REAL sha, never the literal string 'HEAD'. A recorded 'HEAD' is worthless the moment a commit\n // lands on top of it, and the stage receipt has to compare shas to know the tree moved under a review.\n headSha: string;\n // True when the diff includes staged/unstaged/untracked work, i.e. the range is base→working tree.\n dirty: boolean;\n // Exactly WHICH paths make it dirty, so a message can name them instead of asserting dirtiness.\n dirtyFiles: string[];\n // The command that reproduces the WHOLE diff. Correct for both clean and dirty; see DiffBasisResolver.\n diffCommand: string;\n // The same command with a `-- <file>` tail — what every reviewer instruction prints.\n fileDiffCommand: string;\n\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(base = '', headSha = '', dirty = false, dirtyFiles: string[] = [], diffCommand = '', fileDiffCommand = '') {\n this.base = base;\n this.headSha = headSha;\n this.dirty = dirty;\n this.dirtyFiles = dirtyFiles;\n this.diffCommand = diffCommand;\n this.fileDiffCommand = fileDiffCommand;\n }\n\n // True when there is no usable base — callers must SAY so rather than print a command that cannot work.\n get unresolved(): boolean {\n return this.base.trim() === '';\n }\n}\n\n/**\n * Resolves {@link DiffBasis} for a repo. Injects {@link ForkPoint} so the base is the SAME fork-point\n * computation the checklist matching uses — never `DiffScope.resolveBase`, which overlays NX_BASE/NX_HEAD\n * from the environment and would make review coverage depend on an env var.\n *\n * `@injectable(bindingScopeValues.Singleton)` so it is injected by type and drawn in the DI design.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class DiffBasisResolver {\n constructor(private readonly forkPoint: ForkPoint) {}\n\n /**\n * Resolve the basis. The command shape is the whole point:\n *\n * clean tree → `git diff <base> <headSha>` (commit-to-commit; reproducible later, unlike 'HEAD')\n * dirty tree → `git diff <base>` (NO head — this is what includes the working tree)\n *\n * Omitting the head on a dirty tree is not a shortcut, it is the only form that shows uncommitted work,\n * and supplying `HEAD` there is precisely the bug this class exists to make unrepresentable.\n */\n resolve(repoRoot: string): DiffBasis {\n const base = this.forkPoint.resolveForkPoint(repoRoot);\n const headSha = this.gitOut(repoRoot, ['rev-parse', 'HEAD']);\n const dirtyFiles = this.dirtyPaths(repoRoot);\n const dirty = dirtyFiles.length > 0;\n if (base === '') return new DiffBasis('', headSha, dirty, dirtyFiles);\n const whole = dirty ? `git diff ${base}` : `git diff ${base} ${headSha}`;\n return new DiffBasis(base, headSha, dirty, dirtyFiles, whole, `${whole} -- <file>`);\n }\n\n /**\n * Every path that makes the tree dirty: tracked modifications/staged changes PLUS untracked files.\n * Untracked must be included — `git status --porcelain` reports them as `??`, and they are exactly the\n * files a brand-new migration or terraform rule arrives as, which is when a checklist most wants to fire.\n */\n private dirtyPaths(repoRoot: string): string[] {\n const out = this.gitOut(repoRoot, ['status', '--porcelain', '--untracked-files=all']);\n if (out === '') return [];\n const paths: string[] = [];\n for (const line of out.split('\\n')) {\n // Porcelain v1: 2 status chars, a space, then the path. A rename reads `R old -> new`; the new\n // name is the one that exists on disk, so it is the one a reviewer can open.\n const raw = line.slice(3).trim();\n if (raw === '') continue;\n const arrow = raw.indexOf(' -> ');\n paths.push(arrow === -1 ? raw : raw.slice(arrow + 4));\n }\n return paths;\n }\n\n private gitOut(repoRoot: string, args: string[]): string {\n const result = spawnSync('git', args, { cwd: repoRoot, encoding: 'utf8' });\n return result.status === 0 ? (result.stdout ?? '').trim() : '';\n }\n}\n"]}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { ReviewJsonService } from '@webpieces/rules-config';
|
|
2
|
+
import { DiffBasis } from './diff-basis';
|
|
3
|
+
export declare const FILE_DIFF_MAX_BYTES: number;
|
|
4
|
+
export declare const ALL_DIFF_MAX_BYTES: number;
|
|
5
|
+
/** One materialized file's diff. Data-only (per CLAUDE.md). */
|
|
6
|
+
export declare class DiffManifestEntry {
|
|
7
|
+
file: string;
|
|
8
|
+
diffFile: string;
|
|
9
|
+
status: string;
|
|
10
|
+
bytes: number;
|
|
11
|
+
truncated: boolean;
|
|
12
|
+
constructor(file: string, diffFile: string, status: string, bytes: number, truncated?: boolean);
|
|
13
|
+
}
|
|
14
|
+
/** The authoritative map from a mangled `.diff` name back to its real path, plus what was dropped. */
|
|
15
|
+
export declare class DiffManifest {
|
|
16
|
+
base: string;
|
|
17
|
+
head: string;
|
|
18
|
+
dirty: boolean;
|
|
19
|
+
diffCommand: string;
|
|
20
|
+
entries: DiffManifestEntry[];
|
|
21
|
+
excluded: string[];
|
|
22
|
+
omittedFromAllDiff: string[];
|
|
23
|
+
constructor(base: string, head: string, dirty: boolean, diffCommand: string, entries?: DiffManifestEntry[], excluded?: string[], omittedFromAllDiff?: string[]);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Extracts this branch's diff ONCE, to disk, so a reviewer subagent reads it instead of reconstructing it.
|
|
27
|
+
*
|
|
28
|
+
* The motivating measurement: a reviewer on monorepo-nx2 spent 14 of its 26 tool calls on context
|
|
29
|
+
* archaeology, and the very first `git diff` it was told to run returned nothing (see {@link DiffBasis}).
|
|
30
|
+
* Handing it a file it can simply open removes both failure modes — the empty range and the shell-out.
|
|
31
|
+
*
|
|
32
|
+
* Cost discipline: **two git invocations total, regardless of file count.** One `git diff` for everything,
|
|
33
|
+
* split on `diff --git` headers, plus one `ls-files --others` for untracked files. A per-file `git diff`
|
|
34
|
+
* loop would be N spawns for the same bytes.
|
|
35
|
+
*
|
|
36
|
+
* `@injectable(bindingScopeValues.Singleton)` so it is injected by type and drawn in the DI design.
|
|
37
|
+
*/
|
|
38
|
+
export declare class DiffMaterializer {
|
|
39
|
+
private readonly reviewJsonService;
|
|
40
|
+
constructor(reviewJsonService: ReviewJsonService);
|
|
41
|
+
/** The dir this writes into: `.webpieces/pr-review/<feature>/diff`. */
|
|
42
|
+
diffDirFor(repoRoot: string, featureName: string): string;
|
|
43
|
+
/**
|
|
44
|
+
* Write `diff/ALL.diff`, `diff/files/*.diff` and `diff/manifest.json`. Returns the manifest.
|
|
45
|
+
*
|
|
46
|
+
* `changedFiles` is the set the CHECKLIST MATCHING used — passed in rather than recomputed, so the diff
|
|
47
|
+
* a reviewer reads covers exactly the files its checklist matched on. Recomputing here is how the two
|
|
48
|
+
* would drift apart again.
|
|
49
|
+
*/
|
|
50
|
+
materialize(repoRoot: string, featureName: string, basis: DiffBasis, changedFiles: readonly string[], excludeGlobs: readonly string[]): DiffManifest;
|
|
51
|
+
/**
|
|
52
|
+
* ONE `git diff` for the whole branch, split into per-file patches keyed by repo-relative path.
|
|
53
|
+
* Untracked files never appear in `git diff`, so they are synthesized as all-added patches — the same
|
|
54
|
+
* treatment DiffScope.getFileDiff already gives them, kept consistent so a reviewer sees one format.
|
|
55
|
+
*/
|
|
56
|
+
private captureByFile;
|
|
57
|
+
/**
|
|
58
|
+
* The b-side path out of a `diff --git a/x b/x` header. The b-side is the right one: for a rename it is
|
|
59
|
+
* the name that exists on disk, which is the file a reviewer can actually open.
|
|
60
|
+
*/
|
|
61
|
+
private pathFromHeader;
|
|
62
|
+
private untrackedFiles;
|
|
63
|
+
private syntheticAddedDiff;
|
|
64
|
+
/** Write ONE file's diff, recording it (or its exclusion) in the manifest. Never drops a file silently. */
|
|
65
|
+
private writeOne;
|
|
66
|
+
private statusOf;
|
|
67
|
+
/**
|
|
68
|
+
* Truncate to the cap WITH a footer. Never silently: a truncated diff that looks whole is how a reviewer
|
|
69
|
+
* reports "reviewed" on a tenth of a change — the same trap `formatFileList` states its dropped count for.
|
|
70
|
+
*/
|
|
71
|
+
private capped;
|
|
72
|
+
/**
|
|
73
|
+
* `ALL.diff` — the single Read that answers "what changed on this branch?". Files are appended smallest
|
|
74
|
+
* first so a cap drops the fewest of them, and anything dropped is named in a footer AND in the manifest.
|
|
75
|
+
*/
|
|
76
|
+
private writeAllDiff;
|
|
77
|
+
/**
|
|
78
|
+
* `a/b/c.ts` → `a__b__c.ts.diff`, reusing the SAME `/`→`__` convention MergeState.perFileContextDir
|
|
79
|
+
* already uses for 3-point merge context. One mangling scheme in the repo, not two. A real `__` in a
|
|
80
|
+
* path can collide, so collisions get a numeric suffix and manifest.json stays authoritative.
|
|
81
|
+
*/
|
|
82
|
+
private safeName;
|
|
83
|
+
private writeManifest;
|
|
84
|
+
private gitOut;
|
|
85
|
+
}
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DiffMaterializer = exports.DiffManifest = exports.DiffManifestEntry = exports.ALL_DIFF_MAX_BYTES = exports.FILE_DIFF_MAX_BYTES = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const child_process_1 = require("child_process");
|
|
6
|
+
const fs = tslib_1.__importStar(require("fs"));
|
|
7
|
+
const path = tslib_1.__importStar(require("path"));
|
|
8
|
+
const rules_config_1 = require("@webpieces/rules-config");
|
|
9
|
+
const inversify_1 = require("inversify");
|
|
10
|
+
// Per-file cap. Over it, the file's diff is truncated WITH a footer naming the command that gets the rest.
|
|
11
|
+
exports.FILE_DIFF_MAX_BYTES = 256 * 1024;
|
|
12
|
+
// Cap for the combined ALL.diff. Over it, whole files are omitted — again, named, never silently.
|
|
13
|
+
exports.ALL_DIFF_MAX_BYTES = 2 * 1024 * 1024;
|
|
14
|
+
/** One materialized file's diff. Data-only (per CLAUDE.md). */
|
|
15
|
+
class DiffManifestEntry {
|
|
16
|
+
file; // repo-relative source path
|
|
17
|
+
diffFile; // repo-relative path of the .diff under diff/files/
|
|
18
|
+
status; // 'M' | 'A' | 'D' | 'U' (untracked) | 'X' (excluded by config)
|
|
19
|
+
bytes;
|
|
20
|
+
truncated;
|
|
21
|
+
// eslint-disable-next-line @typescript-eslint/max-params
|
|
22
|
+
constructor(file, diffFile, status, bytes, truncated = false) {
|
|
23
|
+
this.file = file;
|
|
24
|
+
this.diffFile = diffFile;
|
|
25
|
+
this.status = status;
|
|
26
|
+
this.bytes = bytes;
|
|
27
|
+
this.truncated = truncated;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
exports.DiffManifestEntry = DiffManifestEntry;
|
|
31
|
+
/** The authoritative map from a mangled `.diff` name back to its real path, plus what was dropped. */
|
|
32
|
+
class DiffManifest {
|
|
33
|
+
base;
|
|
34
|
+
head;
|
|
35
|
+
dirty;
|
|
36
|
+
diffCommand;
|
|
37
|
+
entries;
|
|
38
|
+
excluded; // matched pr-gate.reviewDiffExclude — present, stubbed, NOT materialized
|
|
39
|
+
omittedFromAllDiff; // too big for ALL.diff; their per-file .diff still exists
|
|
40
|
+
// eslint-disable-next-line @typescript-eslint/max-params
|
|
41
|
+
constructor(base, head, dirty, diffCommand, entries = [], excluded = [], omittedFromAllDiff = []) {
|
|
42
|
+
this.base = base;
|
|
43
|
+
this.head = head;
|
|
44
|
+
this.dirty = dirty;
|
|
45
|
+
this.diffCommand = diffCommand;
|
|
46
|
+
this.entries = entries;
|
|
47
|
+
this.excluded = excluded;
|
|
48
|
+
this.omittedFromAllDiff = omittedFromAllDiff;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
exports.DiffManifest = DiffManifest;
|
|
52
|
+
/**
|
|
53
|
+
* Extracts this branch's diff ONCE, to disk, so a reviewer subagent reads it instead of reconstructing it.
|
|
54
|
+
*
|
|
55
|
+
* The motivating measurement: a reviewer on monorepo-nx2 spent 14 of its 26 tool calls on context
|
|
56
|
+
* archaeology, and the very first `git diff` it was told to run returned nothing (see {@link DiffBasis}).
|
|
57
|
+
* Handing it a file it can simply open removes both failure modes — the empty range and the shell-out.
|
|
58
|
+
*
|
|
59
|
+
* Cost discipline: **two git invocations total, regardless of file count.** One `git diff` for everything,
|
|
60
|
+
* split on `diff --git` headers, plus one `ls-files --others` for untracked files. A per-file `git diff`
|
|
61
|
+
* loop would be N spawns for the same bytes.
|
|
62
|
+
*
|
|
63
|
+
* `@injectable(bindingScopeValues.Singleton)` so it is injected by type and drawn in the DI design.
|
|
64
|
+
*/
|
|
65
|
+
let DiffMaterializer = class DiffMaterializer {
|
|
66
|
+
reviewJsonService;
|
|
67
|
+
constructor(reviewJsonService) {
|
|
68
|
+
this.reviewJsonService = reviewJsonService;
|
|
69
|
+
}
|
|
70
|
+
/** The dir this writes into: `.webpieces/pr-review/<feature>/diff`. */
|
|
71
|
+
diffDirFor(repoRoot, featureName) {
|
|
72
|
+
return path.join(this.reviewJsonService.prDirFor(repoRoot, featureName), 'diff');
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Write `diff/ALL.diff`, `diff/files/*.diff` and `diff/manifest.json`. Returns the manifest.
|
|
76
|
+
*
|
|
77
|
+
* `changedFiles` is the set the CHECKLIST MATCHING used — passed in rather than recomputed, so the diff
|
|
78
|
+
* a reviewer reads covers exactly the files its checklist matched on. Recomputing here is how the two
|
|
79
|
+
* would drift apart again.
|
|
80
|
+
*/
|
|
81
|
+
materialize(repoRoot, featureName, basis, changedFiles, excludeGlobs) {
|
|
82
|
+
const dir = this.diffDirFor(repoRoot, featureName);
|
|
83
|
+
const filesDir = path.join(dir, 'files');
|
|
84
|
+
fs.rmSync(dir, { recursive: true, force: true }); // a stale diff read as current is worse than none
|
|
85
|
+
fs.mkdirSync(filesDir, { recursive: true });
|
|
86
|
+
const manifest = new DiffManifest(basis.base, basis.headSha, basis.dirty, basis.diffCommand);
|
|
87
|
+
if (basis.unresolved)
|
|
88
|
+
return this.writeManifest(dir, manifest);
|
|
89
|
+
const byFile = this.captureByFile(repoRoot, basis);
|
|
90
|
+
const used = new Set();
|
|
91
|
+
for (const file of [...changedFiles].sort()) {
|
|
92
|
+
this.writeOne(repoRoot, filesDir, file, byFile, used, excludeGlobs, manifest);
|
|
93
|
+
}
|
|
94
|
+
this.writeAllDiff(dir, filesDir, manifest);
|
|
95
|
+
return this.writeManifest(dir, manifest);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* ONE `git diff` for the whole branch, split into per-file patches keyed by repo-relative path.
|
|
99
|
+
* Untracked files never appear in `git diff`, so they are synthesized as all-added patches — the same
|
|
100
|
+
* treatment DiffScope.getFileDiff already gives them, kept consistent so a reviewer sees one format.
|
|
101
|
+
*/
|
|
102
|
+
captureByFile(repoRoot, basis) {
|
|
103
|
+
const args = basis.dirty ? ['diff', basis.base] : ['diff', basis.base, basis.headSha];
|
|
104
|
+
const out = this.gitOut(repoRoot, args);
|
|
105
|
+
const byFile = new Map();
|
|
106
|
+
// Split BEFORE each `diff --git` header while keeping the header on its chunk.
|
|
107
|
+
for (const chunk of out.split(/\n(?=diff --git )/)) {
|
|
108
|
+
if (!chunk.startsWith('diff --git '))
|
|
109
|
+
continue;
|
|
110
|
+
const file = this.pathFromHeader(chunk);
|
|
111
|
+
if (file !== '')
|
|
112
|
+
byFile.set(file, chunk.endsWith('\n') ? chunk : `${chunk}\n`);
|
|
113
|
+
}
|
|
114
|
+
for (const file of this.untrackedFiles(repoRoot, basis)) {
|
|
115
|
+
if (!byFile.has(file))
|
|
116
|
+
byFile.set(file, this.syntheticAddedDiff(repoRoot, file));
|
|
117
|
+
}
|
|
118
|
+
return byFile;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* The b-side path out of a `diff --git a/x b/x` header. The b-side is the right one: for a rename it is
|
|
122
|
+
* the name that exists on disk, which is the file a reviewer can actually open.
|
|
123
|
+
*/
|
|
124
|
+
pathFromHeader(chunk) {
|
|
125
|
+
const header = chunk.slice(0, chunk.indexOf('\n') === -1 ? chunk.length : chunk.indexOf('\n'));
|
|
126
|
+
const match = header.match(/^diff --git a\/(.+) b\/(.+)$/);
|
|
127
|
+
return match ? match[2] : '';
|
|
128
|
+
}
|
|
129
|
+
untrackedFiles(repoRoot, basis) {
|
|
130
|
+
if (!basis.dirty)
|
|
131
|
+
return [];
|
|
132
|
+
const out = this.gitOut(repoRoot, ['ls-files', '--others', '--exclude-standard']);
|
|
133
|
+
return out === '' ? [] : out.split('\n').filter((f) => f.trim() !== '');
|
|
134
|
+
}
|
|
135
|
+
syntheticAddedDiff(repoRoot, file) {
|
|
136
|
+
const abs = path.join(repoRoot, file);
|
|
137
|
+
if (!fs.existsSync(abs))
|
|
138
|
+
return '';
|
|
139
|
+
// webpieces-disable no-unmanaged-exceptions -- chokepoint: an unreadable/binary untracked file yields a note, never a crash
|
|
140
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
141
|
+
try {
|
|
142
|
+
const body = fs.readFileSync(abs, 'utf8').split('\n').map((l) => `+${l}`).join('\n');
|
|
143
|
+
return `diff --git a/${file} b/${file}\nnew file (untracked)\n--- /dev/null\n+++ b/${file}\n${body}\n`;
|
|
144
|
+
}
|
|
145
|
+
catch (err) {
|
|
146
|
+
const error = (0, rules_config_1.toError)(err);
|
|
147
|
+
void error; // a binary/unreadable untracked file gets a NOTE, never a crash
|
|
148
|
+
return `diff --git a/${file} b/${file}\nnew file (untracked, unreadable as text)\n`;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/** Write ONE file's diff, recording it (or its exclusion) in the manifest. Never drops a file silently. */
|
|
152
|
+
// eslint-disable-next-line @typescript-eslint/max-params
|
|
153
|
+
writeOne(repoRoot, filesDir, file, byFile, used, excludeGlobs, manifest) {
|
|
154
|
+
const name = this.safeName(file, used);
|
|
155
|
+
const abs = path.join(filesDir, name);
|
|
156
|
+
const rel = path.relative(repoRoot, abs);
|
|
157
|
+
// Excluded files stay in the manifest and keep a stub. Dropping them entirely would read as "this
|
|
158
|
+
// file did not change"; they are noise to READ, not facts to hide — and they still match checklists.
|
|
159
|
+
if (excludeGlobs.length > 0 && (0, rules_config_1.isPathExcluded)(file, [...excludeGlobs])) {
|
|
160
|
+
const stub = `[excluded from materialization by pr-gate.reviewDiffExclude]\nIf you need it: git diff ${manifest.base} -- ${file}\n`;
|
|
161
|
+
fs.writeFileSync(abs, stub);
|
|
162
|
+
manifest.excluded.push(file);
|
|
163
|
+
manifest.entries.push(new DiffManifestEntry(file, rel, 'X', stub.length));
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
const raw = byFile.get(file) ?? '';
|
|
167
|
+
const status = this.statusOf(file, raw, repoRoot);
|
|
168
|
+
const body = this.capped(raw, file, manifest.base);
|
|
169
|
+
fs.writeFileSync(abs, body);
|
|
170
|
+
manifest.entries.push(new DiffManifestEntry(file, rel, status, body.length, body !== raw));
|
|
171
|
+
}
|
|
172
|
+
statusOf(file, raw, repoRoot) {
|
|
173
|
+
if (raw.includes('\nnew file (untracked)'))
|
|
174
|
+
return 'U';
|
|
175
|
+
if (raw.includes('\nnew file mode '))
|
|
176
|
+
return 'A';
|
|
177
|
+
if (raw.includes('\ndeleted file mode '))
|
|
178
|
+
return 'D';
|
|
179
|
+
return fs.existsSync(path.join(repoRoot, file)) ? 'M' : 'D';
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Truncate to the cap WITH a footer. Never silently: a truncated diff that looks whole is how a reviewer
|
|
183
|
+
* reports "reviewed" on a tenth of a change — the same trap `formatFileList` states its dropped count for.
|
|
184
|
+
*/
|
|
185
|
+
capped(raw, file, base) {
|
|
186
|
+
if (raw.length <= exports.FILE_DIFF_MAX_BYTES)
|
|
187
|
+
return raw;
|
|
188
|
+
return `${raw.slice(0, exports.FILE_DIFF_MAX_BYTES)}\n` +
|
|
189
|
+
`\n… TRUNCATED at ${exports.FILE_DIFF_MAX_BYTES} bytes (full diff is ${raw.length} bytes).\n` +
|
|
190
|
+
` Read the rest with: git diff ${base} -- ${file}\n`;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* `ALL.diff` — the single Read that answers "what changed on this branch?". Files are appended smallest
|
|
194
|
+
* first so a cap drops the fewest of them, and anything dropped is named in a footer AND in the manifest.
|
|
195
|
+
*/
|
|
196
|
+
writeAllDiff(dir, filesDir, manifest) {
|
|
197
|
+
const ordered = [...manifest.entries].sort((a, b) => a.bytes - b.bytes);
|
|
198
|
+
const parts = [];
|
|
199
|
+
let total = 0;
|
|
200
|
+
for (const entry of ordered) {
|
|
201
|
+
const body = fs.readFileSync(path.join(filesDir, path.basename(entry.diffFile)), 'utf8');
|
|
202
|
+
if (total + body.length > exports.ALL_DIFF_MAX_BYTES) {
|
|
203
|
+
manifest.omittedFromAllDiff.push(entry.file);
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
parts.push(body);
|
|
207
|
+
total += body.length;
|
|
208
|
+
}
|
|
209
|
+
let out = parts.join('\n');
|
|
210
|
+
if (manifest.omittedFromAllDiff.length > 0) {
|
|
211
|
+
out += `\n\n… ${manifest.omittedFromAllDiff.length} file(s) OMITTED from this combined view (it hit the ` +
|
|
212
|
+
`${exports.ALL_DIFF_MAX_BYTES}-byte cap). Each still has its own diff under files/ — see manifest.json:\n` +
|
|
213
|
+
manifest.omittedFromAllDiff.map((f) => ` ${f}`).join('\n') + '\n';
|
|
214
|
+
}
|
|
215
|
+
fs.writeFileSync(path.join(dir, 'ALL.diff'), out);
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* `a/b/c.ts` → `a__b__c.ts.diff`, reusing the SAME `/`→`__` convention MergeState.perFileContextDir
|
|
219
|
+
* already uses for 3-point merge context. One mangling scheme in the repo, not two. A real `__` in a
|
|
220
|
+
* path can collide, so collisions get a numeric suffix and manifest.json stays authoritative.
|
|
221
|
+
*/
|
|
222
|
+
safeName(file, used) {
|
|
223
|
+
const flat = `${file.replace(/\//g, '__')}.diff`;
|
|
224
|
+
if (!used.has(flat)) {
|
|
225
|
+
used.add(flat);
|
|
226
|
+
return flat;
|
|
227
|
+
}
|
|
228
|
+
for (let n = 2;; n++) {
|
|
229
|
+
const candidate = `${file.replace(/\//g, '__')}-${n}.diff`;
|
|
230
|
+
if (!used.has(candidate)) {
|
|
231
|
+
used.add(candidate);
|
|
232
|
+
return candidate;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
writeManifest(dir, manifest) {
|
|
237
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
238
|
+
fs.writeFileSync(path.join(dir, 'manifest.json'), JSON.stringify(manifest, null, 2) + '\n');
|
|
239
|
+
return manifest;
|
|
240
|
+
}
|
|
241
|
+
gitOut(repoRoot, args) {
|
|
242
|
+
// maxBuffer: a monorepo diff routinely exceeds node's 1MB default, and the failure mode is a
|
|
243
|
+
// truncated capture that looks like a complete one.
|
|
244
|
+
const result = (0, child_process_1.spawnSync)('git', args, { cwd: repoRoot, encoding: 'utf8', maxBuffer: 256 * 1024 * 1024 });
|
|
245
|
+
return result.status === 0 ? (result.stdout ?? '') : '';
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
exports.DiffMaterializer = DiffMaterializer;
|
|
249
|
+
exports.DiffMaterializer = DiffMaterializer = tslib_1.__decorate([
|
|
250
|
+
(0, inversify_1.injectable)(inversify_1.bindingScopeValues.Singleton),
|
|
251
|
+
tslib_1.__metadata("design:paramtypes", [rules_config_1.ReviewJsonService])
|
|
252
|
+
], DiffMaterializer);
|
|
253
|
+
//# sourceMappingURL=diff-materializer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"diff-materializer.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/diff-materializer.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawnSync } from 'child_process';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { isPathExcluded, ReviewJsonService, toError } from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { DiffBasis } from './diff-basis';\n\n// Per-file cap. Over it, the file's diff is truncated WITH a footer naming the command that gets the rest.\nexport const FILE_DIFF_MAX_BYTES = 256 * 1024;\n// Cap for the combined ALL.diff. Over it, whole files are omitted — again, named, never silently.\nexport const ALL_DIFF_MAX_BYTES = 2 * 1024 * 1024;\n\n/** One materialized file's diff. Data-only (per CLAUDE.md). */\nexport class DiffManifestEntry {\n file: string; // repo-relative source path\n diffFile: string; // repo-relative path of the .diff under diff/files/\n status: string; // 'M' | 'A' | 'D' | 'U' (untracked) | 'X' (excluded by config)\n bytes: number;\n truncated: boolean;\n\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(file: string, diffFile: string, status: string, bytes: number, truncated = false) {\n this.file = file;\n this.diffFile = diffFile;\n this.status = status;\n this.bytes = bytes;\n this.truncated = truncated;\n }\n}\n\n/** The authoritative map from a mangled `.diff` name back to its real path, plus what was dropped. */\nexport class DiffManifest {\n base: string;\n head: string;\n dirty: boolean;\n diffCommand: string;\n entries: DiffManifestEntry[];\n excluded: string[]; // matched pr-gate.reviewDiffExclude — present, stubbed, NOT materialized\n omittedFromAllDiff: string[]; // too big for ALL.diff; their per-file .diff still exists\n\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(\n base: string, head: string, dirty: boolean, diffCommand: string,\n entries: DiffManifestEntry[] = [], excluded: string[] = [], omittedFromAllDiff: string[] = [],\n ) {\n this.base = base;\n this.head = head;\n this.dirty = dirty;\n this.diffCommand = diffCommand;\n this.entries = entries;\n this.excluded = excluded;\n this.omittedFromAllDiff = omittedFromAllDiff;\n }\n}\n\n/**\n * Extracts this branch's diff ONCE, to disk, so a reviewer subagent reads it instead of reconstructing it.\n *\n * The motivating measurement: a reviewer on monorepo-nx2 spent 14 of its 26 tool calls on context\n * archaeology, and the very first `git diff` it was told to run returned nothing (see {@link DiffBasis}).\n * Handing it a file it can simply open removes both failure modes — the empty range and the shell-out.\n *\n * Cost discipline: **two git invocations total, regardless of file count.** One `git diff` for everything,\n * split on `diff --git` headers, plus one `ls-files --others` for untracked files. A per-file `git diff`\n * loop would be N spawns for the same bytes.\n *\n * `@injectable(bindingScopeValues.Singleton)` so it is injected by type and drawn in the DI design.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class DiffMaterializer {\n constructor(private readonly reviewJsonService: ReviewJsonService) {}\n\n /** The dir this writes into: `.webpieces/pr-review/<feature>/diff`. */\n diffDirFor(repoRoot: string, featureName: string): string {\n return path.join(this.reviewJsonService.prDirFor(repoRoot, featureName), 'diff');\n }\n\n /**\n * Write `diff/ALL.diff`, `diff/files/*.diff` and `diff/manifest.json`. Returns the manifest.\n *\n * `changedFiles` is the set the CHECKLIST MATCHING used — passed in rather than recomputed, so the diff\n * a reviewer reads covers exactly the files its checklist matched on. Recomputing here is how the two\n * would drift apart again.\n */\n materialize(repoRoot: string, featureName: string, basis: DiffBasis, changedFiles: readonly string[], excludeGlobs: readonly string[]): DiffManifest {\n const dir = this.diffDirFor(repoRoot, featureName);\n const filesDir = path.join(dir, 'files');\n fs.rmSync(dir, { recursive: true, force: true }); // a stale diff read as current is worse than none\n fs.mkdirSync(filesDir, { recursive: true });\n const manifest = new DiffManifest(basis.base, basis.headSha, basis.dirty, basis.diffCommand);\n if (basis.unresolved) return this.writeManifest(dir, manifest);\n const byFile = this.captureByFile(repoRoot, basis);\n const used = new Set<string>();\n for (const file of [...changedFiles].sort()) {\n this.writeOne(repoRoot, filesDir, file, byFile, used, excludeGlobs, manifest);\n }\n this.writeAllDiff(dir, filesDir, manifest);\n return this.writeManifest(dir, manifest);\n }\n\n /**\n * ONE `git diff` for the whole branch, split into per-file patches keyed by repo-relative path.\n * Untracked files never appear in `git diff`, so they are synthesized as all-added patches — the same\n * treatment DiffScope.getFileDiff already gives them, kept consistent so a reviewer sees one format.\n */\n private captureByFile(repoRoot: string, basis: DiffBasis): Map<string, string> {\n const args = basis.dirty ? ['diff', basis.base] : ['diff', basis.base, basis.headSha];\n const out = this.gitOut(repoRoot, args);\n const byFile = new Map<string, string>();\n // Split BEFORE each `diff --git` header while keeping the header on its chunk.\n for (const chunk of out.split(/\\n(?=diff --git )/)) {\n if (!chunk.startsWith('diff --git ')) continue;\n const file = this.pathFromHeader(chunk);\n if (file !== '') byFile.set(file, chunk.endsWith('\\n') ? chunk : `${chunk}\\n`);\n }\n for (const file of this.untrackedFiles(repoRoot, basis)) {\n if (!byFile.has(file)) byFile.set(file, this.syntheticAddedDiff(repoRoot, file));\n }\n return byFile;\n }\n\n /**\n * The b-side path out of a `diff --git a/x b/x` header. The b-side is the right one: for a rename it is\n * the name that exists on disk, which is the file a reviewer can actually open.\n */\n private pathFromHeader(chunk: string): string {\n const header = chunk.slice(0, chunk.indexOf('\\n') === -1 ? chunk.length : chunk.indexOf('\\n'));\n const match = header.match(/^diff --git a\\/(.+) b\\/(.+)$/);\n return match ? match[2] : '';\n }\n\n private untrackedFiles(repoRoot: string, basis: DiffBasis): string[] {\n if (!basis.dirty) return [];\n const out = this.gitOut(repoRoot, ['ls-files', '--others', '--exclude-standard']);\n return out === '' ? [] : out.split('\\n').filter((f: string): boolean => f.trim() !== '');\n }\n\n private syntheticAddedDiff(repoRoot: string, file: string): string {\n const abs = path.join(repoRoot, file);\n if (!fs.existsSync(abs)) return '';\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: an unreadable/binary untracked file yields a note, never a crash\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const body = fs.readFileSync(abs, 'utf8').split('\\n').map((l: string): string => `+${l}`).join('\\n');\n return `diff --git a/${file} b/${file}\\nnew file (untracked)\\n--- /dev/null\\n+++ b/${file}\\n${body}\\n`;\n } catch (err: unknown) {\n const error = toError(err);\n void error; // a binary/unreadable untracked file gets a NOTE, never a crash\n return `diff --git a/${file} b/${file}\\nnew file (untracked, unreadable as text)\\n`;\n }\n }\n\n /** Write ONE file's diff, recording it (or its exclusion) in the manifest. Never drops a file silently. */\n // eslint-disable-next-line @typescript-eslint/max-params\n private writeOne(\n repoRoot: string, filesDir: string, file: string, byFile: Map<string, string>,\n used: Set<string>, excludeGlobs: readonly string[], manifest: DiffManifest,\n ): void {\n const name = this.safeName(file, used);\n const abs = path.join(filesDir, name);\n const rel = path.relative(repoRoot, abs);\n // Excluded files stay in the manifest and keep a stub. Dropping them entirely would read as \"this\n // file did not change\"; they are noise to READ, not facts to hide — and they still match checklists.\n if (excludeGlobs.length > 0 && isPathExcluded(file, [...excludeGlobs])) {\n const stub = `[excluded from materialization by pr-gate.reviewDiffExclude]\\nIf you need it: git diff ${manifest.base} -- ${file}\\n`;\n fs.writeFileSync(abs, stub);\n manifest.excluded.push(file);\n manifest.entries.push(new DiffManifestEntry(file, rel, 'X', stub.length));\n return;\n }\n const raw = byFile.get(file) ?? '';\n const status = this.statusOf(file, raw, repoRoot);\n const body = this.capped(raw, file, manifest.base);\n fs.writeFileSync(abs, body);\n manifest.entries.push(new DiffManifestEntry(file, rel, status, body.length, body !== raw));\n }\n\n private statusOf(file: string, raw: string, repoRoot: string): string {\n if (raw.includes('\\nnew file (untracked)')) return 'U';\n if (raw.includes('\\nnew file mode ')) return 'A';\n if (raw.includes('\\ndeleted file mode ')) return 'D';\n return fs.existsSync(path.join(repoRoot, file)) ? 'M' : 'D';\n }\n\n /**\n * Truncate to the cap WITH a footer. Never silently: a truncated diff that looks whole is how a reviewer\n * reports \"reviewed\" on a tenth of a change — the same trap `formatFileList` states its dropped count for.\n */\n private capped(raw: string, file: string, base: string): string {\n if (raw.length <= FILE_DIFF_MAX_BYTES) return raw;\n return `${raw.slice(0, FILE_DIFF_MAX_BYTES)}\\n` +\n `\\n… TRUNCATED at ${FILE_DIFF_MAX_BYTES} bytes (full diff is ${raw.length} bytes).\\n` +\n ` Read the rest with: git diff ${base} -- ${file}\\n`;\n }\n\n /**\n * `ALL.diff` — the single Read that answers \"what changed on this branch?\". Files are appended smallest\n * first so a cap drops the fewest of them, and anything dropped is named in a footer AND in the manifest.\n */\n private writeAllDiff(dir: string, filesDir: string, manifest: DiffManifest): void {\n const ordered = [...manifest.entries].sort((a: DiffManifestEntry, b: DiffManifestEntry): number => a.bytes - b.bytes);\n const parts: string[] = [];\n let total = 0;\n for (const entry of ordered) {\n const body = fs.readFileSync(path.join(filesDir, path.basename(entry.diffFile)), 'utf8');\n if (total + body.length > ALL_DIFF_MAX_BYTES) {\n manifest.omittedFromAllDiff.push(entry.file);\n continue;\n }\n parts.push(body);\n total += body.length;\n }\n let out = parts.join('\\n');\n if (manifest.omittedFromAllDiff.length > 0) {\n out += `\\n\\n… ${manifest.omittedFromAllDiff.length} file(s) OMITTED from this combined view (it hit the ` +\n `${ALL_DIFF_MAX_BYTES}-byte cap). Each still has its own diff under files/ — see manifest.json:\\n` +\n manifest.omittedFromAllDiff.map((f: string): string => ` ${f}`).join('\\n') + '\\n';\n }\n fs.writeFileSync(path.join(dir, 'ALL.diff'), out);\n }\n\n /**\n * `a/b/c.ts` → `a__b__c.ts.diff`, reusing the SAME `/`→`__` convention MergeState.perFileContextDir\n * already uses for 3-point merge context. One mangling scheme in the repo, not two. A real `__` in a\n * path can collide, so collisions get a numeric suffix and manifest.json stays authoritative.\n */\n private safeName(file: string, used: Set<string>): string {\n const flat = `${file.replace(/\\//g, '__')}.diff`;\n if (!used.has(flat)) {\n used.add(flat);\n return flat;\n }\n for (let n = 2; ; n++) {\n const candidate = `${file.replace(/\\//g, '__')}-${n}.diff`;\n if (!used.has(candidate)) {\n used.add(candidate);\n return candidate;\n }\n }\n }\n\n private writeManifest(dir: string, manifest: DiffManifest): DiffManifest {\n fs.mkdirSync(dir, { recursive: true });\n fs.writeFileSync(path.join(dir, 'manifest.json'), JSON.stringify(manifest, null, 2) + '\\n');\n return manifest;\n }\n\n private gitOut(repoRoot: string, args: string[]): string {\n // maxBuffer: a monorepo diff routinely exceeds node's 1MB default, and the failure mode is a\n // truncated capture that looks like a complete one.\n const result = spawnSync('git', args, { cwd: repoRoot, encoding: 'utf8', maxBuffer: 256 * 1024 * 1024 });\n return result.status === 0 ? (result.stdout ?? '') : '';\n }\n}\n"]}
|
|
@@ -19,7 +19,7 @@ export declare class ForkPoint {
|
|
|
19
19
|
* and works offline. `findForkPoint` below still fetches, because the MERGE flow additionally needs
|
|
20
20
|
* main's CURRENT head as hash point C, which genuinely does require a fetch.
|
|
21
21
|
*
|
|
22
|
-
* Also writes no files and runs no merge-commit scan, so it cannot throw: `wp-
|
|
22
|
+
* Also writes no files and runs no merge-commit scan, so it cannot throw: `wp-review-upsert-pr` must always
|
|
23
23
|
* succeed, and a raw merge-from-main is the merge flow's problem to report, not the checklist's.
|
|
24
24
|
*/
|
|
25
25
|
resolveForkPoint(repoRoot: string): string;
|
|
@@ -38,7 +38,7 @@ let ForkPoint = class ForkPoint {
|
|
|
38
38
|
* and works offline. `findForkPoint` below still fetches, because the MERGE flow additionally needs
|
|
39
39
|
* main's CURRENT head as hash point C, which genuinely does require a fetch.
|
|
40
40
|
*
|
|
41
|
-
* Also writes no files and runs no merge-commit scan, so it cannot throw: `wp-
|
|
41
|
+
* Also writes no files and runs no merge-commit scan, so it cannot throw: `wp-review-upsert-pr` must always
|
|
42
42
|
* succeed, and a raw merge-from-main is the merge flow's problem to report, not the checklist's.
|
|
43
43
|
*/
|
|
44
44
|
resolveForkPoint(repoRoot) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"git-findForkPoint.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/git-findForkPoint.ts"],"names":[],"mappings":"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","sourcesContent":["import { execSync, spawnSync } from 'child_process';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { prDirFor, CliExitError, RepoRootFinder } from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { AiBranchName } from './git-readAiBranchName';\nimport { MergeState } from './merge-state';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n/** Computes the 3-point fork point and guards against improper merges-from-main. */\n@injectable(bindingScopeValues.Singleton)\nexport class ForkPoint {\n constructor(\n private readonly repoRootFinder: RepoRootFinder,\n private readonly aiBranchName: AiBranchName,\n private readonly mergeState: MergeState,\n ) {}\n\n // Single source of truth for the per-feature dir fork-point output is written to — the SAME nested\n // home the readers use (git-gatherInfo / merge-start read updatemain-hashes.json from mergeDirFor;\n // the review flow reads from prDirFor). Routing the writer through these same helpers is what keeps\n // them from diverging again — guarded by git-findForkPoint.spec.ts.\n forkPointOutputDir(repoRoot: string, featureName: string, workflow: string): string {\n return workflow === 'review' ? prDirFor(repoRoot, featureName) : this.mergeState.mergeDirFor(repoRoot, featureName);\n }\n\n /**\n * The 3-point FORK POINT: where this branch diverged from main. `git merge-base origin/main HEAD`,\n * falling back to a local `main`, and '' when neither ref resolves.\n *\n * Deliberately does NOT fetch. The fork point is ABSOLUTE — advancing main cannot move it, because\n * merge-base is the most recent common ancestor and main's new commits are not on this branch. (Verified\n * on this repo: main went f415456 → ba8f674 → 61432d6 while merge-base with one branch stayed f415456\n * throughout.) So a caller that needs only the fork point — see ChecklistScanner — pays no network cost\n * and works offline. `findForkPoint` below still fetches, because the MERGE flow additionally needs\n * main's CURRENT head as hash point C, which genuinely does require a fetch.\n *\n * Also writes no files and runs no merge-commit scan, so it cannot throw: `wp-checklist` must always\n * succeed, and a raw merge-from-main is the merge flow's problem to report, not the checklist's.\n */\n resolveForkPoint(repoRoot: string): string {\n for (const ref of ['origin/main', 'main']) {\n const result = spawnSync('git', ['merge-base', ref, 'HEAD'], { cwd: repoRoot, encoding: 'utf8' });\n const sha = (result.stdout ?? '').trim();\n if (result.status === 0 && sha !== '') return sha;\n }\n return '';\n }\n\n async findForkPoint(workflow: string): Promise<void> {\n if (workflow !== 'review' && workflow !== 'merge') {\n throw new CliExitError(1,\n 'ERROR: Workflow argument required\\n' +\n 'Usage: git-findForkPoint <workflow>\\n' +\n \" workflow: 'review' or 'merge'\",\n );\n }\n\n const featureName = this.aiBranchName.getFeatureName();\n const currentBranch = execSync('git branch --show-current', { encoding: 'utf8' }).trim();\n const repoRoot = this.repoRootFinder.resolveRepoRoot(process.cwd());\n\n const outputDir = this.forkPointOutputDir(repoRoot, featureName, workflow);\n const prefix = workflow === 'review' ? 'review-' : 'updatemain-';\n fs.mkdirSync(outputDir, { recursive: true });\n\n spawnSync('git', ['fetch', 'origin', 'main'], { stdio: 'ignore' });\n\n const featureHead = execSync('git rev-parse HEAD', { encoding: 'utf8' }).trim();\n const originMain = execSync('git rev-parse origin/main', { encoding: 'utf8' }).trim();\n\n process.stderr.write('Finding fork point using git merge-base...\\n');\n\n // ONE implementation of the fork-point computation, shared with ChecklistScanner. The fetch above is\n // what this flow needs beyond it (hash point C = main's current head), not the fork point itself.\n const forkPoint = this.resolveForkPoint(repoRoot);\n\n if (!forkPoint) {\n throw new CliExitError(1, 'ERROR: Could not find common ancestor with origin/main');\n }\n\n process.stderr.write(`✅ Fork point found: ${forkPoint.slice(0, 7)}\\n`);\n process.stderr.write('Checking for improper merges from main...\\n');\n\n const mergeCommitsResult = spawnSync('git', ['log', `${forkPoint}..HEAD`, '--merges', '--format=%H'], { encoding: 'utf8' });\n const mergeCommitsRaw = (mergeCommitsResult.stdout ?? '').trim();\n const mergeCommits = mergeCommitsRaw ? mergeCommitsRaw.split('\\n') : [];\n\n if (mergeCommits.length > 0) {\n this.checkMergeCommits(mergeCommits, outputDir, prefix, featureName, currentBranch);\n } else {\n process.stderr.write('✅ No merge commits found (clean history)\\n');\n }\n\n const hashesJson = JSON.stringify({\n hashForkPoint: forkPoint,\n hashFeatureHead: featureHead,\n hashMainHead: originMain,\n timestamp: new Date().toISOString(),\n }, null, 2);\n\n fs.writeFileSync(path.join(outputDir, `${prefix}hashes.json`), hashesJson + '\\n');\n process.stderr.write(`✅ Hash points written to: ${outputDir}/${prefix}hashes.json\\n`);\n }\n\n private checkMergeCommits(mergeCommits: string[], outputDir: string, prefix: string, featureName: string, currentBranch: string): void {\n process.stderr.write(`Found ${mergeCommits.length} merge commit(s) to check...\\n`);\n\n for (const commit of mergeCommits) {\n const parentsResult = spawnSync('git', ['rev-list', '--parents', '-n', '1', commit], { encoding: 'utf8' });\n const parents = (parentsResult.stdout ?? '').trim().split(' ').slice(1);\n\n for (const parent of parents) {\n const ancestorCheck = spawnSync('git', ['merge-base', '--is-ancestor', parent, 'origin/main']);\n const reverseCheck = spawnSync('git', ['merge-base', '--is-ancestor', 'origin/main', parent]);\n\n if (ancestorCheck.status === 0 && reverseCheck.status === 0) {\n const errorJson = JSON.stringify({\n error: 'Merge from main detected',\n mergeCommit: commit,\n parentFromMain: parent,\n timestamp: new Date().toISOString(),\n }, null, 2);\n\n fs.writeFileSync(path.join(outputDir, `${prefix}forkpoint-error.json`), errorJson + '\\n');\n this.printMergeFromMainError(commit, parent, featureName, currentBranch);\n throw new CliExitError(1, '');\n }\n }\n }\n process.stderr.write('✅ No improper merges from main detected\\n');\n }\n\n private printMergeFromMainError(commit: string, parent: string, featureName: string, currentBranch: string): void {\n process.stderr.write('\\n');\n process.stderr.write(SEP);\n process.stderr.write('❌ This branch merged main without the gated update (pnpm wp-start-update, or\\n');\n process.stderr.write(' pnpm wp-start-upsert-pr when a PR is open)\\n');\n process.stderr.write(SEP);\n process.stderr.write('\\n');\n process.stderr.write(`Merge commit detected: ${commit}\\n`);\n process.stderr.write(`Parent from main: ${parent}\\n`);\n process.stderr.write('\\n');\n process.stderr.write('This prevents clean squash-merge. To recover, follow these steps:\\n');\n process.stderr.write('\\n');\n process.stderr.write('1. Fetch the latest main (works on the primary repo AND in a worktree):\\n');\n process.stderr.write(' git fetch origin main\\n');\n process.stderr.write('\\n');\n // The \"do NOT `git checkout main`\" caveat is scoped to the WORKTREE case, where it is true.\n // Stated unconditionally it forbids a command that is perfectly good in the primary clone —\n // and that CLAUDE.md's own post-merge cleanup recipe uses — which is how an agent talked\n // itself into believing a new branch was its only exit.\n process.stderr.write('2. Create a new branch OFF origin/main (this form works from ANY tree; inside a\\n');\n process.stderr.write(' worktree do NOT `git checkout main` first — it fatals with \"main is already\\n');\n process.stderr.write(' checked out at <primary>\"):\\n');\n process.stderr.write(` git checkout -b ${featureName}-v2 origin/main\\n`);\n process.stderr.write('\\n');\n process.stderr.write('3. Squash merge your old branch — THIS STEP IS FOR THE HUMAN TO RUN:\\n');\n process.stderr.write(` git merge --squash ${currentBranch}\\n`);\n process.stderr.write(' AI: `git merge` is blocked for you (redirect-how-to-merge-main) and you must NOT\\n');\n process.stderr.write(' work around it. Ask the human to run that one command, and warn them:\\n');\n process.stderr.write(' \"This is a raw git merge. It is only correct here because the branch is already\\n');\n process.stderr.write(' broken and is being rebuilt onto origin/main. For a NORMAL update from main,\\n');\n process.stderr.write(' push back and tell me to use the gated 3-point merge instead: `pnpm wp-start-update`\\n');\n process.stderr.write(' (paired with `pnpm wp-finish-update`) when no PR is open, or `pnpm wp-start-upsert-pr`\\n');\n process.stderr.write(' (paired with `pnpm wp-finish-upsert-pr`) when one is.\"\\n');\n process.stderr.write('\\n');\n process.stderr.write('4. Commit the squashed changes:\\n');\n process.stderr.write(` git add -A && git commit -m \"Squashed from ${currentBranch}\"\\n`);\n process.stderr.write('\\n');\n process.stderr.write('5. If you have an existing PR:\\n');\n process.stderr.write(` - Create a NEW PR for ${featureName}-v2\\n`);\n process.stderr.write(` - Close the old PR for ${currentBranch}\\n`);\n process.stderr.write('\\n');\n process.stderr.write(SEP);\n process.stderr.write('\\n');\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"git-findForkPoint.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/git-findForkPoint.ts"],"names":[],"mappings":"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","sourcesContent":["import { execSync, spawnSync } from 'child_process';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { prDirFor, CliExitError, RepoRootFinder } from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { AiBranchName } from './git-readAiBranchName';\nimport { MergeState } from './merge-state';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n/** Computes the 3-point fork point and guards against improper merges-from-main. */\n@injectable(bindingScopeValues.Singleton)\nexport class ForkPoint {\n constructor(\n private readonly repoRootFinder: RepoRootFinder,\n private readonly aiBranchName: AiBranchName,\n private readonly mergeState: MergeState,\n ) {}\n\n // Single source of truth for the per-feature dir fork-point output is written to — the SAME nested\n // home the readers use (git-gatherInfo / merge-start read updatemain-hashes.json from mergeDirFor;\n // the review flow reads from prDirFor). Routing the writer through these same helpers is what keeps\n // them from diverging again — guarded by git-findForkPoint.spec.ts.\n forkPointOutputDir(repoRoot: string, featureName: string, workflow: string): string {\n return workflow === 'review' ? prDirFor(repoRoot, featureName) : this.mergeState.mergeDirFor(repoRoot, featureName);\n }\n\n /**\n * The 3-point FORK POINT: where this branch diverged from main. `git merge-base origin/main HEAD`,\n * falling back to a local `main`, and '' when neither ref resolves.\n *\n * Deliberately does NOT fetch. The fork point is ABSOLUTE — advancing main cannot move it, because\n * merge-base is the most recent common ancestor and main's new commits are not on this branch. (Verified\n * on this repo: main went f415456 → ba8f674 → 61432d6 while merge-base with one branch stayed f415456\n * throughout.) So a caller that needs only the fork point — see ChecklistScanner — pays no network cost\n * and works offline. `findForkPoint` below still fetches, because the MERGE flow additionally needs\n * main's CURRENT head as hash point C, which genuinely does require a fetch.\n *\n * Also writes no files and runs no merge-commit scan, so it cannot throw: `wp-review-upsert-pr` must always\n * succeed, and a raw merge-from-main is the merge flow's problem to report, not the checklist's.\n */\n resolveForkPoint(repoRoot: string): string {\n for (const ref of ['origin/main', 'main']) {\n const result = spawnSync('git', ['merge-base', ref, 'HEAD'], { cwd: repoRoot, encoding: 'utf8' });\n const sha = (result.stdout ?? '').trim();\n if (result.status === 0 && sha !== '') return sha;\n }\n return '';\n }\n\n async findForkPoint(workflow: string): Promise<void> {\n if (workflow !== 'review' && workflow !== 'merge') {\n throw new CliExitError(1,\n 'ERROR: Workflow argument required\\n' +\n 'Usage: git-findForkPoint <workflow>\\n' +\n \" workflow: 'review' or 'merge'\",\n );\n }\n\n const featureName = this.aiBranchName.getFeatureName();\n const currentBranch = execSync('git branch --show-current', { encoding: 'utf8' }).trim();\n const repoRoot = this.repoRootFinder.resolveRepoRoot(process.cwd());\n\n const outputDir = this.forkPointOutputDir(repoRoot, featureName, workflow);\n const prefix = workflow === 'review' ? 'review-' : 'updatemain-';\n fs.mkdirSync(outputDir, { recursive: true });\n\n spawnSync('git', ['fetch', 'origin', 'main'], { stdio: 'ignore' });\n\n const featureHead = execSync('git rev-parse HEAD', { encoding: 'utf8' }).trim();\n const originMain = execSync('git rev-parse origin/main', { encoding: 'utf8' }).trim();\n\n process.stderr.write('Finding fork point using git merge-base...\\n');\n\n // ONE implementation of the fork-point computation, shared with ChecklistScanner. The fetch above is\n // what this flow needs beyond it (hash point C = main's current head), not the fork point itself.\n const forkPoint = this.resolveForkPoint(repoRoot);\n\n if (!forkPoint) {\n throw new CliExitError(1, 'ERROR: Could not find common ancestor with origin/main');\n }\n\n process.stderr.write(`✅ Fork point found: ${forkPoint.slice(0, 7)}\\n`);\n process.stderr.write('Checking for improper merges from main...\\n');\n\n const mergeCommitsResult = spawnSync('git', ['log', `${forkPoint}..HEAD`, '--merges', '--format=%H'], { encoding: 'utf8' });\n const mergeCommitsRaw = (mergeCommitsResult.stdout ?? '').trim();\n const mergeCommits = mergeCommitsRaw ? mergeCommitsRaw.split('\\n') : [];\n\n if (mergeCommits.length > 0) {\n this.checkMergeCommits(mergeCommits, outputDir, prefix, featureName, currentBranch);\n } else {\n process.stderr.write('✅ No merge commits found (clean history)\\n');\n }\n\n const hashesJson = JSON.stringify({\n hashForkPoint: forkPoint,\n hashFeatureHead: featureHead,\n hashMainHead: originMain,\n timestamp: new Date().toISOString(),\n }, null, 2);\n\n fs.writeFileSync(path.join(outputDir, `${prefix}hashes.json`), hashesJson + '\\n');\n process.stderr.write(`✅ Hash points written to: ${outputDir}/${prefix}hashes.json\\n`);\n }\n\n private checkMergeCommits(mergeCommits: string[], outputDir: string, prefix: string, featureName: string, currentBranch: string): void {\n process.stderr.write(`Found ${mergeCommits.length} merge commit(s) to check...\\n`);\n\n for (const commit of mergeCommits) {\n const parentsResult = spawnSync('git', ['rev-list', '--parents', '-n', '1', commit], { encoding: 'utf8' });\n const parents = (parentsResult.stdout ?? '').trim().split(' ').slice(1);\n\n for (const parent of parents) {\n const ancestorCheck = spawnSync('git', ['merge-base', '--is-ancestor', parent, 'origin/main']);\n const reverseCheck = spawnSync('git', ['merge-base', '--is-ancestor', 'origin/main', parent]);\n\n if (ancestorCheck.status === 0 && reverseCheck.status === 0) {\n const errorJson = JSON.stringify({\n error: 'Merge from main detected',\n mergeCommit: commit,\n parentFromMain: parent,\n timestamp: new Date().toISOString(),\n }, null, 2);\n\n fs.writeFileSync(path.join(outputDir, `${prefix}forkpoint-error.json`), errorJson + '\\n');\n this.printMergeFromMainError(commit, parent, featureName, currentBranch);\n throw new CliExitError(1, '');\n }\n }\n }\n process.stderr.write('✅ No improper merges from main detected\\n');\n }\n\n private printMergeFromMainError(commit: string, parent: string, featureName: string, currentBranch: string): void {\n process.stderr.write('\\n');\n process.stderr.write(SEP);\n process.stderr.write('❌ This branch merged main without the gated update (pnpm wp-start-update, or\\n');\n process.stderr.write(' pnpm wp-start-upsert-pr when a PR is open)\\n');\n process.stderr.write(SEP);\n process.stderr.write('\\n');\n process.stderr.write(`Merge commit detected: ${commit}\\n`);\n process.stderr.write(`Parent from main: ${parent}\\n`);\n process.stderr.write('\\n');\n process.stderr.write('This prevents clean squash-merge. To recover, follow these steps:\\n');\n process.stderr.write('\\n');\n process.stderr.write('1. Fetch the latest main (works on the primary repo AND in a worktree):\\n');\n process.stderr.write(' git fetch origin main\\n');\n process.stderr.write('\\n');\n // The \"do NOT `git checkout main`\" caveat is scoped to the WORKTREE case, where it is true.\n // Stated unconditionally it forbids a command that is perfectly good in the primary clone —\n // and that CLAUDE.md's own post-merge cleanup recipe uses — which is how an agent talked\n // itself into believing a new branch was its only exit.\n process.stderr.write('2. Create a new branch OFF origin/main (this form works from ANY tree; inside a\\n');\n process.stderr.write(' worktree do NOT `git checkout main` first — it fatals with \"main is already\\n');\n process.stderr.write(' checked out at <primary>\"):\\n');\n process.stderr.write(` git checkout -b ${featureName}-v2 origin/main\\n`);\n process.stderr.write('\\n');\n process.stderr.write('3. Squash merge your old branch — THIS STEP IS FOR THE HUMAN TO RUN:\\n');\n process.stderr.write(` git merge --squash ${currentBranch}\\n`);\n process.stderr.write(' AI: `git merge` is blocked for you (redirect-how-to-merge-main) and you must NOT\\n');\n process.stderr.write(' work around it. Ask the human to run that one command, and warn them:\\n');\n process.stderr.write(' \"This is a raw git merge. It is only correct here because the branch is already\\n');\n process.stderr.write(' broken and is being rebuilt onto origin/main. For a NORMAL update from main,\\n');\n process.stderr.write(' push back and tell me to use the gated 3-point merge instead: `pnpm wp-start-update`\\n');\n process.stderr.write(' (paired with `pnpm wp-finish-update`) when no PR is open, or `pnpm wp-start-upsert-pr`\\n');\n process.stderr.write(' (paired with `pnpm wp-finish-upsert-pr`) when one is.\"\\n');\n process.stderr.write('\\n');\n process.stderr.write('4. Commit the squashed changes:\\n');\n process.stderr.write(` git add -A && git commit -m \"Squashed from ${currentBranch}\"\\n`);\n process.stderr.write('\\n');\n process.stderr.write('5. If you have an existing PR:\\n');\n process.stderr.write(` - Create a NEW PR for ${featureName}-v2\\n`);\n process.stderr.write(` - Close the old PR for ${currentBranch}\\n`);\n process.stderr.write('\\n');\n process.stderr.write(SEP);\n process.stderr.write('\\n');\n }\n}\n"]}
|