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
@@ -36,15 +36,21 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.SelaEngine = void 0;
37
37
  const fs = __importStar(require("fs"));
38
38
  const path = __importStar(require("path"));
39
- const child_process_1 = require("child_process");
40
39
  const LLMService_1 = require("../services/LLMService");
40
+ const WorkspaceSnapshotService_1 = require("../services/WorkspaceSnapshotService");
41
+ const IsolatedDiff_1 = require("../utils/IsolatedDiff");
41
42
  const SnapshotService_1 = require("../services/SnapshotService");
42
43
  const DOMUtils_1 = require("../utils/DOMUtils");
43
44
  const SourceUpdater_1 = require("../services/SourceUpdater");
45
+ const ASTSourceUpdater_1 = require("../services/ASTSourceUpdater");
44
46
  const SafetyGuard_1 = require("../services/SafetyGuard");
47
+ const HealingCacheService_1 = require("../services/HealingCacheService");
45
48
  const ConfigLoader_1 = require("../config/ConfigLoader");
49
+ const DryRunGuard_1 = require("../config/DryRunGuard");
46
50
  const HealReportService_1 = require("../services/HealReportService");
47
51
  const PRAutomationService_1 = require("../services/PRAutomationService");
52
+ const PendingPromptLedger_1 = require("../services/PendingPromptLedger");
53
+ const SelaError_1 = require("../errors/SelaError");
48
54
  /**
49
55
  * Graceful percentage formatter for engine-side log lines. Mirrors the helper
50
56
  * in PRAutomationService / HealReportService so a missing AI/auditor
@@ -53,10 +59,25 @@ const PRAutomationService_1 = require("../services/PRAutomationService");
53
59
  function fmtPctEngine(v) {
54
60
  return typeof v === "number" && Number.isFinite(v) ? `${v}%` : "n/a";
55
61
  }
62
+ /**
63
+ * Truthy when the current process is a CI runner. We accept the standard
64
+ * `CI=1`/`CI=true` convention used by GitHub Actions, GitLab, CircleCI,
65
+ * Buildkite, etc. Anything else (empty string, undefined, `0`, `false`)
66
+ * is treated as Local DX mode.
67
+ */
68
+ function isCiEnvironment() {
69
+ const raw = process.env.CI;
70
+ if (raw === undefined || raw === null)
71
+ return false;
72
+ const v = String(raw).trim().toLowerCase();
73
+ return v !== "" && v !== "0" && v !== "false";
74
+ }
56
75
  class SelaEngine {
57
76
  llmService;
58
77
  snapshotService;
59
78
  safetyGuard;
79
+ astDirectiveScanner;
80
+ healingCache;
60
81
  config;
61
82
  dnaBuffer = new Map();
62
83
  healMetaBuffer = new Map();
@@ -66,6 +87,14 @@ class SelaEngine {
66
87
  this.llmService = new LLMService_1.LLMService();
67
88
  this.snapshotService = new SnapshotService_1.SnapshotService();
68
89
  this.safetyGuard = new SafetyGuard_1.SafetyGuard(this.config.thresholds);
90
+ // Dedicated lightweight ts-morph project used only for the leading-comment
91
+ // pre-scan. Keeping it separate from the SourceUpdater's project avoids
92
+ // polluting that cache with files we may never actually mutate.
93
+ this.astDirectiveScanner = new ASTSourceUpdater_1.ASTSourceUpdater();
94
+ // Local healing cache — short-circuits the LLM call when an identical
95
+ // (filePath, oldSelector) pair was healed safely in a prior session.
96
+ // Disabled when SELA_NO_CACHE=1; reads/writes skipped during dry runs.
97
+ this.healingCache = HealingCacheService_1.sharedHealingCache;
69
98
  }
70
99
  // ─────────────────────────────────────────────────────────────
71
100
  // healArgument — repairs a wrong action argument (e.g. selectOption)
@@ -76,10 +105,10 @@ class SelaEngine {
76
105
  const { dom } = await DOMUtils_1.DOMUtils.getNeighborhoodDom(page, selector);
77
106
  const neighborhoodDom = dom;
78
107
  const targetIntent = action === "selectOption"
79
- ? `ARGUMENT_HEALING: The selectOption argument "${oldArgument}" does not exist.
80
- Look at the <select> element in the DOM and find all <option> elements inside it.
81
- Return in 'new_selector' ONLY the TEXT CONTENT of the closest matching <option>.
82
- For example: if the options are "Male" and "Female" and the old argument was "Man", return "Male".
108
+ ? `ARGUMENT_HEALING: The selectOption argument "${oldArgument}" does not exist.
109
+ Look at the <select> element in the DOM and find all <option> elements inside it.
110
+ Return in 'new_selector' ONLY the TEXT CONTENT of the closest matching <option>.
111
+ For example: if the options are "Male" and "Female" and the old argument was "Man", return "Male".
83
112
  CRITICAL: Do NOT return a CSS selector like "#my-select". Return plain text only, exactly as it appears in the option.`
84
113
  : `Fix the broken argument "${oldArgument}" for action "${action}". Return the correct value.`;
85
114
  const aiFix = await this.llmService.getFix({
@@ -108,6 +137,37 @@ CRITICAL: Do NOT return a CSS selector like "#my-select". Return plain text only
108
137
  // Resolve file path for report purposes (project-relative when possible).
109
138
  const reportFile = this._toReportFile(filePath);
110
139
  const testTitle = this.testTitleBuffer.get(stableId);
140
+ // ── Developer fail-fast directive pre-gate ──────────────────────
141
+ // Scan the failing statement's leading comments for `// sela-fail-fast`
142
+ // BEFORE the expensive LLM round-trip. When present, record a
143
+ // "Skipped by Developer" FailedEvent and propagate the canonical
144
+ // SelaError(AST_HEAL_DISABLED_BY_DIRECTIVE) so retries are skipped too.
145
+ try {
146
+ this.astDirectiveScanner.enforceFailFastDirective({ filePath, line });
147
+ }
148
+ catch (directiveErr) {
149
+ if ((0, SelaError_1.isSelaErrorFrom)(directiveErr, "ASTUpdater") &&
150
+ directiveErr.code === SelaError_1.AST_UPDATER_CODES.HEAL_DISABLED_BY_DIRECTIVE) {
151
+ console.warn(`[Sela] 🛑 Skipped by Developer — ${directiveErr.reason}`);
152
+ const skipEvent = {
153
+ kind: "FAILED",
154
+ stableId,
155
+ sourceFile: reportFile,
156
+ sourceLine: line,
157
+ testTitle,
158
+ timestamp: new Date().toISOString(),
159
+ framePath: [],
160
+ inIframe: false,
161
+ inShadowDom: false,
162
+ oldSelector: elementSelectorOnly,
163
+ reason: "Skipped by Developer (sela-fail-fast directive)",
164
+ dnaBefore: null,
165
+ };
166
+ HealReportService_1.sharedHealReport.record(skipEvent);
167
+ throw directiveErr;
168
+ }
169
+ throw directiveErr;
170
+ }
111
171
  // Snapshot for report event context (loaded once below, used at all exit paths).
112
172
  let lastKnownStateForReport = null;
113
173
  let aiFixForReport = null;
@@ -134,19 +194,19 @@ CRITICAL: Do NOT return a CSS selector like "#my-select". Return plain text only
134
194
  const intentDescription = lastKnownState
135
195
  ? `The element that was a ${lastKnownState.tagName} with text "${lastKnownState.text}"`
136
196
  : `Element identified by ${stableId}`;
137
- const contentChangeInstructions = `
138
-
139
- [CONTENT CHANGE DETECTION]:
140
- If the element's VISIBLE TEXT has changed compared to its previous value${oldText ? ` ("${oldText}")` : ""}, you MUST include a "contentChange" field.
141
- NOTE: This change will NOT be applied automatically to the source code to prevent masking potential bugs.
142
- It will only be presented as a suggested fix in the terminal.
143
- {
144
- "contentChange": {
145
- "oldText": "<the previous visible text>",
146
- "newText": "<the new visible text as it appears in the current DOM>"
147
- }
148
- }
149
- This is required so that any expect(...).toHaveText("${oldText ?? "..."}") assertions in the test file can be automatically updated to match the new text.
197
+ const contentChangeInstructions = `
198
+
199
+ [CONTENT CHANGE DETECTION]:
200
+ If the element's VISIBLE TEXT has changed compared to its previous value${oldText ? ` ("${oldText}")` : ""}, you MUST include a "contentChange" field.
201
+ NOTE: This change will NOT be applied automatically to the source code to prevent masking potential bugs.
202
+ It will only be presented as a suggested fix in the terminal.
203
+ {
204
+ "contentChange": {
205
+ "oldText": "<the previous visible text>",
206
+ "newText": "<the new visible text as it appears in the current DOM>"
207
+ }
208
+ }
209
+ This is required so that any expect(...).toHaveText("${oldText ?? "..."}") assertions in the test file can be automatically updated to match the new text.
150
210
  If the text has NOT changed, omit the "contentChange" field entirely.`;
151
211
  // ── Semantic anchor context hint for the LLM ─────────────────────
152
212
  // When DNA v2 anchor data exists, tell the AI which label and row
@@ -171,16 +231,56 @@ If the text has NOT changed, omit the "contentChange" field entirely.`;
171
231
  "\n" +
172
232
  "Prioritize selectors that preserve these relationships (e.g. filter by row key, then target by label).");
173
233
  })();
