sela-core 1.0.5 → 1.0.7
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 +1 -1
- package/dist/config/ConfigLoader.d.ts +1 -0
- package/dist/config/ConfigLoader.d.ts.map +1 -1
- package/dist/config/ConfigLoader.js +10 -0
- package/dist/config/DryRunGuard.d.ts +14 -0
- package/dist/config/DryRunGuard.d.ts.map +1 -0
- package/dist/config/DryRunGuard.js +79 -0
- package/dist/config/SelaConfig.d.ts +15 -1
- package/dist/config/SelaConfig.d.ts.map +1 -1
- package/dist/config/SelaConfig.js +27 -1
- package/dist/engine/SelaEngine.d.ts +26 -6
- package/dist/engine/SelaEngine.d.ts.map +1 -1
- package/dist/engine/SelaEngine.js +329 -77
- package/dist/errors/SelaError.d.ts +133 -0
- package/dist/errors/SelaError.d.ts.map +1 -0
- package/dist/errors/SelaError.js +155 -0
- package/dist/fixtures/expectProxy.d.ts.map +1 -1
- package/dist/fixtures/expectProxy.js +7 -0
- package/dist/fixtures/index.d.ts +7 -1
- package/dist/fixtures/index.d.ts.map +1 -1
- package/dist/fixtures/index.js +15 -0
- package/dist/fixtures/proxyTag.d.ts +12 -0
- package/dist/fixtures/proxyTag.d.ts.map +1 -0
- package/dist/fixtures/proxyTag.js +45 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -1
- package/dist/reporter/SelaReporter.d.ts +110 -0
- package/dist/reporter/SelaReporter.d.ts.map +1 -0
- package/dist/reporter/SelaReporter.js +328 -0
- package/dist/services/ASTSourceUpdater.d.ts +19 -0
- package/dist/services/ASTSourceUpdater.d.ts.map +1 -1
- package/dist/services/ASTSourceUpdater.js +173 -29
- package/dist/services/HealReportService.d.ts +62 -3
- package/dist/services/HealReportService.d.ts.map +1 -1
- package/dist/services/HealReportService.js +184 -14
- package/dist/services/HealingCacheService.d.ts +135 -0
- package/dist/services/HealingCacheService.d.ts.map +1 -0
- package/dist/services/HealingCacheService.js +460 -0
- package/dist/services/InitializerUpdater.d.ts.map +1 -1
- package/dist/services/InitializerUpdater.js +20 -1
- package/dist/services/IntentAuditor.d.ts +18 -1
- package/dist/services/IntentAuditor.d.ts.map +1 -1
- package/dist/services/IntentAuditor.js +19 -6
- package/dist/services/InteractiveReview.d.ts +24 -0
- package/dist/services/InteractiveReview.d.ts.map +1 -0
- package/dist/services/InteractiveReview.js +218 -0
- package/dist/services/LLMService.d.ts +23 -2
- package/dist/services/LLMService.d.ts.map +1 -1
- package/dist/services/LLMService.js +48 -12
- package/dist/services/PRAutomationService.d.ts +23 -3
- package/dist/services/PRAutomationService.d.ts.map +1 -1
- package/dist/services/PRAutomationService.js +325 -79
- package/dist/services/PendingPromptLedger.d.ts +44 -0
- package/dist/services/PendingPromptLedger.d.ts.map +1 -0
- package/dist/services/PendingPromptLedger.js +180 -0
- package/dist/services/ReportGenerator.d.ts +70 -0
- package/dist/services/ReportGenerator.d.ts.map +1 -0
- package/dist/services/ReportGenerator.js +191 -0
- package/dist/services/SafetyGuard.d.ts +34 -2
- package/dist/services/SafetyGuard.d.ts.map +1 -1
- package/dist/services/SafetyGuard.js +65 -13
- package/dist/services/SourceUpdater.d.ts.map +1 -1
- package/dist/services/SourceUpdater.js +44 -13
- package/dist/services/WorkspaceSnapshotService.d.ts +71 -0
- package/dist/services/WorkspaceSnapshotService.d.ts.map +1 -0
- package/dist/services/WorkspaceSnapshotService.js +202 -0
- package/dist/utils/IsolatedDiff.d.ts +45 -0
- package/dist/utils/IsolatedDiff.d.ts.map +1 -0
- package/dist/utils/IsolatedDiff.js +379 -0
- package/package.json +71 -67
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
4
|
exports.SafetyGuard = void 0;
|
|
5
5
|
const IntentAuditor_1 = require("./IntentAuditor");
|
|
6
|
+
const SelaError_1 = require("../errors/SelaError");
|
|
6
7
|
// ═══════════════════════════════════════════════════════════════════
|
|
7
8
|
// DEFAULT THRESHOLDS (match pre-config hardcoded values exactly)
|
|
8
9
|
// ═══════════════════════════════════════════════════════════════════
|
|
@@ -55,6 +56,9 @@ const SEMANTIC_OPPOSITE_PAIRS = new Set([
|
|
|
55
56
|
"valid|invalid",
|
|
56
57
|
"invalid|valid",
|
|
57
58
|
]);
|
|
59
|
+
function escapeRegExp(s) {
|
|
60
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
61
|
+
}
|
|
58
62
|
const ROLE_KEYWORDS = new Map([
|
|
59
63
|
[
|
|
60
64
|
"SUBMIT",
|
|
@@ -185,21 +189,47 @@ const ROLE_KEYWORDS = new Map([
|
|
|
185
189
|
]),
|
|
186
190
|
],
|
|
187
191
|
]);
|
|
188
|
-
//
|
|
189
|
-
//
|
|
190
|
-
//
|
|
192
|
+
// Pre-compiled word-boundary regexes for the role classifier. Built
|
|
193
|
+
// once at module load — classifyFunctionalRole() is on the hot heal
|
|
194
|
+
// path, so we don't pay for regex construction per call.
|
|
195
|
+
const ROLE_KEYWORD_REGEXES = Array.from(ROLE_KEYWORDS, ([role, keywords]) => ({
|
|
196
|
+
role,
|
|
197
|
+
regexes: Array.from(keywords, (kw) => new RegExp(`\\b${escapeRegExp(kw)}\\b`, "i")),
|
|
198
|
+
}));
|
|
191
199
|
class SafetyGuard {
|
|
192
200
|
intentAuditor;
|
|
193
201
|
thresholds;
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
202
|
+
/**
|
|
203
|
+
* Accepts either the legacy positional `thresholds` argument or an
|
|
204
|
+
* options bag (DI-friendly). Existing call sites keep working
|
|
205
|
+
* unchanged.
|
|
206
|
+
*/
|
|
207
|
+
constructor(arg) {
|
|
208
|
+
const opts = SafetyGuard.normalizeArgs(arg);
|
|
209
|
+
this.thresholds = opts.thresholds ?? DEFAULT_THRESHOLDS;
|
|
210
|
+
this.intentAuditor = opts.intentAuditor ?? new IntentAuditor_1.IntentAuditor();
|
|
211
|
+
}
|
|
212
|
+
static normalizeArgs(arg) {
|
|
213
|
+
if (!arg)
|
|
214
|
+
return {};
|
|
215
|
+
// Heuristic: an options bag has either `intentAuditor` or a nested
|
|
216
|
+
// `thresholds` field; otherwise the value itself is a ResolvedThresholds.
|
|
217
|
+
if ("intentAuditor" in arg || "thresholds" in arg) {
|
|
218
|
+
return arg;
|
|
219
|
+
}
|
|
220
|
+
return { thresholds: arg };
|
|
197
221
|
}
|
|
198
222
|
// ─────────────────────────────────────────────────────────────
|
|
199
223
|
// PUBLIC ENTRY POINT
|
|
200
224
|
// ─────────────────────────────────────────────────────────────
|
|
201
225
|
async evaluate(context, aiFix) {
|
|
202
|
-
const {
|
|
226
|
+
const { liveText, dnaBaselineText } = aiFix;
|
|
227
|
+
// Conservative fallback for missing confidence: treat as 0 so the
|
|
228
|
+
// hardStop / reviewThreshold rules still gate the heal. Reporting layers
|
|
229
|
+
// separately render the absence as "n/a" via fmtPct.
|
|
230
|
+
const confidence = typeof aiFix.confidence === "number" && Number.isFinite(aiFix.confidence)
|
|
231
|
+
? aiFix.confidence
|
|
232
|
+
: 0;
|
|
203
233
|
const { mode } = context;
|
|
204
234
|
// ── 0. Visibility Gate ───────────────────────────────────────
|
|
205
235
|
// Runs before any confidence or content checks. A ghost element
|
|
@@ -211,6 +241,7 @@ class SafetyGuard {
|
|
|
211
241
|
if (vis.isGhost) {
|
|
212
242
|
return {
|
|
213
243
|
level: "FAIL_HARD",
|
|
244
|
+
code: SelaError_1.SAFETY_GUARD_CODES.FAIL_HARD_VISIBILITY_GHOST,
|
|
214
245
|
reason: `Visibility Gate: candidate element is a Ghost (${vis.ghostReason}) — ` +
|
|
215
246
|
`functionally absent from the user's perspective`,
|
|
216
247
|
canProceed: false,
|
|
@@ -221,6 +252,7 @@ class SafetyGuard {
|
|
|
221
252
|
if (isMainBranch) {
|
|
222
253
|
return {
|
|
223
254
|
level: "FAIL_HARD",
|
|
255
|
+
code: SelaError_1.SAFETY_GUARD_CODES.FAIL_HARD_VISIBILITY_OCCLUDED,
|
|
224
256
|
reason: `Visibility Gate: candidate element is occluded by an overlay on branch "${context.branch}" — ` +
|
|
225
257
|
`hard-fail policy active on main`,
|
|
226
258
|
canProceed: false,
|
|
@@ -240,6 +272,7 @@ class SafetyGuard {
|
|
|
240
272
|
if (confidence < this.thresholds.confidenceHardStop) {
|
|
241
273
|
return {
|
|
242
274
|
level: "BLOCKED",
|
|
275
|
+
code: SelaError_1.SAFETY_GUARD_CODES.BLOCKED_LOW_CONFIDENCE,
|
|
243
276
|
reason: `Confidence too low (${confidence}/100 < ${this.thresholds.confidenceHardStop}) — refusing to heal`,
|
|
244
277
|
canProceed: false,
|
|
245
278
|
};
|
|
@@ -284,6 +317,7 @@ class SafetyGuard {
|
|
|
284
317
|
if (this.isSemanticOpposite(oldText, newText)) {
|
|
285
318
|
return {
|
|
286
319
|
level: "FAIL_HARD",
|
|
320
|
+
code: SelaError_1.SAFETY_GUARD_CODES.FAIL_HARD_SEMANTIC_INVERSION,
|
|
287
321
|
reason: `Semantic inversion detected: "${oldText}" ↔ "${newText}" — ` +
|
|
288
322
|
`state inversions mask logical regressions`,
|
|
289
323
|
canProceed: false,
|
|
@@ -299,6 +333,7 @@ class SafetyGuard {
|
|
|
299
333
|
if (isStatusInversion) {
|
|
300
334
|
return {
|
|
301
335
|
level: "FAIL_HARD",
|
|
336
|
+
code: SelaError_1.SAFETY_GUARD_CODES.FAIL_HARD_STATUS_INVERSION,
|
|
302
337
|
reason: `Status inversion: "${oldText}" (${oldRole}) → "${newText}" (${newRole}) — ` +
|
|
303
338
|
`masking a logic failure is not permitted`,
|
|
304
339
|
canProceed: false,
|
|
@@ -313,6 +348,7 @@ class SafetyGuard {
|
|
|
313
348
|
// function being found is almost certainly a wrong element fix.
|
|
314
349
|
return {
|
|
315
350
|
level: "BLOCKED",
|
|
351
|
+
code: SelaError_1.SAFETY_GUARD_CODES.BLOCKED_ROLE_SWAP_ASSERTION,
|
|
316
352
|
reason: `Functional role swap in assertion: "${oldText}" (${oldRole}) → "${newText}" (${newRole}) — ` +
|
|
317
353
|
`the assertion was verifying a specific functional state`,
|
|
318
354
|
canProceed: false,
|
|
@@ -355,6 +391,7 @@ class SafetyGuard {
|
|
|
355
391
|
if (auditVerdict.verdict === "INCONSISTENT") {
|
|
356
392
|
return {
|
|
357
393
|
level: "FAIL_HARD",
|
|
394
|
+
code: SelaError_1.SAFETY_GUARD_CODES.FAIL_HARD_AUDITOR_INCONSISTENT,
|
|
358
395
|
reason: `Intent Auditor — INCONSISTENT: ${auditVerdict.reason}` +
|
|
359
396
|
(auditVerdict.inversionType
|
|
360
397
|
? ` [inversion: ${auditVerdict.inversionType}]`
|
|
@@ -371,6 +408,7 @@ class SafetyGuard {
|
|
|
371
408
|
if (!this.thresholds.allowSuspicious && mode === "assertion") {
|
|
372
409
|
return {
|
|
373
410
|
level: "BLOCKED",
|
|
411
|
+
code: SelaError_1.SAFETY_GUARD_CODES.BLOCKED_AUDITOR_SUSPICIOUS,
|
|
374
412
|
reason: `Intent Auditor — SUSPICIOUS in assertion mode: ${auditVerdict.reason}`,
|
|
375
413
|
canProceed: false,
|
|
376
414
|
meta,
|
|
@@ -415,6 +453,7 @@ class SafetyGuard {
|
|
|
415
453
|
if (similarity < this.thresholds.assertionSimilarityFloor) {
|
|
416
454
|
return {
|
|
417
455
|
level: "BLOCKED",
|
|
456
|
+
code: SelaError_1.SAFETY_GUARD_CODES.BLOCKED_SIMILARITY_FLOOR,
|
|
418
457
|
reason: `Text divergence too high in assertion: "${oldText}" → "${newText}" ` +
|
|
419
458
|
`(similarity=${(similarity * 100).toFixed(1)}% < ${(this.thresholds.assertionSimilarityFloor * 100).toFixed(0)}%) — ` +
|
|
420
459
|
`possible content regression`,
|
|
@@ -449,16 +488,29 @@ class SafetyGuard {
|
|
|
449
488
|
// ─────────────────────────────────────────────────────────────
|
|
450
489
|
// FUNCTIONAL ROLE CLASSIFICATION
|
|
451
490
|
//
|
|
452
|
-
// Returns the first role whose keyword
|
|
453
|
-
// (
|
|
491
|
+
// Returns the first role whose keyword matches the text as a whole
|
|
492
|
+
// word (delimited by \b boundaries). Returns UNKNOWN when no role
|
|
493
|
+
// matches.
|
|
494
|
+
//
|
|
495
|
+
// Why \b instead of `.includes(kw)`:
|
|
496
|
+
// - "unauthorized".includes("authorize") → true ← bug
|
|
497
|
+
// - "offline".includes("on") → true ← bug
|
|
498
|
+
// - "disconnected".includes("connected") → true ← bug
|
|
499
|
+
// - "inactive".includes("active") → true ← bug
|
|
500
|
+
// The substring approach mis-classified negations into the
|
|
501
|
+
// positive role of their root word, silently bypassing
|
|
502
|
+
// FAIL_HARD_STATUS_INVERSION. Word-boundary regex eliminates
|
|
503
|
+
// these collisions while keeping multi-word keywords ("sign in")
|
|
504
|
+
// and punctuation-flanked matches ("Submit!") working.
|
|
454
505
|
// ─────────────────────────────────────────────────────────────
|
|
455
506
|
classifyFunctionalRole(text) {
|
|
456
507
|
const normalized = text.toLowerCase().trim();
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
508
|
+
if (normalized.length === 0)
|
|
509
|
+
return "UNKNOWN";
|
|
510
|
+
for (const { role, regexes } of ROLE_KEYWORD_REGEXES) {
|
|
511
|
+
for (const re of regexes) {
|
|
512
|
+
if (re.test(normalized))
|
|
460
513
|
return role;
|
|
461
|
-
}
|
|
462
514
|
}
|
|
463
515
|
}
|
|
464
516
|
return "UNKNOWN";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SourceUpdater.d.ts","sourceRoot":"","sources":["../../src/services/SourceUpdater.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAE/C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"SourceUpdater.d.ts","sourceRoot":"","sources":["../../src/services/SourceUpdater.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAE/C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AA4BtD,UAAU,YAAY;IACpB,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iFAAiF;IACjF,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,wEAAwE;IACxE,cAAc,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IAChD,iEAAiE;IACjE,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,UAAU,aAAa;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;CACd;AAqZD,qBAAa,aAAa;IAMxB,MAAM,CAAC,MAAM,CACX,MAAM,EAAE,aAAa,EACrB,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,EACnB,UAAU,CAAC,EAAE,MAAM,EACnB,WAAW,CAAC,EAAE,MAAM,EACpB,mBAAmB,CAAC,EAAE,MAAM,EAC5B,UAAU,CAAC,EAAE,eAAe,EAAE,EAC9B,aAAa,CAAC,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,EACpD,kBAAkB,CAAC,EAAE,iBAAiB,EAAE,EACxC,iBAAiB,CAAC,EAAE,iBAAiB,EAAE,EACvC,cAAc,CAAC,EAAE,SAAS,GAAG,aAAa,GAAG,QAAQ,EACrD,UAAU,CAAC,EAAE,OAAO,GACnB,YAAY;IAoDf,OAAO,CAAC,MAAM,CAAC,gBAAgB;IAwC/B,OAAO,CAAC,MAAM,CAAC,WAAW;IAiT1B,OAAO,CAAC,MAAM,CAAC,2BAA2B;IA8E1C,OAAO,CAAC,MAAM,CAAC,wBAAwB;IA8EvC,OAAO,CAAC,MAAM,CAAC,sBAAsB;IAoErC,OAAO,CAAC,MAAM,CAAC,kBAAkB;IAsBjC,OAAO,CAAC,MAAM,CAAC,qBAAqB;IA6DpC,OAAO,CAAC,MAAM,CAAC,qBAAqB;IAuCpC,OAAO,CAAC,MAAM,CAAC,wBAAwB;IA6CvC,OAAO,CAAC,MAAM,CAAC,sBAAsB;IAqCrC,OAAO,CAAC,MAAM,CAAC,sBAAsB;IAqGrC,OAAO,CAAC,MAAM,CAAC,kBAAkB;IA0BjC,OAAO,CAAC,MAAM,CAAC,uBAAuB;IAatC,MAAM,CAAC,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAgBlD,MAAM,CAAC,cAAc,CACnB,MAAM,EAAE,aAAa,EACrB,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,GAClB,YAAY;IA0Cf,0FAA0F;IAC1F,MAAM,CAAC,eAAe,IAAI,IAAI;CAG/B"}
|
|
@@ -38,6 +38,23 @@ const fs = __importStar(require("fs"));
|
|
|
38
38
|
const path = __importStar(require("path"));
|
|
39
39
|
const child_process_1 = require("child_process");
|
|
40
40
|
const ASTSourceUpdater_1 = require("./ASTSourceUpdater");
|
|
41
|
+
const DryRunGuard_1 = require("../config/DryRunGuard");
|
|
42
|
+
const WorkspaceSnapshotService_1 = require("./WorkspaceSnapshotService");
|
|
43
|
+
const SelaError_1 = require("../errors/SelaError");
|
|
44
|
+
// Centralised write gate — every disk-mutating `fs.writeFileSync` call in
|
|
45
|
+
// this module routes through `persistFile` so the SELA_DRY_RUN bypass is
|
|
46
|
+
// enforced in one place, and the Clean-Room WorkspaceSnapshotService
|
|
47
|
+
// records contentBefore/contentAfter for isolated in-memory diffing.
|
|
48
|
+
function persistFile(filePath, content) {
|
|
49
|
+
if (DryRunGuard_1.DryRunGuard.active()) {
|
|
50
|
+
DryRunGuard_1.DryRunGuard.logSkippedWrite("writeFileSync", filePath);
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
WorkspaceSnapshotService_1.sharedWorkspaceSnapshot.preWrite(filePath);
|
|
54
|
+
fs.writeFileSync(filePath, content, "utf8");
|
|
55
|
+
WorkspaceSnapshotService_1.sharedWorkspaceSnapshot.postWrite(filePath);
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
41
58
|
// ─────────────────────────────────────────────────────────────────
|
|
42
59
|
// QUOTE MANAGEMENT
|
|
43
60
|
// ─────────────────────────────────────────────────────────────────
|
|
@@ -301,6 +318,13 @@ function updateTemplateLiteralPreservingVars(line, oldSelector, newSelector, dom
|
|
|
301
318
|
// ─────────────────────────────────────────────────────────────────
|
|
302
319
|
function runGitOperations(absoluteFilePath, createBranch, relativeFilePath) {
|
|
303
320
|
const filename = path.basename(relativeFilePath);
|
|
321
|
+
// Dry-run gate — suppress branch creation, `git add`, and auto-commit.
|
|
322
|
+
// Mirrors the persistFile() skip so a dry run is fully observable in CI
|
|
323
|
+
// without ever producing a commit.
|
|
324
|
+
if (DryRunGuard_1.DryRunGuard.active()) {
|
|
325
|
+
console.log(`[SourceUpdater] 🌵 Dry Run — skip git operations for ${filename}`);
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
304
328
|
try {
|
|
305
329
|
// Bail out if HEAD is detached (CI SHA checkout, rebase mid-flight, etc.)
|
|
306
330
|
try {
|
|
@@ -367,7 +391,7 @@ class SourceUpdater {
|
|
|
367
391
|
const indent = lines[lineIdx].match(/^(\s*)/)?.[1] ?? "";
|
|
368
392
|
const comment = `${indent}// TODO [Sela]: replace "${oldSelector}" with "${newSelector}"`;
|
|
369
393
|
lines.splice(lineIdx, 0, comment);
|
|
370
|
-
|
|
394
|
+
persistFile(filePath, lines.join("\n"));
|
|
371
395
|
console.log(`[SourceUpdater] 💬 Comment-only: inserted TODO at line ${caller.line} in ${path.basename(filePath)}`);
|
|
372
396
|
return {
|
|
373
397
|
success: false,
|
|
@@ -441,6 +465,13 @@ class SourceUpdater {
|
|
|
441
465
|
console.log(`[SourceUpdater] ⚠️ AST engine exhausted (${astResult.reason}), falling back to Regex`);
|
|
442
466
|
}
|
|
443
467
|
catch (astError) {
|
|
468
|
+
// Developer pinned the failing statement with `// sela-fail-fast` —
|
|
469
|
+
// propagate the directive instead of swallowing into the Regex
|
|
470
|
+
// fallback chain (which would defeat the whole point of the gate).
|
|
471
|
+
if ((0, SelaError_1.isSelaErrorFrom)(astError, "ASTUpdater") &&
|
|
472
|
+
astError.code === SelaError_1.AST_UPDATER_CODES.HEAL_DISABLED_BY_DIRECTIVE) {
|
|
473
|
+
throw astError;
|
|
474
|
+
}
|
|
444
475
|
console.warn(`[SourceUpdater] ⚠️ AST engine error: ${astError.message}. Falling back to Regex`);
|
|
445
476
|
}
|
|
446
477
|
// ── 2. Regex Fallback ──────────────────────────────────────────
|
|
@@ -452,7 +483,7 @@ class SourceUpdater {
|
|
|
452
483
|
if (contentChange) {
|
|
453
484
|
const assertResult = SourceUpdater.strategyAssertionHealing(lines, 1, contentChange, true);
|
|
454
485
|
if (assertResult.success) {
|
|
455
|
-
|
|
486
|
+
persistFile(filePath, lines.join("\n"));
|
|
456
487
|
console.log(`[SourceUpdater] ✅ Regex Strategy G (Assertion Healing) SUCCESS`);
|
|
457
488
|
return assertResult;
|
|
458
489
|
}
|
|
@@ -479,7 +510,7 @@ class SourceUpdater {
|
|
|
479
510
|
const layeredResult = SourceUpdater.strategyLayeredHealing(lines, callerLine, oldSelector, aiSegments, fullSelectorContext);
|
|
480
511
|
if (layeredResult.success) {
|
|
481
512
|
tryAssertionHealing();
|
|
482
|
-
|
|
513
|
+
persistFile(filePath, lines.join("\n"));
|
|
483
514
|
console.log(`[SourceUpdater] ✅ Strategy 0 (Layered Healing) SUCCESS — line ${layeredResult.lineUpdated + 1}`);
|
|
484
515
|
return layeredResult;
|
|
485
516
|
}
|
|
@@ -488,7 +519,7 @@ class SourceUpdater {
|
|
|
488
519
|
const chainCollapseResult = SourceUpdater.strategyChainCollapse_Regex(lines, callerLine, oldSelector, effectiveNewSelector);
|
|
489
520
|
if (chainCollapseResult.success) {
|
|
490
521
|
tryAssertionHealing();
|
|
491
|
-
|
|
522
|
+
persistFile(filePath, lines.join("\n"));
|
|
492
523
|
console.log(`[SourceUpdater] ✅ Strategy F (Chain Collapse) SUCCESS`);
|
|
493
524
|
return chainCollapseResult;
|
|
494
525
|
}
|
|
@@ -496,7 +527,7 @@ class SourceUpdater {
|
|
|
496
527
|
const chainResult = SourceUpdater.strategyChainCollapse(lines, callerLine, oldSelector, effectiveNewSelector);
|
|
497
528
|
if (chainResult.success) {
|
|
498
529
|
tryAssertionHealing();
|
|
499
|
-
|
|
530
|
+
persistFile(filePath, lines.join("\n"));
|
|
500
531
|
console.log(`[SourceUpdater] ✅ Strategy 1 (Chain Collapse) SUCCESS`);
|
|
501
532
|
return chainResult;
|
|
502
533
|
}
|
|
@@ -504,7 +535,7 @@ class SourceUpdater {
|
|
|
504
535
|
const directResult = SourceUpdater.strategyDirectLiteral(lines, callerLine, oldSelector, effectiveNewSelector);
|
|
505
536
|
if (directResult.success) {
|
|
506
537
|
tryAssertionHealing();
|
|
507
|
-
|
|
538
|
+
persistFile(filePath, lines.join("\n"));
|
|
508
539
|
console.log(`[SourceUpdater] ✅ Strategy 2 (Direct Literal) SUCCESS`);
|
|
509
540
|
return directResult;
|
|
510
541
|
}
|
|
@@ -512,7 +543,7 @@ class SourceUpdater {
|
|
|
512
543
|
const varResult = SourceUpdater.strategyUpstreamVariable(lines, callerLine, oldSelector, effectiveNewSelector);
|
|
513
544
|
if (varResult.success) {
|
|
514
545
|
tryAssertionHealing();
|
|
515
|
-
|
|
546
|
+
persistFile(filePath, lines.join("\n"));
|
|
516
547
|
console.log(`[SourceUpdater] ✅ Strategy 3 (Upstream Variable) SUCCESS`);
|
|
517
548
|
return varResult;
|
|
518
549
|
}
|
|
@@ -520,7 +551,7 @@ class SourceUpdater {
|
|
|
520
551
|
const chainDedupResult = SourceUpdater.strategyChainedLocator(lines, callerLine, oldSelector, effectiveNewSelector);
|
|
521
552
|
if (chainDedupResult.success) {
|
|
522
553
|
tryAssertionHealing();
|
|
523
|
-
|
|
554
|
+
persistFile(filePath, lines.join("\n"));
|
|
524
555
|
console.log(`[SourceUpdater] ✅ Strategy 4 (Chained Dedup) SUCCESS`);
|
|
525
556
|
return chainDedupResult;
|
|
526
557
|
}
|
|
@@ -528,7 +559,7 @@ class SourceUpdater {
|
|
|
528
559
|
const fnResult = SourceUpdater.strategyFunctionReturn(lines, callerLine, oldSelector, effectiveNewSelector, domContext, dnaAttrHint);
|
|
529
560
|
if (fnResult.success) {
|
|
530
561
|
tryAssertionHealing();
|
|
531
|
-
|
|
562
|
+
persistFile(filePath, lines.join("\n"));
|
|
532
563
|
console.log(`[SourceUpdater] ✅ Strategy 5 (Function Return) SUCCESS`);
|
|
533
564
|
return fnResult;
|
|
534
565
|
}
|
|
@@ -536,7 +567,7 @@ class SourceUpdater {
|
|
|
536
567
|
const scanResult = SourceUpdater.strategyGlobalScan(lines, oldSelector, effectiveNewSelector);
|
|
537
568
|
if (scanResult.success) {
|
|
538
569
|
tryAssertionHealing();
|
|
539
|
-
|
|
570
|
+
persistFile(filePath, lines.join("\n"));
|
|
540
571
|
console.log(`[SourceUpdater] ✅ Strategy 6 (Global Scan) SUCCESS`);
|
|
541
572
|
return scanResult;
|
|
542
573
|
}
|
|
@@ -544,7 +575,7 @@ class SourceUpdater {
|
|
|
544
575
|
if (contentChange) {
|
|
545
576
|
const assertResult = SourceUpdater.strategyAssertionHealing(lines, callerLine, contentChange);
|
|
546
577
|
if (assertResult.success) {
|
|
547
|
-
|
|
578
|
+
persistFile(filePath, lines.join("\n"));
|
|
548
579
|
console.log(`[SourceUpdater] ✅ Strategy G (Assertion Healing) SUCCESS`);
|
|
549
580
|
return assertResult;
|
|
550
581
|
}
|
|
@@ -1008,7 +1039,7 @@ class SourceUpdater {
|
|
|
1008
1039
|
if (pattern.test(lines[i])) {
|
|
1009
1040
|
const safeNew = sanitizeForInjection(newArgument, q);
|
|
1010
1041
|
lines[i] = lines[i].replace(pattern, `$1${safeNew}$2`);
|
|
1011
|
-
|
|
1042
|
+
persistFile(filePath, lines.join("\n"));
|
|
1012
1043
|
return { success: true, reason: "argument updated", lineUpdated: i };
|
|
1013
1044
|
}
|
|
1014
1045
|
}
|
|
@@ -1016,7 +1047,7 @@ class SourceUpdater {
|
|
|
1016
1047
|
if (pattern.test(lines[i])) {
|
|
1017
1048
|
const safeNew = sanitizeForInjection(newArgument, q);
|
|
1018
1049
|
lines[i] = lines[i].replace(pattern, `$1${safeNew}$2`);
|
|
1019
|
-
|
|
1050
|
+
persistFile(filePath, lines.join("\n"));
|
|
1020
1051
|
return { success: true, reason: "argument updated", lineUpdated: i };
|
|
1021
1052
|
}
|
|
1022
1053
|
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
export interface FileSnapshot {
|
|
2
|
+
/** Absolute, normalised path of the tracked file. */
|
|
3
|
+
absolutePath: string;
|
|
4
|
+
/** Project-relative path (posix slashes) — for diff headers + PR commits. */
|
|
5
|
+
relativePath: string;
|
|
6
|
+
/**
|
|
7
|
+
* Disk content captured BEFORE Sela's first write to this file.
|
|
8
|
+
* May contain the developer's uncommitted edits — that's the point.
|
|
9
|
+
*/
|
|
10
|
+
contentBefore: string;
|
|
11
|
+
/** Disk content captured AFTER Sela's latest write. `null` until first postWrite(). */
|
|
12
|
+
contentAfter: string | null;
|
|
13
|
+
/** Content of the file at `HEAD` (last committed) — `null` when untracked / no git. */
|
|
14
|
+
headContent: string | null;
|
|
15
|
+
/** True when `contentBefore !== headContent`, i.e. developer had dirty edits. */
|
|
16
|
+
hadUncommittedChanges: boolean;
|
|
17
|
+
/** True when the file did not exist on disk before Sela touched it. */
|
|
18
|
+
createdBySela: boolean;
|
|
19
|
+
}
|
|
20
|
+
export declare class WorkspaceSnapshotService {
|
|
21
|
+
private snapshots;
|
|
22
|
+
private cwd;
|
|
23
|
+
constructor(cwd?: string);
|
|
24
|
+
/** Override the cwd used for relative-path + HEAD lookups (tests). */
|
|
25
|
+
setCwd(cwd: string): void;
|
|
26
|
+
/**
|
|
27
|
+
* Record the pre-mutation state of `filePath` if not already tracked.
|
|
28
|
+
* Idempotent — only the FIRST call per file matters.
|
|
29
|
+
*/
|
|
30
|
+
preWrite(filePath: string): void;
|
|
31
|
+
/**
|
|
32
|
+
* Refresh the post-mutation state. Safe to call multiple times — each
|
|
33
|
+
* call overwrites `contentAfter` with the latest disk state.
|
|
34
|
+
*/
|
|
35
|
+
postWrite(filePath: string): void;
|
|
36
|
+
/** Lookup by absolute or relative path. Returns `null` when untracked. */
|
|
37
|
+
getSnapshot(filePath: string): FileSnapshot | null;
|
|
38
|
+
/** Every tracked snapshot. Insertion order. */
|
|
39
|
+
getAllSnapshots(): FileSnapshot[];
|
|
40
|
+
/**
|
|
41
|
+
* Did Sela actually mutate this file? True when contentAfter exists and
|
|
42
|
+
* differs from contentBefore.
|
|
43
|
+
*/
|
|
44
|
+
isMutated(filePath: string): boolean;
|
|
45
|
+
/** All snapshots with a real mutation recorded. */
|
|
46
|
+
getMutatedSnapshots(): FileSnapshot[];
|
|
47
|
+
/**
|
|
48
|
+
* Rewind every tracked file on disk to `contentBefore` — restoring the
|
|
49
|
+
* developer's pre-Sela workspace state byte-for-byte.
|
|
50
|
+
*/
|
|
51
|
+
restoreOriginal(): void;
|
|
52
|
+
/**
|
|
53
|
+
* Rewind a SINGLE tracked file on disk to `contentBefore`. Used by the
|
|
54
|
+
* Local DX interactive review flow when the developer rejects a fix.
|
|
55
|
+
* Returns true when the rewind happened, false when the file is untracked.
|
|
56
|
+
*/
|
|
57
|
+
restoreOriginalByPath(filePath: string): boolean;
|
|
58
|
+
private rewindSnapshot;
|
|
59
|
+
/**
|
|
60
|
+
* Forward every tracked file on disk to `contentAfter` (no-op for files
|
|
61
|
+
* that were never written). Used by PRAutomationService to reapply
|
|
62
|
+
* Sela's mutations after a `git stash pop` round-trip.
|
|
63
|
+
*/
|
|
64
|
+
restoreMutated(): void;
|
|
65
|
+
clear(): void;
|
|
66
|
+
private normalise;
|
|
67
|
+
private toRelative;
|
|
68
|
+
private readHeadContent;
|
|
69
|
+
}
|
|
70
|
+
export declare const sharedWorkspaceSnapshot: WorkspaceSnapshotService;
|
|
71
|
+
//# sourceMappingURL=WorkspaceSnapshotService.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"WorkspaceSnapshotService.d.ts","sourceRoot":"","sources":["../../src/services/WorkspaceSnapshotService.ts"],"names":[],"mappings":"AAyBA,MAAM,WAAW,YAAY;IAC3B,qDAAqD;IACrD,YAAY,EAAE,MAAM,CAAC;IACrB,6EAA6E;IAC7E,YAAY,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,aAAa,EAAE,MAAM,CAAC;IACtB,uFAAuF;IACvF,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,uFAAuF;IACvF,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,iFAAiF;IACjF,qBAAqB,EAAE,OAAO,CAAC;IAC/B,uEAAuE;IACvE,aAAa,EAAE,OAAO,CAAC;CACxB;AAED,qBAAa,wBAAwB;IACnC,OAAO,CAAC,SAAS,CAAmC;IACpD,OAAO,CAAC,GAAG,CAAS;gBAER,GAAG,GAAE,MAAsB;IAIvC,sEAAsE;IACtE,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAIzB;;;OAGG;IACH,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;IAqBhC;;;OAGG;IACH,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;IASjC,0EAA0E;IAC1E,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI;IAIlD,+CAA+C;IAC/C,eAAe,IAAI,YAAY,EAAE;IAIjC;;;OAGG;IACH,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO;IAMpC,mDAAmD;IACnD,mBAAmB,IAAI,YAAY,EAAE;IAMrC;;;OAGG;IACH,eAAe,IAAI,IAAI;IAMvB;;;;OAIG;IACH,qBAAqB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO;IAOhD,OAAO,CAAC,cAAc;IAUtB;;;;OAIG;IACH,cAAc,IAAI,IAAI;IAOtB,KAAK,IAAI,IAAI;IAQb,OAAO,CAAC,SAAS;IAMjB,OAAO,CAAC,UAAU;IAKlB,OAAO,CAAC,eAAe;CAcxB;AAED,eAAO,MAAM,uBAAuB,0BAAiC,CAAC"}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// src/services/WorkspaceSnapshotService.ts
|
|
3
|
+
//
|
|
4
|
+
// Clean-Room Architecture — file 1 of 2.
|
|
5
|
+
//
|
|
6
|
+
// Tracks the EXACT content of every file Sela mutates during a session, so:
|
|
7
|
+
// • The HTML report diff can be generated from an isolated in-memory diff
|
|
8
|
+
// (developer's dirty pre-existing edits NEVER bleed in).
|
|
9
|
+
// • PRAutomationService can restore the developer's pre-Sela workspace
|
|
10
|
+
// byte-for-byte after a PR is opened on an isolated branch.
|
|
11
|
+
//
|
|
12
|
+
// Lifecycle:
|
|
13
|
+
// preWrite(path) — first call per file records `contentBefore` (disk
|
|
14
|
+
// snapshot right before Sela touches it) + HEAD blob.
|
|
15
|
+
// postWrite(path) — refreshes `contentAfter` (latest disk state).
|
|
16
|
+
// restoreOriginal()— writes contentBefore back to every tracked file.
|
|
17
|
+
// restoreMutated() — writes contentAfter back to every tracked file.
|
|
18
|
+
// clear() — empties the ledger.
|
|
19
|
+
//
|
|
20
|
+
// Singleton `sharedWorkspaceSnapshot` is the canonical instance the
|
|
21
|
+
// SourceUpdater / ASTSourceUpdater write hooks talk to.
|
|
22
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
23
|
+
if (k2 === undefined) k2 = k;
|
|
24
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
25
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
26
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
27
|
+
}
|
|
28
|
+
Object.defineProperty(o, k2, desc);
|
|
29
|
+
}) : (function(o, m, k, k2) {
|
|
30
|
+
if (k2 === undefined) k2 = k;
|
|
31
|
+
o[k2] = m[k];
|
|
32
|
+
}));
|
|
33
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
34
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
35
|
+
}) : function(o, v) {
|
|
36
|
+
o["default"] = v;
|
|
37
|
+
});
|
|
38
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
39
|
+
var ownKeys = function(o) {
|
|
40
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
41
|
+
var ar = [];
|
|
42
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
43
|
+
return ar;
|
|
44
|
+
};
|
|
45
|
+
return ownKeys(o);
|
|
46
|
+
};
|
|
47
|
+
return function (mod) {
|
|
48
|
+
if (mod && mod.__esModule) return mod;
|
|
49
|
+
var result = {};
|
|
50
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
51
|
+
__setModuleDefault(result, mod);
|
|
52
|
+
return result;
|
|
53
|
+
};
|
|
54
|
+
})();
|
|
55
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
56
|
+
exports.sharedWorkspaceSnapshot = exports.WorkspaceSnapshotService = void 0;
|
|
57
|
+
const fs = __importStar(require("fs"));
|
|
58
|
+
const path = __importStar(require("path"));
|
|
59
|
+
const child_process_1 = require("child_process");
|
|
60
|
+
class WorkspaceSnapshotService {
|
|
61
|
+
snapshots = new Map();
|
|
62
|
+
cwd;
|
|
63
|
+
constructor(cwd = process.cwd()) {
|
|
64
|
+
this.cwd = cwd;
|
|
65
|
+
}
|
|
66
|
+
/** Override the cwd used for relative-path + HEAD lookups (tests). */
|
|
67
|
+
setCwd(cwd) {
|
|
68
|
+
this.cwd = cwd;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Record the pre-mutation state of `filePath` if not already tracked.
|
|
72
|
+
* Idempotent — only the FIRST call per file matters.
|
|
73
|
+
*/
|
|
74
|
+
preWrite(filePath) {
|
|
75
|
+
const abs = this.normalise(filePath);
|
|
76
|
+
if (this.snapshots.has(abs))
|
|
77
|
+
return;
|
|
78
|
+
const exists = fs.existsSync(abs);
|
|
79
|
+
const contentBefore = exists ? fs.readFileSync(abs, "utf8") : "";
|
|
80
|
+
const headContent = this.readHeadContent(abs);
|
|
81
|
+
const hadUncommittedChanges = headContent !== null && headContent !== contentBefore;
|
|
82
|
+
this.snapshots.set(abs, {
|
|
83
|
+
absolutePath: abs,
|
|
84
|
+
relativePath: this.toRelative(abs),
|
|
85
|
+
contentBefore,
|
|
86
|
+
contentAfter: null,
|
|
87
|
+
headContent,
|
|
88
|
+
hadUncommittedChanges,
|
|
89
|
+
createdBySela: !exists,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Refresh the post-mutation state. Safe to call multiple times — each
|
|
94
|
+
* call overwrites `contentAfter` with the latest disk state.
|
|
95
|
+
*/
|
|
96
|
+
postWrite(filePath) {
|
|
97
|
+
const abs = this.normalise(filePath);
|
|
98
|
+
const snap = this.snapshots.get(abs);
|
|
99
|
+
if (!snap)
|
|
100
|
+
return;
|
|
101
|
+
snap.contentAfter = fs.existsSync(abs)
|
|
102
|
+
? fs.readFileSync(abs, "utf8")
|
|
103
|
+
: "";
|
|
104
|
+
}
|
|
105
|
+
/** Lookup by absolute or relative path. Returns `null` when untracked. */
|
|
106
|
+
getSnapshot(filePath) {
|
|
107
|
+
return this.snapshots.get(this.normalise(filePath)) ?? null;
|
|
108
|
+
}
|
|
109
|
+
/** Every tracked snapshot. Insertion order. */
|
|
110
|
+
getAllSnapshots() {
|
|
111
|
+
return Array.from(this.snapshots.values());
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Did Sela actually mutate this file? True when contentAfter exists and
|
|
115
|
+
* differs from contentBefore.
|
|
116
|
+
*/
|
|
117
|
+
isMutated(filePath) {
|
|
118
|
+
const snap = this.getSnapshot(filePath);
|
|
119
|
+
if (!snap || snap.contentAfter === null)
|
|
120
|
+
return false;
|
|
121
|
+
return snap.contentAfter !== snap.contentBefore;
|
|
122
|
+
}
|
|
123
|
+
/** All snapshots with a real mutation recorded. */
|
|
124
|
+
getMutatedSnapshots() {
|
|
125
|
+
return this.getAllSnapshots().filter((s) => s.contentAfter !== null && s.contentAfter !== s.contentBefore);
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Rewind every tracked file on disk to `contentBefore` — restoring the
|
|
129
|
+
* developer's pre-Sela workspace state byte-for-byte.
|
|
130
|
+
*/
|
|
131
|
+
restoreOriginal() {
|
|
132
|
+
for (const snap of this.snapshots.values()) {
|
|
133
|
+
this.rewindSnapshot(snap);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Rewind a SINGLE tracked file on disk to `contentBefore`. Used by the
|
|
138
|
+
* Local DX interactive review flow when the developer rejects a fix.
|
|
139
|
+
* Returns true when the rewind happened, false when the file is untracked.
|
|
140
|
+
*/
|
|
141
|
+
restoreOriginalByPath(filePath) {
|
|
142
|
+
const snap = this.getSnapshot(filePath);
|
|
143
|
+
if (!snap)
|
|
144
|
+
return false;
|
|
145
|
+
this.rewindSnapshot(snap);
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
rewindSnapshot(snap) {
|
|
149
|
+
if (snap.createdBySela) {
|
|
150
|
+
if (fs.existsSync(snap.absolutePath)) {
|
|
151
|
+
fs.unlinkSync(snap.absolutePath);
|
|
152
|
+
}
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
fs.writeFileSync(snap.absolutePath, snap.contentBefore, "utf8");
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Forward every tracked file on disk to `contentAfter` (no-op for files
|
|
159
|
+
* that were never written). Used by PRAutomationService to reapply
|
|
160
|
+
* Sela's mutations after a `git stash pop` round-trip.
|
|
161
|
+
*/
|
|
162
|
+
restoreMutated() {
|
|
163
|
+
for (const snap of this.snapshots.values()) {
|
|
164
|
+
if (snap.contentAfter === null)
|
|
165
|
+
continue;
|
|
166
|
+
fs.writeFileSync(snap.absolutePath, snap.contentAfter, "utf8");
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
clear() {
|
|
170
|
+
this.snapshots.clear();
|
|
171
|
+
}
|
|
172
|
+
// ────────────────────────────────────────────────────────────────
|
|
173
|
+
// INTERNAL
|
|
174
|
+
// ────────────────────────────────────────────────────────────────
|
|
175
|
+
normalise(raw) {
|
|
176
|
+
if (!raw)
|
|
177
|
+
return raw;
|
|
178
|
+
const abs = path.isAbsolute(raw) ? raw : path.resolve(this.cwd, raw);
|
|
179
|
+
return path.normalize(abs);
|
|
180
|
+
}
|
|
181
|
+
toRelative(abs) {
|
|
182
|
+
const rel = path.relative(this.cwd, abs);
|
|
183
|
+
return rel.split(path.sep).join("/");
|
|
184
|
+
}
|
|
185
|
+
readHeadContent(abs) {
|
|
186
|
+
try {
|
|
187
|
+
const rel = this.toRelative(abs);
|
|
188
|
+
const out = (0, child_process_1.execSync)(`git show HEAD:"${rel}"`, {
|
|
189
|
+
cwd: this.cwd,
|
|
190
|
+
encoding: "utf8",
|
|
191
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
192
|
+
windowsHide: true,
|
|
193
|
+
});
|
|
194
|
+
return out.toString();
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
exports.WorkspaceSnapshotService = WorkspaceSnapshotService;
|
|
202
|
+
exports.sharedWorkspaceSnapshot = new WorkspaceSnapshotService();
|