@webpieces/pr-gate 0.4.518 → 0.4.520
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/workflow/cleanTmp.d.ts +3 -2
- package/src/scripts/workflow/cleanTmp.js +9 -3
- package/src/scripts/workflow/cleanTmp.js.map +1 -1
- package/src/scripts/workflow/merge-info-index.d.ts +3 -1
- package/src/scripts/workflow/merge-info-index.js +11 -5
- package/src/scripts/workflow/merge-info-index.js.map +1 -1
- package/src/scripts/workflow/merge-start.js +2 -2
- package/src/scripts/workflow/merge-start.js.map +1 -1
- package/src/scripts/workflow/merge-state.d.ts +14 -3
- package/src/scripts/workflow/merge-state.js +21 -8
- package/src/scripts/workflow/merge-state.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.520",
|
|
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",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"directory": "packages/tooling/pr-gate"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@webpieces/rules-config": "0.4.
|
|
29
|
+
"@webpieces/rules-config": "0.4.520",
|
|
30
30
|
"@inversifyjs/binding-decorators": "1.1.5",
|
|
31
31
|
"inversify": "7.10.4",
|
|
32
32
|
"reflect-metadata": "0.2.2"
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { RepoRootFinder } from '@webpieces/rules-config';
|
|
1
|
+
import { DotWebpieces, RepoRootFinder } from '@webpieces/rules-config';
|
|
2
2
|
/**
|
|
3
3
|
* 30-day garbage collection of the whole `.webpieces` tree.
|
|
4
4
|
*
|
|
@@ -14,7 +14,8 @@ import { RepoRootFinder } from '@webpieces/rules-config';
|
|
|
14
14
|
*/
|
|
15
15
|
export declare class CleanTmp {
|
|
16
16
|
private readonly repoRootFinder;
|
|
17
|
-
|
|
17
|
+
private readonly dotDir;
|
|
18
|
+
constructor(repoRootFinder: RepoRootFinder, dotDir?: DotWebpieces);
|
|
18
19
|
cleanTmp(): Promise<void>;
|
|
19
20
|
private sweep;
|
|
20
21
|
}
|
|
@@ -32,12 +32,17 @@ class SweepResult {
|
|
|
32
32
|
*/
|
|
33
33
|
let CleanTmp = class CleanTmp {
|
|
34
34
|
repoRootFinder;
|
|
35
|
-
|
|
35
|
+
dotDir;
|
|
36
|
+
constructor(repoRootFinder, dotDir = rules_config_1.dotWebpieces) {
|
|
36
37
|
this.repoRootFinder = repoRootFinder;
|
|
38
|
+
this.dotDir = dotDir;
|
|
37
39
|
}
|
|
38
40
|
async cleanTmp() {
|
|
39
41
|
const repoRoot = this.repoRootFinder.resolveRepoRoot(process.cwd());
|
|
40
|
-
|
|
42
|
+
// LOCAL scope: a worktree garbage-collects its OWN merge-info/pr-review scratch dirs. It must
|
|
43
|
+
// never sweep the repo-wide dir, whose entries (merged-branches.json, the main-sync status and
|
|
44
|
+
// its lock) belong to every worktree at once.
|
|
45
|
+
const tmpBase = this.dotDir.local(repoRoot);
|
|
41
46
|
if (!fs.existsSync(tmpBase)) {
|
|
42
47
|
return;
|
|
43
48
|
}
|
|
@@ -103,6 +108,7 @@ let CleanTmp = class CleanTmp {
|
|
|
103
108
|
exports.CleanTmp = CleanTmp;
|
|
104
109
|
exports.CleanTmp = CleanTmp = tslib_1.__decorate([
|
|
105
110
|
(0, inversify_1.injectable)(inversify_1.bindingScopeValues.Singleton),
|
|
106
|
-
tslib_1.__metadata("design:paramtypes", [rules_config_1.RepoRootFinder
|
|
111
|
+
tslib_1.__metadata("design:paramtypes", [rules_config_1.RepoRootFinder,
|
|
112
|
+
rules_config_1.DotWebpieces])
|
|
107
113
|
], CleanTmp);
|
|
108
114
|
//# sourceMappingURL=cleanTmp.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cleanTmp.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/cleanTmp.ts"],"names":[],"mappings":";;;;AAAA,+CAAyB;AACzB,mDAA6B;AAC7B,
|
|
1
|
+
{"version":3,"file":"cleanTmp.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/cleanTmp.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\nimport { DotWebpieces, dotWebpieces, RepoRootFinder } from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\n\nconst CUTOFF_DAYS = 30;\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n// Result of one sweep: how many files were reaped for age, and how many now-empty dirs were pruned.\nclass SweepResult {\n constructor(\n public files: number,\n public dirs: number,\n ) {}\n}\n\n/**\n * 30-day garbage collection of the whole `.webpieces` tree.\n *\n * Runs at the end of every merge/PR flow (see merge-end.ts). It walks `.webpieces` depth-first and:\n * 1. deletes ANY file whose mtime is older than the cutoff, anywhere in the tree, and\n * 2. removes ANY directory that is left empty afterwards (including dirs that were already empty).\n *\n * The `.webpieces` root itself is never removed. Everything the tooling still needs is rewritten on\n * each run under a fresh mtime (instruct-ai/ docs, the active hooks/ logs, the *-status.json state\n * files), and every writer `mkdirSync(..., { recursive: true })`s its target first — so pruning a\n * stale home dir is harmless; the next write recreates it. Stale per-feature merge-info/pr-review\n * subdirs and the retired legacy flat layout all fall out of this single pass with no special-casing.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class CleanTmp {\n constructor(\n private readonly repoRootFinder: RepoRootFinder,\n private readonly dotDir: DotWebpieces = dotWebpieces,\n ) {}\n\n async cleanTmp(): Promise<void> {\n const repoRoot = this.repoRootFinder.resolveRepoRoot(process.cwd());\n // LOCAL scope: a worktree garbage-collects its OWN merge-info/pr-review scratch dirs. It must\n // never sweep the repo-wide dir, whose entries (merged-branches.json, the main-sync status and\n // its lock) belong to every worktree at once.\n const tmpBase = this.dotDir.local(repoRoot);\n\n if (!fs.existsSync(tmpBase)) {\n return;\n }\n\n process.stdout.write('\\n');\n process.stdout.write(SEP);\n process.stdout.write('🧹 Garbage-Collecting .webpieces\\n');\n process.stdout.write(SEP);\n process.stdout.write('\\n');\n process.stdout.write(`Location: ${tmpBase}\\n`);\n process.stdout.write(`Retention: ${CUTOFF_DAYS} days (older files reaped; empty dirs pruned)\\n`);\n process.stdout.write('\\n');\n\n const cutoffMs = CUTOFF_DAYS * 24 * 60 * 60 * 1000;\n const now = Date.now();\n\n // `true` => this is the root, which is swept into but never itself removed. This generic\n // depth-first sweep needs NO knowledge of the merge-info layout — including the new\n // staged/<feature> and merged/<feature> split, which sits one level deeper than the old\n // per-feature dirs. That is precisely why it replaced the per-home special-casing.\n const result = this.sweep(tmpBase, tmpBase, now, cutoffMs, true);\n\n if (result.files === 0 && result.dirs === 0) {\n process.stdout.write(` ✅ Nothing older than ${CUTOFF_DAYS} days; no empty directories\\n`);\n } else {\n const fileWord = result.files === 1 ? 'file' : 'files';\n const dirWord = result.dirs === 1 ? 'directory' : 'directories';\n process.stdout.write('\\n');\n process.stdout.write(` ✅ Reaped ${result.files} old ${fileWord} and pruned ${result.dirs} empty ${dirWord}\\n`);\n }\n\n process.stdout.write('\\n');\n process.stdout.write(SEP);\n process.stdout.write('\\n');\n }\n\n // Depth-first, post-order sweep of `dir` (rooted at `tmpBase`, only used for tidy relative logging).\n // Files older than the cutoff are deleted; a directory left empty once its children are processed is\n // pruned — except the root, which is kept even when empty so `.webpieces/` itself survives.\n private sweep(dir: string, tmpBase: string, now: number, cutoffMs: number, isRoot: boolean): SweepResult {\n let files = 0;\n let dirs = 0;\n\n for (const entry of fs.readdirSync(dir)) {\n const fullPath = path.join(dir, entry);\n // lstat, not stat: a symlink is treated as a leaf (aged out like a file), never followed —\n // so we can never wander outside `.webpieces` or delete a link target elsewhere.\n const stat = fs.lstatSync(fullPath);\n if (stat.isDirectory()) {\n const nested = this.sweep(fullPath, tmpBase, now, cutoffMs, false);\n files += nested.files;\n dirs += nested.dirs;\n } else if (now - stat.mtimeMs >= cutoffMs) {\n process.stdout.write(` 🗑️ file: ${path.relative(tmpBase, fullPath)}\\n`);\n fs.rmSync(fullPath, { force: true });\n files += 1;\n }\n }\n\n // Post-order: prune this dir if reaping its children (or nothing) left it empty.\n if (!isRoot && fs.readdirSync(dir).length === 0) {\n process.stdout.write(` 🗑️ dir: ${path.relative(tmpBase, dir)}/\\n`);\n fs.rmdirSync(dir);\n dirs += 1;\n }\n\n return new SweepResult(files, dirs);\n }\n}\n"]}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { AtomicFile } from '@webpieces/rules-config';
|
|
1
2
|
import { MergeState } from './merge-state';
|
|
2
3
|
/**
|
|
3
4
|
* `.webpieces/merge-info/index.json` — the one answer to "which merges, across ALL branches, were
|
|
@@ -42,7 +43,8 @@ export declare class MergeInfoIndexFile {
|
|
|
42
43
|
}
|
|
43
44
|
export declare class MergeInfoIndex {
|
|
44
45
|
private readonly mergeState;
|
|
45
|
-
|
|
46
|
+
private readonly atomicFile;
|
|
47
|
+
constructor(mergeState: MergeState, atomicFile?: AtomicFile);
|
|
46
48
|
indexPath(repoRoot: string): string;
|
|
47
49
|
/**
|
|
48
50
|
* MOVE a landed feature's whole `staged/<feature>/` tree to `merged/<feature>/`, drop its
|
|
@@ -77,8 +77,10 @@ class MergeInfoIndexFile {
|
|
|
77
77
|
exports.MergeInfoIndexFile = MergeInfoIndexFile;
|
|
78
78
|
let MergeInfoIndex = class MergeInfoIndex {
|
|
79
79
|
mergeState;
|
|
80
|
-
|
|
80
|
+
atomicFile;
|
|
81
|
+
constructor(mergeState, atomicFile = new rules_config_1.AtomicFile()) {
|
|
81
82
|
this.mergeState = mergeState;
|
|
83
|
+
this.atomicFile = atomicFile;
|
|
82
84
|
}
|
|
83
85
|
indexPath(repoRoot) {
|
|
84
86
|
return path.join(this.mergeState.mergeInfoRoot(repoRoot), merge_state_1.MERGE_INDEX_FILE);
|
|
@@ -121,9 +123,12 @@ let MergeInfoIndex = class MergeInfoIndex {
|
|
|
121
123
|
}
|
|
122
124
|
}
|
|
123
125
|
const index = new MergeInfoIndexFile(entries);
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
126
|
+
// ATOMIC write. index.json is the one file under merge-info/ that is shared BETWEEN the
|
|
127
|
+
// features of a worktree rather than owned by one of them, so two commands promoting different
|
|
128
|
+
// branches at once can overlap here. Temp + rename means a reader never sees it torn. (It is a
|
|
129
|
+
// pure projection of `merged/` on disk, so a lost update self-heals on the next rebuild — this
|
|
130
|
+
// is not, and must not be assumed to be, a transaction.)
|
|
131
|
+
this.atomicFile.writeJsonAtomic(this.indexPath(repoRoot), index);
|
|
127
132
|
return index;
|
|
128
133
|
}
|
|
129
134
|
// One landed branch, derived entirely from its own directory: the archive record it was promoted
|
|
@@ -159,6 +164,7 @@ let MergeInfoIndex = class MergeInfoIndex {
|
|
|
159
164
|
exports.MergeInfoIndex = MergeInfoIndex;
|
|
160
165
|
exports.MergeInfoIndex = MergeInfoIndex = tslib_1.__decorate([
|
|
161
166
|
(0, inversify_1.injectable)(inversify_1.bindingScopeValues.Singleton),
|
|
162
|
-
tslib_1.__metadata("design:paramtypes", [merge_state_1.MergeState
|
|
167
|
+
tslib_1.__metadata("design:paramtypes", [merge_state_1.MergeState,
|
|
168
|
+
rules_config_1.AtomicFile])
|
|
163
169
|
], MergeInfoIndex);
|
|
164
170
|
//# sourceMappingURL=merge-info-index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"merge-info-index.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/merge-info-index.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\nimport { toError } from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\n\nimport { ARCHIVE_RECORD_FILE, MERGED_DIR, MERGE_INDEX_FILE, MergeState } from './merge-state';\n\n/**\n * `.webpieces/merge-info/index.json` — the one answer to \"which merges, across ALL branches, were\n * 3-point?\", which is the actual review question.\n *\n * WHY AN INDEX IS REQUIRED and a directory name will not do: a single branch can alternate\n * clean → 3-point → clean → 3-point across its syncs. So the clean/3-point axis belongs to the MERGE,\n * not the branch, and cannot be encoded in `merged/<branch>/`. With the index, one jq lists every\n * 3-point merge in the repo and stays correct for alternating branches:\n *\n * jq -r '.merged | to_entries[] | .key as $b | .value.merges[] |\n * select(.threeWay) | \"\\($b) merge-\\(.n)\"' .webpieces/merge-info/index.json\n *\n * WHY IT IS DERIVED, NOT INCREMENTALLY WRITTEN: every fact in it already exists on disk — `threeWay`\n * is the presence of `conflicts.md` in the run dir, `conflicts` is that file's list, and the archive\n * fields come from `archive.json`. Rebuilding by scanning `merged/` makes the index a pure projection\n * that cannot drift from the directories, and makes a lost or corrupt index self-healing.\n */\n\n// One merge slot within a branch. Data-only (per CLAUDE.md, classes for data).\nexport class MergeRecord {\n n: number;\n threeWay: boolean;\n // Only non-empty when threeWay — the files the AI actually had to resolve.\n conflicts: string[];\n\n constructor(n: number, threeWay: boolean, conflicts: string[]) {\n this.n = n;\n this.threeWay = threeWay;\n this.conflicts = conflicts;\n }\n}\n\n// `{ archiveTag, tipSha, baseSha, pr, mergedAt }` — the landed branch's archive record, written into\n// `merged/<feature>/archive.json` and mirrored into the index so one file answers everything.\nexport class ArchiveRecord {\n archiveTag: string;\n tipSha: string;\n baseSha: string;\n pr: number;\n mergedAt: string;\n\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(archiveTag: string, tipSha: string, baseSha: string, pr: number, mergedAt: string) {\n this.archiveTag = archiveTag;\n this.tipSha = tipSha;\n this.baseSha = baseSha;\n this.pr = pr;\n this.mergedAt = mergedAt;\n }\n}\n\n// One landed branch's whole story: which PR, where its history is archived, and every merge it took.\nexport class MergedBranchEntry {\n pr: number;\n archiveTag: string;\n merges: MergeRecord[];\n\n constructor(pr: number, archiveTag: string, merges: MergeRecord[]) {\n this.pr = pr;\n this.archiveTag = archiveTag;\n this.merges = merges;\n }\n}\n\n// The whole file. `merged` is keyed by feature slug, matching the `merged/<feature>/` dir names.\nexport class MergeInfoIndexFile {\n merged: Record<string, MergedBranchEntry>;\n\n constructor(merged: Record<string, MergedBranchEntry>) {\n this.merged = merged;\n }\n}\n\n// Raw JSON shape for the cast at the parse boundary.\ninterface RawArchiveRecord {\n archiveTag?: string;\n tipSha?: string;\n baseSha?: string;\n pr?: number;\n mergedAt?: string;\n}\n\n@injectable(bindingScopeValues.Singleton)\nexport class MergeInfoIndex {\n constructor(private readonly mergeState: MergeState) {}\n\n indexPath(repoRoot: string): string {\n return path.join(this.mergeState.mergeInfoRoot(repoRoot), MERGE_INDEX_FILE);\n }\n\n /**\n * MOVE a landed feature's whole `staged/<feature>/` tree to `merged/<feature>/`, drop its\n * `archive.json` in, and rebuild the index. This is what makes `staged/` self-cleaning: it holds\n * only branches still in flight, instead of growing for the life of the repo.\n *\n * Returns false when there was nothing staged to promote (a branch that landed without ever syncing\n * from main), which is a normal outcome and not an error.\n */\n promoteToMerged(repoRoot: string, featureName: string, archive: ArchiveRecord): boolean {\n const staged = this.mergeState.stagedDirFor(repoRoot, featureName);\n const merged = this.mergeState.mergedDirFor(repoRoot, featureName);\n if (!fs.existsSync(staged)) {\n this.rebuildIndex(repoRoot);\n return false;\n }\n fs.mkdirSync(path.dirname(merged), { recursive: true });\n // An existing merged/<feature> means the same branch name landed before. Keep BOTH — the older\n // record moves aside rather than being overwritten, since it is the audit trail of a real PR.\n if (fs.existsSync(merged)) fs.renameSync(merged, `${merged}-prev-${String(Date.now())}`);\n fs.renameSync(staged, merged);\n fs.writeFileSync(path.join(merged, ARCHIVE_RECORD_FILE), JSON.stringify(archive, null, 2) + '\\n');\n this.rebuildIndex(repoRoot);\n return true;\n }\n\n /** Rebuild `index.json` by scanning `merged/` from scratch. Pure projection of what is on disk. */\n rebuildIndex(repoRoot: string): MergeInfoIndexFile {\n const mergedRoot = path.join(this.mergeState.mergeInfoRoot(repoRoot), MERGED_DIR);\n const entries: Record<string, MergedBranchEntry> = {};\n if (fs.existsSync(mergedRoot)) {\n for (const feature of fs.readdirSync(mergedRoot)) {\n const home = path.join(mergedRoot, feature);\n if (!fs.statSync(home).isDirectory()) continue;\n entries[feature] = this.entryFor(home);\n }\n }\n const index = new MergeInfoIndexFile(entries);\n const target = this.indexPath(repoRoot);\n fs.mkdirSync(path.dirname(target), { recursive: true });\n fs.writeFileSync(target, JSON.stringify(index, null, 2) + '\\n');\n return index;\n }\n\n // One landed branch, derived entirely from its own directory: the archive record it was promoted\n // with, plus one MergeRecord per `merge-<n>/` whose threeWay is the presence of conflicts.md.\n private entryFor(home: string): MergedBranchEntry {\n const archive = this.readArchive(home);\n const merges: MergeRecord[] = [];\n for (const n of this.mergeState.listMergeRunDirs(home)) {\n const runDir = path.join(home, `merge-${String(n)}`);\n const threeWay = this.mergeState.wasThreeWay(runDir);\n merges.push(new MergeRecord(n, threeWay, threeWay ? this.mergeState.readConflictedFiles(runDir) : []));\n }\n return new MergedBranchEntry(archive.pr, archive.archiveTag, merges);\n }\n\n // Missing/corrupt archive.json degrades to empty fields rather than failing the whole index — the\n // merge records are still worth publishing, and a lost archive tag is not worth losing them over.\n private readArchive(home: string): ArchiveRecord {\n const filePath = path.join(home, ARCHIVE_RECORD_FILE);\n if (!fs.existsSync(filePath)) return new ArchiveRecord('', '', '', 0, '');\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const raw = JSON.parse(fs.readFileSync(filePath, 'utf8')) as RawArchiveRecord;\n return new ArchiveRecord(\n raw.archiveTag ?? '', raw.tipSha ?? '', raw.baseSha ?? '', raw.pr ?? 0, raw.mergedAt ?? '');\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return new ArchiveRecord('', '', '', 0, '');\n }\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"merge-info-index.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/merge-info-index.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\nimport { AtomicFile, toError } from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\n\nimport { ARCHIVE_RECORD_FILE, MERGED_DIR, MERGE_INDEX_FILE, MergeState } from './merge-state';\n\n/**\n * `.webpieces/merge-info/index.json` — the one answer to \"which merges, across ALL branches, were\n * 3-point?\", which is the actual review question.\n *\n * WHY AN INDEX IS REQUIRED and a directory name will not do: a single branch can alternate\n * clean → 3-point → clean → 3-point across its syncs. So the clean/3-point axis belongs to the MERGE,\n * not the branch, and cannot be encoded in `merged/<branch>/`. With the index, one jq lists every\n * 3-point merge in the repo and stays correct for alternating branches:\n *\n * jq -r '.merged | to_entries[] | .key as $b | .value.merges[] |\n * select(.threeWay) | \"\\($b) merge-\\(.n)\"' .webpieces/merge-info/index.json\n *\n * WHY IT IS DERIVED, NOT INCREMENTALLY WRITTEN: every fact in it already exists on disk — `threeWay`\n * is the presence of `conflicts.md` in the run dir, `conflicts` is that file's list, and the archive\n * fields come from `archive.json`. Rebuilding by scanning `merged/` makes the index a pure projection\n * that cannot drift from the directories, and makes a lost or corrupt index self-healing.\n */\n\n// One merge slot within a branch. Data-only (per CLAUDE.md, classes for data).\nexport class MergeRecord {\n n: number;\n threeWay: boolean;\n // Only non-empty when threeWay — the files the AI actually had to resolve.\n conflicts: string[];\n\n constructor(n: number, threeWay: boolean, conflicts: string[]) {\n this.n = n;\n this.threeWay = threeWay;\n this.conflicts = conflicts;\n }\n}\n\n// `{ archiveTag, tipSha, baseSha, pr, mergedAt }` — the landed branch's archive record, written into\n// `merged/<feature>/archive.json` and mirrored into the index so one file answers everything.\nexport class ArchiveRecord {\n archiveTag: string;\n tipSha: string;\n baseSha: string;\n pr: number;\n mergedAt: string;\n\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(archiveTag: string, tipSha: string, baseSha: string, pr: number, mergedAt: string) {\n this.archiveTag = archiveTag;\n this.tipSha = tipSha;\n this.baseSha = baseSha;\n this.pr = pr;\n this.mergedAt = mergedAt;\n }\n}\n\n// One landed branch's whole story: which PR, where its history is archived, and every merge it took.\nexport class MergedBranchEntry {\n pr: number;\n archiveTag: string;\n merges: MergeRecord[];\n\n constructor(pr: number, archiveTag: string, merges: MergeRecord[]) {\n this.pr = pr;\n this.archiveTag = archiveTag;\n this.merges = merges;\n }\n}\n\n// The whole file. `merged` is keyed by feature slug, matching the `merged/<feature>/` dir names.\nexport class MergeInfoIndexFile {\n merged: Record<string, MergedBranchEntry>;\n\n constructor(merged: Record<string, MergedBranchEntry>) {\n this.merged = merged;\n }\n}\n\n// Raw JSON shape for the cast at the parse boundary.\ninterface RawArchiveRecord {\n archiveTag?: string;\n tipSha?: string;\n baseSha?: string;\n pr?: number;\n mergedAt?: string;\n}\n\n@injectable(bindingScopeValues.Singleton)\nexport class MergeInfoIndex {\n constructor(\n private readonly mergeState: MergeState,\n private readonly atomicFile: AtomicFile = new AtomicFile(),\n ) {}\n\n indexPath(repoRoot: string): string {\n return path.join(this.mergeState.mergeInfoRoot(repoRoot), MERGE_INDEX_FILE);\n }\n\n /**\n * MOVE a landed feature's whole `staged/<feature>/` tree to `merged/<feature>/`, drop its\n * `archive.json` in, and rebuild the index. This is what makes `staged/` self-cleaning: it holds\n * only branches still in flight, instead of growing for the life of the repo.\n *\n * Returns false when there was nothing staged to promote (a branch that landed without ever syncing\n * from main), which is a normal outcome and not an error.\n */\n promoteToMerged(repoRoot: string, featureName: string, archive: ArchiveRecord): boolean {\n const staged = this.mergeState.stagedDirFor(repoRoot, featureName);\n const merged = this.mergeState.mergedDirFor(repoRoot, featureName);\n if (!fs.existsSync(staged)) {\n this.rebuildIndex(repoRoot);\n return false;\n }\n fs.mkdirSync(path.dirname(merged), { recursive: true });\n // An existing merged/<feature> means the same branch name landed before. Keep BOTH — the older\n // record moves aside rather than being overwritten, since it is the audit trail of a real PR.\n if (fs.existsSync(merged)) fs.renameSync(merged, `${merged}-prev-${String(Date.now())}`);\n fs.renameSync(staged, merged);\n fs.writeFileSync(path.join(merged, ARCHIVE_RECORD_FILE), JSON.stringify(archive, null, 2) + '\\n');\n this.rebuildIndex(repoRoot);\n return true;\n }\n\n /** Rebuild `index.json` by scanning `merged/` from scratch. Pure projection of what is on disk. */\n rebuildIndex(repoRoot: string): MergeInfoIndexFile {\n const mergedRoot = path.join(this.mergeState.mergeInfoRoot(repoRoot), MERGED_DIR);\n const entries: Record<string, MergedBranchEntry> = {};\n if (fs.existsSync(mergedRoot)) {\n for (const feature of fs.readdirSync(mergedRoot)) {\n const home = path.join(mergedRoot, feature);\n if (!fs.statSync(home).isDirectory()) continue;\n entries[feature] = this.entryFor(home);\n }\n }\n const index = new MergeInfoIndexFile(entries);\n // ATOMIC write. index.json is the one file under merge-info/ that is shared BETWEEN the\n // features of a worktree rather than owned by one of them, so two commands promoting different\n // branches at once can overlap here. Temp + rename means a reader never sees it torn. (It is a\n // pure projection of `merged/` on disk, so a lost update self-heals on the next rebuild — this\n // is not, and must not be assumed to be, a transaction.)\n this.atomicFile.writeJsonAtomic(this.indexPath(repoRoot), index);\n return index;\n }\n\n // One landed branch, derived entirely from its own directory: the archive record it was promoted\n // with, plus one MergeRecord per `merge-<n>/` whose threeWay is the presence of conflicts.md.\n private entryFor(home: string): MergedBranchEntry {\n const archive = this.readArchive(home);\n const merges: MergeRecord[] = [];\n for (const n of this.mergeState.listMergeRunDirs(home)) {\n const runDir = path.join(home, `merge-${String(n)}`);\n const threeWay = this.mergeState.wasThreeWay(runDir);\n merges.push(new MergeRecord(n, threeWay, threeWay ? this.mergeState.readConflictedFiles(runDir) : []));\n }\n return new MergedBranchEntry(archive.pr, archive.archiveTag, merges);\n }\n\n // Missing/corrupt archive.json degrades to empty fields rather than failing the whole index — the\n // merge records are still worth publishing, and a lost archive tag is not worth losing them over.\n private readArchive(home: string): ArchiveRecord {\n const filePath = path.join(home, ARCHIVE_RECORD_FILE);\n if (!fs.existsSync(filePath)) return new ArchiveRecord('', '', '', 0, '');\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const raw = JSON.parse(fs.readFileSync(filePath, 'utf8')) as RawArchiveRecord;\n return new ArchiveRecord(\n raw.archiveTag ?? '', raw.tipSha ?? '', raw.baseSha ?? '', raw.pr ?? 0, raw.mergedAt ?? '');\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return new ArchiveRecord('', '', '', 0, '');\n }\n }\n}\n"]}
|
|
@@ -275,7 +275,7 @@ let MergeStart = class MergeStart {
|
|
|
275
275
|
}
|
|
276
276
|
// Returns the absolute path of the written doc.
|
|
277
277
|
writeMergeProcessDoc(repoRoot, mergeDir, squashBranch, conflictedFiles, finishCommand) {
|
|
278
|
-
const docDir =
|
|
278
|
+
const docDir = rules_config_1.dotWebpieces.localFile(repoRoot, 'instruct-ai');
|
|
279
279
|
fs.mkdirSync(docDir, { recursive: true });
|
|
280
280
|
const docPath = path.join(docDir, 'webpieces.mergeprocess.md');
|
|
281
281
|
fs.writeFileSync(docPath, this.mergeProcessDoc(mergeDir, squashBranch, conflictedFiles, finishCommand));
|
|
@@ -336,7 +336,7 @@ let MergeStart = class MergeStart {
|
|
|
336
336
|
event.conflictFiles = files;
|
|
337
337
|
event.artifacts = [
|
|
338
338
|
path.join(mergeDir, 'updatemain-<file>'),
|
|
339
|
-
|
|
339
|
+
rules_config_1.dotWebpieces.localFile(repoRoot, 'instruct-ai', 'webpieces.mergeprocess.md'),
|
|
340
340
|
];
|
|
341
341
|
(0, rules_config_1.logBranchMutation)(repoRoot, event);
|
|
342
342
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"merge-start.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/merge-start.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 {\n WEBPIECES_TMP_DIR, MERGE_EXPLANATION_FILE, CliExitError,\n MutationVerb, BranchMutationEvent, logBranchMutation, SyncFlowGuidance,\n} from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { GatherInfo } from '../git-gatherInfo';\nimport { BranchNaming } from './branch-naming';\nimport { GitExec } from './git-exec';\nimport { MergeState, MergeMarker } from './merge-state';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\ninterface HashPoints {\n hashForkPoint: string;\n hashFeatureHead: string;\n hashMainHead: string;\n}\n\n// The four branch names merge-END needs to finalize a merge (swap squash→feature, push, clean up).\nexport class MergeContext {\n currentBranch: string;\n squashBranch: string;\n backupBranch: string;\n prNumber: string;\n\n constructor(currentBranch: string, squashBranch: string, backupBranch: string, prNumber: string) {\n this.currentBranch = currentBranch;\n this.squashBranch = squashBranch;\n this.backupBranch = backupBranch;\n this.prNumber = prNumber;\n }\n}\n\n// Outcome of merge-start: 'clean' carries the context for merge-END to finalize; 'conflict' means the\n// marker + context files were written and the caller should hand back to the AI (exit 2).\nexport class MergeStartResult {\n status: 'clean' | 'conflict';\n context: MergeContext | null;\n runDir: string; // this sync's numbered `merge-<n>/` dir — passed to merge-END so it reads THIS marker\n\n constructor(status: 'clean' | 'conflict', context: MergeContext | null, runDir: string) {\n this.status = status;\n this.context = context;\n this.runDir = runDir;\n }\n}\n\n// The one number `n` for a sync and the things it names: the pre-merge backup branch, its paired\n// conflict-context run dir, and (via `n`) the `preMerge<n>.hash` record of the tip it snapshotted.\nclass SyncSlot {\n backupBranch: string;\n runDir: string;\n n: number;\n\n constructor(backupBranch: string, runDir: string, n: number) {\n this.backupBranch = backupBranch;\n this.runDir = runDir;\n this.n = n;\n }\n}\n\n// Single source of truth for the merge process (written at conflict time, parameterized with the live\n// MERGE_DIR + conflicted-file list so it can never drift from the actual layout).\nconst MERGE_PROCESS_TEMPLATE = `# AI-Assisted Squash-Merge Conflict Resolution (generated)\n\nThis file was generated by \\`pnpm {{START_COMMAND}}\\` when the 3-point squash-merge hit conflicts.\nIt is the single source of truth for the merge process — follow it exactly.\n\nYou are on branch \\`{{SQUASH_BRANCH}}\\` with conflict markers in the working tree.\n\\`MERGE_DIR = {{MERGE_DIR}}\\`\n\n## How the gate works\n\n- Resolve every conflicted file in the working tree.\n- Run **\\`pnpm {{FINISH_COMMAND}}\\`** — the validation + finish gate. It scans for leftover conflict\n markers, checks each conflicted file has a written merge explanation, validates, and commits the\n merge. It is the paired half of \\`{{START_COMMAND}}\\` — never finish with the other flow's command\n (\\`wp-start-update\\` pairs with \\`wp-finish-update\\`; \\`wp-start-upsert-pr\\` pairs with\n \\`wp-finish-upsert-pr\\`, which additionally runs the \\`nx affected\\` build, renders the dashboard,\n and creates/updates the PR).\n- **DO \\`git add\\` each file you resolve.** Staging your resolutions is your job, not the gate's, and\n \\`git add\\` is **not** blocked by any guard. The gate stages with \\`git add -u\\`, which only re-stages\n already-tracked paths — leave a resolution unstaged and the gate stops with \"Git still reports\n unmerged files\" and sends you back to \\`git add\\` it.\n- **Do NOT run \\`git commit\\` / \\`git push\\` / \\`gh pr create|edit|merge\\` yourself** (nor \\`git merge\\` /\n \\`git rebase\\`). The \\`merge-in-progress-guard\\` hook blocks exactly those until the gate validates.\n The gate does the commit. See \\`webpieces.git-workflow.md\\` → \"Outcome B — CONFLICT merge\" for the\n full who-does-what; this file does not restate it.\n\n## STEP 1 — Load the merge context\n\nPer conflicted file, \\`MERGE_DIR/updatemain-<safe_path>/\\` holds (\\`<safe_path>\\` = path with \\`/\\`→\\`__\\`):\n\n\\`\\`\\`\nA-forkpoint.txt # file at fork point (base)\nB-feature.txt # file on your feature branch\nC-main.txt # file on main\nB-A.diff # what your feature changed (B−A)\nC-A.diff # what main changed (C−A)\n\\`\\`\\`\n\n\\`updatemain-hashes.json\\` holds A/B/C commit hashes. To see why main changed:\n\\`git log <A>..<C> --oneline\\`.\n\nWhy A/B/C are trustworthy here — and why you must never \\`git merge\\`/\\`git rebase\\` main in yourself —\nis the fork-point invariant: see \\`webpieces.git-workflow.md\\` → \"THE FORK POINT INVARIANT\". Read it\nonce if \\`B-A.diff\\` or \\`C-A.diff\\` ever shows you changes you do not recognize; that is the symptom\nof a polluted fork point, not of a bad diff.\n\n## STEP 2 — Resolve each conflicted file\n\nFor each file: read the working-tree file (the markers) and its \\`B-A.diff\\` / \\`C-A.diff\\` (intent),\nthen Edit to the resolved version, removing ALL conflict markers.\n\nStrategies: goals align & non-overlapping → merge both · one side removes what the other modifies\n→ prefer the removal · same lines, simple (imports/format) → merge both · same lines, complex or\nconflicting goals → ask the user · feature re-implements what main already squashed → prefer\nmain's, then re-apply only the genuinely new feature work.\n\n**Then write a merge explanation** for each conflicted file — NOT a comment in the source (that\nbreaks for JSON and deleted files). Write it next to that file's diffs, at:\n\n\\`\\`\\`\nMERGE_DIR/updatemain-<safe_path>/{{EXPLANATION_FILE}}\n\\`\\`\\`\n\n(\\`<safe_path>\\` = the conflict file path with \\`/\\` → \\`__\\`, the same dir that holds its\n\\`A-forkpoint.txt\\` / \\`B-A.diff\\` / \\`C-A.diff\\`.) In it, explain in a few sentences how you resolved\nthis file: which side you took where, what you combined from B-A.diff vs C-A.diff, and why. The\ngate fails if any conflicted file's explanation is missing or empty. Do not paste A/B/C context\nblocks into the source code.\n\n## STEP 3 — Run the gate (validates the merge AND finalizes it)\n\n\\`\\`\\`\npnpm {{FINISH_COMMAND}}\n\\`\\`\\`\n\n- Leftover conflict markers → fix those files and re-run.\n- Missing merge explanation → write it (see STEP 2) and re-run.\n- Build failure → fix the TypeScript/lint errors and re-run (the gate re-stages for you).\n- Missing review.json (PR flow only) → write it in the printed format (your PR review), then re-run.\n- On success it commits and finalizes the merge (in the PR flow it also renders the dashboard and\n creates/updates the PR).\n\n## Conflicted files\n\n{{FILE_LIST}}\n\n## If you need to bail out\n\nA numbered backup branch was created (e.g. \\`<feature>PreMerge1\\`). To abandon:\n\n\\`\\`\\`\ngit merge --abort 2>/dev/null; git checkout <feature> ; git branch -D {{SQUASH_BRANCH}}\n\\`\\`\\`\n\nThen delete \\`{{MERGE_DIR}}/\\` for a clean slate.\n`;\n\n// merge-START: the first half of the 3-point squash-merge lifecycle. Brings origin/main into a fresh\n// `<branch>Squash`, and on conflict writes the 3-point context + unvalidated marker + process doc,\n// then hands control back to the AI. On a clean merge it commits the squash and returns the branch\n// context so the caller (RunUpdate) can run merge-END to finalize. NEVER finalizes or posts a PR.\n@injectable(bindingScopeValues.Singleton)\nexport class MergeStart {\n constructor(\n private readonly gatherInfo: GatherInfo,\n private readonly branchNaming: BranchNaming,\n private readonly gitExec: GitExec,\n private readonly mergeState: MergeState,\n ) {}\n\n async mergeStart(repoRoot: string, verb: MutationVerb, home: string, finishCommand: string): Promise<MergeStartResult> {\n const currentBranch = execSync('git branch --show-current', { encoding: 'utf8' }).trim();\n if (currentBranch.endsWith('Squash')) {\n throw new CliExitError(1, `❌ On a leftover ${currentBranch} branch with no merge marker. Clean up: git branch -D ${currentBranch}`);\n }\n\n // One number `n` for this sync drives BOTH the backup branch and its `merge-<n>/` context dir.\n const slot = this.chooseSyncSlot(home, currentBranch);\n const backupBranch = slot.backupBranch;\n const mergeDir = slot.runDir;\n\n process.stdout.write('\\n' + SEP + '🔄 Squash-Merge Update from Main\\n' + SEP + '\\n');\n const info = await this.gatherInfo.gatherInfo();\n const hashes = info.hashes;\n if (info.alreadyUpToDate) {\n process.stdout.write('ℹ️ Branch already even with main; nothing to merge, continuing to push/build.\\n');\n }\n\n const prNumber = this.detectPr(this.branchNaming.baseBranchName(currentBranch));\n process.stdout.write(prNumber ? `Existing PR #${prNumber} will be updated.\\n` : 'No existing PR (one can be created later).\\n');\n\n this.createBackup(currentBranch, backupBranch);\n // Record THIS sync's pre-merge tip as `staged/<feature>/preMerge<n>.hash` — every intermediate\n // state, not just the last one. It is what lets the `<feature>PreMerge<n>` snapshot BRANCH be\n // disposable (branches count toward the branch cap; a written-down hash does not), and it is the\n // ref the archive tag is cut from when the PR lands.\n this.mergeState.writePreMergeHash(home, slot.n, this.fullSha(currentBranch));\n const backupEvent = new BranchMutationEvent(verb, 'BACKUP');\n backupEvent.fromBranch = currentBranch;\n backupEvent.toBranch = backupBranch;\n logBranchMutation(repoRoot, backupEvent);\n\n const squashBranch = `${currentBranch}Squash`;\n if (spawnSync('git', ['show-ref', '--verify', '--quiet', `refs/heads/${squashBranch}`]).status === 0) {\n throw new CliExitError(1, `❌ Stale ${squashBranch} from a previous run. Delete it: git branch -D ${squashBranch}`);\n }\n\n // Branch the squash off origin/main directly — worktree-native. origin/main was already fetched\n // in gatherInfo(), and A/B/C are computed purely from origin/main, so the merge base is identical\n // to the old checkout-main + pull path.\n const originMainSha = this.shortSha('origin/main');\n this.gitExec.runGitChecked(['checkout', '-b', squashBranch, 'origin/main'], 'Failed to create squash branch off origin/main');\n const baseEvent = new BranchMutationEvent(verb, 'PULL');\n baseEvent.newMain = originMainSha;\n logBranchMutation(repoRoot, baseEvent);\n\n process.stdout.write('\\n' + SEP + `🔀 Squash merging ${currentBranch}\\n` + SEP + '\\n');\n const merge = spawnSync('git', ['merge', '--squash', currentBranch], { stdio: 'inherit' });\n if (merge.status !== 0) {\n this.handleConflictsHandback(repoRoot, mergeDir, currentBranch, squashBranch, backupBranch, prNumber, hashes, finishCommand);\n this.logConflict(repoRoot, verb, mergeDir);\n return new MergeStartResult('conflict', null, mergeDir);\n }\n logBranchMutation(repoRoot, new BranchMutationEvent(verb, 'SQUASH'));\n\n const nothingStaged = spawnSync('git', ['diff-index', '--quiet', '--cached', 'HEAD', '--']).status === 0;\n if (nothingStaged) {\n process.stdout.write('ℹ️ Already up-to-date with main (nothing to merge).\\n');\n } else {\n // Internal, transient subject for the single squash commit on the feature branch. It NO LONGER\n // reaches main's history: finish-upsert-pr squash-merges the PR with an explicit\n // `gh pr merge --subject <PR title> --body-file <commit summary>`, so main carries the PR title.\n this.gitExec.runGitChecked(['commit', '-m', `Squash merge of ${currentBranch}`], 'Failed to commit squash merge');\n // Clean merge ⇒ hashes only, no conflicts.md. Its ABSENCE is what marks this merge clean.\n this.mergeState.recordCleanMerge(mergeDir, hashes.hashForkPoint, hashes.hashFeatureHead, hashes.hashMainHead);\n }\n return new MergeStartResult('clean', new MergeContext(currentBranch, squashBranch, backupBranch, prNumber), mergeDir);\n }\n\n // Detect the PR by its STABLE feature branch (a leftover `…wpN` still resolves to the one name the\n // PR lives on).\n private detectPr(baseBranch: string): string {\n const result = spawnSync(\n 'gh', ['pr', 'list', '--head', baseBranch, '--json', 'number', '--jq', '.[0].number'],\n { encoding: 'utf8' },\n );\n return result.status === 0 ? (result.stdout ?? '').trim() : '';\n }\n\n // Pick this sync's slot number MONOTONICALLY from the durable audit dirs, then step past any\n // existing `<branch>PreMerge<n>` branch so the same `n` is free for the paired backup branch.\n private chooseSyncSlot(home: string, currentBranch: string): SyncSlot {\n const branchExists = (name: string): boolean =>\n spawnSync('git', ['show-ref', '--verify', '--quiet', `refs/heads/${name}`]).status === 0;\n let n = this.mergeState.nextMergeSlotNumber(home);\n while (branchExists(this.branchNaming.preMergeBackupName(currentBranch, n))) n += 1;\n return new SyncSlot(this.branchNaming.preMergeBackupName(currentBranch, n), this.mergeState.mergeRunDirFor(home, n), n);\n }\n\n // Snapshot the pre-merge state onto the caller-chosen `backupBranch`, never overwriting.\n private createBackup(currentBranch: string, backupBranch: string): void {\n process.stdout.write('\\n' + SEP + '💾 Creating Pre-Merge Backup\\n' + SEP + '\\n');\n this.gitExec.runGitChecked(['checkout', '-b', backupBranch], 'Failed to create backup branch');\n this.gitExec.runGitChecked(['checkout', currentBranch], 'Failed to return to feature branch');\n process.stdout.write(`✅ Backup created: ${backupBranch}\\n\\n`);\n }\n\n private saveConflictContext(\n conflictedFiles: string[], mergeDir: string, forkPoint: string, featureHead: string, mainHead: string,\n ): void {\n for (const file of conflictedFiles) {\n const fileDir = this.mergeState.perFileContextDir(mergeDir, file);\n fs.mkdirSync(fileDir, { recursive: true });\n\n const fork = spawnSync('git', ['show', `${forkPoint}:${file}`], { encoding: 'utf8' });\n fs.writeFileSync(path.join(fileDir, 'A-forkpoint.txt'), fork.status === 0 ? (fork.stdout ?? '') : '(file did not exist)\\n');\n const feature = spawnSync('git', ['show', `${featureHead}:${file}`], { encoding: 'utf8' });\n fs.writeFileSync(path.join(fileDir, 'B-feature.txt'), feature.status === 0 ? (feature.stdout ?? '') : '(file did not exist)\\n');\n const main = spawnSync('git', ['show', `${mainHead}:${file}`], { encoding: 'utf8' });\n fs.writeFileSync(path.join(fileDir, 'C-main.txt'), main.status === 0 ? (main.stdout ?? '') : '(file did not exist)\\n');\n\n const ba = spawnSync('git', ['diff', forkPoint, featureHead, '--', file], { encoding: 'utf8' });\n fs.writeFileSync(path.join(fileDir, 'B-A.diff'), ba.stdout ?? '');\n const ca = spawnSync('git', ['diff', forkPoint, mainHead, '--', file], { encoding: 'utf8' });\n fs.writeFileSync(path.join(fileDir, 'C-A.diff'), ca.stdout ?? '');\n }\n }\n\n private mergeProcessDoc(mergeDir: string, squashBranch: string, conflictedFiles: string[], finishCommand: string): string {\n const fileList = conflictedFiles.map((f: string): string => `- \\`${f}\\``).join('\\n');\n return MERGE_PROCESS_TEMPLATE\n .replace(/\\{\\{SQUASH_BRANCH\\}\\}/g, squashBranch)\n .replace(/\\{\\{MERGE_DIR\\}\\}/g, mergeDir)\n .replace(/\\{\\{EXPLANATION_FILE\\}\\}/g, MERGE_EXPLANATION_FILE)\n .replace(/\\{\\{FINISH_COMMAND\\}\\}/g, finishCommand)\n .replace(/\\{\\{START_COMMAND\\}\\}/g, new SyncFlowGuidance().pairedStart(finishCommand))\n .replace(/\\{\\{FILE_LIST\\}\\}/g, fileList);\n }\n\n // Returns the absolute path of the written doc.\n private writeMergeProcessDoc(repoRoot: string, mergeDir: string, squashBranch: string, conflictedFiles: string[], finishCommand: string): string {\n const docDir = path.join(repoRoot, WEBPIECES_TMP_DIR, 'instruct-ai');\n fs.mkdirSync(docDir, { recursive: true });\n const docPath = path.join(docDir, 'webpieces.mergeprocess.md');\n fs.writeFileSync(docPath, this.mergeProcessDoc(mergeDir, squashBranch, conflictedFiles, finishCommand));\n return docPath;\n }\n\n // The AI-facing \"what just happened / what to do next\" recap on the conflict path.\n private printConflictHandback(\n docPath: string, mergeDir: string, squashBranch: string, conflictedFiles: string[], finishCommand: string,\n ): void {\n process.stdout.write('\\n' + SEP + `⚠️ Conflicts in ${conflictedFiles.length} file(s) — handing control back to you\\n` + SEP + '\\n');\n process.stdout.write('Here is exactly what I did and what you need to do:\\n\\n');\n process.stdout.write('What I did:\\n');\n process.stdout.write(' 1. snapshotted your pre-merge state to a PreMerge branch\\n');\n process.stdout.write(' 2. pulled origin/main and squash-merged your work onto it\\n');\n process.stdout.write(` 3. hit conflicts — you are now on the transient branch ${squashBranch}\\n\\n`);\n process.stdout.write('What you need to do:\\n');\n process.stdout.write(` 1. read the merge process doc: ${docPath}\\n`);\n process.stdout.write(` 2. resolve each conflicted file below (its 3-point A/B/C context + diffs are in\\n`);\n process.stdout.write(` ${mergeDir}/updatemain-<file>/), \\`git add\\` it, and write that file's merge-explanation.md\\n`);\n process.stdout.write(` 3. run pnpm ${finishCommand} — it validates, commits, and finalizes (do NOT git commit/push yourself)\\n\\n`);\n process.stdout.write('Conflicted files:\\n');\n for (const file of conflictedFiles) process.stdout.write(` - ${file}\\n`);\n process.stdout.write('\\n' + SEP);\n }\n\n // Write the conflict context files + the unvalidated marker + the process doc. Does NOT exit.\n private handleConflictsHandback(\n repoRoot: string, mergeDir: string, currentBranch: string, squashBranch: string,\n backupBranch: string, prNumber: string, hashes: HashPoints, finishCommand: string,\n ): void {\n const raw = execSync('git diff --name-only --diff-filter=U', { encoding: 'utf8' }).trim();\n const conflictedFiles = raw.split('\\n').filter((f: string): boolean => f.trim() !== '');\n fs.mkdirSync(mergeDir, { recursive: true });\n // `conflicts.md` replaces the old `updatemain-conflicted-files.txt` (which nothing read) AND is\n // the 3-point signal itself: this file exists in a run dir if and only if that merge conflicted,\n // which is what lets the index classify each merge without a per-branch directory-name scheme.\n this.mergeState.writeConflicts(mergeDir, conflictedFiles);\n fs.writeFileSync(path.join(mergeDir, 'updatemain-hashes.json'), JSON.stringify(hashes, null, 2) + '\\n');\n this.saveConflictContext(conflictedFiles, mergeDir, hashes.hashForkPoint, hashes.hashFeatureHead, hashes.hashMainHead);\n\n const marker = new MergeMarker(\n currentBranch, squashBranch, backupBranch, prNumber, conflictedFiles,\n hashes.hashForkPoint, hashes.hashFeatureHead, hashes.hashMainHead, false,\n );\n this.mergeState.writeMergeMarker(mergeDir, marker);\n const docPath = this.writeMergeProcessDoc(repoRoot, mergeDir, squashBranch, conflictedFiles, finishCommand);\n this.printConflictHandback(docPath, mergeDir, squashBranch, conflictedFiles, finishCommand);\n }\n\n // Short sha of a ref (best-effort — '' if it can't resolve).\n private shortSha(ref: string): string {\n const result = spawnSync('git', ['rev-parse', '--short', ref], { encoding: 'utf8' });\n return result.status === 0 ? (result.stdout ?? '').trim() : '';\n }\n\n // FULL sha of a ref (best-effort — '' if it can't resolve). Recorded for the pre-merge tips because\n // those hashes are meant to be used later to restore state, and an abbreviated sha can go ambiguous\n // as the repo grows.\n private fullSha(ref: string): string {\n const result = spawnSync('git', ['rev-parse', ref], { encoding: 'utf8' });\n return result.status === 0 ? (result.stdout ?? '').trim() : '';\n }\n\n // Log the CONFLICT phase with the conflicted-file list + artifact paths a resolver needs.\n private logConflict(repoRoot: string, verb: MutationVerb, mergeDir: string): void {\n const raw = spawnSync('git', ['diff', '--name-only', '--diff-filter=U'], { cwd: repoRoot, encoding: 'utf8' });\n const files = (raw.status === 0 ? (raw.stdout ?? '') : '').split('\\n').map((f: string): string => f.trim()).filter((f: string): boolean => f !== '');\n const event = new BranchMutationEvent(verb, 'CONFLICT');\n event.conflict = true;\n event.conflictFiles = files;\n event.artifacts = [\n path.join(mergeDir, 'updatemain-<file>'),\n path.join(repoRoot, WEBPIECES_TMP_DIR, 'instruct-ai', 'webpieces.mergeprocess.md'),\n ];\n logBranchMutation(repoRoot, event);\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"merge-start.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/merge-start.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 {\n dotWebpieces, MERGE_EXPLANATION_FILE, CliExitError,\n MutationVerb, BranchMutationEvent, logBranchMutation, SyncFlowGuidance,\n} from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { GatherInfo } from '../git-gatherInfo';\nimport { BranchNaming } from './branch-naming';\nimport { GitExec } from './git-exec';\nimport { MergeState, MergeMarker } from './merge-state';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\ninterface HashPoints {\n hashForkPoint: string;\n hashFeatureHead: string;\n hashMainHead: string;\n}\n\n// The four branch names merge-END needs to finalize a merge (swap squash→feature, push, clean up).\nexport class MergeContext {\n currentBranch: string;\n squashBranch: string;\n backupBranch: string;\n prNumber: string;\n\n constructor(currentBranch: string, squashBranch: string, backupBranch: string, prNumber: string) {\n this.currentBranch = currentBranch;\n this.squashBranch = squashBranch;\n this.backupBranch = backupBranch;\n this.prNumber = prNumber;\n }\n}\n\n// Outcome of merge-start: 'clean' carries the context for merge-END to finalize; 'conflict' means the\n// marker + context files were written and the caller should hand back to the AI (exit 2).\nexport class MergeStartResult {\n status: 'clean' | 'conflict';\n context: MergeContext | null;\n runDir: string; // this sync's numbered `merge-<n>/` dir — passed to merge-END so it reads THIS marker\n\n constructor(status: 'clean' | 'conflict', context: MergeContext | null, runDir: string) {\n this.status = status;\n this.context = context;\n this.runDir = runDir;\n }\n}\n\n// The one number `n` for a sync and the things it names: the pre-merge backup branch, its paired\n// conflict-context run dir, and (via `n`) the `preMerge<n>.hash` record of the tip it snapshotted.\nclass SyncSlot {\n backupBranch: string;\n runDir: string;\n n: number;\n\n constructor(backupBranch: string, runDir: string, n: number) {\n this.backupBranch = backupBranch;\n this.runDir = runDir;\n this.n = n;\n }\n}\n\n// Single source of truth for the merge process (written at conflict time, parameterized with the live\n// MERGE_DIR + conflicted-file list so it can never drift from the actual layout).\nconst MERGE_PROCESS_TEMPLATE = `# AI-Assisted Squash-Merge Conflict Resolution (generated)\n\nThis file was generated by \\`pnpm {{START_COMMAND}}\\` when the 3-point squash-merge hit conflicts.\nIt is the single source of truth for the merge process — follow it exactly.\n\nYou are on branch \\`{{SQUASH_BRANCH}}\\` with conflict markers in the working tree.\n\\`MERGE_DIR = {{MERGE_DIR}}\\`\n\n## How the gate works\n\n- Resolve every conflicted file in the working tree.\n- Run **\\`pnpm {{FINISH_COMMAND}}\\`** — the validation + finish gate. It scans for leftover conflict\n markers, checks each conflicted file has a written merge explanation, validates, and commits the\n merge. It is the paired half of \\`{{START_COMMAND}}\\` — never finish with the other flow's command\n (\\`wp-start-update\\` pairs with \\`wp-finish-update\\`; \\`wp-start-upsert-pr\\` pairs with\n \\`wp-finish-upsert-pr\\`, which additionally runs the \\`nx affected\\` build, renders the dashboard,\n and creates/updates the PR).\n- **DO \\`git add\\` each file you resolve.** Staging your resolutions is your job, not the gate's, and\n \\`git add\\` is **not** blocked by any guard. The gate stages with \\`git add -u\\`, which only re-stages\n already-tracked paths — leave a resolution unstaged and the gate stops with \"Git still reports\n unmerged files\" and sends you back to \\`git add\\` it.\n- **Do NOT run \\`git commit\\` / \\`git push\\` / \\`gh pr create|edit|merge\\` yourself** (nor \\`git merge\\` /\n \\`git rebase\\`). The \\`merge-in-progress-guard\\` hook blocks exactly those until the gate validates.\n The gate does the commit. See \\`webpieces.git-workflow.md\\` → \"Outcome B — CONFLICT merge\" for the\n full who-does-what; this file does not restate it.\n\n## STEP 1 — Load the merge context\n\nPer conflicted file, \\`MERGE_DIR/updatemain-<safe_path>/\\` holds (\\`<safe_path>\\` = path with \\`/\\`→\\`__\\`):\n\n\\`\\`\\`\nA-forkpoint.txt # file at fork point (base)\nB-feature.txt # file on your feature branch\nC-main.txt # file on main\nB-A.diff # what your feature changed (B−A)\nC-A.diff # what main changed (C−A)\n\\`\\`\\`\n\n\\`updatemain-hashes.json\\` holds A/B/C commit hashes. To see why main changed:\n\\`git log <A>..<C> --oneline\\`.\n\nWhy A/B/C are trustworthy here — and why you must never \\`git merge\\`/\\`git rebase\\` main in yourself —\nis the fork-point invariant: see \\`webpieces.git-workflow.md\\` → \"THE FORK POINT INVARIANT\". Read it\nonce if \\`B-A.diff\\` or \\`C-A.diff\\` ever shows you changes you do not recognize; that is the symptom\nof a polluted fork point, not of a bad diff.\n\n## STEP 2 — Resolve each conflicted file\n\nFor each file: read the working-tree file (the markers) and its \\`B-A.diff\\` / \\`C-A.diff\\` (intent),\nthen Edit to the resolved version, removing ALL conflict markers.\n\nStrategies: goals align & non-overlapping → merge both · one side removes what the other modifies\n→ prefer the removal · same lines, simple (imports/format) → merge both · same lines, complex or\nconflicting goals → ask the user · feature re-implements what main already squashed → prefer\nmain's, then re-apply only the genuinely new feature work.\n\n**Then write a merge explanation** for each conflicted file — NOT a comment in the source (that\nbreaks for JSON and deleted files). Write it next to that file's diffs, at:\n\n\\`\\`\\`\nMERGE_DIR/updatemain-<safe_path>/{{EXPLANATION_FILE}}\n\\`\\`\\`\n\n(\\`<safe_path>\\` = the conflict file path with \\`/\\` → \\`__\\`, the same dir that holds its\n\\`A-forkpoint.txt\\` / \\`B-A.diff\\` / \\`C-A.diff\\`.) In it, explain in a few sentences how you resolved\nthis file: which side you took where, what you combined from B-A.diff vs C-A.diff, and why. The\ngate fails if any conflicted file's explanation is missing or empty. Do not paste A/B/C context\nblocks into the source code.\n\n## STEP 3 — Run the gate (validates the merge AND finalizes it)\n\n\\`\\`\\`\npnpm {{FINISH_COMMAND}}\n\\`\\`\\`\n\n- Leftover conflict markers → fix those files and re-run.\n- Missing merge explanation → write it (see STEP 2) and re-run.\n- Build failure → fix the TypeScript/lint errors and re-run (the gate re-stages for you).\n- Missing review.json (PR flow only) → write it in the printed format (your PR review), then re-run.\n- On success it commits and finalizes the merge (in the PR flow it also renders the dashboard and\n creates/updates the PR).\n\n## Conflicted files\n\n{{FILE_LIST}}\n\n## If you need to bail out\n\nA numbered backup branch was created (e.g. \\`<feature>PreMerge1\\`). To abandon:\n\n\\`\\`\\`\ngit merge --abort 2>/dev/null; git checkout <feature> ; git branch -D {{SQUASH_BRANCH}}\n\\`\\`\\`\n\nThen delete \\`{{MERGE_DIR}}/\\` for a clean slate.\n`;\n\n// merge-START: the first half of the 3-point squash-merge lifecycle. Brings origin/main into a fresh\n// `<branch>Squash`, and on conflict writes the 3-point context + unvalidated marker + process doc,\n// then hands control back to the AI. On a clean merge it commits the squash and returns the branch\n// context so the caller (RunUpdate) can run merge-END to finalize. NEVER finalizes or posts a PR.\n@injectable(bindingScopeValues.Singleton)\nexport class MergeStart {\n constructor(\n private readonly gatherInfo: GatherInfo,\n private readonly branchNaming: BranchNaming,\n private readonly gitExec: GitExec,\n private readonly mergeState: MergeState,\n ) {}\n\n async mergeStart(repoRoot: string, verb: MutationVerb, home: string, finishCommand: string): Promise<MergeStartResult> {\n const currentBranch = execSync('git branch --show-current', { encoding: 'utf8' }).trim();\n if (currentBranch.endsWith('Squash')) {\n throw new CliExitError(1, `❌ On a leftover ${currentBranch} branch with no merge marker. Clean up: git branch -D ${currentBranch}`);\n }\n\n // One number `n` for this sync drives BOTH the backup branch and its `merge-<n>/` context dir.\n const slot = this.chooseSyncSlot(home, currentBranch);\n const backupBranch = slot.backupBranch;\n const mergeDir = slot.runDir;\n\n process.stdout.write('\\n' + SEP + '🔄 Squash-Merge Update from Main\\n' + SEP + '\\n');\n const info = await this.gatherInfo.gatherInfo();\n const hashes = info.hashes;\n if (info.alreadyUpToDate) {\n process.stdout.write('ℹ️ Branch already even with main; nothing to merge, continuing to push/build.\\n');\n }\n\n const prNumber = this.detectPr(this.branchNaming.baseBranchName(currentBranch));\n process.stdout.write(prNumber ? `Existing PR #${prNumber} will be updated.\\n` : 'No existing PR (one can be created later).\\n');\n\n this.createBackup(currentBranch, backupBranch);\n // Record THIS sync's pre-merge tip as `staged/<feature>/preMerge<n>.hash` — every intermediate\n // state, not just the last one. It is what lets the `<feature>PreMerge<n>` snapshot BRANCH be\n // disposable (branches count toward the branch cap; a written-down hash does not), and it is the\n // ref the archive tag is cut from when the PR lands.\n this.mergeState.writePreMergeHash(home, slot.n, this.fullSha(currentBranch));\n const backupEvent = new BranchMutationEvent(verb, 'BACKUP');\n backupEvent.fromBranch = currentBranch;\n backupEvent.toBranch = backupBranch;\n logBranchMutation(repoRoot, backupEvent);\n\n const squashBranch = `${currentBranch}Squash`;\n if (spawnSync('git', ['show-ref', '--verify', '--quiet', `refs/heads/${squashBranch}`]).status === 0) {\n throw new CliExitError(1, `❌ Stale ${squashBranch} from a previous run. Delete it: git branch -D ${squashBranch}`);\n }\n\n // Branch the squash off origin/main directly — worktree-native. origin/main was already fetched\n // in gatherInfo(), and A/B/C are computed purely from origin/main, so the merge base is identical\n // to the old checkout-main + pull path.\n const originMainSha = this.shortSha('origin/main');\n this.gitExec.runGitChecked(['checkout', '-b', squashBranch, 'origin/main'], 'Failed to create squash branch off origin/main');\n const baseEvent = new BranchMutationEvent(verb, 'PULL');\n baseEvent.newMain = originMainSha;\n logBranchMutation(repoRoot, baseEvent);\n\n process.stdout.write('\\n' + SEP + `🔀 Squash merging ${currentBranch}\\n` + SEP + '\\n');\n const merge = spawnSync('git', ['merge', '--squash', currentBranch], { stdio: 'inherit' });\n if (merge.status !== 0) {\n this.handleConflictsHandback(repoRoot, mergeDir, currentBranch, squashBranch, backupBranch, prNumber, hashes, finishCommand);\n this.logConflict(repoRoot, verb, mergeDir);\n return new MergeStartResult('conflict', null, mergeDir);\n }\n logBranchMutation(repoRoot, new BranchMutationEvent(verb, 'SQUASH'));\n\n const nothingStaged = spawnSync('git', ['diff-index', '--quiet', '--cached', 'HEAD', '--']).status === 0;\n if (nothingStaged) {\n process.stdout.write('ℹ️ Already up-to-date with main (nothing to merge).\\n');\n } else {\n // Internal, transient subject for the single squash commit on the feature branch. It NO LONGER\n // reaches main's history: finish-upsert-pr squash-merges the PR with an explicit\n // `gh pr merge --subject <PR title> --body-file <commit summary>`, so main carries the PR title.\n this.gitExec.runGitChecked(['commit', '-m', `Squash merge of ${currentBranch}`], 'Failed to commit squash merge');\n // Clean merge ⇒ hashes only, no conflicts.md. Its ABSENCE is what marks this merge clean.\n this.mergeState.recordCleanMerge(mergeDir, hashes.hashForkPoint, hashes.hashFeatureHead, hashes.hashMainHead);\n }\n return new MergeStartResult('clean', new MergeContext(currentBranch, squashBranch, backupBranch, prNumber), mergeDir);\n }\n\n // Detect the PR by its STABLE feature branch (a leftover `…wpN` still resolves to the one name the\n // PR lives on).\n private detectPr(baseBranch: string): string {\n const result = spawnSync(\n 'gh', ['pr', 'list', '--head', baseBranch, '--json', 'number', '--jq', '.[0].number'],\n { encoding: 'utf8' },\n );\n return result.status === 0 ? (result.stdout ?? '').trim() : '';\n }\n\n // Pick this sync's slot number MONOTONICALLY from the durable audit dirs, then step past any\n // existing `<branch>PreMerge<n>` branch so the same `n` is free for the paired backup branch.\n private chooseSyncSlot(home: string, currentBranch: string): SyncSlot {\n const branchExists = (name: string): boolean =>\n spawnSync('git', ['show-ref', '--verify', '--quiet', `refs/heads/${name}`]).status === 0;\n let n = this.mergeState.nextMergeSlotNumber(home);\n while (branchExists(this.branchNaming.preMergeBackupName(currentBranch, n))) n += 1;\n return new SyncSlot(this.branchNaming.preMergeBackupName(currentBranch, n), this.mergeState.mergeRunDirFor(home, n), n);\n }\n\n // Snapshot the pre-merge state onto the caller-chosen `backupBranch`, never overwriting.\n private createBackup(currentBranch: string, backupBranch: string): void {\n process.stdout.write('\\n' + SEP + '💾 Creating Pre-Merge Backup\\n' + SEP + '\\n');\n this.gitExec.runGitChecked(['checkout', '-b', backupBranch], 'Failed to create backup branch');\n this.gitExec.runGitChecked(['checkout', currentBranch], 'Failed to return to feature branch');\n process.stdout.write(`✅ Backup created: ${backupBranch}\\n\\n`);\n }\n\n private saveConflictContext(\n conflictedFiles: string[], mergeDir: string, forkPoint: string, featureHead: string, mainHead: string,\n ): void {\n for (const file of conflictedFiles) {\n const fileDir = this.mergeState.perFileContextDir(mergeDir, file);\n fs.mkdirSync(fileDir, { recursive: true });\n\n const fork = spawnSync('git', ['show', `${forkPoint}:${file}`], { encoding: 'utf8' });\n fs.writeFileSync(path.join(fileDir, 'A-forkpoint.txt'), fork.status === 0 ? (fork.stdout ?? '') : '(file did not exist)\\n');\n const feature = spawnSync('git', ['show', `${featureHead}:${file}`], { encoding: 'utf8' });\n fs.writeFileSync(path.join(fileDir, 'B-feature.txt'), feature.status === 0 ? (feature.stdout ?? '') : '(file did not exist)\\n');\n const main = spawnSync('git', ['show', `${mainHead}:${file}`], { encoding: 'utf8' });\n fs.writeFileSync(path.join(fileDir, 'C-main.txt'), main.status === 0 ? (main.stdout ?? '') : '(file did not exist)\\n');\n\n const ba = spawnSync('git', ['diff', forkPoint, featureHead, '--', file], { encoding: 'utf8' });\n fs.writeFileSync(path.join(fileDir, 'B-A.diff'), ba.stdout ?? '');\n const ca = spawnSync('git', ['diff', forkPoint, mainHead, '--', file], { encoding: 'utf8' });\n fs.writeFileSync(path.join(fileDir, 'C-A.diff'), ca.stdout ?? '');\n }\n }\n\n private mergeProcessDoc(mergeDir: string, squashBranch: string, conflictedFiles: string[], finishCommand: string): string {\n const fileList = conflictedFiles.map((f: string): string => `- \\`${f}\\``).join('\\n');\n return MERGE_PROCESS_TEMPLATE\n .replace(/\\{\\{SQUASH_BRANCH\\}\\}/g, squashBranch)\n .replace(/\\{\\{MERGE_DIR\\}\\}/g, mergeDir)\n .replace(/\\{\\{EXPLANATION_FILE\\}\\}/g, MERGE_EXPLANATION_FILE)\n .replace(/\\{\\{FINISH_COMMAND\\}\\}/g, finishCommand)\n .replace(/\\{\\{START_COMMAND\\}\\}/g, new SyncFlowGuidance().pairedStart(finishCommand))\n .replace(/\\{\\{FILE_LIST\\}\\}/g, fileList);\n }\n\n // Returns the absolute path of the written doc.\n private writeMergeProcessDoc(repoRoot: string, mergeDir: string, squashBranch: string, conflictedFiles: string[], finishCommand: string): string {\n const docDir = dotWebpieces.localFile(repoRoot, 'instruct-ai');\n fs.mkdirSync(docDir, { recursive: true });\n const docPath = path.join(docDir, 'webpieces.mergeprocess.md');\n fs.writeFileSync(docPath, this.mergeProcessDoc(mergeDir, squashBranch, conflictedFiles, finishCommand));\n return docPath;\n }\n\n // The AI-facing \"what just happened / what to do next\" recap on the conflict path.\n private printConflictHandback(\n docPath: string, mergeDir: string, squashBranch: string, conflictedFiles: string[], finishCommand: string,\n ): void {\n process.stdout.write('\\n' + SEP + `⚠️ Conflicts in ${conflictedFiles.length} file(s) — handing control back to you\\n` + SEP + '\\n');\n process.stdout.write('Here is exactly what I did and what you need to do:\\n\\n');\n process.stdout.write('What I did:\\n');\n process.stdout.write(' 1. snapshotted your pre-merge state to a PreMerge branch\\n');\n process.stdout.write(' 2. pulled origin/main and squash-merged your work onto it\\n');\n process.stdout.write(` 3. hit conflicts — you are now on the transient branch ${squashBranch}\\n\\n`);\n process.stdout.write('What you need to do:\\n');\n process.stdout.write(` 1. read the merge process doc: ${docPath}\\n`);\n process.stdout.write(` 2. resolve each conflicted file below (its 3-point A/B/C context + diffs are in\\n`);\n process.stdout.write(` ${mergeDir}/updatemain-<file>/), \\`git add\\` it, and write that file's merge-explanation.md\\n`);\n process.stdout.write(` 3. run pnpm ${finishCommand} — it validates, commits, and finalizes (do NOT git commit/push yourself)\\n\\n`);\n process.stdout.write('Conflicted files:\\n');\n for (const file of conflictedFiles) process.stdout.write(` - ${file}\\n`);\n process.stdout.write('\\n' + SEP);\n }\n\n // Write the conflict context files + the unvalidated marker + the process doc. Does NOT exit.\n private handleConflictsHandback(\n repoRoot: string, mergeDir: string, currentBranch: string, squashBranch: string,\n backupBranch: string, prNumber: string, hashes: HashPoints, finishCommand: string,\n ): void {\n const raw = execSync('git diff --name-only --diff-filter=U', { encoding: 'utf8' }).trim();\n const conflictedFiles = raw.split('\\n').filter((f: string): boolean => f.trim() !== '');\n fs.mkdirSync(mergeDir, { recursive: true });\n // `conflicts.md` replaces the old `updatemain-conflicted-files.txt` (which nothing read) AND is\n // the 3-point signal itself: this file exists in a run dir if and only if that merge conflicted,\n // which is what lets the index classify each merge without a per-branch directory-name scheme.\n this.mergeState.writeConflicts(mergeDir, conflictedFiles);\n fs.writeFileSync(path.join(mergeDir, 'updatemain-hashes.json'), JSON.stringify(hashes, null, 2) + '\\n');\n this.saveConflictContext(conflictedFiles, mergeDir, hashes.hashForkPoint, hashes.hashFeatureHead, hashes.hashMainHead);\n\n const marker = new MergeMarker(\n currentBranch, squashBranch, backupBranch, prNumber, conflictedFiles,\n hashes.hashForkPoint, hashes.hashFeatureHead, hashes.hashMainHead, false,\n );\n this.mergeState.writeMergeMarker(mergeDir, marker);\n const docPath = this.writeMergeProcessDoc(repoRoot, mergeDir, squashBranch, conflictedFiles, finishCommand);\n this.printConflictHandback(docPath, mergeDir, squashBranch, conflictedFiles, finishCommand);\n }\n\n // Short sha of a ref (best-effort — '' if it can't resolve).\n private shortSha(ref: string): string {\n const result = spawnSync('git', ['rev-parse', '--short', ref], { encoding: 'utf8' });\n return result.status === 0 ? (result.stdout ?? '').trim() : '';\n }\n\n // FULL sha of a ref (best-effort — '' if it can't resolve). Recorded for the pre-merge tips because\n // those hashes are meant to be used later to restore state, and an abbreviated sha can go ambiguous\n // as the repo grows.\n private fullSha(ref: string): string {\n const result = spawnSync('git', ['rev-parse', ref], { encoding: 'utf8' });\n return result.status === 0 ? (result.stdout ?? '').trim() : '';\n }\n\n // Log the CONFLICT phase with the conflicted-file list + artifact paths a resolver needs.\n private logConflict(repoRoot: string, verb: MutationVerb, mergeDir: string): void {\n const raw = spawnSync('git', ['diff', '--name-only', '--diff-filter=U'], { cwd: repoRoot, encoding: 'utf8' });\n const files = (raw.status === 0 ? (raw.stdout ?? '') : '').split('\\n').map((f: string): string => f.trim()).filter((f: string): boolean => f !== '');\n const event = new BranchMutationEvent(verb, 'CONFLICT');\n event.conflict = true;\n event.conflictFiles = files;\n event.artifacts = [\n path.join(mergeDir, 'updatemain-<file>'),\n dotWebpieces.localFile(repoRoot, 'instruct-ai', 'webpieces.mergeprocess.md'),\n ];\n logBranchMutation(repoRoot, event);\n }\n}\n"]}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { DotWebpieces } from '@webpieces/rules-config';
|
|
1
2
|
export declare class MergeMarker {
|
|
2
3
|
currentBranch: string;
|
|
3
4
|
squashBranch: string;
|
|
@@ -48,12 +49,22 @@ export declare const ARCHIVE_RECORD_FILE = "archive.json";
|
|
|
48
49
|
export declare const MERGE_INDEX_FILE = "index.json";
|
|
49
50
|
/** Filesystem layout + read/write of the per-feature merge run dirs and their conflict markers. */
|
|
50
51
|
export declare class MergeState {
|
|
52
|
+
private readonly dotDir;
|
|
53
|
+
constructor(dotDir?: DotWebpieces);
|
|
51
54
|
mergeDirFor(repoRoot: string, featureName: string): string;
|
|
52
|
-
/**
|
|
55
|
+
/**
|
|
56
|
+
* `<local .webpieces>/merge-info/staged/<feature>/` — the in-flight home.
|
|
57
|
+
*
|
|
58
|
+
* LOCAL scope throughout this class: a 3-point merge belongs to the worktree performing it, and
|
|
59
|
+
* two worktrees are on two different branches, so these paths are disjoint by construction and need
|
|
60
|
+
* no locking. In a linked worktree they now resolve under
|
|
61
|
+
* `<primary>/.webpieces/worktrees/<name>/merge-info/…`, which also means an in-flight merge is not
|
|
62
|
+
* destroyed by removing the worktree directory.
|
|
63
|
+
*/
|
|
53
64
|
stagedDirFor(repoRoot: string, featureName: string): string;
|
|
54
|
-
/**
|
|
65
|
+
/** `<local .webpieces>/merge-info/merged/<feature>/` — the landed home. */
|
|
55
66
|
mergedDirFor(repoRoot: string, featureName: string): string;
|
|
56
|
-
/**
|
|
67
|
+
/** `<local .webpieces>/merge-info/` itself — holds `staged/`, `merged/` and `index.json`. */
|
|
57
68
|
mergeInfoRoot(repoRoot: string): string;
|
|
58
69
|
/**
|
|
59
70
|
* MIGRATION for the pre-`staged/` layout, done IN PLACE and lazily.
|
|
@@ -85,6 +85,10 @@ exports.ARCHIVE_RECORD_FILE = 'archive.json';
|
|
|
85
85
|
exports.MERGE_INDEX_FILE = 'index.json';
|
|
86
86
|
/** Filesystem layout + read/write of the per-feature merge run dirs and their conflict markers. */
|
|
87
87
|
let MergeState = class MergeState {
|
|
88
|
+
dotDir;
|
|
89
|
+
constructor(dotDir = rules_config_1.dotWebpieces) {
|
|
90
|
+
this.dotDir = dotDir;
|
|
91
|
+
}
|
|
88
92
|
// The per-feature "home" dir `.webpieces/merge-info/<slug>/`. It no longer holds the marker/context
|
|
89
93
|
// directly — each sync gets its own numbered `merge-<n>/` run dir underneath (mergeRunDirFor),
|
|
90
94
|
// paired with the sync's `<feature>PreMerge<n>` backup branch. This keeps merge N from ever reusing
|
|
@@ -94,17 +98,25 @@ let MergeState = class MergeState {
|
|
|
94
98
|
this.migrateLegacyHome(repoRoot, featureName, staged);
|
|
95
99
|
return staged;
|
|
96
100
|
}
|
|
97
|
-
/**
|
|
101
|
+
/**
|
|
102
|
+
* `<local .webpieces>/merge-info/staged/<feature>/` — the in-flight home.
|
|
103
|
+
*
|
|
104
|
+
* LOCAL scope throughout this class: a 3-point merge belongs to the worktree performing it, and
|
|
105
|
+
* two worktrees are on two different branches, so these paths are disjoint by construction and need
|
|
106
|
+
* no locking. In a linked worktree they now resolve under
|
|
107
|
+
* `<primary>/.webpieces/worktrees/<name>/merge-info/…`, which also means an in-flight merge is not
|
|
108
|
+
* destroyed by removing the worktree directory.
|
|
109
|
+
*/
|
|
98
110
|
stagedDirFor(repoRoot, featureName) {
|
|
99
|
-
return
|
|
111
|
+
return this.dotDir.localFile(repoRoot, rules_config_1.MERGE_INFO_DIR, exports.STAGED_DIR, featureName);
|
|
100
112
|
}
|
|
101
|
-
/**
|
|
113
|
+
/** `<local .webpieces>/merge-info/merged/<feature>/` — the landed home. */
|
|
102
114
|
mergedDirFor(repoRoot, featureName) {
|
|
103
|
-
return
|
|
115
|
+
return this.dotDir.localFile(repoRoot, rules_config_1.MERGE_INFO_DIR, exports.MERGED_DIR, featureName);
|
|
104
116
|
}
|
|
105
|
-
/**
|
|
117
|
+
/** `<local .webpieces>/merge-info/` itself — holds `staged/`, `merged/` and `index.json`. */
|
|
106
118
|
mergeInfoRoot(repoRoot) {
|
|
107
|
-
return
|
|
119
|
+
return this.dotDir.localFile(repoRoot, rules_config_1.MERGE_INFO_DIR);
|
|
108
120
|
}
|
|
109
121
|
/**
|
|
110
122
|
* MIGRATION for the pre-`staged/` layout, done IN PLACE and lazily.
|
|
@@ -123,7 +135,7 @@ let MergeState = class MergeState {
|
|
|
123
135
|
* ever part of a commit.
|
|
124
136
|
*/
|
|
125
137
|
migrateLegacyHome(repoRoot, featureName, staged) {
|
|
126
|
-
const legacy =
|
|
138
|
+
const legacy = this.dotDir.localFile(repoRoot, rules_config_1.MERGE_INFO_DIR, featureName);
|
|
127
139
|
if (featureName === exports.STAGED_DIR || featureName === exports.MERGED_DIR)
|
|
128
140
|
return;
|
|
129
141
|
if (!fs.existsSync(legacy) || fs.existsSync(staged))
|
|
@@ -316,6 +328,7 @@ let MergeState = class MergeState {
|
|
|
316
328
|
};
|
|
317
329
|
exports.MergeState = MergeState;
|
|
318
330
|
exports.MergeState = MergeState = tslib_1.__decorate([
|
|
319
|
-
(0, inversify_1.injectable)(inversify_1.bindingScopeValues.Singleton)
|
|
331
|
+
(0, inversify_1.injectable)(inversify_1.bindingScopeValues.Singleton),
|
|
332
|
+
tslib_1.__metadata("design:paramtypes", [rules_config_1.DotWebpieces])
|
|
320
333
|
], MergeState);
|
|
321
334
|
//# sourceMappingURL=merge-state.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"merge-state.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/merge-state.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\nimport {\n WEBPIECES_TMP_DIR,\n MERGE_INFO_DIR,\n MERGE_IN_PROGRESS_FILE,\n MERGE_EXPLANATION_FILE,\n} from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\n\n// Proof-obligation marker written when a 3-point squash-merge hits conflicts. Its mere\n// presence (with validated=false) is what the merge-in-progress-guard hook uses to block\n// commit/push/PR until `wp-finish-upsert-pr` validates the resolution and flips it true.\nexport class MergeMarker {\n currentBranch: string;\n squashBranch: string;\n backupBranch: string;\n prNumber: string;\n conflictedFiles: string[];\n forkPoint: string;\n featureHead: string;\n mainHead: string;\n validated: boolean;\n\n constructor(\n currentBranch: string,\n squashBranch: string,\n backupBranch: string,\n prNumber: string,\n conflictedFiles: string[],\n forkPoint: string,\n featureHead: string,\n mainHead: string,\n validated: boolean,\n ) {\n this.currentBranch = currentBranch;\n this.squashBranch = squashBranch;\n this.backupBranch = backupBranch;\n this.prNumber = prNumber;\n this.conflictedFiles = conflictedFiles;\n this.forkPoint = forkPoint;\n this.featureHead = featureHead;\n this.mainHead = mainHead;\n this.validated = validated;\n }\n}\n\nexport class MarkerScanResult {\n clean: boolean;\n filesWithMarkers: string[];\n\n constructor(clean: boolean, filesWithMarkers: string[]) {\n this.clean = clean;\n this.filesWithMarkers = filesWithMarkers;\n }\n}\n\n// Data-only: the three commit points of a 3-point merge as persisted in `updatemain-hashes.json`\n// (the exact key shape the conflict path already writes), plus when it was recorded.\nexport class MergeHashRecord {\n hashForkPoint: string;\n hashFeatureHead: string;\n hashMainHead: string;\n timestamp: string;\n\n constructor(hashForkPoint: string, hashFeatureHead: string, hashMainHead: string, timestamp: string) {\n this.hashForkPoint = hashForkPoint;\n this.hashFeatureHead = hashFeatureHead;\n this.hashMainHead = hashMainHead;\n this.timestamp = timestamp;\n }\n}\n\nconst CONFLICT_MARKER_RE = /^(<{7}|={7}|>{7})/m;\n\n/**\n * The two halves of `.webpieces/merge-info/`.\n *\n * `staged/` mirrors branches that STILL EXIST — it answers \"what am I working on right now\", and it\n * self-cleans when a PR lands (the whole dir is MOVED to `merged/`) instead of growing forever.\n * `merged/` is the post-mortem record, and is where the archive-tag reference lives.\n *\n * Both are reserved names: a feature branch slugifies `/`→`-`, so no feature can ever be called\n * `staged` or `merged` and collide with them.\n */\nexport const STAGED_DIR = 'staged';\nexport const MERGED_DIR = 'merged';\n\n/**\n * The 3-point conflict record. PRESENT ONLY WHEN THE MERGE ACTUALLY CONFLICTED.\n *\n * This replaces the old `no-3point-merge.md`, which was written into EVERY clean merge dir and whose\n * entire content was \"nothing interesting happened here\" plus the same A/B/C shas that\n * `updatemain-hashes.json` already carries, one line below, in machine-readable form. Nothing read it.\n * So ABSENCE is now the signal: \"does this directory contain conflicts.md?\" is the whole question, and\n * the shas a human might have wanted from the marker are still on disk in updatemain-hashes.json.\n */\nexport const CONFLICTS_FILE = 'conflicts.md';\n\n// `{ archiveTag, tipSha, baseSha, pr, mergedAt }` written into `merged/<feature>/` when the PR lands.\nexport const ARCHIVE_RECORD_FILE = 'archive.json';\n\n// The one index that answers \"which merges across ALL branches were 3-point?\" — see MergeInfoIndex.\nexport const MERGE_INDEX_FILE = 'index.json';\n\n/** Filesystem layout + read/write of the per-feature merge run dirs and their conflict markers. */\n@injectable(bindingScopeValues.Singleton)\nexport class MergeState {\n // The per-feature \"home\" dir `.webpieces/merge-info/<slug>/`. It no longer holds the marker/context\n // directly — each sync gets its own numbered `merge-<n>/` run dir underneath (mergeRunDirFor),\n // paired with the sync's `<feature>PreMerge<n>` backup branch. This keeps merge N from ever reusing\n // merge N-1's stale per-file context / merge-explanation.md.\n mergeDirFor(repoRoot: string, featureName: string): string {\n const staged = this.stagedDirFor(repoRoot, featureName);\n this.migrateLegacyHome(repoRoot, featureName, staged);\n return staged;\n }\n\n /** `.webpieces/merge-info/staged/<feature>/` — the in-flight home. */\n stagedDirFor(repoRoot: string, featureName: string): string {\n return path.join(repoRoot, WEBPIECES_TMP_DIR, MERGE_INFO_DIR, STAGED_DIR, featureName);\n }\n\n /** `.webpieces/merge-info/merged/<feature>/` — the landed home. */\n mergedDirFor(repoRoot: string, featureName: string): string {\n return path.join(repoRoot, WEBPIECES_TMP_DIR, MERGE_INFO_DIR, MERGED_DIR, featureName);\n }\n\n /** `.webpieces/merge-info/` itself — the home holding `staged/`, `merged/` and `index.json`. */\n mergeInfoRoot(repoRoot: string): string {\n return path.join(repoRoot, WEBPIECES_TMP_DIR, MERGE_INFO_DIR);\n }\n\n /**\n * MIGRATION for the pre-`staged/` layout, done IN PLACE and lazily.\n *\n * Old layout put the feature home directly at `merge-info/<feature>/`. That dir is real, live state\n * (it can hold an UNVALIDATED merge marker for a merge that is in progress RIGHT NOW), so leaving\n * legacy dirs alone was not an option: the in-flight merge would become invisible to the finish\n * gate, which now looks under `staged/`. Tolerating both layouts on read was the other candidate and\n * was rejected — two homes means every reader, the guard included, needs two lookups forever.\n *\n * So: the first time anything asks for a feature's home, an existing legacy dir is MOVED to\n * `staged/<feature>/`. `fs.renameSync` is atomic within a filesystem and moves the whole tree with no\n * copying, so no merge record can be half-migrated. If `staged/<feature>` already exists the legacy\n * dir is left untouched — never merged, never deleted — because two homes for one feature is exactly\n * the ambiguity a human must resolve. `.webpieces/` is gitignored local state, so nothing here is\n * ever part of a commit.\n */\n private migrateLegacyHome(repoRoot: string, featureName: string, staged: string): void {\n const legacy = path.join(repoRoot, WEBPIECES_TMP_DIR, MERGE_INFO_DIR, featureName);\n if (featureName === STAGED_DIR || featureName === MERGED_DIR) return;\n if (!fs.existsSync(legacy) || fs.existsSync(staged)) return;\n fs.mkdirSync(path.dirname(staged), { recursive: true });\n fs.renameSync(legacy, staged);\n }\n\n /**\n * Record THIS sync's pre-merge tip as `staged/<feature>/preMerge<n>.hash`.\n *\n * Every intermediate state, not just the last: a branch updated from main five times has five\n * genuinely different pre-merge tips, and the fifth tells you nothing about what the second looked\n * like. The hashes are what make the `<feature>PreMerge<n>` snapshot BRANCHES disposable — once the\n * tip is written down (and, at land time, tagged), the branch itself is pure branch-cap pressure.\n */\n writePreMergeHash(home: string, n: number, sha: string): void {\n fs.mkdirSync(home, { recursive: true });\n fs.writeFileSync(path.join(home, `preMerge${String(n)}.hash`), sha + '\\n');\n }\n\n /** Every recorded pre-merge tip for a feature, in slot order. [] when none were recorded. */\n readPreMergeHashes(home: string): string[] {\n if (!fs.existsSync(home)) return [];\n const found = new Map<number, string>();\n for (const entry of fs.readdirSync(home)) {\n const match = entry.match(/^preMerge(\\d+)\\.hash$/);\n if (match === null) continue;\n found.set(parseInt(match[1], 10), fs.readFileSync(path.join(home, entry), 'utf8').trim());\n }\n return [...found.keys()].sort((a: number, b: number): number => a - b)\n .map((n: number): string => found.get(n) ?? '');\n }\n\n // The run dir for sync number `n`: `<home>/merge-<n>/`. Holds this sync's marker + per-file\n // `updatemain-<file>/` context. Numbered to match the sync's `<feature>PreMerge<n>` backup branch.\n mergeRunDirFor(home: string, n: number): string {\n return path.join(home, `merge-${n}`);\n }\n\n // The next NEVER-REUSED merge slot number for a feature home: one past the highest existing\n // `merge-<n>/` dir (or 1 if none). Durable, monotonic source of truth for slot numbers — derived\n // from the audit dirs THEMSELVES, not from which transient `<feature>PreMerge<n>` branch exists.\n nextMergeSlotNumber(home: string): number {\n if (!fs.existsSync(home)) return 1;\n let max = 0;\n for (const entry of fs.readdirSync(home)) {\n const match = entry.match(/^merge-(\\d+)$/);\n if (match === null) continue;\n const n = parseInt(match[1], 10);\n if (n > max) max = n;\n }\n return max + 1;\n }\n\n /**\n * Record a CLEAN (no-conflict) sync: the A/B/C shas in `updatemain-hashes.json`, and NOTHING ELSE.\n *\n * The `no-3point-merge.md` placeholder this used to also write is gone. Its whole content was a\n * sentence saying nothing interesting happened plus the same three shas being written on the very\n * next line in machine-readable form — a file that appeared in every clean directory a human opens,\n * carried no information the JSON did not, and had no consumer anywhere in the codebase. Absence is\n * now the signal instead: a run dir with no {@link CONFLICTS_FILE} was a clean merge.\n */\n recordCleanMerge(mergeDir: string, forkPoint: string, featureHead: string, mainHead: string): void {\n fs.mkdirSync(mergeDir, { recursive: true });\n const record = new MergeHashRecord(forkPoint, featureHead, mainHead, new Date().toISOString());\n fs.writeFileSync(path.join(mergeDir, 'updatemain-hashes.json'), JSON.stringify(record, null, 2) + '\\n');\n }\n\n /** Path of a run dir's conflict record. Its EXISTENCE is what makes the merge \"3-point\". */\n conflictsPath(mergeDir: string): string {\n return path.join(mergeDir, CONFLICTS_FILE);\n }\n\n /** True when this run dir records a 3-point merge — i.e. `conflicts.md` is there. */\n wasThreeWay(mergeDir: string): boolean {\n return fs.existsSync(this.conflictsPath(mergeDir));\n }\n\n /** Write the 3-point conflict record. Only ever called on the conflict path. */\n writeConflicts(mergeDir: string, files: string[]): void {\n fs.mkdirSync(mergeDir, { recursive: true });\n const body =\n '# 3-point squash-merge — conflicts\\n\\n' +\n 'This sync needed a 3-point resolution. Each file below has its A/B/C context and the AI\\'s\\n' +\n `${MERGE_EXPLANATION_FILE} under \\`updatemain-<path with / → __>/\\`, and the A/B/C commit shas\\n` +\n 'are in `updatemain-hashes.json` next to this file.\\n\\n' +\n files.map((file: string): string => `- ${file}`).join('\\n') + '\\n';\n fs.writeFileSync(this.conflictsPath(mergeDir), body);\n }\n\n /** The conflicted-file list back out of `conflicts.md`. [] when the merge was clean. */\n readConflictedFiles(mergeDir: string): string[] {\n const filePath = this.conflictsPath(mergeDir);\n if (!fs.existsSync(filePath)) return [];\n return fs.readFileSync(filePath, 'utf8')\n .split('\\n')\n .filter((line: string): boolean => line.startsWith('- '))\n .map((line: string): string => line.slice(2).trim())\n .filter((line: string): boolean => line !== '');\n }\n\n /** Every `merge-<n>/` run dir under a feature home, in slot order. [] when the home does not exist. */\n listMergeRunDirs(home: string): number[] {\n if (!fs.existsSync(home)) return [];\n const slots: number[] = [];\n for (const entry of fs.readdirSync(home)) {\n const match = entry.match(/^merge-(\\d+)$/);\n if (match !== null) slots.push(parseInt(match[1], 10));\n }\n return slots.sort((a: number, b: number): number => a - b);\n }\n\n // Locate the in-progress merge's run dir: the `<home>/merge-*/` subdir holding a marker. There is\n // at most one; if more than one somehow exists, prefer an UNVALIDATED marker (the live conflict),\n // else return the first found. Null when none.\n findActiveMergeRunDir(home: string): string | null {\n if (!fs.existsSync(home)) return null;\n let fallback: string | null = null;\n for (const entry of fs.readdirSync(home)) {\n if (!entry.startsWith('merge-')) continue;\n const dir = path.join(home, entry);\n const marker = this.readMergeMarker(dir);\n if (marker === null) continue;\n if (!marker.validated) return dir;\n if (fallback === null) fallback = dir;\n }\n return fallback;\n }\n\n // Per-conflicted-file context dir holding A-forkpoint.txt / B-feature.txt / C-main.txt /\n // B-A.diff / C-A.diff (and the AI's merge-explanation.md). Shared so writer and reader agree on the\n // layout: the conflict file path with `/` → `__`, prefixed `updatemain-`.\n perFileContextDir(mergeDir: string, file: string): string {\n return path.join(mergeDir, `updatemain-${file.replace(/\\//g, '__')}`);\n }\n\n markerPath(mergeDir: string): string {\n return path.join(mergeDir, MERGE_IN_PROGRESS_FILE);\n }\n\n readMergeMarker(mergeDir: string): MergeMarker | null {\n const filePath = this.markerPath(mergeDir);\n if (!fs.existsSync(filePath)) return null;\n const raw = JSON.parse(fs.readFileSync(filePath, 'utf8')) as MergeMarker;\n return new MergeMarker(\n raw.currentBranch,\n raw.squashBranch,\n raw.backupBranch,\n raw.prNumber,\n raw.conflictedFiles ?? [],\n raw.forkPoint,\n raw.featureHead,\n raw.mainHead,\n raw.validated === true,\n );\n }\n\n writeMergeMarker(mergeDir: string, marker: MergeMarker): void {\n fs.mkdirSync(mergeDir, { recursive: true });\n fs.writeFileSync(this.markerPath(mergeDir), JSON.stringify(marker, null, 2) + '\\n');\n }\n\n clearMergeMarker(mergeDir: string): void {\n const filePath = this.markerPath(mergeDir);\n if (fs.existsSync(filePath)) fs.rmSync(filePath);\n }\n\n /**\n * Scoped conflict-marker scan: reads ONLY the given conflicted files (relative to repo root),\n * never the whole repo — stays O(conflicts) regardless of monorepo size.\n */\n scanConflictMarkers(repoRoot: string, files: string[]): MarkerScanResult {\n const filesWithMarkers: string[] = [];\n for (const file of files) {\n const abs = path.join(repoRoot, file);\n if (!fs.existsSync(abs)) continue;\n const content = fs.readFileSync(abs, 'utf8');\n if (CONFLICT_MARKER_RE.test(content)) filesWithMarkers.push(file);\n }\n return new MarkerScanResult(filesWithMarkers.length === 0, filesWithMarkers);\n }\n\n /**\n * Explanation scan: every conflicted file the AI resolved must have a non-empty\n * MERGE_EXPLANATION_FILE in its per-file context dir (next to the diffs), proving the AI\n * deliberately 3-point merged it and recording HOW. Returns files whose explanation is missing or\n * empty. Works for every conflicted file type — including comment-less files (JSON) and files\n * resolved by deletion (no working-tree file to inspect).\n */\n scanMergeExplanations(mergeDir: string, files: string[]): MarkerScanResult {\n const filesMissingExplanation: string[] = [];\n for (const file of files) {\n const explPath = path.join(this.perFileContextDir(mergeDir, file), MERGE_EXPLANATION_FILE);\n const present = fs.existsSync(explPath) && fs.readFileSync(explPath, 'utf8').trim() !== '';\n if (!present) filesMissingExplanation.push(file);\n }\n return new MarkerScanResult(filesMissingExplanation.length === 0, filesMissingExplanation);\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"merge-state.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/merge-state.ts"],"names":[],"mappings":";;;;AAAA,+CAAyB;AACzB,mDAA6B;AAC7B,0DAMiC;AACjC,yCAA2D;AAE3D,uFAAuF;AACvF,yFAAyF;AACzF,yFAAyF;AACzF,MAAa,WAAW;IACpB,aAAa,CAAS;IACtB,YAAY,CAAS;IACrB,YAAY,CAAS;IACrB,QAAQ,CAAS;IACjB,eAAe,CAAW;IAC1B,SAAS,CAAS;IAClB,WAAW,CAAS;IACpB,QAAQ,CAAS;IACjB,SAAS,CAAU;IAEnB,YACI,aAAqB,EACrB,YAAoB,EACpB,YAAoB,EACpB,QAAgB,EAChB,eAAyB,EACzB,SAAiB,EACjB,WAAmB,EACnB,QAAgB,EAChB,SAAkB;QAElB,IAAI,CAAC,aAAa,GAAG,aAAa,CAAC;QACnC,IAAI,CAAC,YAAY,GAAG,YAAY,CAAC;QACjC,IAAI,CAAC,YAAY,GAAG,YAAY,CAAC;QACjC,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,eAAe,GAAG,eAAe,CAAC;QACvC,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;QAC3B,IAAI,CAAC,WAAW,GAAG,WAAW,CAAC;QAC/B,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;IAC/B,CAAC;CACJ;AAhCD,kCAgCC;AAED,MAAa,gBAAgB;IACzB,KAAK,CAAU;IACf,gBAAgB,CAAW;IAE3B,YAAY,KAAc,EAAE,gBAA0B;QAClD,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;QACnB,IAAI,CAAC,gBAAgB,GAAG,gBAAgB,CAAC;IAC7C,CAAC;CACJ;AARD,4CAQC;AAED,iGAAiG;AACjG,qFAAqF;AACrF,MAAa,eAAe;IACxB,aAAa,CAAS;IACtB,eAAe,CAAS;IACxB,YAAY,CAAS;IACrB,SAAS,CAAS;IAElB,YAAY,aAAqB,EAAE,eAAuB,EAAE,YAAoB,EAAE,SAAiB;QAC/F,IAAI,CAAC,aAAa,GAAG,aAAa,CAAC;QACnC,IAAI,CAAC,eAAe,GAAG,eAAe,CAAC;QACvC,IAAI,CAAC,YAAY,GAAG,YAAY,CAAC;QACjC,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;IAC/B,CAAC;CACJ;AAZD,0CAYC;AAED,MAAM,kBAAkB,GAAG,oBAAoB,CAAC;AAEhD;;;;;;;;;GASG;AACU,QAAA,UAAU,GAAG,QAAQ,CAAC;AACtB,QAAA,UAAU,GAAG,QAAQ,CAAC;AAEnC;;;;;;;;GAQG;AACU,QAAA,cAAc,GAAG,cAAc,CAAC;AAE7C,sGAAsG;AACzF,QAAA,mBAAmB,GAAG,cAAc,CAAC;AAElD,oGAAoG;AACvF,QAAA,gBAAgB,GAAG,YAAY,CAAC;AAE7C,mGAAmG;AAE5F,IAAM,UAAU,GAAhB,MAAM,UAAU;IACU;IAA7B,YAA6B,SAAuB,2BAAY;QAAnC,WAAM,GAAN,MAAM,CAA6B;IAAG,CAAC;IAEpE,oGAAoG;IACpG,+FAA+F;IAC/F,oGAAoG;IACpG,6DAA6D;IAC7D,WAAW,CAAC,QAAgB,EAAE,WAAmB;QAC7C,MAAM,MAAM,GAAG,IAAI,CAAC,YAAY,CAAC,QAAQ,EAAE,WAAW,CAAC,CAAC;QACxD,IAAI,CAAC,iBAAiB,CAAC,QAAQ,EAAE,WAAW,EAAE,MAAM,CAAC,CAAC;QACtD,OAAO,MAAM,CAAC;IAClB,CAAC;IAED;;;;;;;;OAQG;IACH,YAAY,CAAC,QAAgB,EAAE,WAAmB;QAC9C,OAAO,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,QAAQ,EAAE,6BAAc,EAAE,kBAAU,EAAE,WAAW,CAAC,CAAC;IACpF,CAAC;IAED,2EAA2E;IAC3E,YAAY,CAAC,QAAgB,EAAE,WAAmB;QAC9C,OAAO,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,QAAQ,EAAE,6BAAc,EAAE,kBAAU,EAAE,WAAW,CAAC,CAAC;IACpF,CAAC;IAED,6FAA6F;IAC7F,aAAa,CAAC,QAAgB;QAC1B,OAAO,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,QAAQ,EAAE,6BAAc,CAAC,CAAC;IAC3D,CAAC;IAED;;;;;;;;;;;;;;;OAeG;IACK,iBAAiB,CAAC,QAAgB,EAAE,WAAmB,EAAE,MAAc;QAC3E,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,QAAQ,EAAE,6BAAc,EAAE,WAAW,CAAC,CAAC;QAC5E,IAAI,WAAW,KAAK,kBAAU,IAAI,WAAW,KAAK,kBAAU;YAAE,OAAO;QACrE,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,UAAU,CAAC,MAAM,CAAC;YAAE,OAAO;QAC5D,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACxD,EAAE,CAAC,UAAU,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAClC,CAAC;IAED;;;;;;;OAOG;IACH,iBAAiB,CAAC,IAAY,EAAE,CAAS,EAAE,GAAW;QAClD,EAAE,CAAC,SAAS,CAAC,IAAI,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACxC,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,WAAW,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,GAAG,GAAG,IAAI,CAAC,CAAC;IAC/E,CAAC;IAED,6FAA6F;IAC7F,kBAAkB,CAAC,IAAY;QAC3B,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC;YAAE,OAAO,EAAE,CAAC;QACpC,MAAM,KAAK,GAAG,IAAI,GAAG,EAAkB,CAAC;QACxC,KAAK,MAAM,KAAK,IAAI,EAAE,CAAC,WAAW,CAAC,IAAI,CAAC,EAAE,CAAC;YACvC,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,uBAAuB,CAAC,CAAC;YACnD,IAAI,KAAK,KAAK,IAAI;gBAAE,SAAS;YAC7B,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,KAAK,CAAC,EAAE,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;QAC9F,CAAC;QACD,OAAO,CAAC,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAS,EAAE,CAAS,EAAU,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC;aACjE,GAAG,CAAC,CAAC,CAAS,EAAU,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;IACxD,CAAC;IAED,4FAA4F;IAC5F,mGAAmG;IACnG,cAAc,CAAC,IAAY,EAAE,CAAS;QAClC,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,SAAS,CAAC,EAAE,CAAC,CAAC;IACzC,CAAC;IAED,4FAA4F;IAC5F,iGAAiG;IACjG,iGAAiG;IACjG,mBAAmB,CAAC,IAAY;QAC5B,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC;YAAE,OAAO,CAAC,CAAC;QACnC,IAAI,GAAG,GAAG,CAAC,CAAC;QACZ,KAAK,MAAM,KAAK,IAAI,EAAE,CAAC,WAAW,CAAC,IAAI,CAAC,EAAE,CAAC;YACvC,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,eAAe,CAAC,CAAC;YAC3C,IAAI,KAAK,KAAK,IAAI;gBAAE,SAAS;YAC7B,MAAM,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YACjC,IAAI,CAAC,GAAG,GAAG;gBAAE,GAAG,GAAG,CAAC,CAAC;QACzB,CAAC;QACD,OAAO,GAAG,GAAG,CAAC,CAAC;IACnB,CAAC;IAED;;;;;;;;OAQG;IACH,gBAAgB,CAAC,QAAgB,EAAE,SAAiB,EAAE,WAAmB,EAAE,QAAgB;QACvF,EAAE,CAAC,SAAS,CAAC,QAAQ,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAC5C,MAAM,MAAM,GAAG,IAAI,eAAe,CAAC,SAAS,EAAE,WAAW,EAAE,QAAQ,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,CAAC;QAC/F,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,wBAAwB,CAAC,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;IAC5G,CAAC;IAED,4FAA4F;IAC5F,aAAa,CAAC,QAAgB;QAC1B,OAAO,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,sBAAc,CAAC,CAAC;IAC/C,CAAC;IAED,qFAAqF;IACrF,WAAW,CAAC,QAAgB;QACxB,OAAO,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC,CAAC;IACvD,CAAC;IAED,gFAAgF;IAChF,cAAc,CAAC,QAAgB,EAAE,KAAe;QAC5C,EAAE,CAAC,SAAS,CAAC,QAAQ,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAC5C,MAAM,IAAI,GACN,wCAAwC;YACxC,8FAA8F;YAC9F,GAAG,qCAAsB,wEAAwE;YACjG,wDAAwD;YACxD,KAAK,CAAC,GAAG,CAAC,CAAC,IAAY,EAAU,EAAE,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;QACvE,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,aAAa,CAAC,QAAQ,CAAC,EAAE,IAAI,CAAC,CAAC;IACzD,CAAC;IAED,wFAAwF;IACxF,mBAAmB,CAAC,QAAgB;QAChC,MAAM,QAAQ,GAAG,IAAI,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC;QAC9C,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC;YAAE,OAAO,EAAE,CAAC;QACxC,OAAO,EAAE,CAAC,YAAY,CAAC,QAAQ,EAAE,MAAM,CAAC;aACnC,KAAK,CAAC,IAAI,CAAC;aACX,MAAM,CAAC,CAAC,IAAY,EAAW,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;aACxD,GAAG,CAAC,CAAC,IAAY,EAAU,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;aACnD,MAAM,CAAC,CAAC,IAAY,EAAW,EAAE,CAAC,IAAI,KAAK,EAAE,CAAC,CAAC;IACxD,CAAC;IAED,uGAAuG;IACvG,gBAAgB,CAAC,IAAY;QACzB,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC;YAAE,OAAO,EAAE,CAAC;QACpC,MAAM,KAAK,GAAa,EAAE,CAAC;QAC3B,KAAK,MAAM,KAAK,IAAI,EAAE,CAAC,WAAW,CAAC,IAAI,CAAC,EAAE,CAAC;YACvC,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,eAAe,CAAC,CAAC;YAC3C,IAAI,KAAK,KAAK,IAAI;gBAAE,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;QAC3D,CAAC;QACD,OAAO,KAAK,CAAC,IAAI,CAAC,CAAC,CAAS,EAAE,CAAS,EAAU,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IAC/D,CAAC;IAED,kGAAkG;IAClG,kGAAkG;IAClG,+CAA+C;IAC/C,qBAAqB,CAAC,IAAY;QAC9B,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC;YAAE,OAAO,IAAI,CAAC;QACtC,IAAI,QAAQ,GAAkB,IAAI,CAAC;QACnC,KAAK,MAAM,KAAK,IAAI,EAAE,CAAC,WAAW,CAAC,IAAI,CAAC,EAAE,CAAC;YACvC,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,QAAQ,CAAC;gBAAE,SAAS;YAC1C,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;YACnC,MAAM,MAAM,GAAG,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,CAAC;YACzC,IAAI,MAAM,KAAK,IAAI;gBAAE,SAAS;YAC9B,IAAI,CAAC,MAAM,CAAC,SAAS;gBAAE,OAAO,GAAG,CAAC;YAClC,IAAI,QAAQ,KAAK,IAAI;gBAAE,QAAQ,GAAG,GAAG,CAAC;QAC1C,CAAC;QACD,OAAO,QAAQ,CAAC;IACpB,CAAC;IAED,yFAAyF;IACzF,oGAAoG;IACpG,0EAA0E;IAC1E,iBAAiB,CAAC,QAAgB,EAAE,IAAY;QAC5C,OAAO,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,cAAc,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,IAAI,CAAC,EAAE,CAAC,CAAC;IAC1E,CAAC;IAED,UAAU,CAAC,QAAgB;QACvB,OAAO,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,qCAAsB,CAAC,CAAC;IACvD,CAAC;IAED,eAAe,CAAC,QAAgB;QAC5B,MAAM,QAAQ,GAAG,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC;QAC3C,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC;YAAE,OAAO,IAAI,CAAC;QAC1C,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAgB,CAAC;QACzE,OAAO,IAAI,WAAW,CAClB,GAAG,CAAC,aAAa,EACjB,GAAG,CAAC,YAAY,EAChB,GAAG,CAAC,YAAY,EAChB,GAAG,CAAC,QAAQ,EACZ,GAAG,CAAC,eAAe,IAAI,EAAE,EACzB,GAAG,CAAC,SAAS,EACb,GAAG,CAAC,WAAW,EACf,GAAG,CAAC,QAAQ,EACZ,GAAG,CAAC,SAAS,KAAK,IAAI,CACzB,CAAC;IACN,CAAC;IAED,gBAAgB,CAAC,QAAgB,EAAE,MAAmB;QAClD,EAAE,CAAC,SAAS,CAAC,QAAQ,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAC5C,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;IACxF,CAAC;IAED,gBAAgB,CAAC,QAAgB;QAC7B,MAAM,QAAQ,GAAG,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC;QAC3C,IAAI,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC;YAAE,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;IACrD,CAAC;IAED;;;OAGG;IACH,mBAAmB,CAAC,QAAgB,EAAE,KAAe;QACjD,MAAM,gBAAgB,GAAa,EAAE,CAAC;QACtC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACvB,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;YACtC,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC;gBAAE,SAAS;YAClC,MAAM,OAAO,GAAG,EAAE,CAAC,YAAY,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;YAC7C,IAAI,kBAAkB,CAAC,IAAI,CAAC,OAAO,CAAC;gBAAE,gBAAgB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACtE,CAAC;QACD,OAAO,IAAI,gBAAgB,CAAC,gBAAgB,CAAC,MAAM,KAAK,CAAC,EAAE,gBAAgB,CAAC,CAAC;IACjF,CAAC;IAED;;;;;;OAMG;IACH,qBAAqB,CAAC,QAAgB,EAAE,KAAe;QACnD,MAAM,uBAAuB,GAAa,EAAE,CAAC;QAC7C,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACvB,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,iBAAiB,CAAC,QAAQ,EAAE,IAAI,CAAC,EAAE,qCAAsB,CAAC,CAAC;YAC3F,MAAM,OAAO,GAAG,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,YAAY,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC;YAC3F,IAAI,CAAC,OAAO;gBAAE,uBAAuB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACrD,CAAC;QACD,OAAO,IAAI,gBAAgB,CAAC,uBAAuB,CAAC,MAAM,KAAK,CAAC,EAAE,uBAAuB,CAAC,CAAC;IAC/F,CAAC;CACJ,CAAA;AA5PY,gCAAU;qBAAV,UAAU;IADtB,IAAA,sBAAU,EAAC,8BAAkB,CAAC,SAAS,CAAC;6CAEA,2BAAY;GADxC,UAAU,CA4PtB","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\nimport {\n DotWebpieces,\n dotWebpieces,\n MERGE_INFO_DIR,\n MERGE_IN_PROGRESS_FILE,\n MERGE_EXPLANATION_FILE,\n} from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\n\n// Proof-obligation marker written when a 3-point squash-merge hits conflicts. Its mere\n// presence (with validated=false) is what the merge-in-progress-guard hook uses to block\n// commit/push/PR until `wp-finish-upsert-pr` validates the resolution and flips it true.\nexport class MergeMarker {\n currentBranch: string;\n squashBranch: string;\n backupBranch: string;\n prNumber: string;\n conflictedFiles: string[];\n forkPoint: string;\n featureHead: string;\n mainHead: string;\n validated: boolean;\n\n constructor(\n currentBranch: string,\n squashBranch: string,\n backupBranch: string,\n prNumber: string,\n conflictedFiles: string[],\n forkPoint: string,\n featureHead: string,\n mainHead: string,\n validated: boolean,\n ) {\n this.currentBranch = currentBranch;\n this.squashBranch = squashBranch;\n this.backupBranch = backupBranch;\n this.prNumber = prNumber;\n this.conflictedFiles = conflictedFiles;\n this.forkPoint = forkPoint;\n this.featureHead = featureHead;\n this.mainHead = mainHead;\n this.validated = validated;\n }\n}\n\nexport class MarkerScanResult {\n clean: boolean;\n filesWithMarkers: string[];\n\n constructor(clean: boolean, filesWithMarkers: string[]) {\n this.clean = clean;\n this.filesWithMarkers = filesWithMarkers;\n }\n}\n\n// Data-only: the three commit points of a 3-point merge as persisted in `updatemain-hashes.json`\n// (the exact key shape the conflict path already writes), plus when it was recorded.\nexport class MergeHashRecord {\n hashForkPoint: string;\n hashFeatureHead: string;\n hashMainHead: string;\n timestamp: string;\n\n constructor(hashForkPoint: string, hashFeatureHead: string, hashMainHead: string, timestamp: string) {\n this.hashForkPoint = hashForkPoint;\n this.hashFeatureHead = hashFeatureHead;\n this.hashMainHead = hashMainHead;\n this.timestamp = timestamp;\n }\n}\n\nconst CONFLICT_MARKER_RE = /^(<{7}|={7}|>{7})/m;\n\n/**\n * The two halves of `.webpieces/merge-info/`.\n *\n * `staged/` mirrors branches that STILL EXIST — it answers \"what am I working on right now\", and it\n * self-cleans when a PR lands (the whole dir is MOVED to `merged/`) instead of growing forever.\n * `merged/` is the post-mortem record, and is where the archive-tag reference lives.\n *\n * Both are reserved names: a feature branch slugifies `/`→`-`, so no feature can ever be called\n * `staged` or `merged` and collide with them.\n */\nexport const STAGED_DIR = 'staged';\nexport const MERGED_DIR = 'merged';\n\n/**\n * The 3-point conflict record. PRESENT ONLY WHEN THE MERGE ACTUALLY CONFLICTED.\n *\n * This replaces the old `no-3point-merge.md`, which was written into EVERY clean merge dir and whose\n * entire content was \"nothing interesting happened here\" plus the same A/B/C shas that\n * `updatemain-hashes.json` already carries, one line below, in machine-readable form. Nothing read it.\n * So ABSENCE is now the signal: \"does this directory contain conflicts.md?\" is the whole question, and\n * the shas a human might have wanted from the marker are still on disk in updatemain-hashes.json.\n */\nexport const CONFLICTS_FILE = 'conflicts.md';\n\n// `{ archiveTag, tipSha, baseSha, pr, mergedAt }` written into `merged/<feature>/` when the PR lands.\nexport const ARCHIVE_RECORD_FILE = 'archive.json';\n\n// The one index that answers \"which merges across ALL branches were 3-point?\" — see MergeInfoIndex.\nexport const MERGE_INDEX_FILE = 'index.json';\n\n/** Filesystem layout + read/write of the per-feature merge run dirs and their conflict markers. */\n@injectable(bindingScopeValues.Singleton)\nexport class MergeState {\n constructor(private readonly dotDir: DotWebpieces = dotWebpieces) {}\n\n // The per-feature \"home\" dir `.webpieces/merge-info/<slug>/`. It no longer holds the marker/context\n // directly — each sync gets its own numbered `merge-<n>/` run dir underneath (mergeRunDirFor),\n // paired with the sync's `<feature>PreMerge<n>` backup branch. This keeps merge N from ever reusing\n // merge N-1's stale per-file context / merge-explanation.md.\n mergeDirFor(repoRoot: string, featureName: string): string {\n const staged = this.stagedDirFor(repoRoot, featureName);\n this.migrateLegacyHome(repoRoot, featureName, staged);\n return staged;\n }\n\n /**\n * `<local .webpieces>/merge-info/staged/<feature>/` — the in-flight home.\n *\n * LOCAL scope throughout this class: a 3-point merge belongs to the worktree performing it, and\n * two worktrees are on two different branches, so these paths are disjoint by construction and need\n * no locking. In a linked worktree they now resolve under\n * `<primary>/.webpieces/worktrees/<name>/merge-info/…`, which also means an in-flight merge is not\n * destroyed by removing the worktree directory.\n */\n stagedDirFor(repoRoot: string, featureName: string): string {\n return this.dotDir.localFile(repoRoot, MERGE_INFO_DIR, STAGED_DIR, featureName);\n }\n\n /** `<local .webpieces>/merge-info/merged/<feature>/` — the landed home. */\n mergedDirFor(repoRoot: string, featureName: string): string {\n return this.dotDir.localFile(repoRoot, MERGE_INFO_DIR, MERGED_DIR, featureName);\n }\n\n /** `<local .webpieces>/merge-info/` itself — holds `staged/`, `merged/` and `index.json`. */\n mergeInfoRoot(repoRoot: string): string {\n return this.dotDir.localFile(repoRoot, MERGE_INFO_DIR);\n }\n\n /**\n * MIGRATION for the pre-`staged/` layout, done IN PLACE and lazily.\n *\n * Old layout put the feature home directly at `merge-info/<feature>/`. That dir is real, live state\n * (it can hold an UNVALIDATED merge marker for a merge that is in progress RIGHT NOW), so leaving\n * legacy dirs alone was not an option: the in-flight merge would become invisible to the finish\n * gate, which now looks under `staged/`. Tolerating both layouts on read was the other candidate and\n * was rejected — two homes means every reader, the guard included, needs two lookups forever.\n *\n * So: the first time anything asks for a feature's home, an existing legacy dir is MOVED to\n * `staged/<feature>/`. `fs.renameSync` is atomic within a filesystem and moves the whole tree with no\n * copying, so no merge record can be half-migrated. If `staged/<feature>` already exists the legacy\n * dir is left untouched — never merged, never deleted — because two homes for one feature is exactly\n * the ambiguity a human must resolve. `.webpieces/` is gitignored local state, so nothing here is\n * ever part of a commit.\n */\n private migrateLegacyHome(repoRoot: string, featureName: string, staged: string): void {\n const legacy = this.dotDir.localFile(repoRoot, MERGE_INFO_DIR, featureName);\n if (featureName === STAGED_DIR || featureName === MERGED_DIR) return;\n if (!fs.existsSync(legacy) || fs.existsSync(staged)) return;\n fs.mkdirSync(path.dirname(staged), { recursive: true });\n fs.renameSync(legacy, staged);\n }\n\n /**\n * Record THIS sync's pre-merge tip as `staged/<feature>/preMerge<n>.hash`.\n *\n * Every intermediate state, not just the last: a branch updated from main five times has five\n * genuinely different pre-merge tips, and the fifth tells you nothing about what the second looked\n * like. The hashes are what make the `<feature>PreMerge<n>` snapshot BRANCHES disposable — once the\n * tip is written down (and, at land time, tagged), the branch itself is pure branch-cap pressure.\n */\n writePreMergeHash(home: string, n: number, sha: string): void {\n fs.mkdirSync(home, { recursive: true });\n fs.writeFileSync(path.join(home, `preMerge${String(n)}.hash`), sha + '\\n');\n }\n\n /** Every recorded pre-merge tip for a feature, in slot order. [] when none were recorded. */\n readPreMergeHashes(home: string): string[] {\n if (!fs.existsSync(home)) return [];\n const found = new Map<number, string>();\n for (const entry of fs.readdirSync(home)) {\n const match = entry.match(/^preMerge(\\d+)\\.hash$/);\n if (match === null) continue;\n found.set(parseInt(match[1], 10), fs.readFileSync(path.join(home, entry), 'utf8').trim());\n }\n return [...found.keys()].sort((a: number, b: number): number => a - b)\n .map((n: number): string => found.get(n) ?? '');\n }\n\n // The run dir for sync number `n`: `<home>/merge-<n>/`. Holds this sync's marker + per-file\n // `updatemain-<file>/` context. Numbered to match the sync's `<feature>PreMerge<n>` backup branch.\n mergeRunDirFor(home: string, n: number): string {\n return path.join(home, `merge-${n}`);\n }\n\n // The next NEVER-REUSED merge slot number for a feature home: one past the highest existing\n // `merge-<n>/` dir (or 1 if none). Durable, monotonic source of truth for slot numbers — derived\n // from the audit dirs THEMSELVES, not from which transient `<feature>PreMerge<n>` branch exists.\n nextMergeSlotNumber(home: string): number {\n if (!fs.existsSync(home)) return 1;\n let max = 0;\n for (const entry of fs.readdirSync(home)) {\n const match = entry.match(/^merge-(\\d+)$/);\n if (match === null) continue;\n const n = parseInt(match[1], 10);\n if (n > max) max = n;\n }\n return max + 1;\n }\n\n /**\n * Record a CLEAN (no-conflict) sync: the A/B/C shas in `updatemain-hashes.json`, and NOTHING ELSE.\n *\n * The `no-3point-merge.md` placeholder this used to also write is gone. Its whole content was a\n * sentence saying nothing interesting happened plus the same three shas being written on the very\n * next line in machine-readable form — a file that appeared in every clean directory a human opens,\n * carried no information the JSON did not, and had no consumer anywhere in the codebase. Absence is\n * now the signal instead: a run dir with no {@link CONFLICTS_FILE} was a clean merge.\n */\n recordCleanMerge(mergeDir: string, forkPoint: string, featureHead: string, mainHead: string): void {\n fs.mkdirSync(mergeDir, { recursive: true });\n const record = new MergeHashRecord(forkPoint, featureHead, mainHead, new Date().toISOString());\n fs.writeFileSync(path.join(mergeDir, 'updatemain-hashes.json'), JSON.stringify(record, null, 2) + '\\n');\n }\n\n /** Path of a run dir's conflict record. Its EXISTENCE is what makes the merge \"3-point\". */\n conflictsPath(mergeDir: string): string {\n return path.join(mergeDir, CONFLICTS_FILE);\n }\n\n /** True when this run dir records a 3-point merge — i.e. `conflicts.md` is there. */\n wasThreeWay(mergeDir: string): boolean {\n return fs.existsSync(this.conflictsPath(mergeDir));\n }\n\n /** Write the 3-point conflict record. Only ever called on the conflict path. */\n writeConflicts(mergeDir: string, files: string[]): void {\n fs.mkdirSync(mergeDir, { recursive: true });\n const body =\n '# 3-point squash-merge — conflicts\\n\\n' +\n 'This sync needed a 3-point resolution. Each file below has its A/B/C context and the AI\\'s\\n' +\n `${MERGE_EXPLANATION_FILE} under \\`updatemain-<path with / → __>/\\`, and the A/B/C commit shas\\n` +\n 'are in `updatemain-hashes.json` next to this file.\\n\\n' +\n files.map((file: string): string => `- ${file}`).join('\\n') + '\\n';\n fs.writeFileSync(this.conflictsPath(mergeDir), body);\n }\n\n /** The conflicted-file list back out of `conflicts.md`. [] when the merge was clean. */\n readConflictedFiles(mergeDir: string): string[] {\n const filePath = this.conflictsPath(mergeDir);\n if (!fs.existsSync(filePath)) return [];\n return fs.readFileSync(filePath, 'utf8')\n .split('\\n')\n .filter((line: string): boolean => line.startsWith('- '))\n .map((line: string): string => line.slice(2).trim())\n .filter((line: string): boolean => line !== '');\n }\n\n /** Every `merge-<n>/` run dir under a feature home, in slot order. [] when the home does not exist. */\n listMergeRunDirs(home: string): number[] {\n if (!fs.existsSync(home)) return [];\n const slots: number[] = [];\n for (const entry of fs.readdirSync(home)) {\n const match = entry.match(/^merge-(\\d+)$/);\n if (match !== null) slots.push(parseInt(match[1], 10));\n }\n return slots.sort((a: number, b: number): number => a - b);\n }\n\n // Locate the in-progress merge's run dir: the `<home>/merge-*/` subdir holding a marker. There is\n // at most one; if more than one somehow exists, prefer an UNVALIDATED marker (the live conflict),\n // else return the first found. Null when none.\n findActiveMergeRunDir(home: string): string | null {\n if (!fs.existsSync(home)) return null;\n let fallback: string | null = null;\n for (const entry of fs.readdirSync(home)) {\n if (!entry.startsWith('merge-')) continue;\n const dir = path.join(home, entry);\n const marker = this.readMergeMarker(dir);\n if (marker === null) continue;\n if (!marker.validated) return dir;\n if (fallback === null) fallback = dir;\n }\n return fallback;\n }\n\n // Per-conflicted-file context dir holding A-forkpoint.txt / B-feature.txt / C-main.txt /\n // B-A.diff / C-A.diff (and the AI's merge-explanation.md). Shared so writer and reader agree on the\n // layout: the conflict file path with `/` → `__`, prefixed `updatemain-`.\n perFileContextDir(mergeDir: string, file: string): string {\n return path.join(mergeDir, `updatemain-${file.replace(/\\//g, '__')}`);\n }\n\n markerPath(mergeDir: string): string {\n return path.join(mergeDir, MERGE_IN_PROGRESS_FILE);\n }\n\n readMergeMarker(mergeDir: string): MergeMarker | null {\n const filePath = this.markerPath(mergeDir);\n if (!fs.existsSync(filePath)) return null;\n const raw = JSON.parse(fs.readFileSync(filePath, 'utf8')) as MergeMarker;\n return new MergeMarker(\n raw.currentBranch,\n raw.squashBranch,\n raw.backupBranch,\n raw.prNumber,\n raw.conflictedFiles ?? [],\n raw.forkPoint,\n raw.featureHead,\n raw.mainHead,\n raw.validated === true,\n );\n }\n\n writeMergeMarker(mergeDir: string, marker: MergeMarker): void {\n fs.mkdirSync(mergeDir, { recursive: true });\n fs.writeFileSync(this.markerPath(mergeDir), JSON.stringify(marker, null, 2) + '\\n');\n }\n\n clearMergeMarker(mergeDir: string): void {\n const filePath = this.markerPath(mergeDir);\n if (fs.existsSync(filePath)) fs.rmSync(filePath);\n }\n\n /**\n * Scoped conflict-marker scan: reads ONLY the given conflicted files (relative to repo root),\n * never the whole repo — stays O(conflicts) regardless of monorepo size.\n */\n scanConflictMarkers(repoRoot: string, files: string[]): MarkerScanResult {\n const filesWithMarkers: string[] = [];\n for (const file of files) {\n const abs = path.join(repoRoot, file);\n if (!fs.existsSync(abs)) continue;\n const content = fs.readFileSync(abs, 'utf8');\n if (CONFLICT_MARKER_RE.test(content)) filesWithMarkers.push(file);\n }\n return new MarkerScanResult(filesWithMarkers.length === 0, filesWithMarkers);\n }\n\n /**\n * Explanation scan: every conflicted file the AI resolved must have a non-empty\n * MERGE_EXPLANATION_FILE in its per-file context dir (next to the diffs), proving the AI\n * deliberately 3-point merged it and recording HOW. Returns files whose explanation is missing or\n * empty. Works for every conflicted file type — including comment-less files (JSON) and files\n * resolved by deletion (no working-tree file to inspect).\n */\n scanMergeExplanations(mergeDir: string, files: string[]): MarkerScanResult {\n const filesMissingExplanation: string[] = [];\n for (const file of files) {\n const explPath = path.join(this.perFileContextDir(mergeDir, file), MERGE_EXPLANATION_FILE);\n const present = fs.existsSync(explPath) && fs.readFileSync(explPath, 'utf8').trim() !== '';\n if (!present) filesMissingExplanation.push(file);\n }\n return new MarkerScanResult(filesMissingExplanation.length === 0, filesMissingExplanation);\n }\n}\n"]}
|