174
- console.log(`[Sela] 🔍 Step 3: Requesting AI fix from LLM...`);
175
- const aiFix = await this.llmService.getFix({
176
- targetIntent: intentDescription,
177
- failedSelector: fullSelector,
178
- previousState: lastKnownState || {},
179
- currentDom: neighborhoodDom +
180
- contextHint +
181
- anchorContextHint +
182
- contentChangeInstructions,
183
- });
234
+ // ── Cache pre-check ────────────────────────────────────────────
235
+ // When a prior session healed this exact (filePath, oldSelector)
236
+ // pair AND SafetyGuard cleared it, replay the cached fix instead
237
+ // of paying the LLM round-trip. Disabled when SELA_NO_CACHE=1.
238
+ // The synthetic aiFix is fed through SafetyGuard exactly like a
239
+ // fresh LLM response — cache is a latency optimisation, never a
240
+ // safety bypass.
241
+ let aiFix = null;
242
+ let cacheHit = false;
243
+ // Fingerprint inputs the cache key depends on. Recomputed every
244
+ // heal so a DNA rewrite — which changes `dnaContentHash` —
245
+ // automatically invalidates every prior entry without an explicit
246
+ // purge.
247
+ const dnaContentHash = (0, HealingCacheService_1.hashDnaContent)(lastKnownState ?? null);
248
+ const cacheKeyInput = {
249
+ filePath,
250
+ line,
251
+ dnaContentHash,
252
+ brokenSelector: fullSelector,
253
+ mode: healMode,
254
+ };
255
+ if (!this.config.noCache) {
256
+ const cached = this.healingCache.get(cacheKeyInput);
257
+ if (cached) {
258
+ console.log(`[Sela] 🧊 Cache hit (0ms LLM) — replaying healed selector "${cached.healedSelector}"`);
259
+ aiFix = {
260
+ status: "FIXED",
261
+ new_selector: cached.healedSelector,
262
+ chainSegments: cached.chainSegments,
263
+ segments: cached.segments ??
264
+ [{ type: "element", selector: cached.healedSelector }],
265
+ confidence: cached.confidence,
266
+ contentChange: cached.contentChange,
267
+ explanation: cached.explanation ?? "Replayed from local healing cache",
268
+ };
269
+ cacheHit = true;
270
+ }
271
+ }
272
+ if (!cacheHit) {
273
+ console.log(`[Sela] 🔍 Step 3: Requesting AI fix from LLM...`);
274
+ aiFix = await this.llmService.getFix({
275
+ targetIntent: intentDescription,
276
+ failedSelector: fullSelector,
277
+ previousState: lastKnownState || {},
278
+ currentDom: neighborhoodDom +
279
+ contextHint +
280
+ anchorContextHint +
281
+ contentChangeInstructions,
282
+ });
283
+ }
184
284
  aiFixForReport = aiFix;
