@webpieces/pr-gate 0.3.353 → 0.3.355
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 +7 -2
- package/src/dashboard/dashboard.d.ts +12 -3
- package/src/dashboard/dashboard.js +136 -129
- package/src/dashboard/dashboard.js.map +1 -1
- package/src/index.d.ts +2 -1
- package/src/index.js +4 -4
- package/src/index.js.map +1 -1
- package/src/scripts/commands/finish-update-command.d.ts +10 -0
- package/src/scripts/commands/finish-update-command.js +51 -0
- package/src/scripts/commands/finish-update-command.js.map +1 -0
- package/src/scripts/commands/finish-upsert-pr-command.d.ts +22 -0
- package/src/scripts/commands/finish-upsert-pr-command.js +131 -0
- package/src/scripts/commands/finish-upsert-pr-command.js.map +1 -0
- package/src/scripts/commands/start-update-command.d.ts +9 -0
- package/src/scripts/commands/start-update-command.js +67 -0
- package/src/scripts/commands/start-update-command.js.map +1 -0
- package/src/scripts/commands/start-upsert-pr-command.d.ts +15 -0
- package/src/scripts/commands/start-upsert-pr-command.js +73 -0
- package/src/scripts/commands/start-upsert-pr-command.js.map +1 -0
- package/src/scripts/git-finishUpsertPr.d.ts +1 -1
- package/src/scripts/git-finishUpsertPr.js +11 -109
- package/src/scripts/git-finishUpsertPr.js.map +1 -1
- package/src/scripts/git-gatherInfo.d.ts +15 -1
- package/src/scripts/git-gatherInfo.js +79 -61
- package/src/scripts/git-gatherInfo.js.map +1 -1
- package/src/scripts/pr-gate-app.d.ts +25 -0
- package/src/scripts/pr-gate-app.js +56 -0
- package/src/scripts/pr-gate-app.js.map +1 -0
- package/src/scripts/workflow/branch-naming.d.ts +11 -8
- package/src/scripts/workflow/branch-naming.js +30 -21
- package/src/scripts/workflow/branch-naming.js.map +1 -1
- package/src/scripts/workflow/build-affected.d.ts +21 -30
- package/src/scripts/workflow/build-affected.js +57 -63
- package/src/scripts/workflow/build-affected.js.map +1 -1
- package/src/scripts/workflow/cleanTmp.d.ts +6 -1
- package/src/scripts/workflow/cleanTmp.js +83 -78
- package/src/scripts/workflow/cleanTmp.js.map +1 -1
- package/src/scripts/workflow/git-exec.d.ts +32 -28
- package/src/scripts/workflow/git-exec.js +92 -85
- package/src/scripts/workflow/git-exec.js.map +1 -1
- package/src/scripts/workflow/git-findForkPoint.d.ts +12 -3
- package/src/scripts/workflow/git-findForkPoint.js +116 -107
- package/src/scripts/workflow/git-findForkPoint.js.map +1 -1
- package/src/scripts/workflow/git-readAiBranchName.d.ts +7 -2
- package/src/scripts/workflow/git-readAiBranchName.js +24 -15
- package/src/scripts/workflow/git-readAiBranchName.js.map +1 -1
- package/src/scripts/workflow/merge-end.d.ts +21 -6
- package/src/scripts/workflow/merge-end.js +147 -133
- package/src/scripts/workflow/merge-end.js.map +1 -1
- package/src/scripts/workflow/merge-start.d.ts +22 -1
- package/src/scripts/workflow/merge-start.js +180 -186
- package/src/scripts/workflow/merge-start.js.map +1 -1
- package/src/scripts/workflow/merge-state.d.ts +26 -23
- package/src/scripts/workflow/merge-state.js +131 -137
- package/src/scripts/workflow/merge-state.js.map +1 -1
- package/src/scripts/workflow/open-pr-check.d.ts +7 -1
- package/src/scripts/workflow/open-pr-check.js +34 -23
- package/src/scripts/workflow/open-pr-check.js.map +1 -1
- package/src/scripts/workflow/run-update.d.ts +13 -1
- package/src/scripts/workflow/run-update.js +59 -28
- package/src/scripts/workflow/run-update.js.map +1 -1
- package/src/scripts/wp-finish-update.d.ts +1 -1
- package/src/scripts/wp-finish-update.js +11 -30
- package/src/scripts/wp-finish-update.js.map +1 -1
- package/src/scripts/wp-start-update.d.ts +1 -2
- package/src/scripts/wp-start-update.js +11 -51
- package/src/scripts/wp-start-update.js.map +1 -1
- package/src/scripts/wp-start-upsert-pr.d.ts +1 -1
- package/src/scripts/wp-start-upsert-pr.js +11 -47
- package/src/scripts/wp-start-upsert-pr.js.map +1 -1
|
@@ -15,9 +15,6 @@ export declare class MarkerScanResult {
|
|
|
15
15
|
filesWithMarkers: string[];
|
|
16
16
|
constructor(clean: boolean, filesWithMarkers: string[]);
|
|
17
17
|
}
|
|
18
|
-
export declare function mergeDirFor(repoRoot: string, featureName: string): string;
|
|
19
|
-
export declare function mergeRunDirFor(home: string, n: number): string;
|
|
20
|
-
export declare function nextMergeSlotNumber(home: string): number;
|
|
21
18
|
export declare class MergeHashRecord {
|
|
22
19
|
hashForkPoint: string;
|
|
23
20
|
hashFeatureHead: string;
|
|
@@ -26,23 +23,29 @@ export declare class MergeHashRecord {
|
|
|
26
23
|
constructor(hashForkPoint: string, hashFeatureHead: string, hashMainHead: string, timestamp: string);
|
|
27
24
|
}
|
|
28
25
|
export declare const NO_CONFLICT_MARKER_FILE = "no-3point-merge.md";
|
|
29
|
-
|
|
30
|
-
export declare
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
26
|
+
/** Filesystem layout + read/write of the per-feature merge run dirs and their conflict markers. */
|
|
27
|
+
export declare class MergeState {
|
|
28
|
+
mergeDirFor(repoRoot: string, featureName: string): string;
|
|
29
|
+
mergeRunDirFor(home: string, n: number): string;
|
|
30
|
+
nextMergeSlotNumber(home: string): number;
|
|
31
|
+
writeCleanMergeMarker(mergeDir: string, forkPoint: string, featureHead: string, mainHead: string): void;
|
|
32
|
+
findActiveMergeRunDir(home: string): string | null;
|
|
33
|
+
perFileContextDir(mergeDir: string, file: string): string;
|
|
34
|
+
markerPath(mergeDir: string): string;
|
|
35
|
+
readMergeMarker(mergeDir: string): MergeMarker | null;
|
|
36
|
+
writeMergeMarker(mergeDir: string, marker: MergeMarker): void;
|
|
37
|
+
clearMergeMarker(mergeDir: string): void;
|
|
38
|
+
/**
|
|
39
|
+
* Scoped conflict-marker scan: reads ONLY the given conflicted files (relative to repo root),
|
|
40
|
+
* never the whole repo — stays O(conflicts) regardless of monorepo size.
|
|
41
|
+
*/
|
|
42
|
+
scanConflictMarkers(repoRoot: string, files: string[]): MarkerScanResult;
|
|
43
|
+
/**
|
|
44
|
+
* Explanation scan: every conflicted file the AI resolved must have a non-empty
|
|
45
|
+
* MERGE_EXPLANATION_FILE in its per-file context dir (next to the diffs), proving the AI
|
|
46
|
+
* deliberately 3-point merged it and recording HOW. Returns files whose explanation is missing or
|
|
47
|
+
* empty. Works for every conflicted file type — including comment-less files (JSON) and files
|
|
48
|
+
* resolved by deletion (no working-tree file to inspect).
|
|
49
|
+
*/
|
|
50
|
+
scanMergeExplanations(mergeDir: string, files: string[]): MarkerScanResult;
|
|
51
|
+
}
|
|
@@ -1,22 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.NO_CONFLICT_MARKER_FILE = exports.MergeHashRecord = exports.MarkerScanResult = exports.MergeMarker = void 0;
|
|
4
|
-
exports.mergeDirFor = mergeDirFor;
|
|
5
|
-
exports.mergeRunDirFor = mergeRunDirFor;
|
|
6
|
-
exports.nextMergeSlotNumber = nextMergeSlotNumber;
|
|
7
|
-
exports.writeCleanMergeMarker = writeCleanMergeMarker;
|
|
8
|
-
exports.findActiveMergeRunDir = findActiveMergeRunDir;
|
|
9
|
-
exports.perFileContextDir = perFileContextDir;
|
|
10
|
-
exports.markerPath = markerPath;
|
|
11
|
-
exports.readMergeMarker = readMergeMarker;
|
|
12
|
-
exports.writeMergeMarker = writeMergeMarker;
|
|
13
|
-
exports.clearMergeMarker = clearMergeMarker;
|
|
14
|
-
exports.scanConflictMarkers = scanConflictMarkers;
|
|
15
|
-
exports.scanMergeExplanations = scanMergeExplanations;
|
|
3
|
+
exports.MergeState = exports.NO_CONFLICT_MARKER_FILE = exports.MergeHashRecord = exports.MarkerScanResult = exports.MergeMarker = void 0;
|
|
16
4
|
const tslib_1 = require("tslib");
|
|
17
5
|
const fs = tslib_1.__importStar(require("fs"));
|
|
18
6
|
const path = tslib_1.__importStar(require("path"));
|
|
19
7
|
const rules_config_1 = require("@webpieces/rules-config");
|
|
8
|
+
const core_context_1 = require("@webpieces/core-context");
|
|
9
|
+
const inversify_1 = require("inversify");
|
|
20
10
|
// Proof-obligation marker written when a 3-point squash-merge hits conflicts. Its mere
|
|
21
11
|
// presence (with validated=false) is what the merge-in-progress-guard hook uses to block
|
|
22
12
|
// commit/push/PR until `wp-finish-upsert-pr` validates the resolution and flips it true.
|
|
@@ -52,39 +42,6 @@ class MarkerScanResult {
|
|
|
52
42
|
}
|
|
53
43
|
}
|
|
54
44
|
exports.MarkerScanResult = MarkerScanResult;
|
|
55
|
-
const CONFLICT_MARKER_RE = /^(<{7}|={7}|>{7})/m;
|
|
56
|
-
// The per-feature "home" dir `.webpieces/merge-info/<slug>/`. It no longer holds the marker/context
|
|
57
|
-
// directly — each sync gets its own numbered `merge-<n>/` run dir underneath (mergeRunDirFor), paired
|
|
58
|
-
// with the sync's `<feature>PreMerge<n>` backup branch. This keeps merge N from ever reusing merge
|
|
59
|
-
// N-1's stale per-file context / merge-explanation.md.
|
|
60
|
-
function mergeDirFor(repoRoot, featureName) {
|
|
61
|
-
return path.join(repoRoot, rules_config_1.WEBPIECES_TMP_DIR, rules_config_1.MERGE_INFO_DIR, featureName);
|
|
62
|
-
}
|
|
63
|
-
// The run dir for sync number `n`: `<home>/merge-<n>/`. Holds this sync's marker + per-file
|
|
64
|
-
// `updatemain-<file>/` context. Numbered to match the sync's `<feature>PreMerge<n>` backup branch.
|
|
65
|
-
function mergeRunDirFor(home, n) {
|
|
66
|
-
return path.join(home, `merge-${n}`);
|
|
67
|
-
}
|
|
68
|
-
// The next NEVER-REUSED merge slot number for a feature home: one past the highest existing
|
|
69
|
-
// `merge-<n>/` dir (or 1 if none). This is the durable, monotonic source of truth for slot numbers —
|
|
70
|
-
// derived from the audit dirs THEMSELVES, not from which transient `<feature>PreMerge<n>` branch
|
|
71
|
-
// happens to exist. The old branch-existence scheme recycled a number the moment its snapshot was
|
|
72
|
-
// pruned, and the next sync then wiped that slot's `merge-explanation.md`; numbering off the dirs
|
|
73
|
-
// makes every merge keep its own `merge-<n>/` record permanently. Pure fs — unit-testable.
|
|
74
|
-
function nextMergeSlotNumber(home) {
|
|
75
|
-
if (!fs.existsSync(home))
|
|
76
|
-
return 1;
|
|
77
|
-
let max = 0;
|
|
78
|
-
for (const entry of fs.readdirSync(home)) {
|
|
79
|
-
const match = entry.match(/^merge-(\d+)$/);
|
|
80
|
-
if (match === null)
|
|
81
|
-
continue;
|
|
82
|
-
const n = parseInt(match[1], 10);
|
|
83
|
-
if (n > max)
|
|
84
|
-
max = n;
|
|
85
|
-
}
|
|
86
|
-
return max + 1;
|
|
87
|
-
}
|
|
88
45
|
// Data-only: the three commit points of a 3-point merge as persisted in `updatemain-hashes.json`
|
|
89
46
|
// (the exact key shape the conflict path already writes), plus when it was recorded.
|
|
90
47
|
class MergeHashRecord {
|
|
@@ -100,103 +57,140 @@ class MergeHashRecord {
|
|
|
100
57
|
}
|
|
101
58
|
}
|
|
102
59
|
exports.MergeHashRecord = MergeHashRecord;
|
|
60
|
+
const CONFLICT_MARKER_RE = /^(<{7}|={7}|>{7})/m;
|
|
103
61
|
// Marker written for a CLEAN (no-conflict) sync so every merge — not just conflicted ones — leaves a
|
|
104
62
|
// durable, self-describing record under `merge-info/<feature>/merge-<n>/`. A clean merge produces no
|
|
105
63
|
// per-file `merge-explanation.md`; this file is the "yes, a merge happened here, and it needed no
|
|
106
64
|
// 3-point resolution" proof, with the A/B/C shas kept so it stays auditable.
|
|
107
65
|
exports.NO_CONFLICT_MARKER_FILE = 'no-3point-merge.md';
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
` feature (B−A): git diff ${forkPoint} ${featureHead}\n` +
|
|
117
|
-
` main (C−A): git diff ${forkPoint} ${mainHead}\n`;
|
|
118
|
-
fs.writeFileSync(path.join(mergeDir, exports.NO_CONFLICT_MARKER_FILE), body);
|
|
119
|
-
const record = new MergeHashRecord(forkPoint, featureHead, mainHead, new Date().toISOString());
|
|
120
|
-
fs.writeFileSync(path.join(mergeDir, 'updatemain-hashes.json'), JSON.stringify(record, null, 2) + '\n');
|
|
121
|
-
}
|
|
122
|
-
// Locate the in-progress merge's run dir: the `<home>/merge-*/` subdir holding a marker. There is at
|
|
123
|
-
// most one (a fresh sync can't start while a merge is in progress); if more than one somehow exists,
|
|
124
|
-
// prefer an UNVALIDATED marker (the live conflict), else return the first found. Null when none.
|
|
125
|
-
function findActiveMergeRunDir(home) {
|
|
126
|
-
if (!fs.existsSync(home))
|
|
127
|
-
return null;
|
|
128
|
-
let fallback = null;
|
|
129
|
-
for (const entry of fs.readdirSync(home)) {
|
|
130
|
-
if (!entry.startsWith('merge-'))
|
|
131
|
-
continue;
|
|
132
|
-
const dir = path.join(home, entry);
|
|
133
|
-
const marker = readMergeMarker(dir);
|
|
134
|
-
if (marker === null)
|
|
135
|
-
continue;
|
|
136
|
-
if (!marker.validated)
|
|
137
|
-
return dir;
|
|
138
|
-
if (fallback === null)
|
|
139
|
-
fallback = dir;
|
|
66
|
+
/** Filesystem layout + read/write of the per-feature merge run dirs and their conflict markers. */
|
|
67
|
+
let MergeState = class MergeState {
|
|
68
|
+
// The per-feature "home" dir `.webpieces/merge-info/<slug>/`. It no longer holds the marker/context
|
|
69
|
+
// directly — each sync gets its own numbered `merge-<n>/` run dir underneath (mergeRunDirFor),
|
|
70
|
+
// paired with the sync's `<feature>PreMerge<n>` backup branch. This keeps merge N from ever reusing
|
|
71
|
+
// merge N-1's stale per-file context / merge-explanation.md.
|
|
72
|
+
mergeDirFor(repoRoot, featureName) {
|
|
73
|
+
return path.join(repoRoot, rules_config_1.WEBPIECES_TMP_DIR, rules_config_1.MERGE_INFO_DIR, featureName);
|
|
140
74
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
// (saveConflictContext) and the reader (the explanation gate) agree on the layout: the conflict
|
|
146
|
-
// file path with `/` → `__`, prefixed `updatemain-`.
|
|
147
|
-
function perFileContextDir(mergeDir, file) {
|
|
148
|
-
return path.join(mergeDir, `updatemain-${file.replace(/\//g, '__')}`);
|
|
149
|
-
}
|
|
150
|
-
function markerPath(mergeDir) {
|
|
151
|
-
return path.join(mergeDir, rules_config_1.MERGE_IN_PROGRESS_FILE);
|
|
152
|
-
}
|
|
153
|
-
function readMergeMarker(mergeDir) {
|
|
154
|
-
const filePath = markerPath(mergeDir);
|
|
155
|
-
if (!fs.existsSync(filePath))
|
|
156
|
-
return null;
|
|
157
|
-
const raw = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
158
|
-
return new MergeMarker(raw.currentBranch, raw.squashBranch, raw.backupBranch, raw.prNumber, raw.conflictedFiles ?? [], raw.forkPoint, raw.featureHead, raw.mainHead, raw.validated === true);
|
|
159
|
-
}
|
|
160
|
-
function writeMergeMarker(mergeDir, marker) {
|
|
161
|
-
fs.mkdirSync(mergeDir, { recursive: true });
|
|
162
|
-
fs.writeFileSync(markerPath(mergeDir), JSON.stringify(marker, null, 2) + '\n');
|
|
163
|
-
}
|
|
164
|
-
function clearMergeMarker(mergeDir) {
|
|
165
|
-
const filePath = markerPath(mergeDir);
|
|
166
|
-
if (fs.existsSync(filePath))
|
|
167
|
-
fs.rmSync(filePath);
|
|
168
|
-
}
|
|
169
|
-
/**
|
|
170
|
-
* Scoped conflict-marker scan: reads ONLY the given conflicted files (relative to repo
|
|
171
|
-
* root), never the whole repo — stays O(conflicts) regardless of monorepo size.
|
|
172
|
-
*/
|
|
173
|
-
function scanConflictMarkers(repoRoot, files) {
|
|
174
|
-
const filesWithMarkers = [];
|
|
175
|
-
for (const file of files) {
|
|
176
|
-
const abs = path.join(repoRoot, file);
|
|
177
|
-
if (!fs.existsSync(abs))
|
|
178
|
-
continue;
|
|
179
|
-
const content = fs.readFileSync(abs, 'utf8');
|
|
180
|
-
if (CONFLICT_MARKER_RE.test(content))
|
|
181
|
-
filesWithMarkers.push(file);
|
|
75
|
+
// The run dir for sync number `n`: `<home>/merge-<n>/`. Holds this sync's marker + per-file
|
|
76
|
+
// `updatemain-<file>/` context. Numbered to match the sync's `<feature>PreMerge<n>` backup branch.
|
|
77
|
+
mergeRunDirFor(home, n) {
|
|
78
|
+
return path.join(home, `merge-${n}`);
|
|
182
79
|
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
80
|
+
// The next NEVER-REUSED merge slot number for a feature home: one past the highest existing
|
|
81
|
+
// `merge-<n>/` dir (or 1 if none). Durable, monotonic source of truth for slot numbers — derived
|
|
82
|
+
// from the audit dirs THEMSELVES, not from which transient `<feature>PreMerge<n>` branch exists.
|
|
83
|
+
nextMergeSlotNumber(home) {
|
|
84
|
+
if (!fs.existsSync(home))
|
|
85
|
+
return 1;
|
|
86
|
+
let max = 0;
|
|
87
|
+
for (const entry of fs.readdirSync(home)) {
|
|
88
|
+
const match = entry.match(/^merge-(\d+)$/);
|
|
89
|
+
if (match === null)
|
|
90
|
+
continue;
|
|
91
|
+
const n = parseInt(match[1], 10);
|
|
92
|
+
if (n > max)
|
|
93
|
+
max = n;
|
|
94
|
+
}
|
|
95
|
+
return max + 1;
|
|
199
96
|
}
|
|
200
|
-
|
|
201
|
-
|
|
97
|
+
// Write the clean-merge marker + a per-slot `updatemain-hashes.json` copy into the slot's own dir.
|
|
98
|
+
writeCleanMergeMarker(mergeDir, forkPoint, featureHead, mainHead) {
|
|
99
|
+
fs.mkdirSync(mergeDir, { recursive: true });
|
|
100
|
+
const body = '# Clean squash-merge — no 3-point conflict resolution needed\n\n' +
|
|
101
|
+
'This sync merged main into the feature with no conflicts, so the AI wrote no per-file\n' +
|
|
102
|
+
'merge-explanation.md. The 3-point shas below are kept so the merge is still auditable.\n\n' +
|
|
103
|
+
`3-point shas: A(fork)=${forkPoint} B(feature)=${featureHead} C(main)=${mainHead}\n\n` +
|
|
104
|
+
'Reconstruct what each side changed:\n' +
|
|
105
|
+
` feature (B−A): git diff ${forkPoint} ${featureHead}\n` +
|
|
106
|
+
` main (C−A): git diff ${forkPoint} ${mainHead}\n`;
|
|
107
|
+
fs.writeFileSync(path.join(mergeDir, exports.NO_CONFLICT_MARKER_FILE), body);
|
|
108
|
+
const record = new MergeHashRecord(forkPoint, featureHead, mainHead, new Date().toISOString());
|
|
109
|
+
fs.writeFileSync(path.join(mergeDir, 'updatemain-hashes.json'), JSON.stringify(record, null, 2) + '\n');
|
|
110
|
+
}
|
|
111
|
+
// Locate the in-progress merge's run dir: the `<home>/merge-*/` subdir holding a marker. There is
|
|
112
|
+
// at most one; if more than one somehow exists, prefer an UNVALIDATED marker (the live conflict),
|
|
113
|
+
// else return the first found. Null when none.
|
|
114
|
+
findActiveMergeRunDir(home) {
|
|
115
|
+
if (!fs.existsSync(home))
|
|
116
|
+
return null;
|
|
117
|
+
let fallback = null;
|
|
118
|
+
for (const entry of fs.readdirSync(home)) {
|
|
119
|
+
if (!entry.startsWith('merge-'))
|
|
120
|
+
continue;
|
|
121
|
+
const dir = path.join(home, entry);
|
|
122
|
+
const marker = this.readMergeMarker(dir);
|
|
123
|
+
if (marker === null)
|
|
124
|
+
continue;
|
|
125
|
+
if (!marker.validated)
|
|
126
|
+
return dir;
|
|
127
|
+
if (fallback === null)
|
|
128
|
+
fallback = dir;
|
|
129
|
+
}
|
|
130
|
+
return fallback;
|
|
131
|
+
}
|
|
132
|
+
// Per-conflicted-file context dir holding A-forkpoint.txt / B-feature.txt / C-main.txt /
|
|
133
|
+
// B-A.diff / C-A.diff (and the AI's merge-explanation.md). Shared so writer and reader agree on the
|
|
134
|
+
// layout: the conflict file path with `/` → `__`, prefixed `updatemain-`.
|
|
135
|
+
perFileContextDir(mergeDir, file) {
|
|
136
|
+
return path.join(mergeDir, `updatemain-${file.replace(/\//g, '__')}`);
|
|
137
|
+
}
|
|
138
|
+
markerPath(mergeDir) {
|
|
139
|
+
return path.join(mergeDir, rules_config_1.MERGE_IN_PROGRESS_FILE);
|
|
140
|
+
}
|
|
141
|
+
readMergeMarker(mergeDir) {
|
|
142
|
+
const filePath = this.markerPath(mergeDir);
|
|
143
|
+
if (!fs.existsSync(filePath))
|
|
144
|
+
return null;
|
|
145
|
+
const raw = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
146
|
+
return new MergeMarker(raw.currentBranch, raw.squashBranch, raw.backupBranch, raw.prNumber, raw.conflictedFiles ?? [], raw.forkPoint, raw.featureHead, raw.mainHead, raw.validated === true);
|
|
147
|
+
}
|
|
148
|
+
writeMergeMarker(mergeDir, marker) {
|
|
149
|
+
fs.mkdirSync(mergeDir, { recursive: true });
|
|
150
|
+
fs.writeFileSync(this.markerPath(mergeDir), JSON.stringify(marker, null, 2) + '\n');
|
|
151
|
+
}
|
|
152
|
+
clearMergeMarker(mergeDir) {
|
|
153
|
+
const filePath = this.markerPath(mergeDir);
|
|
154
|
+
if (fs.existsSync(filePath))
|
|
155
|
+
fs.rmSync(filePath);
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Scoped conflict-marker scan: reads ONLY the given conflicted files (relative to repo root),
|
|
159
|
+
* never the whole repo — stays O(conflicts) regardless of monorepo size.
|
|
160
|
+
*/
|
|
161
|
+
scanConflictMarkers(repoRoot, files) {
|
|
162
|
+
const filesWithMarkers = [];
|
|
163
|
+
for (const file of files) {
|
|
164
|
+
const abs = path.join(repoRoot, file);
|
|
165
|
+
if (!fs.existsSync(abs))
|
|
166
|
+
continue;
|
|
167
|
+
const content = fs.readFileSync(abs, 'utf8');
|
|
168
|
+
if (CONFLICT_MARKER_RE.test(content))
|
|
169
|
+
filesWithMarkers.push(file);
|
|
170
|
+
}
|
|
171
|
+
return new MarkerScanResult(filesWithMarkers.length === 0, filesWithMarkers);
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Explanation scan: every conflicted file the AI resolved must have a non-empty
|
|
175
|
+
* MERGE_EXPLANATION_FILE in its per-file context dir (next to the diffs), proving the AI
|
|
176
|
+
* deliberately 3-point merged it and recording HOW. Returns files whose explanation is missing or
|
|
177
|
+
* empty. Works for every conflicted file type — including comment-less files (JSON) and files
|
|
178
|
+
* resolved by deletion (no working-tree file to inspect).
|
|
179
|
+
*/
|
|
180
|
+
scanMergeExplanations(mergeDir, files) {
|
|
181
|
+
const filesMissingExplanation = [];
|
|
182
|
+
for (const file of files) {
|
|
183
|
+
const explPath = path.join(this.perFileContextDir(mergeDir, file), rules_config_1.MERGE_EXPLANATION_FILE);
|
|
184
|
+
const present = fs.existsSync(explPath) && fs.readFileSync(explPath, 'utf8').trim() !== '';
|
|
185
|
+
if (!present)
|
|
186
|
+
filesMissingExplanation.push(file);
|
|
187
|
+
}
|
|
188
|
+
return new MarkerScanResult(filesMissingExplanation.length === 0, filesMissingExplanation);
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
exports.MergeState = MergeState;
|
|
192
|
+
exports.MergeState = MergeState = tslib_1.__decorate([
|
|
193
|
+
(0, core_context_1.provideSingleton)(),
|
|
194
|
+
(0, inversify_1.injectable)()
|
|
195
|
+
], MergeState);
|
|
202
196
|
//# 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';\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\nconst CONFLICT_MARKER_RE = /^(<{7}|={7}|>{7})/m;\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), paired\n// with the sync's `<feature>PreMerge<n>` backup branch. This keeps merge N from ever reusing merge\n// N-1's stale per-file context / merge-explanation.md.\nexport function 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.\nexport function 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). This is the durable, monotonic source of truth for slot numbers —\n// derived from the audit dirs THEMSELVES, not from which transient `<feature>PreMerge<n>` branch\n// happens to exist. The old branch-existence scheme recycled a number the moment its snapshot was\n// pruned, and the next sync then wiped that slot's `merge-explanation.md`; numbering off the dirs\n// makes every merge keep its own `merge-<n>/` record permanently. Pure fs — unit-testable.\nexport function 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// 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\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// Write the clean-merge marker + a per-slot `updatemain-hashes.json` copy into the slot's own dir.\nexport function writeCleanMergeMarker(\n mergeDir: string, forkPoint: string, featureHead: string, mainHead: string,\n): 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 at\n// most one (a fresh sync can't start while a merge is in progress); if more than one somehow exists,\n// prefer an UNVALIDATED marker (the live conflict), else return the first found. Null when none.\nexport function 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 = 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 the writer\n// (saveConflictContext) and the reader (the explanation gate) agree on the layout: the conflict\n// file path with `/` → `__`, prefixed `updatemain-`.\nexport function perFileContextDir(mergeDir: string, file: string): string {\n return path.join(mergeDir, `updatemain-${file.replace(/\\//g, '__')}`);\n}\n\nexport function markerPath(mergeDir: string): string {\n return path.join(mergeDir, MERGE_IN_PROGRESS_FILE);\n}\n\nexport function readMergeMarker(mergeDir: string): MergeMarker | null {\n const filePath = 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\nexport function writeMergeMarker(mergeDir: string, marker: MergeMarker): void {\n fs.mkdirSync(mergeDir, { recursive: true });\n fs.writeFileSync(markerPath(mergeDir), JSON.stringify(marker, null, 2) + '\\n');\n}\n\nexport function clearMergeMarker(mergeDir: string): void {\n const filePath = 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\n * root), never the whole repo — stays O(conflicts) regardless of monorepo size.\n */\nexport function 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 sitting in its per-file context dir (next to the diffs), proving the AI\n * deliberately 3-point merged it and recording HOW. Returns the files whose explanation is\n * missing or empty. Works for every conflicted file regardless of type — including comment-less\n * files (JSON) and files resolved by deletion (no working-tree file to inspect).\n */\nexport function scanMergeExplanations(mergeDir: string, files: string[]): MarkerScanResult {\n const filesMissingExplanation: string[] = [];\n for (const file of files) {\n const explPath = path.join(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"]}
|
|
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 { provideSingleton } from '@webpieces/core-context';\nimport { injectable } 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@provideSingleton()\n@injectable()\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 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
import { BranchNaming } from './branch-naming';
|
|
2
|
+
/** Hard gate: whether an OPEN PR already tracks a feature branch (by its stable base name). */
|
|
3
|
+
export declare class OpenPrCheck {
|
|
4
|
+
private readonly branchNaming;
|
|
5
|
+
constructor(branchNaming: BranchNaming);
|
|
6
|
+
openPrForBranch(repoRoot: string, currentBranch: string): string;
|
|
7
|
+
}
|
|
@@ -1,31 +1,42 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
3
|
+
exports.OpenPrCheck = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
4
5
|
const child_process_1 = require("child_process");
|
|
5
6
|
const rules_config_1 = require("@webpieces/rules-config");
|
|
7
|
+
const core_context_1 = require("@webpieces/core-context");
|
|
8
|
+
const inversify_1 = require("inversify");
|
|
6
9
|
const branch_naming_1 = require("./branch-naming");
|
|
7
10
|
const SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n';
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
// authenticated, offline). We must NOT degrade "couldn't ask GitHub" into "no PR" — a 3-point update
|
|
14
|
-
// makes a new branch generation and the existing PR would be left pointing at the OLD branch (stale),
|
|
15
|
-
// so running wp-start-update blind when a PR might exist is exactly the disaster we are guarding
|
|
16
|
-
// against. Refuse rather than guess.
|
|
17
|
-
function openPrForBranch(repoRoot, currentBranch) {
|
|
18
|
-
const base = (0, branch_naming_1.baseBranchName)(currentBranch);
|
|
19
|
-
const result = (0, child_process_1.spawnSync)('gh', ['pr', 'list', '--head', base, '--state', 'open', '--json', 'number', '--jq', '.[0].number'], { cwd: repoRoot, encoding: 'utf8' });
|
|
20
|
-
if (result.status !== 0) {
|
|
21
|
-
const detail = (result.stderr ?? '').trim() || (result.error ? result.error.message : 'gh exited non-zero');
|
|
22
|
-
throw new rules_config_1.CliExitError(2, '\n' + SEP + '❌ Could not ask GitHub whether an open PR exists for this branch\n' + SEP + '\n' +
|
|
23
|
-
`gh failed for base branch "${base}": ${detail}\n\n` +
|
|
24
|
-
'This check must NOT be skipped: a 3-point update creates a NEW branch generation, so if a\n' +
|
|
25
|
-
'PR already exists, wp-start-update would leave it pointing at the OLD branch. Fix gh first\n' +
|
|
26
|
-
'(install / `gh auth login` / restore network), then re-run — or, if a PR does exist, use\n' +
|
|
27
|
-
'the PR flow: pnpm wp-start-upsert-pr → /wp-merge → pnpm wp-finish-upsert-pr\n');
|
|
11
|
+
/** Hard gate: whether an OPEN PR already tracks a feature branch (by its stable base name). */
|
|
12
|
+
let OpenPrCheck = class OpenPrCheck {
|
|
13
|
+
branchNaming;
|
|
14
|
+
constructor(branchNaming) {
|
|
15
|
+
this.branchNaming = branchNaming;
|
|
28
16
|
}
|
|
29
|
-
|
|
30
|
-
|
|
17
|
+
// The open PR (if any) tracking a feature branch, looked up by its STABLE base name. Returns '' ONLY
|
|
18
|
+
// when GitHub answered and there is genuinely no open PR. HARD GATE: FAILS FAST if `gh` cannot
|
|
19
|
+
// answer — we must NOT degrade "couldn't ask GitHub" into "no PR" (a 3-point update makes a new
|
|
20
|
+
// branch generation and the existing PR would be stranded on the OLD branch). Refuse rather than guess.
|
|
21
|
+
openPrForBranch(repoRoot, currentBranch) {
|
|
22
|
+
const base = this.branchNaming.baseBranchName(currentBranch);
|
|
23
|
+
const result = (0, child_process_1.spawnSync)('gh', ['pr', 'list', '--head', base, '--state', 'open', '--json', 'number', '--jq', '.[0].number'], { cwd: repoRoot, encoding: 'utf8' });
|
|
24
|
+
if (result.status !== 0) {
|
|
25
|
+
const detail = (result.stderr ?? '').trim() || (result.error ? result.error.message : 'gh exited non-zero');
|
|
26
|
+
throw new rules_config_1.CliExitError(2, '\n' + SEP + '❌ Could not ask GitHub whether an open PR exists for this branch\n' + SEP + '\n' +
|
|
27
|
+
`gh failed for base branch "${base}": ${detail}\n\n` +
|
|
28
|
+
'This check must NOT be skipped: a 3-point update creates a NEW branch generation, so if a\n' +
|
|
29
|
+
'PR already exists, wp-start-update would leave it pointing at the OLD branch. Fix gh first\n' +
|
|
30
|
+
'(install / `gh auth login` / restore network), then re-run — or, if a PR does exist, use\n' +
|
|
31
|
+
'the PR flow: pnpm wp-start-upsert-pr → /wp-merge → pnpm wp-finish-upsert-pr\n');
|
|
32
|
+
}
|
|
33
|
+
return (result.stdout ?? '').trim();
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
exports.OpenPrCheck = OpenPrCheck;
|
|
37
|
+
exports.OpenPrCheck = OpenPrCheck = tslib_1.__decorate([
|
|
38
|
+
(0, core_context_1.provideSingleton)(),
|
|
39
|
+
(0, inversify_1.injectable)(),
|
|
40
|
+
tslib_1.__metadata("design:paramtypes", [branch_naming_1.BranchNaming])
|
|
41
|
+
], OpenPrCheck);
|
|
31
42
|
//# sourceMappingURL=open-pr-check.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"open-pr-check.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/open-pr-check.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"open-pr-check.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/open-pr-check.ts"],"names":[],"mappings":";;;;AAAA,iDAA0C;AAC1C,0DAAuD;AACvD,0DAA2D;AAC3D,yCAAuC;AACvC,mDAA+C;AAE/C,MAAM,GAAG,GAAG,0DAA0D,CAAC;AAEvE,+FAA+F;AAGxF,IAAM,WAAW,GAAjB,MAAM,WAAW;IACS;IAA7B,YAA6B,YAA0B;QAA1B,iBAAY,GAAZ,YAAY,CAAc;IAAG,CAAC;IAE3D,qGAAqG;IACrG,+FAA+F;IAC/F,gGAAgG;IAChG,wGAAwG;IACxG,eAAe,CAAC,QAAgB,EAAE,aAAqB;QACnD,MAAM,IAAI,GAAG,IAAI,CAAC,YAAY,CAAC,cAAc,CAAC,aAAa,CAAC,CAAC;QAC7D,MAAM,MAAM,GAAG,IAAA,yBAAS,EACpB,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,aAAa,CAAC,EAClG,EAAE,GAAG,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,CACtC,CAAC;QACF,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACtB,MAAM,MAAM,GAAG,CAAC,MAAM,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,oBAAoB,CAAC,CAAC;YAC5G,MAAM,IAAI,2BAAY,CAAC,CAAC,EACpB,IAAI,GAAG,GAAG,GAAG,oEAAoE,GAAG,GAAG,GAAG,IAAI;gBAC9F,8BAA8B,IAAI,MAAM,MAAM,MAAM;gBACpD,6FAA6F;gBAC7F,8FAA8F;gBAC9F,4FAA4F;gBAC5F,+EAA+E,CAClF,CAAC;QACN,CAAC;QACD,OAAO,CAAC,MAAM,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IACxC,CAAC;CACJ,CAAA;AA1BY,kCAAW;sBAAX,WAAW;IAFvB,IAAA,+BAAgB,GAAE;IAClB,IAAA,sBAAU,GAAE;6CAEkC,4BAAY;GAD9C,WAAW,CA0BvB","sourcesContent":["import { spawnSync } from 'child_process';\nimport { CliExitError } from '@webpieces/rules-config';\nimport { provideSingleton } from '@webpieces/core-context';\nimport { injectable } from 'inversify';\nimport { BranchNaming } from './branch-naming';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n/** Hard gate: whether an OPEN PR already tracks a feature branch (by its stable base name). */\n@provideSingleton()\n@injectable()\nexport class OpenPrCheck {\n constructor(private readonly branchNaming: BranchNaming) {}\n\n // The open PR (if any) tracking a feature branch, looked up by its STABLE base name. Returns '' ONLY\n // when GitHub answered and there is genuinely no open PR. HARD GATE: FAILS FAST if `gh` cannot\n // answer — we must NOT degrade \"couldn't ask GitHub\" into \"no PR\" (a 3-point update makes a new\n // branch generation and the existing PR would be stranded on the OLD branch). Refuse rather than guess.\n openPrForBranch(repoRoot: string, currentBranch: string): string {\n const base = this.branchNaming.baseBranchName(currentBranch);\n const result = spawnSync(\n 'gh', ['pr', 'list', '--head', base, '--state', 'open', '--json', 'number', '--jq', '.[0].number'],\n { cwd: repoRoot, encoding: 'utf8' },\n );\n if (result.status !== 0) {\n const detail = (result.stderr ?? '').trim() || (result.error ? result.error.message : 'gh exited non-zero');\n throw new CliExitError(2,\n '\\n' + SEP + '❌ Could not ask GitHub whether an open PR exists for this branch\\n' + SEP + '\\n' +\n `gh failed for base branch \"${base}\": ${detail}\\n\\n` +\n 'This check must NOT be skipped: a 3-point update creates a NEW branch generation, so if a\\n' +\n 'PR already exists, wp-start-update would leave it pointing at the OLD branch. Fix gh first\\n' +\n '(install / `gh auth login` / restore network), then re-run — or, if a PR does exist, use\\n' +\n 'the PR flow: pnpm wp-start-upsert-pr → /wp-merge → pnpm wp-finish-upsert-pr\\n',\n );\n }\n return (result.stdout ?? '').trim();\n }\n}\n"]}
|
|
@@ -1,3 +1,15 @@
|
|
|
1
1
|
import { MutationVerb } from '@webpieces/rules-config';
|
|
2
|
+
import { AiBranchName } from './git-readAiBranchName';
|
|
3
|
+
import { MergeState } from './merge-state';
|
|
4
|
+
import { MergeStart } from './merge-start';
|
|
5
|
+
import { MergeEnd } from './merge-end';
|
|
2
6
|
export type UpdateOutcome = 'finalized' | 'conflict' | 'unvalidatedResume';
|
|
3
|
-
export declare
|
|
7
|
+
export declare class RunUpdate {
|
|
8
|
+
private readonly aiBranchName;
|
|
9
|
+
private readonly mergeState;
|
|
10
|
+
private readonly mergeStart;
|
|
11
|
+
private readonly mergeEnd;
|
|
12
|
+
constructor(aiBranchName: AiBranchName, mergeState: MergeState, mergeStart: MergeStart, mergeEnd: MergeEnd);
|
|
13
|
+
runUpdateFromMain(repoRoot: string, verb: MutationVerb, finishCommand: string): Promise<UpdateOutcome>;
|
|
14
|
+
private runUpdate;
|
|
15
|
+
}
|