@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.
Files changed (70) hide show
  1. package/package.json +7 -2
  2. package/src/dashboard/dashboard.d.ts +12 -3
  3. package/src/dashboard/dashboard.js +136 -129
  4. package/src/dashboard/dashboard.js.map +1 -1
  5. package/src/index.d.ts +2 -1
  6. package/src/index.js +4 -4
  7. package/src/index.js.map +1 -1
  8. package/src/scripts/commands/finish-update-command.d.ts +10 -0
  9. package/src/scripts/commands/finish-update-command.js +51 -0
  10. package/src/scripts/commands/finish-update-command.js.map +1 -0
  11. package/src/scripts/commands/finish-upsert-pr-command.d.ts +22 -0
  12. package/src/scripts/commands/finish-upsert-pr-command.js +131 -0
  13. package/src/scripts/commands/finish-upsert-pr-command.js.map +1 -0
  14. package/src/scripts/commands/start-update-command.d.ts +9 -0
  15. package/src/scripts/commands/start-update-command.js +67 -0
  16. package/src/scripts/commands/start-update-command.js.map +1 -0
  17. package/src/scripts/commands/start-upsert-pr-command.d.ts +15 -0
  18. package/src/scripts/commands/start-upsert-pr-command.js +73 -0
  19. package/src/scripts/commands/start-upsert-pr-command.js.map +1 -0
  20. package/src/scripts/git-finishUpsertPr.d.ts +1 -1
  21. package/src/scripts/git-finishUpsertPr.js +11 -109
  22. package/src/scripts/git-finishUpsertPr.js.map +1 -1
  23. package/src/scripts/git-gatherInfo.d.ts +15 -1
  24. package/src/scripts/git-gatherInfo.js +79 -61
  25. package/src/scripts/git-gatherInfo.js.map +1 -1
  26. package/src/scripts/pr-gate-app.d.ts +25 -0
  27. package/src/scripts/pr-gate-app.js +56 -0
  28. package/src/scripts/pr-gate-app.js.map +1 -0
  29. package/src/scripts/workflow/branch-naming.d.ts +11 -8
  30. package/src/scripts/workflow/branch-naming.js +30 -21
  31. package/src/scripts/workflow/branch-naming.js.map +1 -1
  32. package/src/scripts/workflow/build-affected.d.ts +21 -30
  33. package/src/scripts/workflow/build-affected.js +57 -63
  34. package/src/scripts/workflow/build-affected.js.map +1 -1
  35. package/src/scripts/workflow/cleanTmp.d.ts +6 -1
  36. package/src/scripts/workflow/cleanTmp.js +83 -78
  37. package/src/scripts/workflow/cleanTmp.js.map +1 -1
  38. package/src/scripts/workflow/git-exec.d.ts +32 -28
  39. package/src/scripts/workflow/git-exec.js +92 -85
  40. package/src/scripts/workflow/git-exec.js.map +1 -1
  41. package/src/scripts/workflow/git-findForkPoint.d.ts +12 -3
  42. package/src/scripts/workflow/git-findForkPoint.js +116 -107
  43. package/src/scripts/workflow/git-findForkPoint.js.map +1 -1
  44. package/src/scripts/workflow/git-readAiBranchName.d.ts +7 -2
  45. package/src/scripts/workflow/git-readAiBranchName.js +24 -15
  46. package/src/scripts/workflow/git-readAiBranchName.js.map +1 -1
  47. package/src/scripts/workflow/merge-end.d.ts +21 -6
  48. package/src/scripts/workflow/merge-end.js +147 -133
  49. package/src/scripts/workflow/merge-end.js.map +1 -1
  50. package/src/scripts/workflow/merge-start.d.ts +22 -1
  51. package/src/scripts/workflow/merge-start.js +180 -186
  52. package/src/scripts/workflow/merge-start.js.map +1 -1
  53. package/src/scripts/workflow/merge-state.d.ts +26 -23
  54. package/src/scripts/workflow/merge-state.js +131 -137
  55. package/src/scripts/workflow/merge-state.js.map +1 -1
  56. package/src/scripts/workflow/open-pr-check.d.ts +7 -1
  57. package/src/scripts/workflow/open-pr-check.js +34 -23
  58. package/src/scripts/workflow/open-pr-check.js.map +1 -1
  59. package/src/scripts/workflow/run-update.d.ts +13 -1
  60. package/src/scripts/workflow/run-update.js +59 -28
  61. package/src/scripts/workflow/run-update.js.map +1 -1
  62. package/src/scripts/wp-finish-update.d.ts +1 -1
  63. package/src/scripts/wp-finish-update.js +11 -30
  64. package/src/scripts/wp-finish-update.js.map +1 -1
  65. package/src/scripts/wp-start-update.d.ts +1 -2
  66. package/src/scripts/wp-start-update.js +11 -51
  67. package/src/scripts/wp-start-update.js.map +1 -1
  68. package/src/scripts/wp-start-upsert-pr.d.ts +1 -1
  69. package/src/scripts/wp-start-upsert-pr.js +11 -47
  70. 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