185
285
  if (aiFix && aiFix.status === "FIXED") {
186
286
  console.log(`[Sela] 🔍 Step 3: AI responded ✅`);
@@ -278,7 +378,24 @@ If the text has NOT changed, omit the "contentChange" field entirely.`;
278
378
  contentChange: aiFix.contentChange,
279
379
  };
280
380
  HealReportService_1.sharedHealReport.record(protectedEvent);
281
- throw new Error(`[SafetyGuard] ${safetyDecision.level}: ${safetyDecision.reason}`);
381
+ throw new SelaError_1.SelaError({
382
+ subsystem: "SafetyGuard",
383
+ code: safetyDecision.code ?? SelaError_1.SAFETY_GUARD_CODES.GENERIC_REJECTION,
384
+ reason: safetyDecision.reason,
385
+ context: {
386
+ filePath: reportFile,
387
+ line,
388
+ selector: elementSelectorOnly,
389
+ healedSelector: newFullSelector,
390
+ stableId,
391
+ testTitle,
392
+ branch,
393
+ safetyLevel: safetyDecision.level,
394
+ auditorVerdict: safetyDecision.meta?.auditorVerdict,
395
+ auditorConfidence: safetyDecision.meta?.auditorConfidence,
396
+ aiConfidence: aiFix.confidence,
397
+ },
398
+ });
282
399
  }
283
400
  // ── Heal-completion score summary ─────────────────────────────────
284
401
  // Emits a single consolidated line showing AI confidence plus the
@@ -312,13 +429,14 @@ If the text has NOT changed, omit the "contentChange" field entirely.`;
312
429
  (sameFileDef ? defSite?.line : undefined) ??
313
430
  line;
314
431
  const mutatedFilePath = !sameFileDef && defSite ? defSite.file : filePath;
315
- // Capture the native unified diff straight from git. This runs AFTER
316
- // SourceUpdater flushed the file to disk and BEFORE any `git add` —
317
- // so `git diff HEAD` reports the exact pending change at the real
318
- // mutated line. No in-process line-index math, no off-by-one when
319
- // JumpToDef lands above the failure site, and cross-file healing
320
- // falls out for free.
321
- const gitUnifiedDiff = this._captureGitDiff(mutatedFilePath);
432
+ // Clean-Room isolated diff. We diff the in-memory snapshot recorded
433
+ // by WorkspaceSnapshotService (contentBefore disk state right
434
+ // before Sela's first write to this file) against the post-write
435
+ // disk state. The result contains EXCLUSIVELY Sela's selector swap;
436
+ // any developer dirty edits sit identically in both buffers and
437
+ // therefore cancel out. Falls back to "" when the snapshot is
438
+ // missing (cross-file healing into an untracked path, etc.).
439
+ const gitUnifiedDiff = this._captureIsolatedDiff(mutatedFilePath);
322
440
  const firstChange = this._extractFirstChangeFromDiff(gitUnifiedDiff);
323
441
  // Backward-compat: keep oldCodeLine/newCodeLine populated from the
324
442
  // first -/+ pair inside the diff. When git produced no diff (file
@@ -368,6 +486,7 @@ If the text has NOT changed, omit the "contentChange" field entirely.`;
368
486
  framePath: successfulPath,
369
487
  inIframe: successfulPath.length > 0,
370
488
  inShadowDom: liveSnapshot?.isInShadowDom ?? false,
489
+ dryRun: DryRunGuard_1.DryRunGuard.active() ? true : undefined,
371
490
  oldSelector: elementSelectorOnly,
372
491
  newSelector: canonicalSelector,
373
492
  oldCodeLine,
@@ -396,6 +515,25 @@ If the text has NOT changed, omit the "contentChange" field entirely.`;
396
515
  contentChange: aiFix.contentChange,
397
516
  };
