@webpieces/pr-gate 0.4.494 → 0.4.496
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/scripts/commands/cleanup-command.d.ts +28 -6
- package/src/scripts/commands/cleanup-command.js +121 -19
- package/src/scripts/commands/cleanup-command.js.map +1 -1
- package/src/scripts/commands/land-pr-command.d.ts +19 -2
- package/src/scripts/commands/land-pr-command.js +51 -3
- package/src/scripts/commands/land-pr-command.js.map +1 -1
- package/src/scripts/workflow/cleanTmp.js +4 -1
- package/src/scripts/workflow/cleanTmp.js.map +1 -1
- package/src/scripts/workflow/git-findForkPoint.js +7 -2
- package/src/scripts/workflow/git-findForkPoint.js.map +1 -1
- package/src/scripts/workflow/merge-info-index.d.ts +60 -0
- package/src/scripts/workflow/merge-info-index.js +164 -0
- package/src/scripts/workflow/merge-info-index.js.map +1 -0
- package/src/scripts/workflow/merge-start.d.ts +1 -0
- package/src/scripts/workflow/merge-start.js +24 -6
- package/src/scripts/workflow/merge-start.js.map +1 -1
- package/src/scripts/workflow/merge-state.d.ts +78 -2
- package/src/scripts/workflow/merge-state.js +144 -17
- package/src/scripts/workflow/merge-state.js.map +1 -1
|
@@ -22,13 +22,89 @@ export declare class MergeHashRecord {
|
|
|
22
22
|
timestamp: string;
|
|
23
23
|
constructor(hashForkPoint: string, hashFeatureHead: string, hashMainHead: string, timestamp: string);
|
|
24
24
|
}
|
|
25
|
-
|
|
25
|
+
/**
|
|
26
|
+
* The two halves of `.webpieces/merge-info/`.
|
|
27
|
+
*
|
|
28
|
+
* `staged/` mirrors branches that STILL EXIST — it answers "what am I working on right now", and it
|
|
29
|
+
* self-cleans when a PR lands (the whole dir is MOVED to `merged/`) instead of growing forever.
|
|
30
|
+
* `merged/` is the post-mortem record, and is where the archive-tag reference lives.
|
|
31
|
+
*
|
|
32
|
+
* Both are reserved names: a feature branch slugifies `/`→`-`, so no feature can ever be called
|
|
33
|
+
* `staged` or `merged` and collide with them.
|
|
34
|
+
*/
|
|
35
|
+
export declare const STAGED_DIR = "staged";
|
|
36
|
+
export declare const MERGED_DIR = "merged";
|
|
37
|
+
/**
|
|
38
|
+
* The 3-point conflict record. PRESENT ONLY WHEN THE MERGE ACTUALLY CONFLICTED.
|
|
39
|
+
*
|
|
40
|
+
* This replaces the old `no-3point-merge.md`, which was written into EVERY clean merge dir and whose
|
|
41
|
+
* entire content was "nothing interesting happened here" plus the same A/B/C shas that
|
|
42
|
+
* `updatemain-hashes.json` already carries, one line below, in machine-readable form. Nothing read it.
|
|
43
|
+
* So ABSENCE is now the signal: "does this directory contain conflicts.md?" is the whole question, and
|
|
44
|
+
* the shas a human might have wanted from the marker are still on disk in updatemain-hashes.json.
|
|
45
|
+
*/
|
|
46
|
+
export declare const CONFLICTS_FILE = "conflicts.md";
|
|
47
|
+
export declare const ARCHIVE_RECORD_FILE = "archive.json";
|
|
48
|
+
export declare const MERGE_INDEX_FILE = "index.json";
|
|
26
49
|
/** Filesystem layout + read/write of the per-feature merge run dirs and their conflict markers. */
|
|
27
50
|
export declare class MergeState {
|
|
28
51
|
mergeDirFor(repoRoot: string, featureName: string): string;
|
|
52
|
+
/** `.webpieces/merge-info/staged/<feature>/` — the in-flight home. */
|
|
53
|
+
stagedDirFor(repoRoot: string, featureName: string): string;
|
|
54
|
+
/** `.webpieces/merge-info/merged/<feature>/` — the landed home. */
|
|
55
|
+
mergedDirFor(repoRoot: string, featureName: string): string;
|
|
56
|
+
/** `.webpieces/merge-info/` itself — the home holding `staged/`, `merged/` and `index.json`. */
|
|
57
|
+
mergeInfoRoot(repoRoot: string): string;
|
|
58
|
+
/**
|
|
59
|
+
* MIGRATION for the pre-`staged/` layout, done IN PLACE and lazily.
|
|
60
|
+
*
|
|
61
|
+
* Old layout put the feature home directly at `merge-info/<feature>/`. That dir is real, live state
|
|
62
|
+
* (it can hold an UNVALIDATED merge marker for a merge that is in progress RIGHT NOW), so leaving
|
|
63
|
+
* legacy dirs alone was not an option: the in-flight merge would become invisible to the finish
|
|
64
|
+
* gate, which now looks under `staged/`. Tolerating both layouts on read was the other candidate and
|
|
65
|
+
* was rejected — two homes means every reader, the guard included, needs two lookups forever.
|
|
66
|
+
*
|
|
67
|
+
* So: the first time anything asks for a feature's home, an existing legacy dir is MOVED to
|
|
68
|
+
* `staged/<feature>/`. `fs.renameSync` is atomic within a filesystem and moves the whole tree with no
|
|
69
|
+
* copying, so no merge record can be half-migrated. If `staged/<feature>` already exists the legacy
|
|
70
|
+
* dir is left untouched — never merged, never deleted — because two homes for one feature is exactly
|
|
71
|
+
* the ambiguity a human must resolve. `.webpieces/` is gitignored local state, so nothing here is
|
|
72
|
+
* ever part of a commit.
|
|
73
|
+
*/
|
|
74
|
+
private migrateLegacyHome;
|
|
75
|
+
/**
|
|
76
|
+
* Record THIS sync's pre-merge tip as `staged/<feature>/preMerge<n>.hash`.
|
|
77
|
+
*
|
|
78
|
+
* Every intermediate state, not just the last: a branch updated from main five times has five
|
|
79
|
+
* genuinely different pre-merge tips, and the fifth tells you nothing about what the second looked
|
|
80
|
+
* like. The hashes are what make the `<feature>PreMerge<n>` snapshot BRANCHES disposable — once the
|
|
81
|
+
* tip is written down (and, at land time, tagged), the branch itself is pure branch-cap pressure.
|
|
82
|
+
*/
|
|
83
|
+
writePreMergeHash(home: string, n: number, sha: string): void;
|
|
84
|
+
/** Every recorded pre-merge tip for a feature, in slot order. [] when none were recorded. */
|
|
85
|
+
readPreMergeHashes(home: string): string[];
|
|
29
86
|
mergeRunDirFor(home: string, n: number): string;
|
|
30
87
|
nextMergeSlotNumber(home: string): number;
|
|
31
|
-
|
|
88
|
+
/**
|
|
89
|
+
* Record a CLEAN (no-conflict) sync: the A/B/C shas in `updatemain-hashes.json`, and NOTHING ELSE.
|
|
90
|
+
*
|
|
91
|
+
* The `no-3point-merge.md` placeholder this used to also write is gone. Its whole content was a
|
|
92
|
+
* sentence saying nothing interesting happened plus the same three shas being written on the very
|
|
93
|
+
* next line in machine-readable form — a file that appeared in every clean directory a human opens,
|
|
94
|
+
* carried no information the JSON did not, and had no consumer anywhere in the codebase. Absence is
|
|
95
|
+
* now the signal instead: a run dir with no {@link CONFLICTS_FILE} was a clean merge.
|
|
96
|
+
*/
|
|
97
|
+
recordCleanMerge(mergeDir: string, forkPoint: string, featureHead: string, mainHead: string): void;
|
|
98
|
+
/** Path of a run dir's conflict record. Its EXISTENCE is what makes the merge "3-point". */
|
|
99
|
+
conflictsPath(mergeDir: string): string;
|
|
100
|
+
/** True when this run dir records a 3-point merge — i.e. `conflicts.md` is there. */
|
|
101
|
+
wasThreeWay(mergeDir: string): boolean;
|
|
102
|
+
/** Write the 3-point conflict record. Only ever called on the conflict path. */
|
|
103
|
+
writeConflicts(mergeDir: string, files: string[]): void;
|
|
104
|
+
/** The conflicted-file list back out of `conflicts.md`. [] when the merge was clean. */
|
|
105
|
+
readConflictedFiles(mergeDir: string): string[];
|
|
106
|
+
/** Every `merge-<n>/` run dir under a feature home, in slot order. [] when the home does not exist. */
|
|
107
|
+
listMergeRunDirs(home: string): number[];
|
|
32
108
|
findActiveMergeRunDir(home: string): string | null;
|
|
33
109
|
perFileContextDir(mergeDir: string, file: string): string;
|
|
34
110
|
markerPath(mergeDir: string): string;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.MergeState = exports.
|
|
3
|
+
exports.MergeState = exports.MERGE_INDEX_FILE = exports.ARCHIVE_RECORD_FILE = exports.CONFLICTS_FILE = exports.MERGED_DIR = exports.STAGED_DIR = exports.MergeHashRecord = exports.MarkerScanResult = exports.MergeMarker = void 0;
|
|
4
4
|
const tslib_1 = require("tslib");
|
|
5
5
|
const fs = tslib_1.__importStar(require("fs"));
|
|
6
6
|
const path = tslib_1.__importStar(require("path"));
|
|
@@ -57,11 +57,32 @@ class MergeHashRecord {
|
|
|
57
57
|
}
|
|
58
58
|
exports.MergeHashRecord = MergeHashRecord;
|
|
59
59
|
const CONFLICT_MARKER_RE = /^(<{7}|={7}|>{7})/m;
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
60
|
+
/**
|
|
61
|
+
* The two halves of `.webpieces/merge-info/`.
|
|
62
|
+
*
|
|
63
|
+
* `staged/` mirrors branches that STILL EXIST — it answers "what am I working on right now", and it
|
|
64
|
+
* self-cleans when a PR lands (the whole dir is MOVED to `merged/`) instead of growing forever.
|
|
65
|
+
* `merged/` is the post-mortem record, and is where the archive-tag reference lives.
|
|
66
|
+
*
|
|
67
|
+
* Both are reserved names: a feature branch slugifies `/`→`-`, so no feature can ever be called
|
|
68
|
+
* `staged` or `merged` and collide with them.
|
|
69
|
+
*/
|
|
70
|
+
exports.STAGED_DIR = 'staged';
|
|
71
|
+
exports.MERGED_DIR = 'merged';
|
|
72
|
+
/**
|
|
73
|
+
* The 3-point conflict record. PRESENT ONLY WHEN THE MERGE ACTUALLY CONFLICTED.
|
|
74
|
+
*
|
|
75
|
+
* This replaces the old `no-3point-merge.md`, which was written into EVERY clean merge dir and whose
|
|
76
|
+
* entire content was "nothing interesting happened here" plus the same A/B/C shas that
|
|
77
|
+
* `updatemain-hashes.json` already carries, one line below, in machine-readable form. Nothing read it.
|
|
78
|
+
* So ABSENCE is now the signal: "does this directory contain conflicts.md?" is the whole question, and
|
|
79
|
+
* the shas a human might have wanted from the marker are still on disk in updatemain-hashes.json.
|
|
80
|
+
*/
|
|
81
|
+
exports.CONFLICTS_FILE = 'conflicts.md';
|
|
82
|
+
// `{ archiveTag, tipSha, baseSha, pr, mergedAt }` written into `merged/<feature>/` when the PR lands.
|
|
83
|
+
exports.ARCHIVE_RECORD_FILE = 'archive.json';
|
|
84
|
+
// The one index that answers "which merges across ALL branches were 3-point?" — see MergeInfoIndex.
|
|
85
|
+
exports.MERGE_INDEX_FILE = 'index.json';
|
|
65
86
|
/** Filesystem layout + read/write of the per-feature merge run dirs and their conflict markers. */
|
|
66
87
|
let MergeState = class MergeState {
|
|
67
88
|
// The per-feature "home" dir `.webpieces/merge-info/<slug>/`. It no longer holds the marker/context
|
|
@@ -69,7 +90,72 @@ let MergeState = class MergeState {
|
|
|
69
90
|
// paired with the sync's `<feature>PreMerge<n>` backup branch. This keeps merge N from ever reusing
|
|
70
91
|
// merge N-1's stale per-file context / merge-explanation.md.
|
|
71
92
|
mergeDirFor(repoRoot, featureName) {
|
|
72
|
-
|
|
93
|
+
const staged = this.stagedDirFor(repoRoot, featureName);
|
|
94
|
+
this.migrateLegacyHome(repoRoot, featureName, staged);
|
|
95
|
+
return staged;
|
|
96
|
+
}
|
|
97
|
+
/** `.webpieces/merge-info/staged/<feature>/` — the in-flight home. */
|
|
98
|
+
stagedDirFor(repoRoot, featureName) {
|
|
99
|
+
return path.join(repoRoot, rules_config_1.WEBPIECES_TMP_DIR, rules_config_1.MERGE_INFO_DIR, exports.STAGED_DIR, featureName);
|
|
100
|
+
}
|
|
101
|
+
/** `.webpieces/merge-info/merged/<feature>/` — the landed home. */
|
|
102
|
+
mergedDirFor(repoRoot, featureName) {
|
|
103
|
+
return path.join(repoRoot, rules_config_1.WEBPIECES_TMP_DIR, rules_config_1.MERGE_INFO_DIR, exports.MERGED_DIR, featureName);
|
|
104
|
+
}
|
|
105
|
+
/** `.webpieces/merge-info/` itself — the home holding `staged/`, `merged/` and `index.json`. */
|
|
106
|
+
mergeInfoRoot(repoRoot) {
|
|
107
|
+
return path.join(repoRoot, rules_config_1.WEBPIECES_TMP_DIR, rules_config_1.MERGE_INFO_DIR);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* MIGRATION for the pre-`staged/` layout, done IN PLACE and lazily.
|
|
111
|
+
*
|
|
112
|
+
* Old layout put the feature home directly at `merge-info/<feature>/`. That dir is real, live state
|
|
113
|
+
* (it can hold an UNVALIDATED merge marker for a merge that is in progress RIGHT NOW), so leaving
|
|
114
|
+
* legacy dirs alone was not an option: the in-flight merge would become invisible to the finish
|
|
115
|
+
* gate, which now looks under `staged/`. Tolerating both layouts on read was the other candidate and
|
|
116
|
+
* was rejected — two homes means every reader, the guard included, needs two lookups forever.
|
|
117
|
+
*
|
|
118
|
+
* So: the first time anything asks for a feature's home, an existing legacy dir is MOVED to
|
|
119
|
+
* `staged/<feature>/`. `fs.renameSync` is atomic within a filesystem and moves the whole tree with no
|
|
120
|
+
* copying, so no merge record can be half-migrated. If `staged/<feature>` already exists the legacy
|
|
121
|
+
* dir is left untouched — never merged, never deleted — because two homes for one feature is exactly
|
|
122
|
+
* the ambiguity a human must resolve. `.webpieces/` is gitignored local state, so nothing here is
|
|
123
|
+
* ever part of a commit.
|
|
124
|
+
*/
|
|
125
|
+
migrateLegacyHome(repoRoot, featureName, staged) {
|
|
126
|
+
const legacy = path.join(repoRoot, rules_config_1.WEBPIECES_TMP_DIR, rules_config_1.MERGE_INFO_DIR, featureName);
|
|
127
|
+
if (featureName === exports.STAGED_DIR || featureName === exports.MERGED_DIR)
|
|
128
|
+
return;
|
|
129
|
+
if (!fs.existsSync(legacy) || fs.existsSync(staged))
|
|
130
|
+
return;
|
|
131
|
+
fs.mkdirSync(path.dirname(staged), { recursive: true });
|
|
132
|
+
fs.renameSync(legacy, staged);
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Record THIS sync's pre-merge tip as `staged/<feature>/preMerge<n>.hash`.
|
|
136
|
+
*
|
|
137
|
+
* Every intermediate state, not just the last: a branch updated from main five times has five
|
|
138
|
+
* genuinely different pre-merge tips, and the fifth tells you nothing about what the second looked
|
|
139
|
+
* like. The hashes are what make the `<feature>PreMerge<n>` snapshot BRANCHES disposable — once the
|
|
140
|
+
* tip is written down (and, at land time, tagged), the branch itself is pure branch-cap pressure.
|
|
141
|
+
*/
|
|
142
|
+
writePreMergeHash(home, n, sha) {
|
|
143
|
+
fs.mkdirSync(home, { recursive: true });
|
|
144
|
+
fs.writeFileSync(path.join(home, `preMerge${String(n)}.hash`), sha + '\n');
|
|
145
|
+
}
|
|
146
|
+
/** Every recorded pre-merge tip for a feature, in slot order. [] when none were recorded. */
|
|
147
|
+
readPreMergeHashes(home) {
|
|
148
|
+
if (!fs.existsSync(home))
|
|
149
|
+
return [];
|
|
150
|
+
const found = new Map();
|
|
151
|
+
for (const entry of fs.readdirSync(home)) {
|
|
152
|
+
const match = entry.match(/^preMerge(\d+)\.hash$/);
|
|
153
|
+
if (match === null)
|
|
154
|
+
continue;
|
|
155
|
+
found.set(parseInt(match[1], 10), fs.readFileSync(path.join(home, entry), 'utf8').trim());
|
|
156
|
+
}
|
|
157
|
+
return [...found.keys()].sort((a, b) => a - b)
|
|
158
|
+
.map((n) => found.get(n) ?? '');
|
|
73
159
|
}
|
|
74
160
|
// The run dir for sync number `n`: `<home>/merge-<n>/`. Holds this sync's marker + per-file
|
|
75
161
|
// `updatemain-<file>/` context. Numbered to match the sync's `<feature>PreMerge<n>` backup branch.
|
|
@@ -93,20 +179,61 @@ let MergeState = class MergeState {
|
|
|
93
179
|
}
|
|
94
180
|
return max + 1;
|
|
95
181
|
}
|
|
96
|
-
|
|
97
|
-
|
|
182
|
+
/**
|
|
183
|
+
* Record a CLEAN (no-conflict) sync: the A/B/C shas in `updatemain-hashes.json`, and NOTHING ELSE.
|
|
184
|
+
*
|
|
185
|
+
* The `no-3point-merge.md` placeholder this used to also write is gone. Its whole content was a
|
|
186
|
+
* sentence saying nothing interesting happened plus the same three shas being written on the very
|
|
187
|
+
* next line in machine-readable form — a file that appeared in every clean directory a human opens,
|
|
188
|
+
* carried no information the JSON did not, and had no consumer anywhere in the codebase. Absence is
|
|
189
|
+
* now the signal instead: a run dir with no {@link CONFLICTS_FILE} was a clean merge.
|
|
190
|
+
*/
|
|
191
|
+
recordCleanMerge(mergeDir, forkPoint, featureHead, mainHead) {
|
|
98
192
|
fs.mkdirSync(mergeDir, { recursive: true });
|
|
99
|
-
const body = '# Clean squash-merge — no 3-point conflict resolution needed\n\n' +
|
|
100
|
-
'This sync merged main into the feature with no conflicts, so the AI wrote no per-file\n' +
|
|
101
|
-
'merge-explanation.md. The 3-point shas below are kept so the merge is still auditable.\n\n' +
|
|
102
|
-
`3-point shas: A(fork)=${forkPoint} B(feature)=${featureHead} C(main)=${mainHead}\n\n` +
|
|
103
|
-
'Reconstruct what each side changed:\n' +
|
|
104
|
-
` feature (B−A): git diff ${forkPoint} ${featureHead}\n` +
|
|
105
|
-
` main (C−A): git diff ${forkPoint} ${mainHead}\n`;
|
|
106
|
-
fs.writeFileSync(path.join(mergeDir, exports.NO_CONFLICT_MARKER_FILE), body);
|
|
107
193
|
const record = new MergeHashRecord(forkPoint, featureHead, mainHead, new Date().toISOString());
|
|
108
194
|
fs.writeFileSync(path.join(mergeDir, 'updatemain-hashes.json'), JSON.stringify(record, null, 2) + '\n');
|
|
109
195
|
}
|
|
196
|
+
/** Path of a run dir's conflict record. Its EXISTENCE is what makes the merge "3-point". */
|
|
197
|
+
conflictsPath(mergeDir) {
|
|
198
|
+
return path.join(mergeDir, exports.CONFLICTS_FILE);
|
|
199
|
+
}
|
|
200
|
+
/** True when this run dir records a 3-point merge — i.e. `conflicts.md` is there. */
|
|
201
|
+
wasThreeWay(mergeDir) {
|
|
202
|
+
return fs.existsSync(this.conflictsPath(mergeDir));
|
|
203
|
+
}
|
|
204
|
+
/** Write the 3-point conflict record. Only ever called on the conflict path. */
|
|
205
|
+
writeConflicts(mergeDir, files) {
|
|
206
|
+
fs.mkdirSync(mergeDir, { recursive: true });
|
|
207
|
+
const body = '# 3-point squash-merge — conflicts\n\n' +
|
|
208
|
+
'This sync needed a 3-point resolution. Each file below has its A/B/C context and the AI\'s\n' +
|
|
209
|
+
`${rules_config_1.MERGE_EXPLANATION_FILE} under \`updatemain-<path with / → __>/\`, and the A/B/C commit shas\n` +
|
|
210
|
+
'are in `updatemain-hashes.json` next to this file.\n\n' +
|
|
211
|
+
files.map((file) => `- ${file}`).join('\n') + '\n';
|
|
212
|
+
fs.writeFileSync(this.conflictsPath(mergeDir), body);
|
|
213
|
+
}
|
|
214
|
+
/** The conflicted-file list back out of `conflicts.md`. [] when the merge was clean. */
|
|
215
|
+
readConflictedFiles(mergeDir) {
|
|
216
|
+
const filePath = this.conflictsPath(mergeDir);
|
|
217
|
+
if (!fs.existsSync(filePath))
|
|
218
|
+
return [];
|
|
219
|
+
return fs.readFileSync(filePath, 'utf8')
|
|
220
|
+
.split('\n')
|
|
221
|
+
.filter((line) => line.startsWith('- '))
|
|
222
|
+
.map((line) => line.slice(2).trim())
|
|
223
|
+
.filter((line) => line !== '');
|
|
224
|
+
}
|
|
225
|
+
/** Every `merge-<n>/` run dir under a feature home, in slot order. [] when the home does not exist. */
|
|
226
|
+
listMergeRunDirs(home) {
|
|
227
|
+
if (!fs.existsSync(home))
|
|
228
|
+
return [];
|
|
229
|
+
const slots = [];
|
|
230
|
+
for (const entry of fs.readdirSync(home)) {
|
|
231
|
+
const match = entry.match(/^merge-(\d+)$/);
|
|
232
|
+
if (match !== null)
|
|
233
|
+
slots.push(parseInt(match[1], 10));
|
|
234
|
+
}
|
|
235
|
+
return slots.sort((a, b) => a - b);
|
|
236
|
+
}
|
|
110
237
|
// Locate the in-progress merge's run dir: the `<home>/merge-*/` subdir holding a marker. There is
|
|
111
238
|
// at most one; if more than one somehow exists, prefer an UNVALIDATED marker (the live conflict),
|
|
112
239
|
// else return the first found. Null when none.
|
|
@@ -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// Marker written for a CLEAN (no-conflict) sync so every merge — not just conflicted ones — leaves a\n// durable, self-describing record under `merge-info/<feature>/merge-<n>/`. A clean merge produces no\n// per-file `merge-explanation.md`; this file is the \"yes, a merge happened here, and it needed no\n// 3-point resolution\" proof, with the A/B/C shas kept so it stays auditable.\nexport const NO_CONFLICT_MARKER_FILE = 'no-3point-merge.md';\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 return path.join(repoRoot, WEBPIECES_TMP_DIR, MERGE_INFO_DIR, featureName);\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 // Write the clean-merge marker + a per-slot `updatemain-hashes.json` copy into the slot's own dir.\n writeCleanMergeMarker(mergeDir: string, forkPoint: string, featureHead: string, mainHead: string): void {\n fs.mkdirSync(mergeDir, { recursive: true });\n const body =\n '# Clean squash-merge — no 3-point conflict resolution needed\\n\\n' +\n 'This sync merged main into the feature with no conflicts, so the AI wrote no per-file\\n' +\n 'merge-explanation.md. The 3-point shas below are kept so the merge is still auditable.\\n\\n' +\n `3-point shas: A(fork)=${forkPoint} B(feature)=${featureHead} C(main)=${mainHead}\\n\\n` +\n 'Reconstruct what each side changed:\\n' +\n ` feature (B−A): git diff ${forkPoint} ${featureHead}\\n` +\n ` main (C−A): git diff ${forkPoint} ${mainHead}\\n`;\n fs.writeFileSync(path.join(mergeDir, NO_CONFLICT_MARKER_FILE), body);\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 // 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":"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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"]}
|