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.
Files changed (71) hide show
  1. package/README.md +1 -1
  2. package/dist/config/ConfigLoader.d.ts +1 -0
  3. package/dist/config/ConfigLoader.d.ts.map +1 -1
  4. package/dist/config/ConfigLoader.js +10 -0
  5. package/dist/config/DryRunGuard.d.ts +14 -0
  6. package/dist/config/DryRunGuard.d.ts.map +1 -0
  7. package/dist/config/DryRunGuard.js +79 -0
  8. package/dist/config/SelaConfig.d.ts +15 -1
  9. package/dist/config/SelaConfig.d.ts.map +1 -1
  10. package/dist/config/SelaConfig.js +27 -1
  11. package/dist/engine/SelaEngine.d.ts +12 -9
  12. package/dist/engine/SelaEngine.d.ts.map +1 -1
  13. package/dist/engine/SelaEngine.js +249 -72
  14. package/dist/errors/SelaError.d.ts +133 -0
  15. package/dist/errors/SelaError.d.ts.map +1 -0
  16. package/dist/errors/SelaError.js +155 -0
  17. package/dist/fixtures/expectProxy.d.ts.map +1 -1
  18. package/dist/fixtures/expectProxy.js +7 -0
  19. package/dist/fixtures/index.d.ts +7 -1
  20. package/dist/fixtures/index.d.ts.map +1 -1
  21. package/dist/fixtures/index.js +15 -0
  22. package/dist/fixtures/proxyTag.d.ts +12 -0
  23. package/dist/fixtures/proxyTag.d.ts.map +1 -0
  24. package/dist/fixtures/proxyTag.js +45 -0
  25. package/dist/index.d.ts +2 -0
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +6 -1
  28. package/dist/reporter/SelaReporter.d.ts +110 -0
  29. package/dist/reporter/SelaReporter.d.ts.map +1 -0
  30. package/dist/reporter/SelaReporter.js +328 -0
  31. package/dist/services/ASTSourceUpdater.d.ts +19 -0
  32. package/dist/services/ASTSourceUpdater.d.ts.map +1 -1
  33. package/dist/services/ASTSourceUpdater.js +173 -29
  34. package/dist/services/HealReportService.d.ts +42 -0
  35. package/dist/services/HealReportService.d.ts.map +1 -1
  36. package/dist/services/HealReportService.js +80 -1
  37. package/dist/services/HealingCacheService.d.ts +135 -0
  38. package/dist/services/HealingCacheService.d.ts.map +1 -0
  39. package/dist/services/HealingCacheService.js +460 -0
  40. package/dist/services/InitializerUpdater.d.ts.map +1 -1
  41. package/dist/services/InitializerUpdater.js +20 -1
  42. package/dist/services/IntentAuditor.d.ts +18 -1
  43. package/dist/services/IntentAuditor.d.ts.map +1 -1
  44. package/dist/services/IntentAuditor.js +19 -6
  45. package/dist/services/InteractiveReview.d.ts +24 -0
  46. package/dist/services/InteractiveReview.d.ts.map +1 -0
  47. package/dist/services/InteractiveReview.js +218 -0
  48. package/dist/services/LLMService.d.ts +17 -1
  49. package/dist/services/LLMService.d.ts.map +1 -1
  50. package/dist/services/LLMService.js +18 -8
  51. package/dist/services/PRAutomationService.d.ts +14 -1
  52. package/dist/services/PRAutomationService.d.ts.map +1 -1
  53. package/dist/services/PRAutomationService.js +260 -69
  54. package/dist/services/PendingPromptLedger.d.ts +44 -0
  55. package/dist/services/PendingPromptLedger.d.ts.map +1 -0
  56. package/dist/services/PendingPromptLedger.js +180 -0
  57. package/dist/services/ReportGenerator.d.ts +70 -0
  58. package/dist/services/ReportGenerator.d.ts.map +1 -0
  59. package/dist/services/ReportGenerator.js +191 -0
  60. package/dist/services/SafetyGuard.d.ts +27 -1
  61. package/dist/services/SafetyGuard.d.ts.map +1 -1
  62. package/dist/services/SafetyGuard.js +58 -12
  63. package/dist/services/SourceUpdater.d.ts.map +1 -1
  64. package/dist/services/SourceUpdater.js +44 -13
  65. package/dist/services/WorkspaceSnapshotService.d.ts +71 -0
  66. package/dist/services/WorkspaceSnapshotService.d.ts.map +1 -0
  67. package/dist/services/WorkspaceSnapshotService.js +202 -0
  68. package/dist/utils/IsolatedDiff.d.ts +45 -0
  69. package/dist/utils/IsolatedDiff.d.ts.map +1 -0
  70. package/dist/utils/IsolatedDiff.js +379 -0
  71. 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
- fs.writeFileSync(filePath, lines.join("\n"), "utf8");
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
- fs.writeFileSync(filePath, lines.join("\n"), "utf8");
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
- fs.writeFileSync(filePath, lines.join("\n"), "utf8");
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
- fs.writeFileSync(filePath, lines.join("\n"), "utf8");
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
- fs.writeFileSync(filePath, lines.join("\n"), "utf8");
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
- fs.writeFileSync(filePath, lines.join("\n"), "utf8");
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
- fs.writeFileSync(filePath, lines.join("\n"), "utf8");
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
- fs.writeFileSync(filePath, lines.join("\n"), "utf8");
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
- fs.writeFileSync(filePath, lines.join("\n"), "utf8");
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
- fs.writeFileSync(filePath, lines.join("\n"), "utf8");
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
- fs.writeFileSync(filePath, lines.join("\n"), "utf8");
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
- fs.writeFileSync(filePath, lines.join("\n"), "utf8");
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
- fs.writeFileSync(filePath, lines.join("\n"), "utf8");
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"}