sela-core 1.0.7 → 1.0.9
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/README.md +27 -17
- package/dist/cli/ErrorHandler.d.ts +1 -1
- package/dist/cli/ErrorHandler.js +14 -14
- package/dist/cli/commands/bulk.d.ts +1 -1
- package/dist/cli/commands/bulk.d.ts.map +1 -1
- package/dist/cli/commands/bulk.js +51 -33
- package/dist/cli/commands/init.d.ts +1 -1
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +26 -18
- package/dist/cli/commands/merge.d.ts +13 -0
- package/dist/cli/commands/merge.d.ts.map +1 -0
- package/dist/cli/commands/merge.js +119 -0
- package/dist/cli/commands/showReport.d.ts +1 -1
- package/dist/cli/commands/showReport.d.ts.map +1 -1
- package/dist/cli/commands/showReport.js +12 -10
- package/dist/cli/commands/status.d.ts +1 -1
- package/dist/cli/commands/status.d.ts.map +1 -1
- package/dist/cli/commands/status.js +8 -8
- package/dist/cli/commands/sync.d.ts +1 -1
- package/dist/cli/commands/sync.d.ts.map +1 -1
- package/dist/cli/commands/sync.js +38 -30
- package/dist/cli/index.js +12 -9
- package/dist/cli/ui/DnaTable.d.ts +1 -1
- package/dist/cli/ui/DnaTable.d.ts.map +1 -1
- package/dist/cli/ui/DnaTable.js +23 -16
- package/dist/cli/ui/RefactorWizard.d.ts +3 -3
- package/dist/cli/ui/RefactorWizard.d.ts.map +1 -1
- package/dist/cli/ui/RefactorWizard.js +108 -82
- package/dist/config/ConfigLoader.d.ts +2 -2
- package/dist/config/ConfigLoader.d.ts.map +1 -1
- package/dist/config/ConfigLoader.js +15 -15
- package/dist/config/DryRunGuard.d.ts +1 -1
- package/dist/config/DryRunGuard.js +7 -7
- package/dist/config/SelaConfig.d.ts +13 -17
- package/dist/config/SelaConfig.d.ts.map +1 -1
- package/dist/config/SelaConfig.js +32 -26
- package/dist/engine/HealingRegistry.d.ts +1 -1
- package/dist/engine/HealingRegistry.js +3 -3
- package/dist/engine/SelaEngine.d.ts +1 -1
- package/dist/engine/SelaEngine.d.ts.map +1 -1
- package/dist/engine/SelaEngine.js +117 -51
- package/dist/errors/SelaError.d.ts +8 -1
- package/dist/errors/SelaError.d.ts.map +1 -1
- package/dist/errors/SelaError.js +14 -7
- package/dist/fixtures/expectProxy.d.ts +2 -2
- package/dist/fixtures/expectProxy.js +16 -16
- package/dist/fixtures/index.d.ts +18 -2
- package/dist/fixtures/index.d.ts.map +1 -1
- package/dist/fixtures/index.js +47 -27
- package/dist/fixtures/moduleExpect.js +7 -7
- package/dist/fixtures/proxyTag.d.ts +1 -1
- package/dist/fixtures/proxyTag.js +4 -4
- package/dist/index.d.ts +8 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -4
- package/dist/reporter/SelaReporter.d.ts +45 -12
- package/dist/reporter/SelaReporter.d.ts.map +1 -1
- package/dist/reporter/SelaReporter.js +139 -39
- package/dist/services/ASTSourceUpdater.d.ts +48 -3
- package/dist/services/ASTSourceUpdater.d.ts.map +1 -1
- package/dist/services/ASTSourceUpdater.js +283 -105
- package/dist/services/AnchorResolver.d.ts +157 -0
- package/dist/services/AnchorResolver.d.ts.map +1 -0
- package/dist/services/AnchorResolver.js +289 -0
- package/dist/services/ArgumentTypeAnalyzer.js +7 -7
- package/dist/services/BlastRadiusAnalyzer.d.ts.map +1 -1
- package/dist/services/BlastRadiusAnalyzer.js +5 -2
- package/dist/services/ChainValidator.js +22 -22
- package/dist/services/CrossFileHealer.js +7 -7
- package/dist/services/DecisionEngine.d.ts +51 -0
- package/dist/services/DecisionEngine.d.ts.map +1 -0
- package/dist/services/DecisionEngine.js +260 -0
- package/dist/services/DefinitionTracer.d.ts +1 -1
- package/dist/services/DefinitionTracer.d.ts.map +1 -1
- package/dist/services/DefinitionTracer.js +28 -13
- package/dist/services/DnaEditorService.d.ts +2 -2
- package/dist/services/DnaEditorService.d.ts.map +1 -1
- package/dist/services/DnaEditorService.js +81 -62
- package/dist/services/DnaIndexService.d.ts +1 -1
- package/dist/services/DnaIndexService.d.ts.map +1 -1
- package/dist/services/DnaIndexService.js +14 -14
- package/dist/services/HealReportService.d.ts +31 -55
- package/dist/services/HealReportService.d.ts.map +1 -1
- package/dist/services/HealReportService.js +864 -1240
- package/dist/services/HealingAdvisory.d.ts +9 -1
- package/dist/services/HealingAdvisory.d.ts.map +1 -1
- package/dist/services/HealingAdvisory.js +9 -1
- package/dist/services/HealingCacheService.d.ts +8 -8
- package/dist/services/HealingCacheService.d.ts.map +1 -1
- package/dist/services/HealingCacheService.js +24 -24
- package/dist/services/HealthReportService.d.ts +1 -1
- package/dist/services/HealthReportService.d.ts.map +1 -1
- package/dist/services/HealthReportService.js +25 -25
- package/dist/services/InitializerUpdater.d.ts.map +1 -1
- package/dist/services/InitializerUpdater.js +12 -2
- package/dist/services/IntentAuditor.d.ts.map +1 -1
- package/dist/services/IntentAuditor.js +32 -21
- package/dist/services/InteractiveReview.d.ts +8 -17
- package/dist/services/InteractiveReview.d.ts.map +1 -1
- package/dist/services/InteractiveReview.js +34 -124
- package/dist/services/LLMService.d.ts +1 -1
- package/dist/services/LLMService.js +16 -16
- package/dist/services/MutationApplier.d.ts +55 -0
- package/dist/services/MutationApplier.d.ts.map +1 -0
- package/dist/services/MutationApplier.js +322 -0
- package/dist/services/PRAutomationService.d.ts +2 -2
- package/dist/services/PRAutomationService.d.ts.map +1 -1
- package/dist/services/PRAutomationService.js +43 -39
- package/dist/services/PendingPromptLedger.d.ts +9 -2
- package/dist/services/PendingPromptLedger.d.ts.map +1 -1
- package/dist/services/PendingPromptLedger.js +31 -6
- package/dist/services/ReportGenerator.d.ts +116 -30
- package/dist/services/ReportGenerator.d.ts.map +1 -1
- package/dist/services/ReportGenerator.js +149 -62
- package/dist/services/ReportMergeService.d.ts +95 -0
- package/dist/services/ReportMergeService.d.ts.map +1 -0
- package/dist/services/ReportMergeService.js +0 -0
- package/dist/services/SafetyGuard.d.ts +4 -7
- package/dist/services/SafetyGuard.d.ts.map +1 -1
- package/dist/services/SafetyGuard.js +29 -106
- package/dist/services/SelectorSanitizer.d.ts +52 -0
- package/dist/services/SelectorSanitizer.d.ts.map +1 -0
- package/dist/services/SelectorSanitizer.js +318 -0
- package/dist/services/SnapshotService.js +6 -6
- package/dist/services/SourceLinkService.d.ts +2 -2
- package/dist/services/SourceLinkService.d.ts.map +1 -1
- package/dist/services/SourceLinkService.js +31 -22
- package/dist/services/SourceUpdater.d.ts +1 -1
- package/dist/services/SourceUpdater.d.ts.map +1 -1
- package/dist/services/SourceUpdater.js +44 -26
- package/dist/services/TemplateDiffService.d.ts.map +1 -1
- package/dist/services/TemplateDiffService.js +18 -15
- package/dist/services/TraceBackEngine.d.ts +67 -0
- package/dist/services/TraceBackEngine.d.ts.map +1 -0
- package/dist/services/TraceBackEngine.js +672 -0
- package/dist/services/WorkspaceSnapshotService.d.ts +6 -6
- package/dist/services/WorkspaceSnapshotService.d.ts.map +1 -1
- package/dist/services/WorkspaceSnapshotService.js +10 -12
- package/dist/types/index.d.ts +1 -1
- package/dist/utils/DOMUtils.d.ts +18 -2
- package/dist/utils/DOMUtils.d.ts.map +1 -1
- package/dist/utils/DOMUtils.js +335 -49
- package/dist/utils/IsolatedDiff.d.ts +2 -2
- package/dist/utils/IsolatedDiff.d.ts.map +1 -1
- package/dist/utils/IsolatedDiff.js +13 -10
- package/package.json +2 -2
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// src/services/MutationApplier.ts
|
|
3
|
+
//
|
|
4
|
+
// Ticket #2 — Stage 4: Write-Discipline & the Drain Loop.
|
|
5
|
+
//
|
|
6
|
+
// Applies MutationPlans to disk under three hard contracts:
|
|
7
|
+
//
|
|
8
|
+
// 1. ZERO LAYOUT NOISE (§4.1) — surgical string splice over the EXACT node
|
|
9
|
+
// span [getStart(), getEnd()). getStart() excludes leading trivia, so
|
|
10
|
+
// comments / indentation / Prettier layout outside the span are byte-
|
|
11
|
+
// identical. We never call sourceFile.formatText() / ts.Printer.
|
|
12
|
+
//
|
|
13
|
+
// 2. TYPE-SAFETY GATE + ROLLBACK (§4.3) — after splicing in memory we
|
|
14
|
+
// re-parse and baseline-diff the diagnostics. A mutation that introduces
|
|
15
|
+
// a NEW real error (syntax break, non-Locator return) is rolled back and
|
|
16
|
+
// the file is left byte-for-byte untouched. A cheap Locator-shape pre-
|
|
17
|
+
// check rejects obviously wrong return types before the splice.
|
|
18
|
+
//
|
|
19
|
+
// 3. BATCH APPLICATION (§3.3) — mutations are grouped by file, anchors are
|
|
20
|
+
// RE-RESOLVED against the fresh parse (never trusting flag-time offsets),
|
|
21
|
+
// edits applied highest-offset-first, then the file is written atomically
|
|
22
|
+
// (temp + rename).
|
|
23
|
+
//
|
|
24
|
+
// Stage 1 (resolveAnchor) provides line-shift-immune relocation; Stage-1
|
|
25
|
+
// applyEdits provides the reverse-offset splice + overlap guard.
|
|
26
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
27
|
+
if (k2 === undefined) k2 = k;
|
|
28
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
29
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
30
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
31
|
+
}
|
|
32
|
+
Object.defineProperty(o, k2, desc);
|
|
33
|
+
}) : (function(o, m, k, k2) {
|
|
34
|
+
if (k2 === undefined) k2 = k;
|
|
35
|
+
o[k2] = m[k];
|
|
36
|
+
}));
|
|
37
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
38
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
39
|
+
}) : function(o, v) {
|
|
40
|
+
o["default"] = v;
|
|
41
|
+
});
|
|
42
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
43
|
+
var ownKeys = function(o) {
|
|
44
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
45
|
+
var ar = [];
|
|
46
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
47
|
+
return ar;
|
|
48
|
+
};
|
|
49
|
+
return ownKeys(o);
|
|
50
|
+
};
|
|
51
|
+
return function (mod) {
|
|
52
|
+
if (mod && mod.__esModule) return mod;
|
|
53
|
+
var result = {};
|
|
54
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
55
|
+
__setModuleDefault(result, mod);
|
|
56
|
+
return result;
|
|
57
|
+
};
|
|
58
|
+
})();
|
|
59
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
60
|
+
exports.MutationApplier = void 0;
|
|
61
|
+
exports.isLocatorReturningExpr = isLocatorReturningExpr;
|
|
62
|
+
const fs = __importStar(require("fs"));
|
|
63
|
+
const ts_morph_1 = require("ts-morph");
|
|
64
|
+
const AnchorResolver_1 = require("./AnchorResolver");
|
|
65
|
+
const DecisionEngine_1 = require("./DecisionEngine");
|
|
66
|
+
const TraceBackEngine_1 = require("./TraceBackEngine");
|
|
67
|
+
const SelectorSanitizer_1 = require("./SelectorSanitizer");
|
|
68
|
+
// Diagnostic codes that are pure "missing types" noise in an in-memory parse
|
|
69
|
+
// without node_modules — identical allowlist to ASTSourceUpdater's validator.
|
|
70
|
+
// Syntax/binder errors (codes 1000–1999) are NEVER allowlisted.
|
|
71
|
+
const NOISE_CODES = new Set([
|
|
72
|
+
2304, 2339, 2345, 2554, 2571, 2580, 2503, 7006, 7016, 7031, 18004,
|
|
73
|
+
]);
|
|
74
|
+
const LOCATOR_RETURNING_CATEGORIES = new Set(["selector", "frame", "modifier"]);
|
|
75
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
76
|
+
// MutationApplier
|
|
77
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
78
|
+
class MutationApplier {
|
|
79
|
+
/** Apply many batches; each file is processed independently. */
|
|
80
|
+
static applyBatches(batches, opts = {}) {
|
|
81
|
+
return batches.map((b) => MutationApplier.applyFileBatch(b, opts));
|
|
82
|
+
}
|
|
83
|
+
static applyFileBatch(batch, opts = {}) {
|
|
84
|
+
const write = opts.write ?? true;
|
|
85
|
+
const respectDryRun = opts.respectDryRun ?? true;
|
|
86
|
+
const dryRun = respectDryRun && process.env.SELA_DRY_RUN === "1";
|
|
87
|
+
const { filePath } = batch;
|
|
88
|
+
const skipped = [];
|
|
89
|
+
let original;
|
|
90
|
+
try {
|
|
91
|
+
original = fs.readFileSync(filePath, "utf8");
|
|
92
|
+
}
|
|
93
|
+
catch (e) {
|
|
94
|
+
return {
|
|
95
|
+
filePath,
|
|
96
|
+
status: "skipped",
|
|
97
|
+
reason: `cannot read file: ${e.message}`,
|
|
98
|
+
written: false,
|
|
99
|
+
finalContent: "",
|
|
100
|
+
skipped,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
// Fresh parse — the single source of truth for span resolution.
|
|
104
|
+
const project = newGateProject();
|
|
105
|
+
const sf = project.createSourceFile(filePath, original, { overwrite: true });
|
|
106
|
+
const baseline = realDiagnosticCounts(sf);
|
|
107
|
+
// ── Resolve each edit to a concrete span ─────────────────────────
|
|
108
|
+
const concrete = [];
|
|
109
|
+
for (const e of batch.edits) {
|
|
110
|
+
let replacement = e.replacement;
|
|
111
|
+
// ABSOLUTE GUARD: translate any internal engine selector to its public
|
|
112
|
+
// method form before writing; refuse if it cannot be translated. No
|
|
113
|
+
// `internal:role=…` string ever reaches the developer's source.
|
|
114
|
+
if (e.kind === "replace") {
|
|
115
|
+
const san = (0, SelectorSanitizer_1.sanitizeLocatorExpression)(replacement);
|
|
116
|
+
if (!san.safe) {
|
|
117
|
+
skipped.push({
|
|
118
|
+
reason: `internal-engine-selector: cannot translate "${truncate(replacement)}" to a public Playwright method`,
|
|
119
|
+
});
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
replacement = san.text;
|
|
123
|
+
}
|
|
124
|
+
const requireLocator = e.requireLocator ?? e.kind === "replace";
|
|
125
|
+
if (requireLocator && !isLocatorReturningExpr(replacement)) {
|
|
126
|
+
skipped.push({
|
|
127
|
+
reason: `type-incompat: replacement is not a Locator expression ("${truncate(replacement)}")`,
|
|
128
|
+
});
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (e.kind === "replace" && e.anchor) {
|
|
132
|
+
const r = (0, AnchorResolver_1.resolveAnchor)(sf, e.anchor);
|
|
133
|
+
if (r.status !== "resolved") {
|
|
134
|
+
skipped.push({
|
|
135
|
+
reason: `anchor ${r.status}: ${"reason" in r ? r.reason : ""}`,
|
|
136
|
+
});
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
concrete.push({
|
|
140
|
+
kind: "replace",
|
|
141
|
+
span: { start: r.node.getStart(), end: r.node.getEnd() },
|
|
142
|
+
replacement,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
else if (e.span) {
|
|
146
|
+
concrete.push({
|
|
147
|
+
kind: e.kind,
|
|
148
|
+
span: e.span,
|
|
149
|
+
replacement,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
skipped.push({ reason: "edit has neither anchor nor span" });
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (concrete.length === 0) {
|
|
157
|
+
return {
|
|
158
|
+
filePath,
|
|
159
|
+
status: "no-op",
|
|
160
|
+
reason: "no applicable edits after resolution",
|
|
161
|
+
written: false,
|
|
162
|
+
finalContent: original,
|
|
163
|
+
skipped,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
// ── Splice (reverse-offset; overlap-guarded) ─────────────────────
|
|
167
|
+
let newContent;
|
|
168
|
+
try {
|
|
169
|
+
newContent = (0, DecisionEngine_1.applyEdits)(original, concrete);
|
|
170
|
+
}
|
|
171
|
+
catch (e) {
|
|
172
|
+
return {
|
|
173
|
+
filePath,
|
|
174
|
+
status: "rolled-back",
|
|
175
|
+
reason: `splice failed: ${e.message}`,
|
|
176
|
+
written: false,
|
|
177
|
+
finalContent: original,
|
|
178
|
+
skipped,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
if (newContent === original) {
|
|
182
|
+
return {
|
|
183
|
+
filePath,
|
|
184
|
+
status: "no-op",
|
|
185
|
+
reason: "edits produced identical content",
|
|
186
|
+
written: false,
|
|
187
|
+
finalContent: original,
|
|
188
|
+
skipped,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
// ── Type-safety gate: baseline-diff diagnostics ──────────────────
|
|
192
|
+
const afterSf = project.createSourceFile(filePath, newContent, {
|
|
193
|
+
overwrite: true,
|
|
194
|
+
});
|
|
195
|
+
const after = realDiagnosticCounts(afterSf);
|
|
196
|
+
const regression = diagnosticRegression(baseline, after);
|
|
197
|
+
if (regression) {
|
|
198
|
+
return {
|
|
199
|
+
filePath,
|
|
200
|
+
status: "rolled-back",
|
|
201
|
+
reason: `type-safety gate: mutation introduced a new error (${regression}) — file left untouched`,
|
|
202
|
+
written: false,
|
|
203
|
+
finalContent: original,
|
|
204
|
+
skipped,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
// ── Atomic write ─────────────────────────────────────────────────
|
|
208
|
+
if (write && !dryRun) {
|
|
209
|
+
try {
|
|
210
|
+
atomicWrite(filePath, newContent);
|
|
211
|
+
}
|
|
212
|
+
catch (e) {
|
|
213
|
+
return {
|
|
214
|
+
filePath,
|
|
215
|
+
status: "rolled-back",
|
|
216
|
+
reason: `atomic write failed: ${e.message}`,
|
|
217
|
+
written: false,
|
|
218
|
+
finalContent: original,
|
|
219
|
+
skipped,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return {
|
|
224
|
+
filePath,
|
|
225
|
+
status: "applied",
|
|
226
|
+
reason: `${concrete.length} edit(s) applied${dryRun ? " (dry-run, not written)" : ""}`,
|
|
227
|
+
written: write && !dryRun,
|
|
228
|
+
finalContent: newContent,
|
|
229
|
+
skipped,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
exports.MutationApplier = MutationApplier;
|
|
234
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
235
|
+
// TYPE-SAFETY HELPERS
|
|
236
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
237
|
+
function newGateProject() {
|
|
238
|
+
return new ts_morph_1.Project({
|
|
239
|
+
useInMemoryFileSystem: true,
|
|
240
|
+
compilerOptions: {
|
|
241
|
+
allowJs: true,
|
|
242
|
+
noEmit: true,
|
|
243
|
+
skipLibCheck: true,
|
|
244
|
+
noResolve: true,
|
|
245
|
+
},
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
/** Map of real (non-noise) diagnostic code → count. */
|
|
249
|
+
function realDiagnosticCounts(sf) {
|
|
250
|
+
const counts = new Map();
|
|
251
|
+
for (const d of sf.getPreEmitDiagnostics()) {
|
|
252
|
+
if (d.getCategory() !== 1 /* Error */)
|
|
253
|
+
continue;
|
|
254
|
+
const code = d.getCode();
|
|
255
|
+
if (NOISE_CODES.has(code))
|
|
256
|
+
continue;
|
|
257
|
+
counts.set(code, (counts.get(code) ?? 0) + 1);
|
|
258
|
+
}
|
|
259
|
+
return counts;
|
|
260
|
+
}
|
|
261
|
+
/** Returns a description of the first NEW/elevated error, or null. */
|
|
262
|
+
function diagnosticRegression(baseline, after) {
|
|
263
|
+
for (const [code, n] of after) {
|
|
264
|
+
if (n > (baseline.get(code) ?? 0)) {
|
|
265
|
+
return `TS${code}`;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Cheap return-type pre-check. The replacement must be an expression whose
|
|
272
|
+
* OUTERMOST call is a Locator-returning Playwright method (locator / getBy* /
|
|
273
|
+
* frameLocator / nth / first / last / filter / and / or). Rejects actions
|
|
274
|
+
* (.click()), string queries (.title(), .textContent()), bare literals, etc.
|
|
275
|
+
*/
|
|
276
|
+
function isLocatorReturningExpr(text) {
|
|
277
|
+
const project = newGateProject();
|
|
278
|
+
let sf;
|
|
279
|
+
try {
|
|
280
|
+
sf = project.createSourceFile("_probe.ts", `const __x = ${text};`, {
|
|
281
|
+
overwrite: true,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
catch {
|
|
285
|
+
return false;
|
|
286
|
+
}
|
|
287
|
+
const decl = sf.getVariableDeclaration("__x");
|
|
288
|
+
const init = decl?.getInitializer();
|
|
289
|
+
if (!init)
|
|
290
|
+
return false;
|
|
291
|
+
// Outermost expression must be a call whose method is Locator-returning.
|
|
292
|
+
if (!ts_morph_1.Node.isCallExpression(init))
|
|
293
|
+
return false;
|
|
294
|
+
const callee = init.getExpression();
|
|
295
|
+
if (!ts_morph_1.Node.isPropertyAccessExpression(callee))
|
|
296
|
+
return false;
|
|
297
|
+
return LOCATOR_RETURNING_CATEGORIES.has((0, TraceBackEngine_1.categorizeMethod)(callee.getName()));
|
|
298
|
+
}
|
|
299
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
300
|
+
// ATOMIC WRITE
|
|
301
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
302
|
+
function atomicWrite(filePath, content) {
|
|
303
|
+
const tmp = `${filePath}.sela-tmp-${process.pid}`;
|
|
304
|
+
fs.writeFileSync(tmp, content, "utf8");
|
|
305
|
+
try {
|
|
306
|
+
fs.renameSync(tmp, filePath); // atomic on POSIX; replace-on-Windows
|
|
307
|
+
}
|
|
308
|
+
catch {
|
|
309
|
+
// Windows can reject rename-over-existing in rare cases — fall back to a
|
|
310
|
+
// direct write, then best-effort remove the temp file.
|
|
311
|
+
fs.writeFileSync(filePath, content, "utf8");
|
|
312
|
+
try {
|
|
313
|
+
fs.unlinkSync(tmp);
|
|
314
|
+
}
|
|
315
|
+
catch {
|
|
316
|
+
/* best effort */
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
function truncate(s, n = 48) {
|
|
321
|
+
return s.length > n ? `${s.slice(0, n)}…` : s;
|
|
322
|
+
}
|
|
@@ -37,14 +37,14 @@ export interface ExecutionResult {
|
|
|
37
37
|
* Clean-Room execution lifecycle.
|
|
38
38
|
*
|
|
39
39
|
* The legacy flow committed the developer's whole dirty working tree onto
|
|
40
|
-
* the PR branch
|
|
40
|
+
* the PR branch - polluting reviewers with `test.only`, commented debug
|
|
41
41
|
* blocks, etc. This refactor isolates Sela's specific AST changes onto an
|
|
42
42
|
* ephemeral branch, leaves the developer's local workspace byte-identical
|
|
43
43
|
* to its pre-Sela state, and is wrapped in a try/finally that restores
|
|
44
44
|
* the workspace even on mid-flight crashes.
|
|
45
45
|
*/
|
|
46
46
|
export declare function execute(cfg: ResolvedPRAutomation, decision: StrategyDecision, heals: HealedEvent[], branchInfo: BranchInfo, ctx: ExecuteContext,
|
|
47
|
-
/** Injectable for tests
|
|
47
|
+
/** Injectable for tests - defaults to the module singleton. */
|
|
48
48
|
workspace?: WorkspaceSnapshotService): Promise<ExecutionResult>;
|
|
49
49
|
export declare function handleBugDetected(action: BugAction, events: ProtectedEvent[], branchInfo: BranchInfo, ctx: ExecuteContext): Promise<void>;
|
|
50
50
|
//# sourceMappingURL=PRAutomationService.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PRAutomationService.d.ts","sourceRoot":"","sources":["../../src/services/PRAutomationService.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EACV,oBAAoB,EACpB,UAAU,EACV,SAAS,EACV,MAAM,sBAAsB,CAAC;AAC9B,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"PRAutomationService.d.ts","sourceRoot":"","sources":["../../src/services/PRAutomationService.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EACV,oBAAoB,EACpB,UAAU,EACV,SAAS,EACV,MAAM,sBAAsB,CAAC;AAC9B,OAAO,KAAK,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACvE,OAAO,EAGL,KAAK,wBAAwB,EAC9B,MAAM,4BAA4B,CAAC;AAOpC,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,IAAI,EAAE,OAAO,CAAC;IACd,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,MAAM,EAAE,KAAK,GAAG,KAAK,GAAG,MAAM,CAAC;CAChC;AAED,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,UAAU,CAAC;IACtB,UAAU,EAAE,UAAU,CAAC;IACvB,eAAe,EAAE,kBAAkB,GAAG,gBAAgB,GAAG,aAAa,GAAG,IAAI,CAAC;IAC9E;;;;OAIG;IACH,eAAe,EAAE,MAAM,GAAG,SAAS,CAAC;IACpC,6EAA6E;IAC7E,oBAAoB,EAAE,MAAM,GAAG,SAAS,CAAC;CAC1C;AAED,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,MAAM,CAAC;IACZ,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/B;AAiFD,wBAAgB,YAAY,CAAC,GAAG,GAAE,MAAsB,GAAG,UAAU,CA+EpE;AAMD,wBAAgB,uBAAuB,CACrC,GAAG,EAAE,oBAAoB,EACzB,MAAM,EAAE,MAAM,GAAG,IAAI,EACrB,KAAK,EAAE,WAAW,EAAE,GACnB,gBAAgB,CAgElB;AAMD,wBAAgB,MAAM,CAAC,CAAC,EAAE,OAAO,GAAG,MAAM,CAEzC;AA6FD,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,UAAU,CAAC;IACtB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED;;;;;;;;;GASG;AACH,wBAAsB,OAAO,CAC3B,GAAG,EAAE,oBAAoB,EACzB,QAAQ,EAAE,gBAAgB,EAC1B,KAAK,EAAE,WAAW,EAAE,EACpB,UAAU,EAAE,UAAU,EACtB,GAAG,EAAE,cAAc;AACnB,+DAA+D;AAC/D,SAAS,GAAE,wBAAkD,GAC5D,OAAO,CAAC,eAAe,CAAC,CAkG1B;AAwTD,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,SAAS,EACjB,MAAM,EAAE,cAAc,EAAE,EACxB,UAAU,EAAE,UAAU,EACtB,GAAG,EAAE,cAAc,GAClB,OAAO,CAAC,IAAI,CAAC,CA4Gf"}
|
|
@@ -93,7 +93,7 @@ function isSelaHealBranch(branch) {
|
|
|
93
93
|
* Resolve the repo's default integration branch.
|
|
94
94
|
* 1. origin/HEAD symbolic-ref (set by `git clone`).
|
|
95
95
|
* 2. First existing local ref among: main, master, develop, dev.
|
|
96
|
-
* Returns null if none found
|
|
96
|
+
* Returns null if none found - caller must handle that.
|
|
97
97
|
*/
|
|
98
98
|
function findDefaultBranch(cwd) {
|
|
99
99
|
const headRef = sh("git symbolic-ref refs/remotes/origin/HEAD", cwd);
|
|
@@ -124,7 +124,7 @@ function detectBranch(cwd = process.cwd()) {
|
|
|
124
124
|
const ghRefName = process.env.GITHUB_REF_NAME; // current branch on push, or "<n>/merge" on PR
|
|
125
125
|
const ghEventName = process.env.GITHUB_EVENT_NAME;
|
|
126
126
|
const ghRef = process.env.GITHUB_REF; // "refs/pull/123/merge"
|
|
127
|
-
// Prefer HEAD_REF on PRs
|
|
127
|
+
// Prefer HEAD_REF on PRs - REF_NAME is the synthetic merge ref.
|
|
128
128
|
if (ghHeadRef && ghHeadRef.length > 0 && !isSelaHealBranch(ghHeadRef)) {
|
|
129
129
|
const prNumber = (() => {
|
|
130
130
|
const m = ghRef?.match(/refs\/pull\/(\d+)\//);
|
|
@@ -175,11 +175,11 @@ function detectBranch(cwd = process.cwd()) {
|
|
|
175
175
|
if (isSelaHealBranch(localBranch)) {
|
|
176
176
|
const fallback = findDefaultBranch(cwd);
|
|
177
177
|
if (fallback) {
|
|
178
|
-
console.warn(`[Sela PR] ⚠️ HEAD is on '${localBranch}' (sela patch branch)
|
|
178
|
+
console.warn(`[Sela PR] ⚠️ HEAD is on '${localBranch}' (sela patch branch) - ` +
|
|
179
179
|
`using '${fallback}' as PR base instead.`);
|
|
180
180
|
return { branch: fallback, isPR: false, prNumber: null, source: "git" };
|
|
181
181
|
}
|
|
182
|
-
console.warn(`[Sela PR] ⚠️ HEAD is on '${localBranch}' and no default branch found
|
|
182
|
+
console.warn(`[Sela PR] ⚠️ HEAD is on '${localBranch}' and no default branch found - ` +
|
|
183
183
|
`skipping PR automation to avoid self-targeting.`);
|
|
184
184
|
return { branch: null, isPR: false, prNumber: null, source: "none" };
|
|
185
185
|
}
|
|
@@ -190,7 +190,7 @@ function detectBranch(cwd = process.cwd()) {
|
|
|
190
190
|
// ═══════════════════════════════════════════════════════════════════
|
|
191
191
|
function decideEffectiveStrategy(cfg, branch, heals) {
|
|
192
192
|
// Compute MIN across only the heals that reported a finite numeric
|
|
193
|
-
// confidence. When *no* heal carried one, the result is `undefined`
|
|
193
|
+
// confidence. When *no* heal carried one, the result is `undefined` -
|
|
194
194
|
// surfaced as "n/a" in reports rather than synthesised as 100% (which
|
|
195
195
|
// would silently mask the absence of evidence) or 0% (which would
|
|
196
196
|
// incorrectly force a downgrade for every clean run on Sonnet).
|
|
@@ -219,7 +219,7 @@ function decideEffectiveStrategy(cfg, branch, heals) {
|
|
|
219
219
|
downgradeReason: "PROTECTED_BRANCH",
|
|
220
220
|
};
|
|
221
221
|
}
|
|
222
|
-
// Rule 2: low AI confidence
|
|
222
|
+
// Rule 2: low AI confidence - only triggers when the measured min is
|
|
223
223
|
// actually below the threshold. Missing confidence is "unknown", not
|
|
224
224
|
// "zero", so we do NOT downgrade purely on absence.
|
|
225
225
|
if (cfg.reviewThresholds.minConfidenceForDirectCommit > 0 &&
|
|
@@ -231,7 +231,7 @@ function decideEffectiveStrategy(cfg, branch, heals) {
|
|
|
231
231
|
downgradeReason: "LOW_CONFIDENCE",
|
|
232
232
|
};
|
|
233
233
|
}
|
|
234
|
-
// Rule 3: low auditor score
|
|
234
|
+
// Rule 3: low auditor score - same "absence ≠ zero" rule as Rule 2.
|
|
235
235
|
if (cfg.reviewThresholds.minAuditorScoreForDirectCommit > 0 &&
|
|
236
236
|
typeof minAud === "number" &&
|
|
237
237
|
minAud < cfg.reviewThresholds.minAuditorScoreForDirectCommit) {
|
|
@@ -285,7 +285,7 @@ function stripDiffPreamble(raw) {
|
|
|
285
285
|
}
|
|
286
286
|
function buildDefaultBody(heals, branchInfo, reportLink, decision) {
|
|
287
287
|
const lines = [];
|
|
288
|
-
lines.push(`# 🤖 Sela Insights
|
|
288
|
+
lines.push(`# 🤖 Sela Insights - Automation Suite Healed!`);
|
|
289
289
|
lines.push("");
|
|
290
290
|
lines.push(`Sela kept your pipeline running by healing ${heals.length} broken locator(s).`);
|
|
291
291
|
lines.push("");
|
|
@@ -301,12 +301,12 @@ function buildDefaultBody(heals, branchInfo, reportLink, decision) {
|
|
|
301
301
|
lines.push(`## 💻 Per-Heal Diff`);
|
|
302
302
|
for (const h of heals) {
|
|
303
303
|
const lineRef = h.newLineNumber ?? h.sourceLine;
|
|
304
|
-
lines.push(`### \`${h.sourceFile}:${lineRef}\`
|
|
304
|
+
lines.push(`### \`${h.sourceFile}:${lineRef}\` - ${h.testTitle ?? "(no test title)"}`);
|
|
305
305
|
lines.push("");
|
|
306
306
|
lines.push("```diff");
|
|
307
307
|
if (h.gitUnifiedDiff && h.gitUnifiedDiff.trim().length > 0) {
|
|
308
308
|
// Emit the real git diff verbatim. This natively handles cross-file
|
|
309
|
-
// healing, multi-line edits, and template-literal changes
|
|
309
|
+
// healing, multi-line edits, and template-literal changes - none of
|
|
310
310
|
// which the legacy single-line -/+ pair could represent.
|
|
311
311
|
lines.push(stripDiffPreamble(h.gitUnifiedDiff));
|
|
312
312
|
}
|
|
@@ -333,14 +333,14 @@ function buildDefaultBody(heals, branchInfo, reportLink, decision) {
|
|
|
333
333
|
* Clean-Room execution lifecycle.
|
|
334
334
|
*
|
|
335
335
|
* The legacy flow committed the developer's whole dirty working tree onto
|
|
336
|
-
* the PR branch
|
|
336
|
+
* the PR branch - polluting reviewers with `test.only`, commented debug
|
|
337
337
|
* blocks, etc. This refactor isolates Sela's specific AST changes onto an
|
|
338
338
|
* ephemeral branch, leaves the developer's local workspace byte-identical
|
|
339
339
|
* to its pre-Sela state, and is wrapped in a try/finally that restores
|
|
340
340
|
* the workspace even on mid-flight crashes.
|
|
341
341
|
*/
|
|
342
342
|
async function execute(cfg, decision, heals, branchInfo, ctx,
|
|
343
|
-
/** Injectable for tests
|
|
343
|
+
/** Injectable for tests - defaults to the module singleton. */
|
|
344
344
|
workspace = WorkspaceSnapshotService_1.sharedWorkspaceSnapshot) {
|
|
345
345
|
const result = {
|
|
346
346
|
effective: decision.effective,
|
|
@@ -349,33 +349,33 @@ workspace = WorkspaceSnapshotService_1.sharedWorkspaceSnapshot) {
|
|
|
349
349
|
errors: [],
|
|
350
350
|
};
|
|
351
351
|
if (decision.effective === "readOnly") {
|
|
352
|
-
console.log(`[Sela PR] 🛡️ readOnly
|
|
352
|
+
console.log(`[Sela PR] 🛡️ readOnly - no git operations performed.`);
|
|
353
353
|
if (ctx.reportHtmlPath) {
|
|
354
354
|
console.log(`[Sela PR] Report artifact: ${ctx.reportHtmlPath}`);
|
|
355
355
|
}
|
|
356
356
|
return result;
|
|
357
357
|
}
|
|
358
358
|
if (heals.length === 0) {
|
|
359
|
-
console.log(`[Sela PR] No HEALED events
|
|
359
|
+
console.log(`[Sela PR] No HEALED events - skipping git operations.`);
|
|
360
360
|
return result;
|
|
361
361
|
}
|
|
362
362
|
if (!gitAvailable(ctx.cwd)) {
|
|
363
363
|
result.errors.push("git CLI not available");
|
|
364
|
-
console.warn(`[Sela PR] ⚠️ git CLI not available
|
|
364
|
+
console.warn(`[Sela PR] ⚠️ git CLI not available - skipping.`);
|
|
365
365
|
return result;
|
|
366
366
|
}
|
|
367
367
|
if (isDetachedHead(ctx.cwd)) {
|
|
368
368
|
result.errors.push("detached HEAD");
|
|
369
|
-
console.warn(`[Sela PR] ⚠️ Detached HEAD
|
|
369
|
+
console.warn(`[Sela PR] ⚠️ Detached HEAD - skipping git operations.`);
|
|
370
370
|
return result;
|
|
371
371
|
}
|
|
372
372
|
const mutatedSnaps = workspace.getMutatedSnapshots();
|
|
373
373
|
if (mutatedSnaps.length === 0) {
|
|
374
374
|
result.errors.push("no mutated files tracked");
|
|
375
|
-
console.warn(`[Sela PR] ⚠️ WorkspaceSnapshotService reports no mutated files
|
|
375
|
+
console.warn(`[Sela PR] ⚠️ WorkspaceSnapshotService reports no mutated files - skipping.`);
|
|
376
376
|
return result;
|
|
377
377
|
}
|
|
378
|
-
// Render title / body up front
|
|
378
|
+
// Render title / body up front - needed by all branches below.
|
|
379
379
|
const first = heals[0];
|
|
380
380
|
const titleVars = {
|
|
381
381
|
testName: first.testTitle ?? "automation",
|
|
@@ -410,7 +410,7 @@ workspace = WorkspaceSnapshotService_1.sharedWorkspaceSnapshot) {
|
|
|
410
410
|
if (decision.effective === "pr" || decision.effective === "draftPR") {
|
|
411
411
|
if (!ghAvailable(ctx.cwd)) {
|
|
412
412
|
result.errors.push("gh CLI not available");
|
|
413
|
-
console.warn(`[Sela PR] ⚠️ gh CLI not installed
|
|
413
|
+
console.warn(`[Sela PR] ⚠️ gh CLI not installed - cannot open PR. Run \`gh auth login\` after install.`);
|
|
414
414
|
return result;
|
|
415
415
|
}
|
|
416
416
|
return runCleanRoom({
|
|
@@ -428,25 +428,25 @@ workspace = WorkspaceSnapshotService_1.sharedWorkspaceSnapshot) {
|
|
|
428
428
|
return result;
|
|
429
429
|
}
|
|
430
430
|
async function runCleanRoom(args) {
|
|
431
|
-
const { mode, cfg, ctx, mutatedSnaps, title, body, branchInfo, decision, result } = args;
|
|
431
|
+
const { mode, cfg, ctx, mutatedSnaps, title, body, branchInfo, decision, result, } = args;
|
|
432
432
|
const id = String(Date.now());
|
|
433
433
|
const stashLabel = `sela:clean-room-${id}`;
|
|
434
434
|
const originalBranch = currentBranch(ctx.cwd);
|
|
435
435
|
const targetBranch = mode === "directCommit"
|
|
436
436
|
? originalBranch
|
|
437
437
|
: render(cfg.branchNameTemplate, { id, timestamp: id });
|
|
438
|
-
// Lifecycle state
|
|
438
|
+
// Lifecycle state - used by the finally{} restoration block.
|
|
439
439
|
let didRestoreOriginal = false;
|
|
440
440
|
let didStash = false;
|
|
441
441
|
let didSwitchToTarget = false;
|
|
442
442
|
let didCommit = false;
|
|
443
443
|
try {
|
|
444
|
-
// 1. State Checkpoint
|
|
444
|
+
// 1. State Checkpoint - rewind developer's mutated files to pre-Sela
|
|
445
445
|
// state on disk. Other dirty files (unrelated to Sela) stay where
|
|
446
446
|
// they are; the subsequent stash will sweep them up.
|
|
447
447
|
workspaceRestoreOriginal(mutatedSnaps);
|
|
448
448
|
didRestoreOriginal = true;
|
|
449
|
-
// 2. Branch Isolation
|
|
449
|
+
// 2. Branch Isolation - stash everything (tracked + untracked) so the
|
|
450
450
|
// worktree mirrors HEAD when we switch / branch off.
|
|
451
451
|
const stash = sh(`git stash push -u -m ${shellQuote(stashLabel)}`, ctx.cwd);
|
|
452
452
|
if (stash.ok) {
|
|
@@ -463,7 +463,7 @@ async function runCleanRoom(args) {
|
|
|
463
463
|
}
|
|
464
464
|
didSwitchToTarget = true;
|
|
465
465
|
}
|
|
466
|
-
// 3. Targeted Hunk Patching
|
|
466
|
+
// 3. Targeted Hunk Patching - apply ONLY Sela's blocks to each file's
|
|
467
467
|
// clean HEAD content. Refuses to write if dirty edits collide with
|
|
468
468
|
// Sela's mutation site (block not found / not unique).
|
|
469
469
|
for (const snap of mutatedSnaps) {
|
|
@@ -517,7 +517,7 @@ async function runCleanRoom(args) {
|
|
|
517
517
|
console.warn(`[Sela PR] ⚠️ Clean-room flow failed: ${err?.message ?? err}`);
|
|
518
518
|
}
|
|
519
519
|
finally {
|
|
520
|
-
// 6. Workspace Restoration
|
|
520
|
+
// 6. Workspace Restoration - ALWAYS attempt, even on mid-flight crash.
|
|
521
521
|
// Order matters:
|
|
522
522
|
// a) Switch back to the developer's original branch (if we left it).
|
|
523
523
|
// b) Pop the stash to restore the dirty pre-Sela working tree.
|
|
@@ -537,7 +537,7 @@ async function runCleanRoom(args) {
|
|
|
537
537
|
}
|
|
538
538
|
}
|
|
539
539
|
if (didRestoreOriginal) {
|
|
540
|
-
// Belt-and-braces
|
|
540
|
+
// Belt-and-braces - every tracked snapshot back to its dirty
|
|
541
541
|
// pre-Sela state. No-op if stash pop already did the job.
|
|
542
542
|
workspaceRestoreOriginal(mutatedSnaps);
|
|
543
543
|
}
|
|
@@ -565,7 +565,9 @@ function openPullRequest(args) {
|
|
|
565
565
|
? cfg.labels.map((l) => `--label ${shellQuote(l)}`).join(" ")
|
|
566
566
|
: "";
|
|
567
567
|
const draftFlag = mode === "draftPR" ? "--draft" : "";
|
|
568
|
-
const baseFlag = branchInfo.branch
|
|
568
|
+
const baseFlag = branchInfo.branch
|
|
569
|
+
? `--base ${shellQuote(branchInfo.branch)}`
|
|
570
|
+
: "";
|
|
569
571
|
const prCmd = `gh pr create ${draftFlag} ${baseFlag} ` +
|
|
570
572
|
`--title ${shellQuote(title)} ` +
|
|
571
573
|
`--body-file ${shellQuote(bodyFile)} ` +
|
|
@@ -614,16 +616,16 @@ function workspaceRestoreOriginal(snaps) {
|
|
|
614
616
|
function applyMutationToClean(snap) {
|
|
615
617
|
const after = snap.contentAfter ?? "";
|
|
616
618
|
// File didn't exist on HEAD (created by Sela or untracked). Write the
|
|
617
|
-
// post-mutation content directly
|
|
619
|
+
// post-mutation content directly - there's no clean base to merge into.
|
|
618
620
|
if (snap.headContent === null) {
|
|
619
621
|
return { ok: true, result: after };
|
|
620
622
|
}
|
|
621
|
-
// No dirty edits
|
|
623
|
+
// No dirty edits - `headContent === contentBefore`, so the mutation
|
|
622
624
|
// applies cleanly as a 1:1 string equivalence.
|
|
623
625
|
if (snap.headContent === snap.contentBefore) {
|
|
624
626
|
return { ok: true, result: after };
|
|
625
627
|
}
|
|
626
|
-
// Dirty edits present
|
|
628
|
+
// Dirty edits present - extract Sela's specific change blocks and
|
|
627
629
|
// re-apply them against the clean HEAD content via unique substring
|
|
628
630
|
// replacement. Fails loudly when the dirty edits overlap Sela's site.
|
|
629
631
|
const blocks = (0, IsolatedDiff_1.extractChangeBlocks)(snap.contentBefore, after);
|
|
@@ -636,7 +638,7 @@ function applyMutationToClean(snap) {
|
|
|
636
638
|
* CI adapters (GitHub Actions, GitLab, Jenkins) often run with no global
|
|
637
639
|
* git identity. `git commit` then fails with `Please tell me who you are`,
|
|
638
640
|
* tanking the whole heal pipeline. Plant a sela bot identity for THIS
|
|
639
|
-
* commit only when no `user.email` / `user.name` is configured
|
|
641
|
+
* commit only when no `user.email` / `user.name` is configured - never
|
|
640
642
|
* overwrite an existing identity.
|
|
641
643
|
*/
|
|
642
644
|
function configureGitIdentityForCI(cwd) {
|
|
@@ -662,18 +664,18 @@ function configureGitIdentityForCI(cwd) {
|
|
|
662
664
|
async function handleBugDetected(action, events, branchInfo, ctx) {
|
|
663
665
|
if (events.length === 0)
|
|
664
666
|
return;
|
|
665
|
-
console.warn(`\n[Sela Protection] ⚠️ ${events.length} potential regression(s) detected
|
|
667
|
+
console.warn(`\n[Sela Protection] ⚠️ ${events.length} potential regression(s) detected - auto-fix blocked.`);
|
|
666
668
|
for (const e of events) {
|
|
667
|
-
console.warn(` • ${e.sourceFile}:${e.sourceLine}
|
|
669
|
+
console.warn(` • ${e.sourceFile}:${e.sourceLine} - ${e.reason} (auditor: ${e.auditor?.verdict ?? "n/a"})`);
|
|
668
670
|
}
|
|
669
671
|
if (action === "failCI") {
|
|
670
|
-
console.warn(`[Sela Protection] 🔴 onBugDetected='failCI'
|
|
672
|
+
console.warn(`[Sela Protection] 🔴 onBugDetected='failCI' - setting process.exitCode = 1`);
|
|
671
673
|
process.exitCode = 1;
|
|
672
674
|
return;
|
|
673
675
|
}
|
|
674
676
|
if (action === "createIssue") {
|
|
675
677
|
if (!ghAvailable(ctx.cwd)) {
|
|
676
|
-
console.warn(`[Sela Protection] ⚠️ gh CLI unavailable
|
|
678
|
+
console.warn(`[Sela Protection] ⚠️ gh CLI unavailable - cannot create issue.`);
|
|
677
679
|
return;
|
|
678
680
|
}
|
|
679
681
|
for (const e of events) {
|
|
@@ -706,7 +708,9 @@ async function handleBugDetected(action, events, branchInfo, ctx) {
|
|
|
706
708
|
try {
|
|
707
709
|
fs.unlinkSync(bodyFile);
|
|
708
710
|
}
|
|
709
|
-
catch {
|
|
711
|
+
catch {
|
|
712
|
+
/* ignore */
|
|
713
|
+
}
|
|
710
714
|
if (!r.ok) {
|
|
711
715
|
console.warn(`[Sela Protection] ⚠️ gh issue create failed: ${r.stderr}`);
|
|
712
716
|
}
|
|
@@ -718,11 +722,11 @@ async function handleBugDetected(action, events, branchInfo, ctx) {
|
|
|
718
722
|
}
|
|
719
723
|
if (action === "labelOnly") {
|
|
720
724
|
if (!ghAvailable(ctx.cwd)) {
|
|
721
|
-
console.warn(`[Sela Protection] ⚠️ gh CLI unavailable
|
|
725
|
+
console.warn(`[Sela Protection] ⚠️ gh CLI unavailable - cannot label PR.`);
|
|
722
726
|
return;
|
|
723
727
|
}
|
|
724
728
|
if (!branchInfo.isPR || !branchInfo.prNumber) {
|
|
725
|
-
console.warn(`[Sela Protection] ⚠️ Not running on a PR
|
|
729
|
+
console.warn(`[Sela Protection] ⚠️ Not running on a PR - labelOnly skipped.`);
|
|
726
730
|
return;
|
|
727
731
|
}
|
|
728
732
|
const labelCmd = `gh pr edit ${branchInfo.prNumber} --add-label sela-bug-detected`;
|
|
@@ -742,7 +746,7 @@ async function handleBugDetected(action, events, branchInfo, ctx) {
|
|
|
742
746
|
}
|
|
743
747
|
}
|
|
744
748
|
// ═══════════════════════════════════════════════════════════════════
|
|
745
|
-
// SHELL QUOTING
|
|
749
|
+
// SHELL QUOTING - cross-platform (POSIX single-quote escape;
|
|
746
750
|
// on Windows execSync uses cmd.exe which handles double-quotes natively).
|
|
747
751
|
// ═══════════════════════════════════════════════════════════════════
|
|
748
752
|
function shellQuote(s) {
|