- export declare function writeCleanMergeMarker(mergeDir: string, forkPoint: string, featureHead: string, mainHead: string): void;
30
- export declare function findActiveMergeRunDir(home: string): string | null;
31
- export declare function perFileContextDir(mergeDir: string, file: string): string;
32
- export declare function markerPath(mergeDir: string): string;
33
- export declare function readMergeMarker(mergeDir: string): MergeMarker | null;
34
- export declare function writeMergeMarker(mergeDir: string, marker: MergeMarker): void;
35
- export declare function clearMergeMarker(mergeDir: string): void;
36
- /**
37
- * Scoped conflict-marker scan: reads ONLY the given conflicted files (relative to repo
38
- * root), never the whole repo — stays O(conflicts) regardless of monorepo size.
39
- */
40
- export declare function scanConflictMarkers(repoRoot: string, files: string[]): MarkerScanResult;
41
- /**
42
- * Explanation scan: every conflicted file the AI resolved must have a non-empty
43
- * MERGE_EXPLANATION_FILE sitting in its per-file context dir (next to the diffs), proving the AI
44
- * deliberately 3-point merged it and recording HOW. Returns the files whose explanation is
45
- * missing or empty. Works for every conflicted file regardless of type — including comment-less
46
- * files (JSON) and files resolved by deletion (no working-tree file to inspect).
47
- */
48
- export declare function scanMergeExplanations(mergeDir: string, files: string[]): MarkerScanResult;
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
- // Write the clean-merge marker + a per-slot `updatemain-hashes.json` copy into the slot's own dir.
109
- function writeCleanMergeMarker(mergeDir, forkPoint, featureHead, mainHead) {
110
- fs.mkdirSync(mergeDir, { recursive: true });
111
- const body = '# Clean squash-merge no 3-point conflict resolution needed\n\n' +
112
- 'This sync merged main into the feature with no conflicts, so the AI wrote no per-file\n' +
113
- 'merge-explanation.md. The 3-point shas below are kept so the merge is still auditable.\n\n' +
114
- `3-point shas: A(fork)=${forkPoint} B(feature)=${featureHead} C(main)=${mainHead}\n\n` +
115
- 'Reconstruct what each side changed:\n' +
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
- return fallback;
142
- }
143
- // Per-conflicted-file context dir holding A-forkpoint.txt / B-feature.txt / C-main.txt /
144
- // B-A.diff / C-A.diff (and the AI's merge-explanation.md). Shared so the writer
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
- return new MarkerScanResult(filesWithMarkers.length === 0, filesWithMarkers);
184
- }
185
- /**
186
- * Explanation scan: every conflicted file the AI resolved must have a non-empty
187
- * MERGE_EXPLANATION_FILE sitting in its per-file context dir (next to the diffs), proving the AI
188
- * deliberately 3-point merged it and recording HOW. Returns the files whose explanation is
189
- * missing or empty. Works for every conflicted file regardless of type — including comment-less
190
- * files (JSON) and files resolved by deletion (no working-tree file to inspect).
191
- */
192
- function scanMergeExplanations(mergeDir, files) {
193
- const filesMissingExplanation = [];
194
- for (const file of files) {
195
- const explPath = path.join(perFileContextDir(mergeDir, file), rules_config_1.MERGE_EXPLANATION_FILE);
196
- const present = fs.existsSync(explPath) && fs.readFileSync(explPath, 'utf8').trim() !== '';
197
- if (!present)
198
- filesMissingExplanation.push(file);
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
- return new MarkerScanResult(filesMissingExplanation.length === 0, filesMissingExplanation);
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
- export declare function openPrForBranch(repoRoot: string, currentBranch: string): string;
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.openPrForBranch = openPrForBranch;
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
- // The open PR (if any) tracking a feature branch, looked up by its STABLE base name (the PR always
9
- // lives on the stable feature name, and a leftover `…wpN` still resolves there). Returns '' ONLY when GitHub
10
- // answered and there is genuinely no open PR.
11
- //
12
- // This is a HARD GATE, not an advisory hint: it FAILS FAST if `gh` cannot answer (not installed, not
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
- return (result.stdout ?? '').trim();
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":";;AAeA,0CAkBC;AAjCD,iDAA0C;AAC1C,0DAAuD;AACvD,mDAAiD;AAEjD,MAAM,GAAG,GAAG,0DAA0D,CAAC;AAEvE,mGAAmG;AACnG,6GAA6G;AAC7G,8CAA8C;AAC9C,EAAE;AACF,qGAAqG;AACrG,qGAAqG;AACrG,sGAAsG;AACtG,iGAAiG;AACjG,qCAAqC;AACrC,SAAgB,eAAe,CAAC,QAAgB,EAAE,aAAqB;IACnE,MAAM,IAAI,GAAG,IAAA,8BAAc,EAAC,aAAa,CAAC,CAAC;IAC3C,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;IACF,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACtB,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;QAC5G,MAAM,IAAI,2BAAY,CAAC,CAAC,EACpB,IAAI,GAAG,GAAG,GAAG,oEAAoE,GAAG,GAAG,GAAG,IAAI;YAC9F,8BAA8B,IAAI,MAAM,MAAM,MAAM;YACpD,6FAA6F;YAC7F,8FAA8F;YAC9F,4FAA4F;YAC5F,+EAA+E,CAClF,CAAC;IACN,CAAC;IACD,OAAO,CAAC,MAAM,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;AACxC,CAAC","sourcesContent":["import { spawnSync } from 'child_process';\nimport { CliExitError } from '@webpieces/rules-config';\nimport { baseBranchName } from './branch-naming';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n// The open PR (if any) tracking a feature branch, looked up by its STABLE base name (the PR always\n// lives on the stable feature name, and a leftover `…wpN` still resolves there). Returns '' ONLY when GitHub\n// answered and there is genuinely no open PR.\n//\n// This is a HARD GATE, not an advisory hint: it FAILS FAST if `gh` cannot answer (not installed, not\n// authenticated, offline). We must NOT degrade \"couldn't ask GitHub\" into \"no PR\" a 3-point update\n// makes a new branch generation and the existing PR would be left pointing at the OLD branch (stale),\n// so running wp-start-update blind when a PR might exist is exactly the disaster we are guarding\n// against. Refuse rather than guess.\nexport function openPrForBranch(repoRoot: string, currentBranch: string): string {\n const base = 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"]}
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 function runUpdateFromMain(repoRoot: string, verb: MutationVerb, finishCommand: string): Promise<UpdateOutcome>;
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
+ }