sela-core 1.0.6 → 1.0.8

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 (124) hide show
  1. package/README.md +1 -1
  2. package/dist/cli/ErrorHandler.d.ts +1 -1
  3. package/dist/cli/ErrorHandler.js +14 -14
  4. package/dist/cli/commands/bulk.d.ts +1 -1
  5. package/dist/cli/commands/bulk.d.ts.map +1 -1
  6. package/dist/cli/commands/bulk.js +51 -33
  7. package/dist/cli/commands/init.d.ts +1 -1
  8. package/dist/cli/commands/init.d.ts.map +1 -1
  9. package/dist/cli/commands/init.js +26 -18
  10. package/dist/cli/commands/showReport.d.ts +1 -1
  11. package/dist/cli/commands/showReport.d.ts.map +1 -1
  12. package/dist/cli/commands/showReport.js +12 -10
  13. package/dist/cli/commands/status.d.ts +1 -1
  14. package/dist/cli/commands/status.d.ts.map +1 -1
  15. package/dist/cli/commands/status.js +8 -8
  16. package/dist/cli/commands/sync.d.ts +1 -1
  17. package/dist/cli/commands/sync.d.ts.map +1 -1
  18. package/dist/cli/commands/sync.js +38 -30
  19. package/dist/cli/index.js +9 -9
  20. package/dist/cli/ui/DnaTable.d.ts +1 -1
  21. package/dist/cli/ui/DnaTable.d.ts.map +1 -1
  22. package/dist/cli/ui/DnaTable.js +23 -16
  23. package/dist/cli/ui/RefactorWizard.d.ts +3 -3
  24. package/dist/cli/ui/RefactorWizard.d.ts.map +1 -1
  25. package/dist/cli/ui/RefactorWizard.js +108 -82
  26. package/dist/config/ConfigLoader.d.ts +3 -2
  27. package/dist/config/ConfigLoader.d.ts.map +1 -1
  28. package/dist/config/ConfigLoader.js +23 -13
  29. package/dist/config/DryRunGuard.d.ts +14 -0
  30. package/dist/config/DryRunGuard.d.ts.map +1 -0
  31. package/dist/config/DryRunGuard.js +79 -0
  32. package/dist/config/SelaConfig.d.ts +26 -16
  33. package/dist/config/SelaConfig.d.ts.map +1 -1
  34. package/dist/config/SelaConfig.js +54 -22
  35. package/dist/engine/HealingRegistry.d.ts +1 -1
  36. package/dist/engine/HealingRegistry.js +3 -3
  37. package/dist/engine/SelaEngine.d.ts +13 -10
  38. package/dist/engine/SelaEngine.d.ts.map +1 -1
  39. package/dist/engine/SelaEngine.js +292 -101
  40. package/dist/errors/SelaError.d.ts +132 -0
  41. package/dist/errors/SelaError.d.ts.map +1 -0
  42. package/dist/errors/SelaError.js +154 -0
  43. package/dist/fixtures/expectProxy.d.ts +1 -1
  44. package/dist/fixtures/expectProxy.d.ts.map +1 -1
  45. package/dist/fixtures/expectProxy.js +17 -10
  46. package/dist/fixtures/index.d.ts +7 -1
  47. package/dist/fixtures/index.d.ts.map +1 -1
  48. package/dist/fixtures/index.js +26 -11
  49. package/dist/fixtures/proxyTag.d.ts +12 -0
  50. package/dist/fixtures/proxyTag.d.ts.map +1 -0
  51. package/dist/fixtures/proxyTag.js +45 -0
  52. package/dist/index.d.ts +8 -6
  53. package/dist/index.d.ts.map +1 -1
  54. package/dist/index.js +9 -4
  55. package/dist/reporter/SelaReporter.d.ts +143 -0
  56. package/dist/reporter/SelaReporter.d.ts.map +1 -0
  57. package/dist/reporter/SelaReporter.js +428 -0
  58. package/dist/services/ASTSourceUpdater.d.ts +20 -1
  59. package/dist/services/ASTSourceUpdater.d.ts.map +1 -1
  60. package/dist/services/ASTSourceUpdater.js +257 -89
  61. package/dist/services/ArgumentTypeAnalyzer.js +7 -7
  62. package/dist/services/BlastRadiusAnalyzer.d.ts.map +1 -1
  63. package/dist/services/BlastRadiusAnalyzer.js +5 -2
  64. package/dist/services/ChainValidator.js +22 -22
  65. package/dist/services/CrossFileHealer.js +7 -7
  66. package/dist/services/DefinitionTracer.d.ts +1 -1
  67. package/dist/services/DefinitionTracer.d.ts.map +1 -1
  68. package/dist/services/DefinitionTracer.js +28 -13
  69. package/dist/services/DnaEditorService.d.ts +2 -2
  70. package/dist/services/DnaEditorService.d.ts.map +1 -1
  71. package/dist/services/DnaEditorService.js +81 -62
  72. package/dist/services/DnaIndexService.d.ts +1 -1
  73. package/dist/services/DnaIndexService.d.ts.map +1 -1
  74. package/dist/services/DnaIndexService.js +14 -14
  75. package/dist/services/HealReportService.d.ts +43 -1
  76. package/dist/services/HealReportService.d.ts.map +1 -1
  77. package/dist/services/HealReportService.js +108 -25
  78. package/dist/services/HealingAdvisory.js +1 -1
  79. package/dist/services/HealingCacheService.d.ts +135 -0
  80. package/dist/services/HealingCacheService.d.ts.map +1 -0
  81. package/dist/services/HealingCacheService.js +460 -0
  82. package/dist/services/HealthReportService.d.ts +1 -1
  83. package/dist/services/HealthReportService.d.ts.map +1 -1
  84. package/dist/services/HealthReportService.js +25 -25
  85. package/dist/services/InitializerUpdater.d.ts.map +1 -1
  86. package/dist/services/InitializerUpdater.js +20 -1
  87. package/dist/services/IntentAuditor.d.ts +18 -1
  88. package/dist/services/IntentAuditor.d.ts.map +1 -1
  89. package/dist/services/IntentAuditor.js +50 -26
  90. package/dist/services/InteractiveReview.d.ts +15 -0
  91. package/dist/services/InteractiveReview.d.ts.map +1 -0
  92. package/dist/services/InteractiveReview.js +125 -0
  93. package/dist/services/LLMService.d.ts +18 -2
  94. package/dist/services/LLMService.d.ts.map +1 -1
  95. package/dist/services/LLMService.js +33 -23
  96. package/dist/services/PRAutomationService.d.ts +14 -1
  97. package/dist/services/PRAutomationService.d.ts.map +1 -1
  98. package/dist/services/PRAutomationService.js +287 -92
  99. package/dist/services/PendingPromptLedger.d.ts +44 -0
  100. package/dist/services/PendingPromptLedger.d.ts.map +1 -0
  101. package/dist/services/PendingPromptLedger.js +180 -0
  102. package/dist/services/ReportGenerator.d.ts +70 -0
  103. package/dist/services/ReportGenerator.d.ts.map +1 -0
  104. package/dist/services/ReportGenerator.js +191 -0
  105. package/dist/services/SafetyGuard.d.ts +30 -7
  106. package/dist/services/SafetyGuard.d.ts.map +1 -1
  107. package/dist/services/SafetyGuard.js +85 -116
  108. package/dist/services/SnapshotService.js +5 -5
  109. package/dist/services/SourceLinkService.d.ts +2 -2
  110. package/dist/services/SourceLinkService.d.ts.map +1 -1
  111. package/dist/services/SourceLinkService.js +31 -22
  112. package/dist/services/SourceUpdater.d.ts +1 -1
  113. package/dist/services/SourceUpdater.d.ts.map +1 -1
  114. package/dist/services/SourceUpdater.js +66 -34
  115. package/dist/services/TemplateDiffService.d.ts.map +1 -1
  116. package/dist/services/TemplateDiffService.js +18 -15
  117. package/dist/services/WorkspaceSnapshotService.d.ts +71 -0
  118. package/dist/services/WorkspaceSnapshotService.d.ts.map +1 -0
  119. package/dist/services/WorkspaceSnapshotService.js +200 -0
  120. package/dist/types/index.d.ts +1 -1
  121. package/dist/utils/IsolatedDiff.d.ts +45 -0
  122. package/dist/utils/IsolatedDiff.d.ts.map +1 -0
  123. package/dist/utils/IsolatedDiff.js +382 -0
  124. 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
  // ─────────────────────────────────────────────────────────────────