398
517
  HealReportService_1.sharedHealReport.record(healedEvent);
518
+ // ── Cache write — persist this safe heal for future sessions ──
519
+ // Best-effort: failures are logged inside the service and never
520
+ // block the heal return. Skipped automatically during dry runs
521
+ // and when SELA_NO_CACHE=1.
522
+ if (!this.config.noCache && !cacheHit) {
523
+ this.healingCache
524
+ .set({
525
+ ...cacheKeyInput,
526
+ healedSelector: canonicalSelector,
527
+ chainSegments: aiFix.chainSegments,
528
+ segments: aiFix.segments,
529
+ confidence: aiFix.confidence,
530
+ contentChange: aiFix.contentChange,
531
+ explanation: aiFix.explanation,
532
+ alternatives: this._buildAlternatives(aiFix.chainSegments, aiFix.segments),
533
+ safe: true,
534
+ })
535
+ .catch((err) => console.warn(`[HealingCache] ⚠️ set() failed: ${err?.message ?? err}`));
536
+ }
399
537
  return canonicalSelector;
400
538
  }
401
539
  // AI did not return FIXED — record as FAILED.
@@ -421,7 +559,11 @@ If the text has NOT changed, omit the "contentChange" field entirely.`;
421
559
  catch (error) {
422
560
  console.error(`[Sela] ❌ Healing Failure:`, error.message);
423
561
  // Avoid double-recording when a SafetyGuard PROTECTED event was already pushed.
424
- const isSafetyBlock = typeof error?.message === "string" && error.message.includes("[SafetyGuard]");
562
+ // Prefer the structured instanceof check; fall back to message-substring
563
+ // detection for any legacy throw sites that have not yet been migrated.
564
+ const isSafetyBlock = (0, SelaError_1.isSelaErrorFrom)(error, "SafetyGuard") ||
565
+ (typeof error?.message === "string" && error.message.includes("[Sela-SafetyGuard]")) ||
566
+ (typeof error?.message === "string" && error.message.includes("[SafetyGuard]"));
425
567
  const isNotFoundAlreadyRecorded = typeof error?.message === "string" && error.message === "AI could not provide a fix";
426
568
  if (!isSafetyBlock && !isNotFoundAlreadyRecorded) {
427
569
  const failedEvent = {
@@ -471,36 +613,37 @@ If the text has NOT changed, omit the "contentChange" field entirely.`;
471
613
  }
