sela-core 1.0.6 → 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 +12 -9
- package/dist/engine/SelaEngine.d.ts.map +1 -1
- package/dist/engine/SelaEngine.js +249 -72
- 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 +42 -0
- package/dist/services/HealReportService.d.ts.map +1 -1
- package/dist/services/HealReportService.js +80 -1
- 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 +17 -1
- package/dist/services/LLMService.d.ts.map +1 -1
- package/dist/services/LLMService.js +18 -8
- package/dist/services/PRAutomationService.d.ts +14 -1
- package/dist/services/PRAutomationService.d.ts.map +1 -1
- package/dist/services/PRAutomationService.js +260 -69
- 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 +27 -1
- package/dist/services/SafetyGuard.d.ts.map +1 -1
- package/dist/services/SafetyGuard.js +58 -12
- 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
|
@@ -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();
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export interface IsolatedDiffOptions {
|
|
2
|
+
/** Lines of unchanged context around each hunk. Default 3 (git default). */
|
|
3
|
+
context?: number;
|
|
4
|
+
/**
|
|
5
|
+
* Path to render in the `--- a/<path>` / `+++ b/<path>` preamble. When
|
|
6
|
+
* omitted the preamble is dropped entirely (hunks only).
|
|
7
|
+
*/
|
|
8
|
+
filePath?: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Generate a `git`-style unified diff between two in-memory strings.
|
|
12
|
+
* Returns `""` when the two buffers are identical.
|
|
13
|
+
*/
|
|
14
|
+
export declare function generateUnifiedDiff(before: string, after: string, opts?: IsolatedDiffOptions): string;
|
|
15
|
+
export interface ChangeBlock {
|
|
16
|
+
/** Old text (newlines preserved). Empty string for pure additions. */
|
|
17
|
+
oldBlock: string;
|
|
18
|
+
/** New text (newlines preserved). Empty string for pure deletions. */
|
|
19
|
+
newBlock: string;
|
|
20
|
+
/** 1-based line in `before` where the block starts (0 if pure add). */
|
|
21
|
+
oldLineStart: number;
|
|
22
|
+
/** 1-based line in `after` where the block starts (0 if pure delete). */
|
|
23
|
+
newLineStart: number;
|
|
24
|
+
/**
|
|
25
|
+
* For pure additions: a non-empty line of unchanged anchor text immediately
|
|
26
|
+
* PRECEDING the new block in the `before` buffer. The replayer uses this
|
|
27
|
+
* to locate the insertion point. `null` when adding at the very top.
|
|
28
|
+
*/
|
|
29
|
+
anchorBefore: string | null;
|
|
30
|
+
}
|
|
31
|
+
export declare function extractChangeBlocks(before: string, after: string): ChangeBlock[];
|
|
32
|
+
export interface ApplyBlocksResult {
|
|
33
|
+
ok: boolean;
|
|
34
|
+
result?: string;
|
|
35
|
+
/** Human-readable failure reason — surfaced in the PR-automation log. */
|
|
36
|
+
reason?: string;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Replay a list of change blocks against a fresh base buffer (typically
|
|
40
|
+
* the file's `HEAD` blob). Each block is applied via UNIQUE substring
|
|
41
|
+
* replacement — when the old block is missing or ambiguous in the base,
|
|
42
|
+
* we fail loudly rather than silently mutating the wrong spot.
|
|
43
|
+
*/
|
|
44
|
+
export declare function applyChangeBlocks(base: string, blocks: ChangeBlock[]): ApplyBlocksResult;
|
|
45
|
+
//# sourceMappingURL=IsolatedDiff.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"IsolatedDiff.d.ts","sourceRoot":"","sources":["../../src/utils/IsolatedDiff.ts"],"names":[],"mappings":"AAcA,MAAM,WAAW,mBAAmB;IAClC,4EAA4E;IAC5E,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,EACb,IAAI,GAAE,mBAAwB,GAC7B,MAAM,CAsBR;AA2PD,MAAM,WAAW,WAAW;IAC1B,sEAAsE;IACtE,QAAQ,EAAE,MAAM,CAAC;IACjB,sEAAsE;IACtE,QAAQ,EAAE,MAAM,CAAC;IACjB,uEAAuE;IACvE,YAAY,EAAE,MAAM,CAAC;IACrB,yEAAyE;IACzE,YAAY,EAAE,MAAM,CAAC;IACrB;;;;OAIG;IACH,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B;AAED,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,WAAW,EAAE,CAoDhF;AAED,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,yEAAyE;IACzE,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,WAAW,EAAE,GACpB,iBAAiB,CAoDnB"}
|