@@ -293,21 +310,28 @@ function updateTemplateLiteralPreservingVars(line, oldSelector, newSelector, dom
293
310
  // GIT HELPER (used by branch strategy and autoCommit)
294
311
  //
295
312
  // Design goals:
296
- // • Lean no stash, no branch-switching, no complex state management.
297
- // • Commit-based write first (inPlace), then commit. If git fails,
313
+ // • Lean - no stash, no branch-switching, no complex state management.
314
+ // • Commit-based - write first (inPlace), then commit. If git fails,
298
315
  // the fix is still on disk (silent inPlace fallback).
299
- // • Detached HEAD aware skips silently in CI environments that check
316
+ // • Detached HEAD aware - skips silently in CI environments that check
300
317
  // out a specific SHA without a branch ref.
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 {
307
331
  (0, child_process_1.execSync)("git symbolic-ref HEAD", { stdio: "pipe" });
308
332
  }
309
333
  catch {
310
- console.warn(`[SourceUpdater] ⚠️ Detached HEAD git operations skipped (fix is saved to disk)`);
334
+ console.warn(`[SourceUpdater] ⚠️ Detached HEAD - git operations skipped (fix is saved to disk)`);
311
335
  return;
312
336
  }
313
337
  if (createBranch) {
@@ -322,7 +346,7 @@ function runGitOperations(absoluteFilePath, createBranch, relativeFilePath) {
322
346
  console.log(`[SourceUpdater] ✅ Auto-committed fix in: ${filename}`);
323
347
  }
324
348
  catch (err) {
325
- console.warn(`[SourceUpdater] ⚠️ Git operation failed: ${err.message} fix written to disk (inPlace fallback)`);
349
+ console.warn(`[SourceUpdater] ⚠️ Git operation failed: ${err.message} - fix written to disk (inPlace fallback)`);
326
350
  }
327
351
  }
328
352
  class SourceUpdater {
@@ -352,7 +376,7 @@ class SourceUpdater {
352
376
  return result;
353
377
  }
354
378
  // ─────────────────────────────────────────────────────────────
355
- // commentOnly helper inserts a TODO comment above the failing
379
+ // commentOnly helper - inserts a TODO comment above the failing
356
380
  // call site and returns success:false so the engine knows the
357
381
  // source was NOT rewritten (no retry suppression needed).
358
382
  // ─────────────────────────────────────────────────────────────
@@ -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,
@@ -378,12 +402,12 @@ class SourceUpdater {
378
402
  catch (err) {
379
403
  return {
380
404
  success: false,
381
- reason: `COMMENT_ONLY: write failed ${err.message}`,
405
+ reason: `COMMENT_ONLY: write failed - ${err.message}`,
382
406
  };
383
407
  }
384
408
  }
385
409
  // ─────────────────────────────────────────────────────────────
386
- // CORE UPDATE Layered Healing (AST primary, Regex fallback)
410
+ // CORE UPDATE - Layered Healing (AST primary, Regex fallback)
387
411
  // Called by update(); never call directly from outside the class.
388
412
  // ─────────────────────────────────────────────────────────────
389
413
  static _coreUpdate(caller, oldSelector, newSelector, domContext, dnaAttrHint, fullSelectorContext, aiSegments, contentChange, smartChainSegments, originalChainHint) {
@@ -418,7 +442,7 @@ class SourceUpdater {
418
442
  if (astResult.success) {
419
443
  console.log(`[SourceUpdater] ✅ AST Strategy ${astResult.strategy} SUCCESS` +
420
444
  (astResult.lineUpdated !== undefined
421
- ? ` line ${astResult.lineUpdated + 1}`
445
+ ? ` - line ${astResult.lineUpdated + 1}`
422
446
  : ""));
423
447
  return {
424
448
  success: true,
@@ -434,13 +458,21 @@ class SourceUpdater {
434
458
  // PROPERTY_ACCESS / TEMPLATE_LITERAL) but could not safely resolve the new
435
459
  // value. Regex cannot do better and will corrupt the definition site.
436
460
  if (astResult.strategy === "semantic-guard") {
437
- console.warn(`[SourceUpdater] 🛑 Regex blocked semantic guard fired. ` +
461
+ console.warn(`[SourceUpdater] 🛑 Regex blocked - semantic guard fired. ` +
438
462
  `Source unchanged. Reason: ${astResult.reason}`);
439
463
  return { success: false, reason: astResult.reason };
440
464
  }
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 ===
473
+ SelaError_1.AST_UPDATER_CODES.HEAL_DISABLED_BY_DIRECTIVE) {
474
+ throw astError;
475
+ }
444
476
  console.warn(`[SourceUpdater] ⚠️ AST engine error: ${astError.message}. Falling back to Regex`);
445
477
  }
446
478
  // ── 2. Regex Fallback ──────────────────────────────────────────
@@ -448,11 +480,11 @@ class SourceUpdater {
448
480
  const raw = fs.readFileSync(filePath, "utf8");
449
481
  const lines = raw.split(/\r?\n/);
450
482
  if (caller.line <= 0) {
451
- console.log(`[SourceUpdater] ⚠️ line ${caller.line} invalid running Regex Strategy G only`);
483
+ console.log(`[SourceUpdater] ⚠️ line ${caller.line} invalid - running Regex Strategy G only`);
452
484
  if (contentChange) {
453
485
  const assertResult = SourceUpdater.strategyAssertionHealing(lines, 1, contentChange, true);
454
486
  if (assertResult.success) {
455
- fs.writeFileSync(filePath, lines.join("\n"), "utf8");
487
+ persistFile(filePath, lines.join("\n"));
456
488
  console.log(`[SourceUpdater] ✅ Regex Strategy G (Assertion Healing) SUCCESS`);
457
489
  return assertResult;
458
490
  }
@@ -479,8 +511,8 @@ class SourceUpdater {
479
511
  const layeredResult = SourceUpdater.strategyLayeredHealing(lines, callerLine, oldSelector, aiSegments, fullSelectorContext);
480
512
  if (layeredResult.success) {
481
513
  tryAssertionHealing();
482
- fs.writeFileSync(filePath, lines.join("\n"), "utf8");
483
- console.log(`[SourceUpdater] ✅ Strategy 0 (Layered Healing) SUCCESS line ${layeredResult.lineUpdated + 1}`);
514
+ persistFile(filePath, lines.join("\n"));
515
+ console.log(`[SourceUpdater] ✅ Strategy 0 (Layered Healing) SUCCESS - line ${layeredResult.lineUpdated + 1}`);
484
516
  return layeredResult;
485
517
  }
486
518
  }
@@ -488,7 +520,7 @@ class SourceUpdater {
488
520
  const chainCollapseResult = SourceUpdater.strategyChainCollapse_Regex(lines, callerLine, oldSelector, effectiveNewSelector);
489
521
  if (chainCollapseResult.success) {
490
522
  tryAssertionHealing();
491
- fs.writeFileSync(filePath, lines.join("\n"), "utf8");
523
+ persistFile(filePath, lines.join("\n"));
492
524
  console.log(`[SourceUpdater] ✅ Strategy F (Chain Collapse) SUCCESS`);
493
525
  return chainCollapseResult;
494
526
  }
@@ -496,7 +528,7 @@ class SourceUpdater {
496
528
  const chainResult = SourceUpdater.strategyChainCollapse(lines, callerLine, oldSelector, effectiveNewSelector);
497
529
  if (chainResult.success) {
498
530
  tryAssertionHealing();
499
- fs.writeFileSync(filePath, lines.join("\n"), "utf8");
531
+ persistFile(filePath, lines.join("\n"));
500
532
  console.log(`[SourceUpdater] ✅ Strategy 1 (Chain Collapse) SUCCESS`);
501
533
  return chainResult;
502
534
  }
@@ -504,7 +536,7 @@ class SourceUpdater {
504
536
  const directResult = SourceUpdater.strategyDirectLiteral(lines, callerLine, oldSelector, effectiveNewSelector);
505
537
  if (directResult.success) {
506
538
  tryAssertionHealing();
507
- fs.writeFileSync(filePath, lines.join("\n"), "utf8");
539
+ persistFile(filePath, lines.join("\n"));
508
540
  console.log(`[SourceUpdater] ✅ Strategy 2 (Direct Literal) SUCCESS`);
509
541
  return directResult;
510
542
  }
@@ -512,7 +544,7 @@ class SourceUpdater {
512
544
  const varResult = SourceUpdater.strategyUpstreamVariable(lines, callerLine, oldSelector, effectiveNewSelector);
513
545
  if (varResult.success) {
514
546
  tryAssertionHealing();
515
- fs.writeFileSync(filePath, lines.join("\n"), "utf8");
547
+ persistFile(filePath, lines.join("\n"));
516
548
  console.log(`[SourceUpdater] ✅ Strategy 3 (Upstream Variable) SUCCESS`);
517
549
  return varResult;
518
550
  }
@@ -520,7 +552,7 @@ class SourceUpdater {
520
552
  const chainDedupResult = SourceUpdater.strategyChainedLocator(lines, callerLine, oldSelector, effectiveNewSelector);
521
553
  if (chainDedupResult.success) {
522
554
  tryAssertionHealing();
523
- fs.writeFileSync(filePath, lines.join("\n"), "utf8");
555
+ persistFile(filePath, lines.join("\n"));
524
556
  console.log(`[SourceUpdater] ✅ Strategy 4 (Chained Dedup) SUCCESS`);
525
557
  return chainDedupResult;
526
558
  }
@@ -528,7 +560,7 @@ class SourceUpdater {
528
560
  const fnResult = SourceUpdater.strategyFunctionReturn(lines, callerLine, oldSelector, effectiveNewSelector, domContext, dnaAttrHint);
529
561
  if (fnResult.success) {
530
562
  tryAssertionHealing();
531
- fs.writeFileSync(filePath, lines.join("\n"), "utf8");
563
+ persistFile(filePath, lines.join("\n"));
532
564
  console.log(`[SourceUpdater] ✅ Strategy 5 (Function Return) SUCCESS`);
533
565
  return fnResult;
534
566
  }
@@ -536,7 +568,7 @@ class SourceUpdater {
536
568
  const scanResult = SourceUpdater.strategyGlobalScan(lines, oldSelector, effectiveNewSelector);
537
569
  if (scanResult.success) {
538
570
  tryAssertionHealing();
539
- fs.writeFileSync(filePath, lines.join("\n"), "utf8");
571
+ persistFile(filePath, lines.join("\n"));
540
572
  console.log(`[SourceUpdater] ✅ Strategy 6 (Global Scan) SUCCESS`);
541
573
  return scanResult;
542
574
  }
@@ -544,7 +576,7 @@ class SourceUpdater {
544
576
  if (contentChange) {
545
577
  const assertResult = SourceUpdater.strategyAssertionHealing(lines, callerLine, contentChange);
546
578
  if (assertResult.success) {
547
- fs.writeFileSync(filePath, lines.join("\n"), "utf8");
579
+ persistFile(filePath, lines.join("\n"));
548
580
  console.log(`[SourceUpdater] ✅ Strategy G (Assertion Healing) SUCCESS`);
549
581
  return assertResult;
550
582
  }
@@ -556,7 +588,7 @@ class SourceUpdater {
556
588
  };
557
589
  }
558
590
  // ─────────────────────────────────────────────────────────────
559
- // STRATEGY F (Regex) Full Chain Collapsing
591
+ // STRATEGY F (Regex) - Full Chain Collapsing
560
592
  // ─────────────────────────────────────────────────────────────
561
593
  static strategyChainCollapse_Regex(lines, callerLine, oldSelector, newSelector) {
562
594
  console.log(`[Strategy-F] ⛓️ Starting Regex Chain Collapse...`);
@@ -607,7 +639,7 @@ class SourceUpdater {
607
639
  return { success: false, reason: "chain detected but could not replace" };
608
640
  }
609
641
  // ─────────────────────────────────────────────────────────────
610
- // STRATEGY G (Regex) Assertion Healing
642
+ // STRATEGY G (Regex) - Assertion Healing
611
643
  // ─────────────────────────────────────────────────────────────
612
644
  static strategyAssertionHealing(lines, callerLine, contentChange, wholeFile = false) {
613
645
  const { oldText, newText } = contentChange;
@@ -662,7 +694,7 @@ class SourceUpdater {
662
694
  return { success: false, reason: "no assertions to heal" };
663
695
  }
664
696
  // ─────────────────────────────────────────────────────────────
665
- // STRATEGY 0 Layered Healing (Upstream Variable Tracing)
697
+ // STRATEGY 0 - Layered Healing (Upstream Variable Tracing)
666
698
  // ─────────────────────────────────────────────────────────────
667
699
  static strategyLayeredHealing(lines, callerLine, oldElementSelector, aiSegments, fullSelectorContext) {
668
700
  console.log(`[SourceUpdater] 🧠 Strategy 0: Starting Layered Healing...`);
@@ -715,7 +747,7 @@ class SourceUpdater {
715
747
  return null;
716
748
  }
717
749
  // ─────────────────────────────────────────────────────────────
718
- // STRATEGY 1 Chain Collapse (leaf-only precision replacement)
750
+ // STRATEGY 1 - Chain Collapse (leaf-only precision replacement)
719
751
  // ─────────────────────────────────────────────────────────────
720
752
  static strategyChainCollapse(lines, callerLine, oldSelector, newSelector) {
721
753
  const oldLeaf = oldSelector
@@ -757,7 +789,7 @@ class SourceUpdater {
757
789
  };
758
790
  }
759
791
  // ─────────────────────────────────────────────────────────────
760
- // STRATEGY 2 Direct Literal
792
+ // STRATEGY 2 - Direct Literal
761
793
  // ─────────────────────────────────────────────────────────────
762
794
  static strategyDirectLiteral(lines, callerLine, oldSelector, newSelector) {
763
795
  const windowSize = 10;
@@ -790,7 +822,7 @@ class SourceUpdater {
790
822
  return { success: false, reason: "no direct literal" };
791
823
  }
792
824
  // ─────────────────────────────────────────────────────────────
793
- // STRATEGY 3 Upstream Variable/Const
825
+ // STRATEGY 3 - Upstream Variable/Const
794
826
  // ─────────────────────────────────────────────────────────────
795
827
  static strategyUpstreamVariable(lines, callerLine, oldSelector, newSelector) {
796
828
  const callBlock = lines
@@ -827,7 +859,7 @@ class SourceUpdater {
827
859
  return { success: false, reason: "no upstream variable" };
828
860
  }
829
861
  // ─────────────────────────────────────────────────────────────
830
- // STRATEGY 4 Chained Locator Dedup
862
+ // STRATEGY 4 - Chained Locator Dedup
831
863
  // ─────────────────────────────────────────────────────────────
832
864
  static strategyChainedLocator(lines, callerLine, oldSelector, newSelector) {
833
865
  const blockStart = Math.max(0, callerLine - 10);
@@ -854,7 +886,7 @@ class SourceUpdater {
854
886
  return { success: false, reason: "dedup found but literal not replaced" };
855
887
  }
856
888
  // ─────────────────────────────────────────────────────────────
857
- // STRATEGY 5 Function/Template with Variable Preservation
889
+ // STRATEGY 5 - Function/Template with Variable Preservation
858
890
  // ─────────────────────────────────────────────────────────────
859
891
  static strategyFunctionReturn(lines, callerLine, oldSelector, newSelector, domContext, dnaAttrHint) {
860
892
  const callBlock = lines
@@ -939,7 +971,7 @@ class SourceUpdater {
939
971
  return { success: false, reason: "function strategy exhausted" };
940
972
  }
941
973
  // ─────────────────────────────────────────────────────────────
942
- // STRATEGY 6 Global Scan
974
+ // STRATEGY 6 - Global Scan
943
975
  // ─────────────────────────────────────────────────────────────
944
976
  static strategyGlobalScan(lines, oldSelector, newSelector) {
945
977
  const lastOldPart = oldSelector.split(">>").pop().trim();
@@ -1008,7 +1040,7 @@ class SourceUpdater {
1008
1040
  if (pattern.test(lines[i])) {
1009
1041
  const safeNew = sanitizeForInjection(newArgument, q);
1010
1042
  lines[i] = lines[i].replace(pattern, `$1${safeNew}$2`);
1011
- fs.writeFileSync(filePath, lines.join("\n"), "utf8");
1043
+ persistFile(filePath, lines.join("\n"));
1012
1044
  return { success: true, reason: "argument updated", lineUpdated: i };
1013
1045
  }
1014
1046
  }
@@ -1016,7 +1048,7 @@ class SourceUpdater {
1016
1048
  if (pattern.test(lines[i])) {
1017
1049
  const safeNew = sanitizeForInjection(newArgument, q);
1018
1050
  lines[i] = lines[i].replace(pattern, `$1${safeNew}$2`);
1019
- fs.writeFileSync(filePath, lines.join("\n"), "utf8");
1051
+ persistFile(filePath, lines.join("\n"));
1020
1052
  return { success: true, reason: "argument updated", lineUpdated: i };
1021
1053
  }
1022
1054
  }
@@ -1 +1 @@
1
- {"version":3,"file":"TemplateDiffService.d.ts","sourceRoot":"","sources":["../../src/services/TemplateDiffService.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,kBAAkB,EAAc,MAAM,UAAU,CAAC;AAMhE,MAAM,MAAM,eAAe,GACvB;IACE,EAAE,EAAE,IAAI,CAAC;IACT,mFAAmF;IACnF,eAAe,EAAE,MAAM,CAAC;IACxB,0DAA0D;IAC1D,qBAAqB,EAAE,MAAM,EAAE,CAAC;CACjC,GACD;IACE,EAAE,EAAE,KAAK,CAAC;IACV,MAAM,EAAE,MAAM,CAAC;IACf;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;CAC1B,CAAC;AA6BN,qBAAa,mBAAmB;IAW9B,MAAM,CAAC,YAAY,CACjB,YAAY,EAAE,MAAM,EACpB,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,GAClB,eAAe;IA6GlB,MAAM,CAAC,gBAAgB,CACrB,YAAY,EAAE,kBAAkB,EAChC,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,GAClB,eAAe;IAoFlB,MAAM,CAAC,qBAAqB,CAC1B,YAAY,EAAE,kBAAkB,EAChC,QAAQ,EAAE,MAAM,GACf,MAAM,EAAE,GAAG,IAAI;CASnB;AAgJD,6DAA6D;AAC7D,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAKtD"}
1
+ {"version":3,"file":"TemplateDiffService.d.ts","sourceRoot":"","sources":["../../src/services/TemplateDiffService.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,kBAAkB,EAAc,MAAM,UAAU,CAAC;AAMhE,MAAM,MAAM,eAAe,GACvB;IACE,EAAE,EAAE,IAAI,CAAC;IACT,mFAAmF;IACnF,eAAe,EAAE,MAAM,CAAC;IACxB,0DAA0D;IAC1D,qBAAqB,EAAE,MAAM,EAAE,CAAC;CACjC,GACD;IACE,EAAE,EAAE,KAAK,CAAC;IACV,MAAM,EAAE,MAAM,CAAC;IACf;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;CAC1B,CAAC;AA6BN,qBAAa,mBAAmB;IAW9B,MAAM,CAAC,YAAY,CACjB,YAAY,EAAE,MAAM,EACpB,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,GAClB,eAAe;IAiHlB,MAAM,CAAC,gBAAgB,CACrB,YAAY,EAAE,kBAAkB,EAChC,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,GAClB,eAAe;IAoFlB,MAAM,CAAC,qBAAqB,CAC1B,YAAY,EAAE,kBAAkB,EAChC,QAAQ,EAAE,MAAM,GACf,MAAM,EAAE,GAAG,IAAI;CASnB;AAgJD,6DAA6D;AAC7D,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAKtD"}
@@ -5,13 +5,13 @@ exports.escapeForTemplate = escapeForTemplate;
5
5
  // ═══════════════════════════════════════════════════════════════════
6
6
  // TemplateDiffService
7
7
  //
8
- // T-8: String-based API diffAndPatch()
8
+ // T-8: String-based API - diffAndPatch()
9
9
  // Given a template source like `input[data-role="${role}_legacy"]`,
10
10
  // its resolved old string, and an AI-proposed new string, compute
11
11
  // the patched template that preserves ${...} spans while updating
12
12
  // only the changed static segments.
13
13
  //
14
- // T-8.1: Node-based API computeNodePatch()
14
+ // T-8.1: Node-based API - computeNodePatch()
15
15
  // Wraps diffAndPatch() using the TemplateExpression AST node as the
16
16
  // source of truth (avoids text-parsing ambiguity). Returns the full
17
17
  // patched template text for use with templateNode.replaceWithText().
@@ -34,7 +34,7 @@ class TemplateDiffService {
34
34
  const exprSegs = segments
35
35
  .filter((s) => s.type === "expr")
36
36
  .map((s) => s.text);
37
- // No spans the "template" is just a plain string.
37
+ // No spans - the "template" is just a plain string.
38
38
  if (exprSegs.length === 0) {
39
39
  if (resolvedOld !== resolvedNew) {
40
40
  return {
@@ -43,18 +43,21 @@ class TemplateDiffService {
43
43
  changedSegmentIndices: [0],
44
44
  };
45
45
  }
46
- return { ok: false, reason: "resolvedOld === resolvedNew — no change needed" };
46
+ return {
47
+ ok: false,
48
+ reason: "resolvedOld === resolvedNew - no change needed",
49
+ };
47
50
  }
48
51
  // Extract span values from resolvedOld using static segments as anchors.
49
52
  const spanValues = extractSpanValues(staticSegs, resolvedOld);
50
53
  if (!spanValues) {
51
54
  return {
52
55
  ok: false,
53
- reason: "template static segments don't align with resolvedOld possible desync. " +
56
+ reason: "template static segments don't align with resolvedOld - possible desync. " +
54
57
  `template="${templateText}", resolvedOld="${resolvedOld}"`,
55
58
  };
56
59
  }
57
- // Verify reconstruction extractSpanValues already guarantees this
60
+ // Verify reconstruction - extractSpanValues already guarantees this
58
61
  // implicitly, but an explicit check catches any edge-case bugs early.
59
62
  {
60
63
  let check = "";
@@ -76,13 +79,13 @@ class TemplateDiffService {
76
79
  return {
77
80
  ok: false,
78
81
  reason: `dynamic span value(s) [${spanValues.map((v) => `"${v}"`).join(", ")}] ` +
79
- `not found in resolvedNew="${resolvedNew}" structural change, manual review required`,
82
+ `not found in resolvedNew="${resolvedNew}" - structural change, manual review required`,
80
83
  };
81
84
  }
82
85
  if (newStaticSegs.length !== staticSegs.length) {
83
86
  return {
84
87
  ok: false,
85
- reason: "static segment count mismatch after alignment structural change",
88
+ reason: "static segment count mismatch after alignment - structural change",
86
89
  };
87
90
  }
88
91
  // Diff: find which static segments changed.
@@ -94,14 +97,14 @@ class TemplateDiffService {
94
97
  if (changedIndices.length === 0) {
95
98
  return {
96
99
  ok: false,
97
- reason: "no static segment changed the difference is in a dynamic span. " +
100
+ reason: "no static segment changed - the difference is in a dynamic span. " +
98
101
  "Update the argument passed to the function instead.",
99
102
  };
100
103
  }
101
104
  if (changedIndices.length > 1) {
102
105
  return {
103
106
  ok: false,
104
- reason: `${changedIndices.length} static segments changed simultaneously ` +
107
+ reason: `${changedIndices.length} static segments changed simultaneously - ` +
105
108
  "ambiguous, manual review required",
106
109
  };
107
110
  }
@@ -119,7 +122,7 @@ class TemplateDiffService {
119
122
  //
120
123
  // Uses the TemplateExpression AST node as the authoritative source
121
124
  // of static/expr segments (avoids text-parsing edge cases).
122
- // Returns `patchedText` pass directly to templateNode.replaceWithText().
125
+ // Returns `patchedText` - pass directly to templateNode.replaceWithText().
123
126
  // ─────────────────────────────────────────────────────────────────
124
127
  static computeNodePatch(templateNode, resolvedOld, resolvedNew) {
125
128
  // Extract static segments and expr texts directly from the AST.
@@ -172,13 +175,13 @@ class TemplateDiffService {
172
175
  if (changedIndices.length === 0) {
173
176
  return {
174
177
  ok: false,
175
- reason: "difference is in dynamic span update call-site argument",
178
+ reason: "difference is in dynamic span - update call-site argument",
176
179
  };
177
180
  }
178
181
  if (changedIndices.length > 1) {
179
182
  return {
180
183
  ok: false,
181
- reason: `${changedIndices.length} static segments changed ambiguous`,
184
+ reason: `${changedIndices.length} static segments changed - ambiguous`,
182
185
  };
183
186
  }
184
187
  const patchedStatics = [...staticSegs];
@@ -238,7 +241,7 @@ function parseTemplateSource(text) {
238
241
  *
239
242
  * Algorithm: iterate over all static segments in order. After consuming
240
243
  * each static segment, find the next static as the delimiter for the
241
- * span value then advance pos to the start of that next static so
244
+ * span value - then advance pos to the start of that next static so
242
245
  * the following iteration can consume it. This ensures pos is always
243
246
  * consistent at the start of each static segment.
244
247
  *
@@ -265,7 +268,7 @@ function extractSpanValues(staticSegments, resolved) {
265
268
  break;
266
269
  }
267
270
  if (nextStatic === "") {
268
- // Empty non-trailing static: adjacent spans boundary is ambiguous.
271
+ // Empty non-trailing static: adjacent spans - boundary is ambiguous.
269
272
  return null;
270
273
  }
271
274
  const delimIdx = resolved.indexOf(nextStatic, pos);
@@ -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;IAOjC,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"}