472
614
  }
473
615
  /**
474
- * Capture the native `git diff --unified=1 HEAD -- <file>` for the file the
475
- * AST mutation just wrote to. Returns an empty string when git is not
476
- * available, the file is not tracked, or any other failure — callers must
477
- * treat empty output as "no diff captured" and fall back to a direct disk
478
- * read of the mutated line.
616
+ * Clean-Room isolated diff: render a unified diff between the file's
617
+ * pre-Sela disk snapshot (`contentBefore`) and the post-write disk state
618
+ * (`contentAfter`) captured by `WorkspaceSnapshotService`. Returns "" when
619
+ * the file was never tracked (no snapshot) or the AST mutation produced
620
+ * no net change.
479
621
  *
480
- * Path safety: the absolute path is always wrapped in double quotes after
481
- * escaping any embedded double quotes, which makes the call safe on Windows
482
- * paths containing spaces or backslashes as well as POSIX paths.
622
+ * This is what makes the HTML report immune to developer dirty edits: any
623
+ * pre-existing uncommitted changes live in BOTH buffers identically and
624
+ * are therefore filtered out by the diff. The output contains only Sela's
625
+ * selector swap.
483
626
  */
484
- _captureGitDiff(filePath) {
627
+ _captureIsolatedDiff(filePath) {
485
628
  try {
486
629
  const cleaned = this._toReportFile(filePath);
487
630
  const abs = path.isAbsolute(cleaned)
488
631
  ? cleaned
489
632
  : path.resolve(process.cwd(), cleaned);
490
- if (!abs || !fs.existsSync(abs))
633
+ if (!abs)
634
+ return "";
635
+ const snap = WorkspaceSnapshotService_1.sharedWorkspaceSnapshot.getSnapshot(abs);
636
+ if (!snap || snap.contentAfter === null)
491
637
  return "";
492
- const quoted = `"${abs.replace(/"/g, '\\"')}"`;
493
- const out = (0, child_process_1.execSync)(`git diff --unified=1 HEAD -- ${quoted}`, {
494
- cwd: process.cwd(),
495
- encoding: "utf-8",
496
- stdio: ["pipe", "pipe", "pipe"],
497
- windowsHide: true,
638
+ if (snap.contentBefore === snap.contentAfter)
639
+ return "";
640
+ return (0, IsolatedDiff_1.generateUnifiedDiff)(snap.contentBefore, snap.contentAfter, {
641
+ filePath: snap.relativePath,
642
+ context: 1,
498
643
  });
499
- return out.toString();
500
644
  }
501
645
  catch (err) {
502
- // git missing / not a repo / file untracked vs HEAD → log & fall back.
503
- console.debug?.(`[Sela] git diff capture skipped: ${err?.message ?? err}`);
646
+ console.debug?.(`[Sela] isolated diff capture skipped: ${err?.message ?? err}`);
504
647
  return "";
505
648
  }
506
649
  }
@@ -681,26 +824,54 @@ If the text has NOT changed, omit the "contentChange" field entirely.`;
681
824
  console.warn(`[Sela] ⚠️ Failed to write insights report: ${err.message}`);
682
825
  }
683
826
  }
684
- // ── PR Automation — batched git-flow at session end ─────────────
685
- if (this.config.prAutomation.enabled) {
686
- try {
687
- const branchInfo = (0, PRAutomationService_1.detectBranch)(process.cwd());
688
- const decision = (0, PRAutomationService_1.decideEffectiveStrategy)(this.config.prAutomation, branchInfo.branch, healedEvents);
689
- if (decision.downgradeReason) {
690
- console.warn(`[Sela PR] ⚠️ Strategy downgraded: '${decision.configured}' → '${decision.effective}' ` +
691
- `(reason: ${decision.downgradeReason}, ` +
692
- `minAI: ${fmtPctEngine(decision.minAiConfidence)}, ` +
693
- `minAuditor: ${fmtPctEngine(decision.minAuditorConfidence)})`);
827
+ // ── Dual-Mode post-run lifecycle ─────────────────────────────────
828
+ //
829
+ // CI (process.env.CI truthy) → Clean-Room PR automation (existing flow).
830
+ // Local DX (CI falsy) → Interactive CLI review of each mutated
831
+ // file; Apply keeps the on-disk fix, Reject
832
+ // rewinds via WorkspaceSnapshotService.
833
+ //
834
+ // The interactive branch deliberately bypasses PR automation because:
835
+ // • AST fixes are ALREADY persisted to disk (the test passed on them).
836
+ // • Local devs want to inspect + cherry-pick, not auto-commit.
837
+ if (isCiEnvironment()) {
838
+ if (this.config.prAutomation.enabled) {
839
+ try {
840
+ const branchInfo = (0, PRAutomationService_1.detectBranch)(process.cwd());
841
+ const decision = (0, PRAutomationService_1.decideEffectiveStrategy)(this.config.prAutomation, branchInfo.branch, healedEvents);
842
+ if (decision.downgradeReason) {
843
+ console.warn(`[Sela PR] ⚠️ Strategy downgraded: '${decision.configured}' → '${decision.effective}' ` +
844
+ `(reason: ${decision.downgradeReason}, ` +
845
+ `minAI: ${fmtPctEngine(decision.minAiConfidence)}, ` +
846
+ `minAuditor: ${fmtPctEngine(decision.minAuditorConfidence)})`);
847
+ }
848
+ else {
849
+ console.log(`[Sela PR] 🎯 Effective strategy: '${decision.effective}' on branch '${branchInfo.branch ?? "unknown"}'`);
850
+ }
851
+ const ctx = { cwd: process.cwd(), reportHtmlPath };
852
+ await (0, PRAutomationService_1.execute)(this.config.prAutomation, decision, healedEvents, branchInfo, ctx);
853
+ await (0, PRAutomationService_1.handleBugDetected)(this.config.prAutomation.onBugDetected, protectedEvents, branchInfo, ctx);
694
854
  }
695
- else {
696
- console.log(`[Sela PR] 🎯 Effective strategy: '${decision.effective}' on branch '${branchInfo.branch ?? "unknown"}'`);
855
+ catch (err) {
856
+ console.warn(`[Sela PR] ⚠️ PR automation step failed: ${err.message}`);
697
857
  }
698
- const ctx = { cwd: process.cwd(), reportHtmlPath };
699
- await (0, PRAutomationService_1.execute)(this.config.prAutomation, decision, healedEvents, branchInfo, ctx);
700
- await (0, PRAutomationService_1.handleBugDetected)(this.config.prAutomation.onBugDetected, protectedEvents, branchInfo, ctx);
858
+ }
859
+ }
860
+ else if (healedEvents.length > 0) {
861
+ // Local DX: do NOT block this process on stdin — workers have no
862
+ // TTY and would deadlock until Playwright's per-test timeout fires.
863
+ // Serialise the mutated snapshots to a cross-process ledger; the
864
+ // SelaReporter drains it from `onEnd()` in the main process where
865
+ // an interactive `readline` is safe.
866
+ try {
867
+ (0, PendingPromptLedger_1.writePendingPromptsLedger)({
868
+ cwd: process.cwd(),
869
+ workspace: WorkspaceSnapshotService_1.sharedWorkspaceSnapshot,
870
+ healedEvents,
871
+ });
701
872
  }
702
873
  catch (err) {
703
- console.warn(`[Sela PR] ⚠️ PR automation step failed: ${err.message}`);
874
+ console.warn(`[Sela] ⚠️ Pending-prompts ledger write failed: ${err.message}`);
704
875
  }
705
876
  }
706
877
  // Clear the report buffer last so any downstream consumers (e.g. test
@@ -708,6 +879,12 @@ If the text has NOT changed, omit the "contentChange" field entirely.`;
708
879
  if (HealReportService_1.sharedHealReport.hasContent()) {
709
880
  HealReportService_1.sharedHealReport.clear();
710
881
  }
882
+ // Drop the Clean-Room workspace snapshot ledger so the next session
883
+ // starts with an empty `contentBefore` map. Without this, a long-lived
884
+ // Node process (`vitest --watch`, IDE test runner) would carry stale
885
+ // pre-Sela snapshots from one session into the next, causing the
886
+ // isolated-diff layer to compare against the wrong baseline.
887
+ WorkspaceSnapshotService_1.sharedWorkspaceSnapshot.clear();
711
888
  }
712
889
  // ─────────────────────────────────────────────────────────────
713
890
  // fetchLiveText — frame-aware innerText fetch (zero-trust probe)
@@ -0,0 +1,133 @@
1
+ export type SelaSubsystem = "SafetyGuard" | "IntentAuditor" | "ASTUpdater" | "Proxy" | "LLMService" | "Registry" | "ChainValidator" | "Engine" | "Report";
2
+ export interface SelaErrorContext {
3
+ filePath?: string;
4
+ line?: number;
5
+ column?: number;
6
+ selector?: string;
7
+ healedSelector?: string;
8
+ stableId?: string;
9
+ testTitle?: string;
10
+ testFile?: string;
11
+ branch?: string;
12
+ /** SafetyGuard verdict level when the error originated in SafetyGuard. */
13
+ safetyLevel?: "BLOCKED" | "FAIL_HARD" | "REQUIRES_REVIEW" | "SAFE";
14
+ /** Auditor verdict surfaced in the context for SafetyGuard rejections. */
15
+ auditorVerdict?: "CONSISTENT" | "INCONSISTENT" | "SUSPICIOUS";
16
+ /** Raw + adjusted LLM confidence so downstream telemetry can plot drift. */
17
+ aiConfidence?: number;
18
+ auditorConfidence?: number;
19
+ /** Free-form extension slot. Prefer adding a named field above when possible. */
20
+ [key: string]: unknown;
21
+ }
22
+ export interface SelaErrorJSON {
23
+ name: string;
24
+ subsystem: SelaSubsystem;
25
+ code: string;
26
+ reason: string;
27
+ message: string;
28
+ context: SelaErrorContext;
29
+ stack?: string;
30
+ cause?: string;
31
+ }
32
+ export interface SelaErrorInit {
33
+ subsystem: SelaSubsystem;
34
+ /**
35
+ * Stable machine-readable error code. Must match `[A-Z][A-Z0-9_]+`.
36
+ * Recommended convention: short subsystem-prefix + verb/noun.
37
+ * See SELA_ERROR_CODES below for the canonical catalogue.
38
+ */
39
+ code: string;
40
+ /** Human-readable explanation. Becomes the tail of the formatted message. */
41
+ reason: string;
42
+ context?: SelaErrorContext;
43
+ /** Original error wrapped by this SelaError, if any. */
44
+ cause?: unknown;
45
+ }
46
+ export declare class SelaError extends Error {
47
+ readonly subsystem: SelaSubsystem;
48
+ readonly code: string;
49
+ readonly reason: string;
50
+ readonly context: SelaErrorContext;
51
+ constructor(init: SelaErrorInit);
52
+ static formatMessage(subsystem: SelaSubsystem, code: string, reason: string): string;
53
+ toJSON(): SelaErrorJSON;
54
+ }
55
+ export declare function isSelaError(err: unknown): err is SelaError;
56
+ export declare function isSelaErrorFrom(err: unknown, subsystem: SelaSubsystem): err is SelaError;
57
+ export declare const SAFETY_GUARD_CODES: {
58
+ readonly FAIL_HARD_VISIBILITY_GHOST: "SG_FAIL_HARD_VISIBILITY_GHOST";
59
+ readonly FAIL_HARD_VISIBILITY_OCCLUDED: "SG_FAIL_HARD_VISIBILITY_OCCLUDED";
60
+ readonly BLOCKED_LOW_CONFIDENCE: "SG_BLOCKED_LOW_CONFIDENCE";
61
+ readonly FAIL_HARD_SEMANTIC_INVERSION: "SG_FAIL_HARD_SEMANTIC_INVERSION";
62
+ readonly FAIL_HARD_STATUS_INVERSION: "SG_FAIL_HARD_STATUS_INVERSION";
63
+ readonly BLOCKED_ROLE_SWAP_ASSERTION: "SG_BLOCKED_ROLE_SWAP_ASSERTION";
64
+ readonly FAIL_HARD_AUDITOR_INCONSISTENT: "SG_FAIL_HARD_AUDITOR_INCONSISTENT";
65
+ readonly BLOCKED_AUDITOR_SUSPICIOUS: "SG_BLOCKED_AUDITOR_SUSPICIOUS";
66
+ readonly BLOCKED_SIMILARITY_FLOOR: "SG_BLOCKED_SIMILARITY_FLOOR";
67
+ /** Generic dispatcher when SafetyDecision arrives without an explicit code. */
68
+ readonly GENERIC_REJECTION: "SG_GENERIC_REJECTION";
69
+ };
70
+ export type SafetyGuardCode = (typeof SAFETY_GUARD_CODES)[keyof typeof SAFETY_GUARD_CODES];
71
+ export declare const INTENT_AUDITOR_CODES: {
72
+ /** Caller asked for an audit but no Anthropic client was configured. */
73
+ readonly DISABLED: "IA_DISABLED";
74
+ readonly INVALID_JSON: "IA_INVALID_JSON";
75
+ readonly CALL_FAILED: "IA_CALL_FAILED";
76
+ };
77
+ export type IntentAuditorCode = (typeof INTENT_AUDITOR_CODES)[keyof typeof INTENT_AUDITOR_CODES];
78
+ export declare const LLM_SERVICE_CODES: {
79
+ readonly NO_API_KEY: "LLM_NO_API_KEY";
80
+ readonly INVALID_JSON: "LLM_INVALID_JSON";
81
+ readonly MISSING_SEGMENTS: "LLM_MISSING_SEGMENTS";
82
+ readonly NOT_FOUND: "LLM_NOT_FOUND";
83
+ readonly API_CALL_FAILED: "LLM_API_CALL_FAILED";
84
+ };
85
+ export type LLMServiceCode = (typeof LLM_SERVICE_CODES)[keyof typeof LLM_SERVICE_CODES];
86
+ export declare const AST_UPDATER_CODES: {
87
+ readonly UNSUPPORTED_NODE: "AST_UNSUPPORTED_NODE";
88
+ readonly TEMPLATE_LITERAL_SKIP: "AST_TEMPLATE_LITERAL_SKIP";
89
+ readonly CIRCULAR_RECEIVER: "AST_CIRCULAR_RECEIVER";
90
+ readonly DEFINITION_NOT_FOUND: "AST_DEFINITION_NOT_FOUND";
91
+ readonly MAX_DEPTH_EXCEEDED: "AST_MAX_DEPTH_EXCEEDED";
92
+ readonly SAVE_FAILED: "AST_SAVE_FAILED";
93
+ /**
94
+ * Developer pinned the failing statement with `// sela-fail-fast`.
95
+ * Heal pipeline is aborted before any LLM call; FailedEvent is
96
+ * recorded with category "Skipped by Developer".
97
+ */
98
+ readonly HEAL_DISABLED_BY_DIRECTIVE: "AST_HEAL_DISABLED_BY_DIRECTIVE";
99
+ };
100
+ export type ASTUpdaterCode = (typeof AST_UPDATER_CODES)[keyof typeof AST_UPDATER_CODES];
101
+ export declare const PROXY_CODES: {
102
+ readonly FRAME_SCOPED: "PRX_FRAME_SCOPED";
103
+ readonly REBUILD_FAILED: "PRX_REBUILD_FAILED";
104
+ readonly NON_LOCATOR_TARGET: "PRX_NON_LOCATOR_TARGET";
105
+ };
106
+ export type ProxyCode = (typeof PROXY_CODES)[keyof typeof PROXY_CODES];
107
+ export declare const REGISTRY_CODES: {
108
+ readonly CONTEXT_MISMATCH: "REG_CONTEXT_MISMATCH";
109
+ readonly FINGERPRINT_COLLISION: "REG_FINGERPRINT_COLLISION";
110
+ };
111
+ export type RegistryCode = (typeof REGISTRY_CODES)[keyof typeof REGISTRY_CODES];
112
+ export declare const REPORT_CODES: {
113
+ /** Event passed to the validator was null, undefined, or not an object. */
114
+ readonly MALFORMED_EVENT: "REP_MALFORMED_EVENT";
115
+ /** Event.kind is not one of HEALED|FAILED|PROTECTED. */
116
+ readonly UNKNOWN_KIND: "REP_UNKNOWN_KIND";
117
+ /** Event is missing a field declared mandatory for its kind. */
118
+ readonly MISSING_FIELD: "REP_MISSING_FIELD";
119
+ /** Event contains a field of the wrong type. */
120
+ readonly INVALID_FIELD_TYPE: "REP_INVALID_FIELD_TYPE";
121
+ /** Disk write failed during flushToDisk(). */
122
+ readonly FLUSH_FAILED: "REP_FLUSH_FAILED";
123
+ };
124
+ export type ReportCode = (typeof REPORT_CODES)[keyof typeof REPORT_CODES];
125
+ export declare const CHAIN_VALIDATOR_CODES: {
126
+ readonly TYPE_FLOW: "CV_TYPE_FLOW";
127
+ readonly SCHEMA: "CV_SCHEMA";
128
+ readonly RECEIVER: "CV_RECEIVER";
129
+ readonly PLACEMENT: "CV_PLACEMENT";
130
+ readonly SEMANTIC_DOWNGRADE: "CV_SEMANTIC_DOWNGRADE";
131
+ };
132
+ export type ChainValidatorCode = (typeof CHAIN_VALIDATOR_CODES)[keyof typeof CHAIN_VALIDATOR_CODES];
133
+ //# sourceMappingURL=SelaError.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SelaError.d.ts","sourceRoot":"","sources":["../../src/errors/SelaError.ts"],"names":[],"mappings":"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"}