sela-core 1.0.8 → 1.0.9

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 (67) hide show
  1. package/README.md +27 -17
  2. package/dist/cli/commands/init.js +1 -1
  3. package/dist/cli/commands/merge.d.ts +13 -0
  4. package/dist/cli/commands/merge.d.ts.map +1 -0
  5. package/dist/cli/commands/merge.js +119 -0
  6. package/dist/cli/index.js +4 -1
  7. package/dist/config/SelaConfig.d.ts +1 -1
  8. package/dist/config/SelaConfig.d.ts.map +1 -1
  9. package/dist/config/SelaConfig.js +1 -1
  10. package/dist/engine/SelaEngine.d.ts.map +1 -1
  11. package/dist/engine/SelaEngine.js +54 -2
  12. package/dist/errors/SelaError.d.ts +8 -0
  13. package/dist/errors/SelaError.d.ts.map +1 -1
  14. package/dist/errors/SelaError.js +8 -0
  15. package/dist/fixtures/expectProxy.d.ts +1 -1
  16. package/dist/fixtures/expectProxy.js +6 -6
  17. package/dist/fixtures/index.d.ts +18 -2
  18. package/dist/fixtures/index.d.ts.map +1 -1
  19. package/dist/fixtures/index.js +34 -14
  20. package/dist/fixtures/moduleExpect.js +7 -7
  21. package/dist/fixtures/proxyTag.js +1 -1
  22. package/dist/services/ASTSourceUpdater.d.ts +45 -0
  23. package/dist/services/ASTSourceUpdater.d.ts.map +1 -1
  24. package/dist/services/ASTSourceUpdater.js +201 -47
  25. package/dist/services/AnchorResolver.d.ts +157 -0
  26. package/dist/services/AnchorResolver.d.ts.map +1 -0
  27. package/dist/services/AnchorResolver.js +289 -0
  28. package/dist/services/DecisionEngine.d.ts +51 -0
  29. package/dist/services/DecisionEngine.d.ts.map +1 -0
  30. package/dist/services/DecisionEngine.js +260 -0
  31. package/dist/services/HealReportService.d.ts +31 -55
  32. package/dist/services/HealReportService.d.ts.map +1 -1
  33. package/dist/services/HealReportService.js +863 -1243
  34. package/dist/services/HealingAdvisory.d.ts +9 -1
  35. package/dist/services/HealingAdvisory.d.ts.map +1 -1
  36. package/dist/services/HealingAdvisory.js +8 -0
  37. package/dist/services/InitializerUpdater.d.ts.map +1 -1
  38. package/dist/services/InitializerUpdater.js +10 -0
  39. package/dist/services/InteractiveReview.d.ts.map +1 -1
  40. package/dist/services/InteractiveReview.js +4 -1
  41. package/dist/services/MutationApplier.d.ts +55 -0
  42. package/dist/services/MutationApplier.d.ts.map +1 -0
  43. package/dist/services/MutationApplier.js +322 -0
  44. package/dist/services/PendingPromptLedger.d.ts +7 -0
  45. package/dist/services/PendingPromptLedger.d.ts.map +1 -1
  46. package/dist/services/PendingPromptLedger.js +25 -0
  47. package/dist/services/ReportGenerator.d.ts +116 -30
  48. package/dist/services/ReportGenerator.d.ts.map +1 -1
  49. package/dist/services/ReportGenerator.js +150 -63
  50. package/dist/services/ReportMergeService.d.ts +95 -0
  51. package/dist/services/ReportMergeService.d.ts.map +1 -0
  52. package/dist/services/ReportMergeService.js +0 -0
  53. package/dist/services/SafetyGuard.d.ts +1 -1
  54. package/dist/services/SafetyGuard.d.ts.map +1 -1
  55. package/dist/services/SelectorSanitizer.d.ts +52 -0
  56. package/dist/services/SelectorSanitizer.d.ts.map +1 -0
  57. package/dist/services/SelectorSanitizer.js +318 -0
  58. package/dist/services/SnapshotService.js +1 -1
  59. package/dist/services/SourceUpdater.d.ts.map +1 -1
  60. package/dist/services/SourceUpdater.js +17 -0
  61. package/dist/services/TraceBackEngine.d.ts +67 -0
  62. package/dist/services/TraceBackEngine.d.ts.map +1 -0
  63. package/dist/services/TraceBackEngine.js +672 -0
  64. package/dist/utils/DOMUtils.d.ts +18 -2
  65. package/dist/utils/DOMUtils.d.ts.map +1 -1
  66. package/dist/utils/DOMUtils.js +335 -49
  67. package/package.json +1 -1
@@ -1,11 +1,24 @@
1
1
  "use strict";
2
2
  // src/services/HealReportService.ts
3
3
  //
4
- // Sela Insights - Transparent Healing Report.
5
- // Buffers a HealEvent per heal attempt (HEALED, FAILED, PROTECTED), then renders
6
- // a single self-contained HTML file (sela-report.html) at commitUpdates() time.
4
+ // Sela Insights Transparent Healing Report.
7
5
  //
8
- // Goal: explain Sela's "brain" to the developer. Maximum trust & observability.
6
+ // Responsibilities:
7
+ // • Buffer HealEvents during a Playwright run (record / attachFailureScreenshot).
8
+ // • On flushToDisk():
9
+ // 1. Read `.sela-history.json` sidecar from the project root.
10
+ // 2. Call ReportGenerator.generate() to validate, compute stats/ROI,
11
+ // and merge the current run into the history window.
12
+ // 3. Write the updated history back to `.sela-history.json`.
13
+ // 4. Render and write `sela-report.html` (the full embedded dashboard).
14
+ // 5. Write `sela-report.json` (the raw payload for CI consumers).
15
+ //
16
+ // Template embedding strategy:
17
+ // CSS and JS are stored as plain string constants using Array.join() instead
18
+ // of template literals so TypeScript never misinterprets backticks or ${...}
19
+ // expressions that belong to the browser runtime. renderHtml() assembles the
20
+ // final HTML via simple string concatenation — zero template literals that
21
+ // could interact with the embedded front-end code.
9
22
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
10
23
  if (k2 === undefined) k2 = k;
11
24
  var desc = Object.getOwnPropertyDescriptor(m, k);
@@ -44,13 +57,14 @@ exports.sharedHealReport = exports.HealReportService = void 0;
44
57
  exports.summariseSnapshot = summariseSnapshot;
45
58
  const fs = __importStar(require("fs"));
46
59
  const path = __importStar(require("path"));
47
- // Time-saved heuristics (minutes). Tuned for "fair midpoint" engineer estimate.
48
- const TIME_PER_HEAL = 12;
49
- const TIME_PER_PROTECT = 18; // catching a masked bug is worth more
50
- const TIME_PER_FAILED = 4; // partial value - narrows debug scope
51
- // ═══════════════════════════════════════════════════════════════════
52
- // SUMMARISERS - ElementSnapshotV2 → DnaSummary
53
- // ═══════════════════════════════════════════════════════════════════
60
+ const ReportGenerator_1 = require("./ReportGenerator");
61
+ // ─────────────────────────────────────────────────────────────────────────────
62
+ // SIDECAR CONFIG
63
+ // ─────────────────────────────────────────────────────────────────────────────
64
+ const HISTORY_FILE = ".sela-history.json";
65
+ // ─────────────────────────────────────────────────────────────────────────────
66
+ // SNAPSHOT SUMMARISER
67
+ // ─────────────────────────────────────────────────────────────────────────────
54
68
  function summariseSnapshot(s) {
55
69
  if (!s)
56
70
  return null;
@@ -69,9 +83,9 @@ function summariseSnapshot(s) {
69
83
  isInShadowDom: s.isInShadowDom ?? false,
70
84
  };
71
85
  }
72
- // ═══════════════════════════════════════════════════════════════════
86
+ // ─────────────────────────────────────────────────────────────────────────────
73
87
  // HealReportService
74
- // ═══════════════════════════════════════════════════════════════════
88
+ // ─────────────────────────────────────────────────────────────────────────────
75
89
  class HealReportService {
76
90
  events = [];
77
91
  startedAt = new Date().toISOString();
@@ -88,35 +102,19 @@ class HealReportService {
88
102
  hasContent() {
89
103
  return this.events.length > 0;
90
104
  }
91
- /** All HEALED events accumulated in this session. */
92
105
  getHealedEvents() {
93
106
  return this.events.filter((e) => e.kind === "HEALED");
94
107
  }
95
- /** All PROTECTED events (SafetyGuard blocks) accumulated in this session. */
96
108
  getProtectedEvents() {
97
109
  return this.events.filter((e) => e.kind === "PROTECTED");
98
110
  }
99
- /** All FAILED events accumulated in this session. */
100
111
  getFailedEvents() {
101
112
  return this.events.filter((e) => e.kind === "FAILED");
102
113
  }
103
114
  /**
104
- * Inject a Playwright failure screenshot (Base64 PNG, no data: prefix)
105
- * onto the FAILED event(s) matching the supplied identity hints. Used
106
- * by `SelaReporter` to enrich engine-emitted FAILED events with the
107
- * Playwright-captured screenshot from `TestResult.attachments`.
108
- *
109
- * Match policy (all supplied hints must agree):
110
- * • `sourceFile` - suffix-matched both directions to absorb
111
- * project-relative vs absolute path mismatches between the engine
112
- * (relative) and Playwright TestInfo.location.file (absolute).
113
- * • `testTitle` - exact match.
114
- * • `stableId` - exact match.
115
- *
116
- * Returns the number of events updated. Zero means the caller should
117
- * keep the screenshot pending until the matching FailedEvent is
118
- * recorded (worker-crash edge case where the heal pipeline did not
119
- * run before the Reporter received the failure).
115
+ * Attach a Playwright failure screenshot (Base64 PNG, no data: prefix)
116
+ * onto the FAILED event(s) matching the supplied identity hints.
117
+ * Returns the number of events updated.
120
118
  */
121
119
  attachFailureScreenshot(match, base64Png) {
122
120
  if (!base64Png)
@@ -142,1228 +140,850 @@ class HealReportService {
142
140
  }
143
141
  return touched;
144
142
  }
143
+ // ── History sidecar ─────────────────────────────────────────────────────
144
+ readHistory(targetDir) {
145
+ const histPath = path.join(targetDir, HISTORY_FILE);
146
+ try {
147
+ const raw = fs.readFileSync(histPath, "utf8");
148
+ const parsed = JSON.parse(raw);
149
+ if (!Array.isArray(parsed))
150
+ return [];
151
+ return parsed;
152
+ }
153
+ catch {
154
+ return [];
155
+ }
156
+ }
157
+ writeHistory(targetDir, runs) {
158
+ const histPath = path.join(targetDir, HISTORY_FILE);
159
+ try {
160
+ fs.writeFileSync(histPath, JSON.stringify(runs, null, 2), "utf8");
161
+ }
162
+ catch (err) {
163
+ console.warn("[sela] Could not write history sidecar:", err);
164
+ }
165
+ }
166
+ // ── Main flush ──────────────────────────────────────────────────────────
145
167
  /**
146
- * Write `sela-report.html` (+ `sela-report.json` debug dump) to cwd.
147
- * Returns the absolute path of the HTML report, or null if nothing to write.
168
+ * Generate and write `sela-report.html` + `sela-report.json` to targetDir.
169
+ * Also updates `.sela-history.json` with the current run's stats.
170
+ * Returns the absolute path of the HTML report, or null when there is
171
+ * nothing to write.
148
172
  */
149
- flushToDisk(targetDir = process.cwd()) {
173
+ flushToDisk(targetDir = process.cwd(), opts = {}) {
150
174
  if (this.events.length === 0)
151
175
  return null;
152
- const html = this.renderHtml();
176
+ const priorHistory = this.readHistory(targetDir);
177
+ const data = ReportGenerator_1.ReportGenerator.generate(this.events, {
178
+ cwd: targetDir,
179
+ startedAt: this.startedAt,
180
+ historicalRuns: priorHistory,
181
+ runId: opts.runId ??
182
+ process.env["GITHUB_RUN_ID"] ??
183
+ process.env["CI_PIPELINE_ID"] ??
184
+ undefined,
185
+ });
186
+ return this.writeReportData(targetDir, data);
187
+ }
188
+ /**
189
+ * Render and persist a pre-computed {@link ReportData} payload to `targetDir`:
190
+ * • `.sela-history.json` (the rolling window from `data.historical_runs`)
191
+ * • `sela-report.html` (the embedded dashboard)
192
+ * • `sela-report.json` (the raw payload for CI consumers)
193
+ *
194
+ * Extracted from {@link flushToDisk} so the CI merge path
195
+ * (`ReportMergeService`) writes a shard-aggregated report through the exact
196
+ * same rendering / serialisation logic — no duplication.
197
+ *
198
+ * Unlike `flushToDisk`, this writes unconditionally: an empty-event payload
199
+ * still produces a report and a `healed:0` history row. That is the desired
200
+ * CI behaviour — a clean, zero-heal pipeline is a healthy data point that
201
+ * must keep the trend chart's x-axis cadence intact.
202
+ *
203
+ * Returns the absolute path of the written HTML report.
204
+ */
205
+ writeReportData(targetDir, data) {
206
+ this.writeHistory(targetDir, data.historical_runs);
207
+ const html = renderHtml(data);
153
208
  const htmlPath = path.join(targetDir, "sela-report.html");
154
- const jsonPath = path.join(targetDir, "sela-report.json");
155
209
  fs.writeFileSync(htmlPath, html, "utf8");
210
+ const jsonPath = path.join(targetDir, "sela-report.json");
156
211
  fs.writeFileSync(jsonPath, JSON.stringify({
157
- generatedAt: new Date().toISOString(),
158
- startedAt: this.startedAt,
159
- events: this.events,
212
+ generatedAt: data.generatedAt,
213
+ startedAt: data.startedAt,
214
+ cwd: data.cwd,
215
+ stats: data.stats,
216
+ roi: data.roi,
217
+ historical_runs: data.historical_runs,
218
+ events: data.events,
160
219
  }, null, 2), "utf8");
161
220
  return htmlPath;
162
221
  }
163
- // ─────────────────────────────────────────────────────────────
164
- // RENDERING
165
- // ─────────────────────────────────────────────────────────────
166
- renderHtml() {
167
- const data = {
168
- generatedAt: new Date().toISOString(),
169
- startedAt: this.startedAt,
170
- cwd: process.cwd(),
171
- events: this.events,
172
- stats: this.computeStats(),
173
- };
174
- return renderHtmlTemplate(data);
175
- }
176
- computeStats() {
177
- let healed = 0, failed = 0, protectedCnt = 0;
178
- let iframeCnt = 0, shadowCnt = 0;
179
- for (const e of this.events) {
180
- if (e.kind === "HEALED")
181
- healed++;
182
- else if (e.kind === "FAILED")
183
- failed++;
184
- else
185
- protectedCnt++;
186
- if (e.inIframe)
187
- iframeCnt++;
188
- if (e.inShadowDom)
189
- shadowCnt++;
190
- }
191
- const timeSavedMin = healed * TIME_PER_HEAL +
192
- protectedCnt * TIME_PER_PROTECT +
193
- failed * TIME_PER_FAILED;
194
- return {
195
- total: this.events.length,
196
- healed,
197
- failed,
198
- protectedCnt,
199
- iframeCnt,
200
- shadowCnt,
201
- timeSavedMin,
202
- };
203
- }
204
222
  }
205
223
  exports.HealReportService = HealReportService;
206
- // ═══════════════════════════════════════════════════════════════════
224
+ // ─────────────────────────────────────────────────────────────────────────────
207
225
  // MODULE-LEVEL SINGLETON
208
- // ═══════════════════════════════════════════════════════════════════
226
+ // ─────────────────────────────────────────────────────────────────────────────
209
227
  exports.sharedHealReport = new HealReportService();
210
- // ═══════════════════════════════════════════════════════════════════
211
- // HTML TEMPLATE (single-file, dark mode, animated)
212
- // ═══════════════════════════════════════════════════════════════════
213
- function htmlEscape(s) {
214
- return s
215
- .replace(/&/g, "&")
216
- .replace(/</g, "&lt;")
217
- .replace(/>/g, "&gt;")
218
- .replace(/"/g, "&quot;")
219
- .replace(/'/g, "&#39;");
220
- }
221
- function renderHtmlTemplate(input) {
222
- // Embed structured data into the page so the front-end script renders it
223
- // - keeps the template compact and avoids server-side template branching.
224
- const payload = JSON.stringify(input).replace(/</g, "\\u003c");
225
- return `<!DOCTYPE html>
226
- <html lang="en" data-theme="dark">
227
- <head>
228
- <meta charset="UTF-8" />
229
- <meta name="viewport" content="width=device-width,initial-scale=1.0" />
230
- <title>Sela Insights - Healing Report</title>
231
- <style>
232
- ${REPORT_CSS}
233
- </style>
234
- </head>
235
- <body>
236
- <div class="bg-aurora"></div>
237
- <div class="bg-grid"></div>
238
-
239
- <header class="hero">
240
- <div class="hero-inner">
241
- <div class="brand">
242
- <div class="brand-mark">
243
- <svg viewBox="0 0 32 32" width="36" height="36" aria-hidden="true">
244
- <defs>
245
- <linearGradient id="g1" x1="0" y1="0" x2="1" y2="1">
246
- <stop offset="0" stop-color="#7c5cff"/>
247
- <stop offset="1" stop-color="#22d3ee"/>
248
- </linearGradient>
249
- </defs>
250
- <path d="M16 2 L29 9 V23 L16 30 L3 23 V9 Z" fill="url(#g1)" opacity="0.18"/>
251
- <path d="M16 2 L29 9 V23 L16 30 L3 23 V9 Z" fill="none" stroke="url(#g1)" stroke-width="1.2"/>
252
- <circle cx="16" cy="16" r="4" fill="url(#g1)"/>
253
- </svg>
254
- </div>
255
- <div>
256
- <div class="brand-title">Sela Insights</div>
257
- <div class="brand-sub">Transparent Healing Report</div>
258
- </div>
259
- </div>
260
- <div class="hero-meta">
261
- <span id="meta-generated"></span>
262
- </div>
263
- </div>
264
-
265
- <section class="executive">
266
- <div class="exec-headline">
267
- <span id="exec-eyebrow" class="eyebrow">Run complete</span>
268
- <h1 id="exec-title">Sela kept your suite alive.</h1>
269
- <p id="exec-sub" class="exec-sub"></p>
270
- </div>
271
- <div class="kpi-row" id="kpi-row"></div>
272
- </section>
273
- </header>
274
-
275
- <main class="container">
276
- <div class="filter-bar">
277
- <div class="filter-group" id="filter-group">
278
- <button class="chip active" data-filter="ALL">All</button>
279
- <button class="chip" data-filter="HEALED">Healed</button>
280
- <button class="chip protect-chip" data-filter="PROTECTED">Protected · Bug Caught</button>
281
- <button class="chip" data-filter="FAILED">Failed</button>
282
- </div>
283
- <div class="search-wrap">
284
- <input id="search" type="search" placeholder="Filter by selector, file, or test…" />
285
- </div>
286
- </div>
287
-
288
- <section id="events" class="events"></section>
289
-
290
- <section id="empty-state" class="empty hidden">
291
- <div class="empty-icon">∅</div>
292
- <div class="empty-title">Nothing matches that filter</div>
293
- <div class="empty-sub">Try a different filter or clear the search box.</div>
294
- </section>
295
- </main>
296
-
297
- <footer class="footer">
298
- <div class="footer-inner">
299
- <div>
300
- Generated by <strong>sela-core</strong> ·
301
- <span class="muted" id="footer-cwd"></span>
302
- </div>
303
- <div class="muted">
304
- Sela is a transparent pair-programmer. Every decision below is yours to keep, roll back, or report.
305
- </div>
306
- </div>
307
- </footer>
308
-
309
- <div id="toast" class="toast hidden"></div>
310
-
311
- <script id="sela-data" type="application/json">${payload}</script>
312
- <script>
313
- ${REPORT_JS}
314
- </script>
315
- </body>
316
- </html>`;
317
- }
318
- // ═══════════════════════════════════════════════════════════════════
319
- // CSS - dark, immersive, animated
320
- // ═══════════════════════════════════════════════════════════════════
321
- const REPORT_CSS = `
322
- :root {
323
- color-scheme: dark;
324
- --bg-0: #08090f;
325
- --bg-1: #0d0f18;
326
- --bg-2: #131623;
327
- --bg-3: #1a1e2e;
328
- --line: #1f2436;
329
- --line-strong: #2a3148;
330
- --text: #e6e8f2;
331
- --text-dim: #9aa1bd;
332
- --text-muted: #6b7393;
333
- --brand: #7c5cff;
334
- --brand-2: #22d3ee;
335
- --good: #34d399;
336
- --good-dim: rgba(52, 211, 153, 0.15);
337
- --warn: #fbbf24;
338
- --warn-dim: rgba(251, 191, 36, 0.15);
339
- --bad: #f87171;
340
- --bad-dim: rgba(248, 113, 113, 0.15);
341
- --protect: #c084fc;
342
- --protect-dim: rgba(192, 132, 252, 0.18);
343
- --radius: 14px;
344
- --radius-sm: 8px;
345
- --shadow-card: 0 1px 0 rgba(255,255,255,0.04) inset, 0 8px 30px rgba(0,0,0,0.35);
346
- --mono: ui-monospace, SFMono-Regular, "JetBrains Mono", Menlo, Consolas, monospace;
347
- --sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Inter, system-ui, sans-serif;
348
- }
349
-
350
- * { box-sizing: border-box; }
351
- html, body { margin: 0; padding: 0; }
352
- body {
353
- font-family: var(--sans);
354
- color: var(--text);
355
- background: var(--bg-0);
356
- min-height: 100vh;
357
- -webkit-font-smoothing: antialiased;
358
- overflow-x: hidden;
359
- }
360
-
361
- a { color: var(--brand-2); text-decoration: none; }
362
- a:hover { text-decoration: underline; }
363
-
364
- /* ── Background flourishes ─────────────────────────────────────── */
365
- .bg-aurora {
366
- position: fixed; inset: -20% -10% auto -10%; height: 70vh; z-index: -2;
367
- background:
368
- radial-gradient(60% 60% at 20% 30%, rgba(124,92,255,0.25), transparent 70%),
369
- radial-gradient(50% 50% at 80% 20%, rgba(34,211,238,0.18), transparent 70%),
370
- radial-gradient(60% 50% at 50% 80%, rgba(192,132,252,0.12), transparent 70%);
371
- filter: blur(40px);
372
- animation: aurora 20s ease-in-out infinite alternate;
373
- }
374
- @keyframes aurora {
375
- 0% { transform: translate3d(0, 0, 0) scale(1); }
376
- 100% { transform: translate3d(-3%, 2%, 0) scale(1.08); }
377
- }
378
- .bg-grid {
379
- position: fixed; inset: 0; z-index: -1; pointer-events: none;
380
- background-image:
381
- linear-gradient(rgba(124,92,255,0.05) 1px, transparent 1px),
382
- linear-gradient(90deg, rgba(124,92,255,0.05) 1px, transparent 1px);
383
- background-size: 48px 48px;
384
- mask-image: radial-gradient(circle at 50% 0%, black 0%, transparent 70%);
385
- }
386
-
387
- /* ── Hero ──────────────────────────────────────────────────────── */
388
- .hero {
389
- padding: 28px 32px 12px;
390
- max-width: 1280px; margin: 0 auto;
391
- }
392
- .hero-inner {
393
- display: flex; align-items: center; justify-content: space-between;
394
- margin-bottom: 36px;
395
- }
396
- .brand { display: flex; align-items: center; gap: 12px; }
397
- .brand-mark {
398
- display: flex; align-items: center; justify-content: center;
399
- width: 44px; height: 44px; border-radius: 12px;
400
- background: linear-gradient(145deg, rgba(124,92,255,0.18), rgba(34,211,238,0.10));
401
- border: 1px solid var(--line-strong);
402
- }
403
- .brand-title { font-weight: 700; font-size: 16px; letter-spacing: 0.2px; }
404
- .brand-sub { font-size: 12px; color: var(--text-muted); letter-spacing: 0.4px; text-transform: uppercase; }
405
- .hero-meta { color: var(--text-muted); font-size: 13px; }
406
-
407
- .executive {
408
- margin-top: 24px;
409
- display: grid; grid-template-columns: 1.4fr 1fr; gap: 36px;
410
- align-items: end;
411
- padding-bottom: 28px;
412
- border-bottom: 1px solid var(--line);
413
- }
414
- @media (max-width: 900px) { .executive { grid-template-columns: 1fr; } }
415
- .eyebrow {
416
- display: inline-block; font-size: 11px; letter-spacing: 1.5px;
417
- text-transform: uppercase; color: var(--brand-2);
418
- padding: 4px 10px; border: 1px solid rgba(34,211,238,0.3); border-radius: 999px;
419
- margin-bottom: 14px;
420
- }
421
- .exec-headline h1 {
422
- margin: 0 0 10px; font-size: 38px; line-height: 1.1;
423
- background: linear-gradient(135deg, #fff 0%, #cdd3f0 50%, #8c96c0 100%);
424
- -webkit-background-clip: text; background-clip: text; color: transparent;
425
- }
426
- .exec-sub { color: var(--text-dim); font-size: 15px; max-width: 56ch; line-height: 1.55; }
427
-
428
- .kpi-row {
429
- display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
430
- gap: 14px;
431
- }
432
- .kpi {
433
- background: linear-gradient(180deg, var(--bg-2), var(--bg-1));
434
- border: 1px solid var(--line);
435
- border-radius: var(--radius);
436
- padding: 16px 16px 14px;
437
- position: relative;
438
- overflow: hidden;
439
- transition: transform .3s ease, border-color .3s ease;
440
- }
441
- .kpi:hover { transform: translateY(-2px); border-color: var(--line-strong); }
442
- .kpi-label { font-size: 11px; letter-spacing: 1px; text-transform: uppercase; color: var(--text-muted); }
443
- .kpi-value { font-size: 30px; font-weight: 700; margin-top: 6px; font-variant-numeric: tabular-nums; }
444
- .kpi-foot { font-size: 12px; color: var(--text-dim); margin-top: 4px; }
445
- .kpi.good .kpi-value { color: var(--good); }
446
- .kpi.protect .kpi-value { color: var(--protect); }
447
- .kpi.bad .kpi-value { color: var(--bad); }
448
- .kpi.brand-accent .kpi-value { background: linear-gradient(135deg, var(--brand), var(--brand-2)); -webkit-background-clip: text; background-clip: text; color: transparent; }
449
-
450
- /* ── Container & filter bar ────────────────────────────────────── */
451
- .container { max-width: 1280px; margin: 0 auto; padding: 28px 32px 64px; }
452
- .filter-bar {
453
- display: flex; gap: 16px; align-items: center; justify-content: space-between;
454
- margin-bottom: 18px; flex-wrap: wrap;
455
- }
456
- .filter-group { display: flex; gap: 8px; flex-wrap: wrap; }
457
- .chip {
458
- border: 1px solid var(--line-strong);
459
- background: var(--bg-2); color: var(--text-dim);
460
- padding: 7px 14px; border-radius: 999px;
461
- font-size: 13px; cursor: pointer; transition: all .2s ease;
462
- font-family: var(--sans);
463
- }
464
- .chip:hover { color: var(--text); border-color: var(--brand); }
465
- .chip.active {
466
- color: #fff; border-color: var(--brand);
467
- background: linear-gradient(135deg, rgba(124,92,255,0.28), rgba(34,211,238,0.18));
468
- box-shadow: 0 0 0 1px rgba(124,92,255,0.4), 0 4px 16px rgba(124,92,255,0.20);
469
- }
470
- .chip.protect-chip { border-color: rgba(192,132,252,0.5); color: var(--protect); }
471
- .chip.protect-chip.active { background: linear-gradient(135deg, rgba(192,132,252,0.3), rgba(124,92,255,0.15)); color: #fff; }
472
-
473
- .search-wrap input {
474
- font-family: var(--sans); font-size: 13px;
475
- padding: 9px 14px; min-width: 280px;
476
- background: var(--bg-2); color: var(--text);
477
- border: 1px solid var(--line-strong); border-radius: 999px;
478
- outline: none; transition: border-color .2s ease, box-shadow .2s ease;
479
- }
480
- .search-wrap input:focus {
481
- border-color: var(--brand);
482
- box-shadow: 0 0 0 3px rgba(124,92,255,0.15);
483
- }
484
-
485
- /* ── Event cards ───────────────────────────────────────────────── */
486
- .events { display: flex; flex-direction: column; gap: 18px; }
487
- .card {
488
- background: linear-gradient(180deg, var(--bg-2), var(--bg-1));
489
- border: 1px solid var(--line);
490
- border-radius: var(--radius);
491
- overflow: hidden;
492
- box-shadow: var(--shadow-card);
493
- animation: cardIn .5s cubic-bezier(.18,.89,.32,1.28) both;
494
- position: relative;
495
- }
496
- .card::before {
497
- content: ""; position: absolute; inset: 0 auto 0 0; width: 3px;
498
- background: var(--line-strong);
499
- }
500
- .card.healed::before { background: linear-gradient(180deg, var(--good), #14b8a6); }
501
- .card.protected::before { background: linear-gradient(180deg, var(--protect), var(--brand)); }
502
- .card.failed::before { background: linear-gradient(180deg, var(--bad), #ef4444); }
503
-
504
- @keyframes cardIn {
505
- 0% { opacity: 0; transform: translateY(8px) scale(.99); }
506
- 100% { opacity: 1; transform: translateY(0) scale(1); }
507
- }
508
-
509
- .card-head {
510
- display: flex; align-items: center; gap: 12px;
511
- padding: 16px 18px;
512
- border-bottom: 1px solid var(--line);
513
- cursor: pointer;
514
- user-select: none;
515
- transition: background .2s ease;
516
- }
517
- .card-head:hover { background: var(--bg-3); }
518
-
519
- .badge {
520
- display: inline-flex; align-items: center; gap: 6px;
521
- font-size: 11px; letter-spacing: 1.5px; text-transform: uppercase;
522
- font-weight: 700;
523
- padding: 5px 10px; border-radius: 999px;
524
- border: 1px solid currentColor;
525
- white-space: nowrap;
526
- }
527
- .badge .dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; box-shadow: 0 0 8px currentColor; }
528
- .badge.healed { color: var(--good); background: var(--good-dim); }
529
- .badge.protected { color: var(--protect); background: var(--protect-dim); animation: pulse 2.6s ease-in-out infinite; }
530
- .badge.failed { color: var(--bad); background: var(--bad-dim); }
531
-
532
- @keyframes pulse {
533
- 0%,100% { box-shadow: 0 0 0 0 rgba(192,132,252,0); }
534
- 50% { box-shadow: 0 0 0 6px rgba(192,132,252,0.18); }
535
- }
536
-
537
- .card-title {
538
- flex: 1; min-width: 0;
539
- display: flex; flex-direction: column; gap: 4px;
540
- }
541
- .card-title .selector {
542
- font-family: var(--mono); font-size: 13px;
543
- color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
544
- }
545
- .card-title .meta-line {
546
- font-size: 12px; color: var(--text-muted);
547
- }
548
- .card-title .meta-line a { color: var(--text-dim); font-family: var(--mono); }
549
- .card-title .meta-line a:hover { color: var(--brand-2); }
550
-
551
- .card-head .chev { width: 16px; height: 16px; transition: transform .25s ease; color: var(--text-muted); }
552
- .card.open .card-head .chev { transform: rotate(90deg); }
553
-
554
- .card-body { padding: 0 18px 20px; display: none; }
555
- .card.open .card-body { display: block; animation: bodyIn .35s ease both; }
556
- @keyframes bodyIn {
557
- 0% { opacity: 0; transform: translateY(-4px); }
558
- 100% { opacity: 1; transform: translateY(0); }
559
- }
560
-
561
- /* ── Section blocks inside cards ───────────────────────────────── */
562
- .section {
563
- margin-top: 18px;
564
- padding-top: 16px;
565
- border-top: 1px dashed var(--line);
566
- }
567
- .section-title {
568
- font-size: 11px; letter-spacing: 1.4px; text-transform: uppercase;
569
- color: var(--text-muted); margin-bottom: 10px;
570
- display: flex; align-items: center; gap: 8px;
571
- }
572
- .section-title .marker {
573
- width: 6px; height: 6px; border-radius: 50%;
574
- background: var(--brand); box-shadow: 0 0 8px var(--brand);
575
- }
576
-
577
- /* DNA evidence table */
578
- .dna-table {
579
- width: 100%; border-collapse: separate; border-spacing: 0;
580
- font-size: 13px;
581
- background: var(--bg-1); border: 1px solid var(--line); border-radius: var(--radius-sm);
582
- overflow: hidden;
583
- }
584
- .dna-table th, .dna-table td {
585
- padding: 9px 12px; text-align: left;
586
- border-bottom: 1px solid var(--line);
587
- vertical-align: top;
588
- }
589
- .dna-table th {
590
- background: var(--bg-2); font-weight: 600; color: var(--text-dim);
591
- font-size: 11px; letter-spacing: 0.8px; text-transform: uppercase;
592
- }
593
- .dna-table tr:last-child td { border-bottom: none; }
594
- .dna-table td.field { color: var(--text-muted); width: 22%; }
595
- .dna-table td.val { font-family: var(--mono); font-size: 12.5px; color: var(--text); }
596
- .dna-table td.icon { width: 36px; text-align: center; font-size: 16px; }
597
- .dna-table tr.changed td.val.new { color: var(--warn); }
598
- .dna-table tr.changed td.val.old { color: var(--text-muted); text-decoration: line-through dotted; }
599
- .dna-table tr.matched td.icon { color: var(--good); }
600
- .dna-table tr.changed td.icon { color: var(--warn); }
601
- .dna-table tr.missing td.icon { color: var(--text-muted); }
602
-
603
- /* CoT bullets */
604
- .cot { display: flex; flex-direction: column; gap: 8px; }
605
- .cot .step {
606
- display: flex; gap: 12px; align-items: flex-start;
607
- padding: 10px 12px;
608
- background: var(--bg-1); border: 1px solid var(--line); border-radius: var(--radius-sm);
609
- }
610
- .cot .step .step-no {
611
- flex-shrink: 0;
612
- font-family: var(--mono); font-size: 11px; color: var(--brand-2);
613
- width: 22px; height: 22px; border-radius: 50%;
614
- background: rgba(34,211,238,0.10);
615
- display: inline-flex; align-items: center; justify-content: center;
616
- border: 1px solid rgba(34,211,238,0.25);
617
- }
618
- .cot .step .step-text { color: var(--text-dim); font-size: 13px; line-height: 1.5; }
619
-
620
- /* Auditor deep dive */
621
- .auditor-grid {
622
- display: grid; grid-template-columns: 200px 1fr; gap: 14px;
623
- background: var(--bg-1); border: 1px solid var(--line); border-radius: var(--radius-sm);
624
- padding: 16px;
625
- }
626
- @media (max-width: 700px) { .auditor-grid { grid-template-columns: 1fr; } }
627
- .gauge {
628
- position: relative; aspect-ratio: 1/1; width: 100%; max-width: 200px; margin: auto;
629
- }
630
- .gauge svg { width: 100%; height: 100%; transform: rotate(-90deg); }
631
- .gauge .track { stroke: var(--line); fill: none; stroke-width: 10; }
632
- .gauge .bar { stroke: var(--brand-2); fill: none; stroke-width: 10; stroke-linecap: round;
633
- transition: stroke-dasharray 1s ease; }
634
- .gauge.consistent .bar { stroke: var(--good); }
635
- .gauge.suspicious .bar { stroke: var(--warn); }
636
- .gauge.inconsistent .bar { stroke: var(--bad); }
637
- .gauge .center {
638
- position: absolute; inset: 0; display: flex; flex-direction: column;
639
- align-items: center; justify-content: center;
640
- }
641
- .gauge .pct { font-size: 28px; font-weight: 700; font-variant-numeric: tabular-nums; }
642
- .gauge .pct-label { font-size: 10px; letter-spacing: 1.2px; text-transform: uppercase; color: var(--text-muted); }
643
-
644
- .audit-detail { display: flex; flex-direction: column; gap: 8px; }
645
- .audit-row { display: flex; gap: 10px; font-size: 13px; }
646
- .audit-row .label { color: var(--text-muted); min-width: 130px; font-size: 12px; text-transform: uppercase; letter-spacing: 0.8px; }
647
- .audit-row .value { color: var(--text); }
648
- .audit-reason {
649
- margin-top: 6px;
650
- padding: 10px 12px; border-left: 3px solid var(--brand);
651
- background: rgba(124,92,255,0.08);
652
- font-size: 13px; color: var(--text); line-height: 1.55;
653
- border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
654
- }
655
-
656
- .verdict-chip {
657
- display: inline-block; padding: 3px 9px; border-radius: 999px;
658
- font-size: 11px; letter-spacing: 0.8px; font-weight: 700;
659
- text-transform: uppercase;
660
- }
661
- .verdict-chip.consistent { color: var(--good); background: var(--good-dim); border: 1px solid var(--good); }
662
- .verdict-chip.suspicious { color: var(--warn); background: var(--warn-dim); border: 1px solid var(--warn); }
663
- .verdict-chip.inconsistent { color: var(--bad); background: var(--bad-dim); border: 1px solid var(--bad); }
664
-
665
- /* Diff */
666
- .diff {
667
- background: var(--bg-1); border: 1px solid var(--line);
668
- border-radius: var(--radius-sm); overflow: hidden;
669
- }
670
- .diff-head {
671
- background: var(--bg-2); padding: 8px 12px;
672
- font-family: var(--mono); font-size: 12px; color: var(--text-dim);
673
- border-bottom: 1px solid var(--line);
674
- display: flex; align-items: center; gap: 10px;
675
- }
676
- .diff-body { padding: 0; }
677
- .diff-line {
678
- display: grid; grid-template-columns: 28px 56px 1fr;
679
- font-family: var(--mono); font-size: 12.5px; line-height: 1.55;
680
- }
681
- .diff-line .gutter { color: var(--text-muted); text-align: center; }
682
- .diff-line .ln { color: var(--text-muted); text-align: right; padding-right: 12px; }
683
- .diff-line .code { white-space: pre-wrap; word-break: break-word; padding-right: 12px; }
684
- .diff-line.add { background: rgba(52,211,153,0.08); }
685
- .diff-line.add .gutter, .diff-line.add .code { color: var(--good); }
686
- .diff-line.del { background: rgba(248,113,113,0.08); }
687
- .diff-line.del .gutter, .diff-line.del .code { color: var(--bad); }
688
- .diff-line.ctx .code { color: var(--text-dim); }
689
-
690
- /* Alternatives list */
691
- .alts { display: flex; flex-direction: column; gap: 6px; }
692
- .alts .alt {
693
- font-family: var(--mono); font-size: 12.5px; color: var(--text-dim);
694
- padding: 8px 12px; background: var(--bg-1); border: 1px solid var(--line); border-radius: var(--radius-sm);
695
- }
696
-
697
- /* Action bar */
698
- .actions {
699
- display: flex; gap: 10px; flex-wrap: wrap;
700
- margin-top: 22px; padding-top: 16px; border-top: 1px dashed var(--line);
701
- }
702
- .btn {
703
- font-family: var(--sans); font-size: 13px;
704
- padding: 9px 16px; border-radius: var(--radius-sm);
705
- border: 1px solid var(--line-strong); background: var(--bg-2);
706
- color: var(--text); cursor: pointer;
707
- transition: all .2s ease;
708
- display: inline-flex; align-items: center; gap: 8px;
709
- }
710
- .btn:hover { border-color: var(--brand); transform: translateY(-1px); }
711
- .btn.primary {
712
- border-color: transparent;
713
- background: linear-gradient(135deg, var(--brand), var(--brand-2));
714
- color: #fff;
715
- }
716
- .btn.primary:hover { box-shadow: 0 6px 24px rgba(124,92,255,0.35); }
717
- .btn.danger { border-color: rgba(248,113,113,0.4); color: var(--bad); }
718
- .btn.danger:hover { background: var(--bad-dim); }
719
- .btn.ghost { background: transparent; }
720
- .btn[data-state="approved"] {
721
- border-color: var(--good); color: var(--good); background: var(--good-dim);
722
- }
723
-
724
- /* Tag chips inside meta */
725
- .tag {
726
- display: inline-block; font-size: 10.5px; letter-spacing: 0.6px;
727
- padding: 2px 8px; border-radius: 4px;
728
- background: var(--bg-3); color: var(--text-dim);
729
- margin-left: 6px; border: 1px solid var(--line);
730
- font-family: var(--mono);
731
- }
732
- .tag.iframe { color: var(--brand-2); border-color: rgba(34,211,238,0.3); }
733
- .tag.shadow { color: var(--protect); border-color: rgba(192,132,252,0.3); }
734
- .tag.ghost { color: var(--warn); border-color: rgba(251,191,36,0.3); }
735
- .tag.occlude { color: var(--bad); border-color: rgba(248,113,113,0.3); }
736
-
737
- /* Failure context - Playwright screenshot rendered inline on FAILED cards */
738
- .failure-img {
739
- background: var(--bg-1); border: 1px solid var(--line);
740
- border-radius: var(--radius-sm); padding: 8px;
741
- overflow: hidden;
742
- }
743
- .failure-img img {
744
- display: block; max-width: 100%; height: auto;
745
- border-radius: 4px;
746
- background: #000;
747
- outline: 1px solid var(--line);
748
- }
749
- .failure-img .img-meta {
750
- font-family: var(--mono); font-size: 11px; color: var(--text-muted);
751
- padding: 6px 4px 0;
752
- }
753
-
754
- /* Empty state */
755
- .empty {
756
- margin-top: 32px; padding: 60px 20px; text-align: center;
757
- border: 1px dashed var(--line-strong); border-radius: var(--radius);
758
- color: var(--text-muted);
759
- }
760
- .empty-icon { font-size: 40px; color: var(--brand); margin-bottom: 12px; }
761
- .empty-title { font-size: 16px; color: var(--text); margin-bottom: 4px; }
762
- .empty-sub { font-size: 13px; }
763
- .hidden { display: none !important; }
764
-
765
- /* Toast */
766
- .toast {
767
- position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%);
768
- background: var(--bg-3); border: 1px solid var(--line-strong);
769
- color: var(--text); padding: 12px 18px; border-radius: 999px;
770
- font-size: 13px; box-shadow: 0 8px 30px rgba(0,0,0,0.4);
771
- animation: toastIn .35s ease;
772
- z-index: 100;
773
- }
774
- @keyframes toastIn {
775
- 0% { opacity: 0; transform: translate(-50%, 10px); }
776
- 100% { opacity: 1; transform: translate(-50%, 0); }
777
- }
778
-
779
- /* Footer */
780
- .footer { border-top: 1px solid var(--line); margin-top: 40px; }
781
- .footer-inner {
782
- max-width: 1280px; margin: 0 auto;
783
- padding: 22px 32px; display: flex; justify-content: space-between;
784
- flex-wrap: wrap; gap: 12px;
785
- color: var(--text-dim); font-size: 12.5px;
786
- }
787
- .muted { color: var(--text-muted); }
788
-
789
- /* Small helpers */
790
- .row { display: flex; gap: 18px; flex-wrap: wrap; }
791
- .col { flex: 1 1 320px; min-width: 0; }
792
- .kbd {
793
- font-family: var(--mono); font-size: 11px;
794
- padding: 1px 5px; border: 1px solid var(--line-strong); border-radius: 4px;
795
- background: var(--bg-2); color: var(--text-dim);
796
- }
797
- .muted-block {
798
- font-size: 13px; color: var(--text-muted);
799
- padding: 14px 16px; background: var(--bg-1);
800
- border: 1px dashed var(--line); border-radius: var(--radius-sm);
801
- }
802
- `;
803
- // ═══════════════════════════════════════════════════════════════════
804
- // FRONT-END JS - renders the embedded payload
805
- // ═══════════════════════════════════════════════════════════════════
806
- const REPORT_JS = String.raw `
807
- (function() {
808
- const dataEl = document.getElementById("sela-data");
809
- const DATA = JSON.parse(dataEl.textContent);
810
- const $ = (sel, root = document) => root.querySelector(sel);
811
- const $$ = (sel, root = document) => Array.from(root.querySelectorAll(sel));
812
-
813
- function esc(s) {
814
- return String(s == null ? "" : s)
815
- .replace(/&/g, "&amp;").replace(/</g, "&lt;")
816
- .replace(/>/g, "&gt;").replace(/"/g, "&quot;");
817
- }
818
- function fmtTime(iso) {
819
- try { return new Date(iso).toLocaleString(); } catch { return iso; }
820
- }
821
- function pluralize(n, one, many) { return n === 1 ? one : (many || one + "s"); }
822
- // Graceful percentage formatter - mirrors the server-side helper in
823
- // PRAutomationService. Anything that is not a finite number renders "n/a"
824
- // instead of being coerced to 0%. Used in every spot that previously
825
- // hard-coded a plain "x + %" concatenation.
826
- function fmtPct(v) {
827
- return (typeof v === "number" && isFinite(v)) ? (v + "%") : "n/a";
828
- }
829
-
830
- // ── Hero meta + KPIs ──────────────────────────────────────────
831
- $("#meta-generated").textContent = "Generated " + fmtTime(DATA.generatedAt);
832
- $("#footer-cwd").textContent = DATA.cwd;
833
-
834
- const s = DATA.stats;
835
- const minutes = s.timeSavedMin;
836
- const hours = Math.floor(minutes / 60);
837
- const remMin = minutes % 60;
838
- const timeSavedText = hours > 0 ? hours + "h " + remMin + "m" : minutes + "m";
839
-
840
- const headline = (() => {
841
- if (s.healed === 0 && s.protectedCnt === 0 && s.failed === 0) {
842
- return ["No healing events", "Sela was idle this run.", "Idle run"];
843
- }
844
- if (s.protectedCnt > 0 && s.healed === 0) {
845
- return [
846
- "Sela protected your suite from " + s.protectedCnt + " masked " + pluralize(s.protectedCnt, "regression") + ".",
847
- "These changes looked safe - but the Intent Auditor caught semantic mismatch and blocked the auto-fix.",
848
- "Bugs caught"
849
- ];
850
- }
851
- if (s.healed > 0 && s.protectedCnt > 0) {
852
- return [
853
- "Sela healed " + s.healed + " " + pluralize(s.healed, "selector") + " and protected " + s.protectedCnt + " " + pluralize(s.protectedCnt, "regression") + ".",
854
- "Your suite stayed green, but two of the would-be fixes were actually masked regressions. Drill in below.",
855
- "Healed + Protected"
856
- ];
857
- }
858
- if (s.healed > 0) {
859
- return [
860
- "Sela healed " + s.healed + " " + pluralize(s.healed, "selector") + " automatically.",
861
- "Every fix below was double-checked by the Intent Auditor before it touched your source code.",
862
- "Clean run"
863
- ];
864
- }
865
- return [
866
- "Sela couldn't heal " + s.failed + " " + pluralize(s.failed, "selector") + ".",
867
- "These need a human hand - but you've got context, candidates, and direct links ready below.",
868
- "Needs attention"
869
- ];
870
- })();
871
- $("#exec-title").textContent = headline[0];
872
- $("#exec-sub").textContent = headline[1];
873
- $("#exec-eyebrow").textContent = headline[2];
874
-
875
- const kpis = [
876
- { label: "Time saved", value: timeSavedText, foot: "estimated developer time", cls: "brand-accent" },
877
- { label: "Healed", value: String(s.healed), foot: "auto-fixed + verified", cls: "good" },
878
- { label: "Protected", value: String(s.protectedCnt), foot: "bugs caught early", cls: "protect" },
879
- { label: "Failed", value: String(s.failed), foot: "needs your eyes", cls: "bad" },
880
- ];
881
- if (s.iframeCnt > 0 || s.shadowCnt > 0) {
882
- kpis.push({
883
- label: "Complex contexts",
884
- value: String(s.iframeCnt + s.shadowCnt),
885
- foot: (s.iframeCnt ? s.iframeCnt + " iframe " : "") + (s.shadowCnt ? s.shadowCnt + " shadow DOM" : ""),
886
- cls: ""
887
- });
888
- }
889
- $("#kpi-row").innerHTML = kpis.map(k => (
890
- '<div class="kpi ' + k.cls + '">' +
891
- '<div class="kpi-label">' + esc(k.label) + '</div>' +
892
- '<div class="kpi-value">' + esc(k.value) + '</div>' +
893
- '<div class="kpi-foot">' + esc(k.foot) + '</div>' +
894
- '</div>'
895
- )).join("");
896
-
897
- // ── Event cards ───────────────────────────────────────────────
898
- function vscodeLink(file, line) {
899
- if (!file) return "";
900
- const abs = file.startsWith("/") || /^[A-Za-z]:/.test(file) ? file : (DATA.cwd + "/" + file);
901
- return "vscode://file/" + encodeURI(abs).replace(/#/g, "%23") + (line ? ":" + line : "");
902
- }
903
-
904
- function badge(kind) {
905
- const map = {
906
- HEALED: { cls: "healed", label: "Healed" },
907
- PROTECTED: { cls: "protected", label: "Protected · Bug Caught" },
908
- FAILED: { cls: "failed", label: "Failed" },
909
- };
910
- const m = map[kind];
911
- return '<span class="badge ' + m.cls + '"><span class="dot"></span>' + m.label + '</span>';
912
- }
913
-
914
- function contextTags(ev) {
915
- const parts = [];
916
- if (ev.inIframe) parts.push('<span class="tag iframe">iframe</span>');
917
- if (ev.inShadowDom) parts.push('<span class="tag shadow">shadow DOM</span>');
918
- const dna = ev.dnaBefore;
919
- if (dna && dna.isGhost) parts.push('<span class="tag ghost">ghost · ' + esc(dna.ghostReason || "") + '</span>');
920
- if (dna && dna.isOccluded) parts.push('<span class="tag occlude">occluded</span>');
921
- return parts.join("");
922
- }
923
-
924
- function renderDnaTable(before, after) {
925
- if (!before) return '<div class="muted-block">No DNA baseline available for this event.</div>';
926
- const rows = [];
927
- function row(field, oldV, newV) {
928
- const oldStr = oldV == null ? "" : (Array.isArray(oldV) ? oldV.join(" / ") : String(oldV));
929
- const newStr = newV == null ? "" : (Array.isArray(newV) ? newV.join(" / ") : String(newV));
930
- const same = oldStr === newStr;
931
- const hasAfter = !!after;
932
- let cls, icon;
933
- if (!hasAfter) { cls = "missing"; icon = "-"; }
934
- else if (same) { cls = "matched"; icon = "✅"; }
935
- else { cls = "changed"; icon = "⚠️"; }
936
- const valCell = !hasAfter
937
- ? '<td class="val">' + esc(oldStr || "-") + '</td>'
938
- : same
939
- ? '<td class="val" colspan="1">' + esc(oldStr || "-") + '</td>'
940
- : '<td class="val"><span class="val old">' + esc(oldStr || "-") + '</span>' +
941
- '<br><span class="val new">' + esc(newStr || "-") + '</span></td>';
942
- rows.push(
943
- '<tr class="' + cls + '">' +
944
- '<td class="field">' + esc(field) + '</td>' +
945
- valCell +
946
- '<td class="icon">' + icon + '</td>' +
947
- '</tr>'
948
- );
949
- }
950
- row("Tag", before.tagName, after && after.tagName);
951
- row("Text", before.text, after && after.text);
952
- row("Role", before.role, after && after.role);
953
- row("Classes", before.classes, after && after.classes);
954
- row("Ancestry", before.ancestry, after && after.ancestry);
955
- row("Closest Label", before.closestLabel, after && after.closestLabel);
956
- row("Row Anchor", before.rowAnchor, after && after.rowAnchor);
957
- const attrsBefore = Object.entries(before.attributes || {}).map(([k,v]) => k + "=" + v).join(" · ");
958
- const attrsAfter = after ? Object.entries(after.attributes || {}).map(([k,v]) => k + "=" + v).join(" · ") : null;
959
- row("Attributes", attrsBefore, attrsAfter);
960
-
961
- return (
962
- '<table class="dna-table">' +
963
- '<thead><tr><th>Field</th><th>Value</th><th>Match</th></tr></thead>' +
964
- '<tbody>' + rows.join("") + '</tbody>' +
965
- '</table>'
966
- );
967
- }
968
-
969
- function renderCoT(steps) {
970
- if (!steps || !steps.length) return '<div class="muted-block">No reasoning trace captured for this event.</div>';
971
- return '<div class="cot">' + steps.map((s, i) =>
972
- '<div class="step">' +
973
- '<span class="step-no">' + (i + 1) + '</span>' +
974
- '<span class="step-text">' + esc(s) + '</span>' +
975
- '</div>'
976
- ).join("") + '</div>';
977
- }
978
-
979
- function renderAuditor(a) {
980
- if (!a) return '<div class="muted-block">The Intent Auditor was not consulted for this event (no AI fix attempted, or auditor unavailable).</div>';
981
- const verdictCls = a.verdict.toLowerCase();
982
- const hasPct = typeof a.confidence === "number" && isFinite(a.confidence);
983
- const pct = hasPct ? Math.max(0, Math.min(100, a.confidence)) : 0;
984
- const radius = 56;
985
- const circ = 2 * Math.PI * radius;
986
- const dash = hasPct ? (pct / 100) * circ : 0;
987
- const adjustments = [];
988
- if (a.penalty != null && a.penalty !== 0) adjustments.push("ancestry drift " + a.penalty);
989
- if (a.bonus != null && a.bonus !== 0) adjustments.push("anchor +" + a.bonus);
990
- const adjLine = adjustments.length
991
- ? '<div class="audit-row"><span class="label">Score adjustments</span><span class="value">raw ' + fmtPct(a.rawConfidence) + ' → ' + fmtPct(hasPct ? pct : undefined) + ' (' + adjustments.join(", ") + ')</span></div>'
992
- : "";
993
- return (
994
- '<div class="auditor-grid">' +
995
- '<div class="gauge ' + verdictCls + '">' +
996
- '<svg viewBox="0 0 140 140">' +
997
- '<circle class="track" cx="70" cy="70" r="' + radius + '"/>' +
998
- '<circle class="bar" cx="70" cy="70" r="' + radius + '" ' +
999
- 'stroke-dasharray="' + dash.toFixed(2) + ' ' + circ.toFixed(2) + '"/>' +
1000
- '</svg>' +
1001
- '<div class="center">' +
1002
- '<div class="pct">' + (hasPct ? pct + "%" : "n/a") + '</div>' +
1003
- '<div class="pct-label">Consistency</div>' +
1004
- '</div>' +
1005
- '</div>' +
1006
- '<div class="audit-detail">' +
1007
- '<div class="audit-row"><span class="label">Verdict</span><span class="value"><span class="verdict-chip ' + verdictCls + '">' + a.verdict + '</span></span></div>' +
1008
- (a.inversionType ? '<div class="audit-row"><span class="label">Inversion type</span><span class="value">' + esc(a.inversionType) + '</span></div>' : "") +
1009
- adjLine +
1010
- '<div class="audit-reason">' + esc(a.reason) + '</div>' +
1011
- '</div>' +
1012
- '</div>'
1013
- );
1014
- }
1015
-
1016
- // Parse "git diff --unified=N" output into structured hunks. Skips file
1017
- // headers ("diff ", "index ", "--- ", "+++ ", rename/similarity lines) and
1018
- // walks each "@@ -a,b +c,d @@" block, tracking real old/new line numbers
1019
- // so the rendered diff shows correct line refs even when the AST mutation
1020
- // landed several lines above the failure site (JumpToDef scenario).
1021
- function parseUnifiedDiff(raw) {
1022
- const out = [];
1023
- if (!raw) return out;
1024
- const rows = raw.split(/\r?\n/);
1025
- let cur = null;
1026
- let oldLn = 0, newLn = 0;
1027
- for (const r of rows) {
1028
- if (
1029
- r.startsWith("diff ") ||
1030
- r.startsWith("index ") ||
1031
- r.startsWith("--- ") ||
1032
- r.startsWith("+++ ") ||
1033
- r.startsWith("new file mode") ||
1034
- r.startsWith("deleted file mode") ||
1035
- r.startsWith("similarity ") ||
1036
- r.startsWith("rename ") ||
1037
- r.startsWith("Binary files")
1038
- ) continue;
1039
- const hunk = r.match(/^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/);
1040
- if (hunk) {
1041
- if (cur) out.push(cur);
1042
- cur = { header: r, lines: [] };
1043
- oldLn = parseInt(hunk[1], 10);
1044
- newLn = parseInt(hunk[3], 10);
1045
- continue;
1046
- }
1047
- if (!cur) continue;
1048
- // "" + truly empty trailing lines: ignore
1049
- if (r.startsWith("\\")) continue;
1050
- if (r.length === 0) continue;
1051
- const head = r.charAt(0);
1052
- const text = r.slice(1);
1053
- if (head === "+") {
1054
- cur.lines.push({ type: "add", text, ln: newLn });
1055
- newLn++;
1056
- } else if (head === "-") {
1057
- cur.lines.push({ type: "del", text, ln: oldLn });
1058
- oldLn++;
1059
- } else if (head === " ") {
1060
- cur.lines.push({ type: "ctx", text, ln: newLn });
1061
- oldLn++; newLn++;
1062
- }
1063
- }
1064
- if (cur) out.push(cur);
1065
- return out;
1066
- }
1067
-
1068
- function renderDiff(ev) {
1069
- if (ev.kind !== "HEALED") {
1070
- if (ev.kind === "PROTECTED") {
1071
- return (
1072
- '<div class="muted-block">No source code was modified. Sela blocked the AI-proposed change because the Intent Auditor flagged a semantic mismatch - your test file is untouched and the underlying behaviour change is still surfacing in CI.</div>'
1073
- );
1074
- }
1075
- return '<div class="muted-block">No source code was modified - the AI could not propose a confident fix for this selector.</div>';
1076
- }
1077
-
1078
- const raw = ev.gitUnifiedDiff || "";
1079
- const hunks = parseUnifiedDiff(raw);
1080
-
1081
- let bodyHtml;
1082
- let sourceTag;
1083
- if (hunks.length > 0) {
1084
- sourceTag = "native git diff";
1085
- bodyHtml = hunks.map(h => {
1086
- const hdr =
1087
- '<div class="diff-line ctx">' +
1088
- '<span class="gutter">@</span><span class="ln"></span><span class="code">' + esc(h.header) + '</span>' +
1089
- '</div>';
1090
- const body = h.lines.map(L => {
1091
- const gutter = L.type === "add" ? "+" : L.type === "del" ? "-" : " ";
1092
- return '<div class="diff-line ' + L.type + '">' +
1093
- '<span class="gutter">' + gutter + '</span>' +
1094
- '<span class="ln">' + L.ln + '</span>' +
1095
- '<span class="code">' + esc(L.text) + '</span>' +
1096
- '</div>';
1097
- }).join("");
1098
- return hdr + body;
1099
- }).join("");
1100
- } else {
1101
- // Fallback when git diff capture was unavailable (no git, untracked
1102
- // file, etc.). Render the single before/after pair the engine carried
1103
- // over from the legacy code path.
1104
- sourceTag = "fallback (no git diff captured)";
1105
- const oldLine = ev.oldCodeLine || "";
1106
- const newLine = ev.newCodeLine || "";
1107
- bodyHtml =
1108
- '<div class="diff-line del">' +
1109
- '<span class="gutter">-</span><span class="ln">' + (ev.newLineNumber || "") + '</span><span class="code">' + esc(oldLine) + '</span>' +
1110
- '</div>' +
1111
- '<div class="diff-line add">' +
1112
- '<span class="gutter">+</span><span class="ln">' + (ev.newLineNumber || "") + '</span><span class="code">' + esc(newLine) + '</span>' +
1113
- '</div>';
1114
- }
1115
-
1116
- return (
1117
- '<div class="diff">' +
1118
- '<div class="diff-head">' +
1119
- '<span>' + esc(ev.sourceFile) + '</span>' +
1120
- '<span class="muted">·</span>' +
1121
- '<span>line ' + ev.newLineNumber + '</span>' +
1122
- (ev.diffStrategy ? '<span class="muted">·</span><span>strategy: ' + esc(ev.diffStrategy) + '</span>' : "") +
1123
- (ev.blastRadius != null ? '<span class="muted">·</span><span>blast radius: ' + ev.blastRadius + '</span>' : "") +
1124
- '<span class="muted">·</span><span>' + sourceTag + '</span>' +
1125
- '</div>' +
1126
- '<div class="diff-body">' + bodyHtml + '</div>' +
1127
- '</div>'
1128
- );
1129
- }
1130
-
1131
- function renderFailureContext(ev) {
1132
- if (ev.kind !== "FAILED" || !ev.failureScreenshotPng) return "";
1133
- return (
1134
- '<div class="section">' +
1135
- '<div class="section-title"><span class="marker"></span>Failure Context · Playwright Screenshot</div>' +
1136
- '<div class="failure-img">' +
1137
- '<img src="data:image/png;base64,' + ev.failureScreenshotPng + '" alt="Playwright failure screenshot" loading="lazy" />' +
1138
- '<div class="img-meta">captured by SelaReporter at test failure · base64-embedded · survives Playwright temp cleanup</div>' +
1139
- '</div>' +
1140
- '</div>'
1141
- );
1142
- }
1143
-
1144
- function renderAlts(ev) {
1145
- if (ev.kind !== "HEALED" || !ev.aiAlternatives || !ev.aiAlternatives.length) return "";
1146
- return (
1147
- '<div class="section">' +
1148
- '<div class="section-title"><span class="marker"></span>Alternative selectors the AI considered</div>' +
1149
- '<div class="alts">' +
1150
- ev.aiAlternatives.map(a => '<div class="alt">' + esc(a) + '</div>').join("") +
1151
- '</div>' +
1152
- '</div>'
1153
- );
1154
- }
1155
-
1156
- function renderActions(ev) {
1157
- const issueUrl = "https://github.com/anthropics/sela-core/issues/new?title=" +
1158
- encodeURIComponent("[Sela Report] " + ev.kind + " - " + ev.stableId) +
1159
- "&body=" + encodeURIComponent(
1160
- "Stable ID: " + ev.stableId + "\n" +
1161
- "File: " + ev.sourceFile + ":" + ev.sourceLine + "\n" +
1162
- "Kind: " + ev.kind + "\n" +
1163
- "Generated: " + DATA.generatedAt
1164
- );
1165
-
1166
- if (ev.kind === "HEALED") {
1167
- return (
1168
- '<div class="actions">' +
1169
- '<button class="btn primary" data-action="keep" data-id="' + ev.stableId + '">✓ Keep changes</button>' +
1170
- '<button class="btn danger" data-action="rollback" data-id="' + ev.stableId + '">↶ Rollback</button>' +
1171
- '<a class="btn ghost" target="_blank" rel="noopener" href="' + issueUrl + '">⚑ Report issue</a>' +
1172
- '</div>'
1173
- );
1174
- }
1175
- if (ev.kind === "PROTECTED") {
1176
- return (
1177
- '<div class="actions">' +
1178
- '<button class="btn primary" data-action="acknowledge" data-id="' + ev.stableId + '">✓ Acknowledge - I will investigate</button>' +
1179
- '<a class="btn ghost" target="_blank" rel="noopener" href="' + issueUrl + '">⚑ File regression</a>' +
1180
- '</div>'
1181
- );
1182
- }
1183
- return (
1184
- '<div class="actions">' +
1185
- '<a class="btn primary" href="' + vscodeLink(ev.sourceFile, ev.sourceLine) + '">→ Open in editor</a>' +
1186
- '<a class="btn ghost" target="_blank" rel="noopener" href="' + issueUrl + '">⚑ Report issue</a>' +
1187
- '</div>'
1188
- );
1189
- }
1190
-
1191
- function renderCardBody(ev) {
1192
- const blocks = [];
1193
-
1194
- // 1. AI Reasoning (CoT)
1195
- if (ev.kind !== "FAILED" || ev.aiExplanation) {
1196
- const steps = ev.kind === "HEALED" ? ev.reasoningSteps :
1197
- ev.kind === "PROTECTED" ? buildProtectedSteps(ev) :
1198
- buildFailedSteps(ev);
1199
- blocks.push(
1200
- '<div class="section">' +
1201
- '<div class="section-title"><span class="marker"></span>AI Reasoning · Chain of Thought</div>' +
1202
- renderCoT(steps) +
1203
- '</div>'
1204
- );
1205
- }
1206
-
1207
- // 2. Intent Auditor deep dive
1208
- if (ev.auditor !== undefined) {
1209
- blocks.push(
1210
- '<div class="section">' +
1211
- '<div class="section-title"><span class="marker"></span>Intent Auditor · Deep Dive</div>' +
1212
- renderAuditor(ev.auditor) +
1213
- '</div>'
1214
- );
1215
- }
1216
-
1217
- // 3. Visual DNA Evidence
1218
- blocks.push(
1219
- '<div class="section">' +
1220
- '<div class="section-title"><span class="marker"></span>Visual DNA Evidence · Old vs New</div>' +
1221
- renderDnaTable(ev.dnaBefore || null, ev.dnaAfter || null) +
1222
- '</div>'
1223
- );
1224
-
1225
- // 4. Diff
1226
- blocks.push(
1227
- '<div class="section">' +
1228
- '<div class="section-title"><span class="marker"></span>Source Code Diff</div>' +
1229
- renderDiff(ev) +
1230
- '</div>'
1231
- );
1232
-
1233
- // 5. Failure Context - Playwright screenshot (FAILED only)
1234
- blocks.push(renderFailureContext(ev));
1235
-
1236
- // 6. Alternatives
1237
- blocks.push(renderAlts(ev));
1238
-
1239
- // 6. Actions
1240
- blocks.push(renderActions(ev));
1241
-
1242
- return blocks.join("");
1243
- }
1244
-
1245
- function buildProtectedSteps(ev) {
1246
- const steps = [];
1247
- steps.push("AI proposed a replacement selector for the broken locator.");
1248
- if (ev.aiExplanation) steps.push("Reasoning given: " + ev.aiExplanation);
1249
- if (ev.candidateNewSelector) steps.push("Candidate selector resolved live - but its semantics changed.");
1250
- if (ev.auditor) steps.push("Intent Auditor verdict: " + ev.auditor.verdict + " · " + ev.auditor.reason);
1251
- steps.push("Sela blocked the auto-fix. Source code untouched. Investigate this as a real regression.");
1252
- return steps;
1253
- }
1254
- function buildFailedSteps(ev) {
1255
- const steps = [];
1256
- steps.push("Locator failed to resolve at runtime - Sela attempted to heal it.");
1257
- if (ev.aiExplanation) steps.push("AI response: " + ev.aiExplanation);
1258
- steps.push("No confident replacement was produced. Reason: " + (ev.reason || "unknown"));
1259
- steps.push("Open the file directly in your editor to inspect the original locator.");
1260
- return steps;
1261
- }
1262
-
1263
- function eventCard(ev) {
1264
- const link = vscodeLink(ev.sourceFile, ev.sourceLine);
1265
- const cardCls = ev.kind === "HEALED" ? "healed" : ev.kind === "PROTECTED" ? "protected" : "failed";
1266
- return (
1267
- '<article class="card ' + cardCls + '" data-kind="' + ev.kind + '" data-haystack="' + esc((ev.oldSelector + " " + (ev.newSelector || "") + " " + ev.sourceFile + " " + (ev.testTitle || "")).toLowerCase()) + '">' +
1268
- '<div class="card-head">' +
1269
- badge(ev.kind) +
1270
- '<div class="card-title">' +
1271
- '<div class="selector">' + esc(ev.kind === "HEALED" ? (ev.newSelector || ev.oldSelector) : ev.oldSelector) + '</div>' +
1272
- '<div class="meta-line">' +
1273
- (ev.testTitle ? esc(ev.testTitle) + ' · ' : "") +
1274
- '<a href="' + link + '">' + esc(ev.sourceFile) + ':' + ev.sourceLine + '</a>' +
1275
- contextTags(ev) +
1276
- ' · ' + fmtTime(ev.timestamp) +
1277
- '</div>' +
1278
- '</div>' +
1279
- '<svg class="chev" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2"><polyline points="5 3 11 8 5 13"/></svg>' +
1280
- '</div>' +
1281
- '<div class="card-body">' + renderCardBody(ev) + '</div>' +
1282
- '</article>'
1283
- );
1284
- }
1285
-
1286
- // ── Mount ─────────────────────────────────────────────────────
1287
- const $events = $("#events");
1288
- if (DATA.events.length === 0) {
1289
- $events.innerHTML = '<div class="muted-block">Sela was idle on this run - no broken selectors encountered.</div>';
1290
- } else {
1291
- $events.innerHTML = DATA.events.map(eventCard).join("");
1292
-
1293
- // Auto-open the first PROTECTED card to surface bugs immediately.
1294
- const firstProtected = $events.querySelector('.card.protected');
1295
- if (firstProtected) firstProtected.classList.add("open");
1296
- else {
1297
- const firstCard = $events.querySelector('.card');
1298
- if (firstCard) firstCard.classList.add("open");
1299
- }
1300
- }
1301
-
1302
- // ── Interactions ──────────────────────────────────────────────
1303
- function showToast(text) {
1304
- const t = $("#toast");
1305
- t.textContent = text;
1306
- t.classList.remove("hidden");
1307
- clearTimeout(showToast._t);
1308
- showToast._t = setTimeout(() => t.classList.add("hidden"), 2400);
1309
- }
1310
-
1311
- $events.addEventListener("click", (e) => {
1312
- const head = e.target.closest(".card-head");
1313
- if (head) {
1314
- const card = head.parentElement;
1315
- card.classList.toggle("open");
1316
- return;
1317
- }
1318
- const btn = e.target.closest("button[data-action]");
1319
- if (!btn) return;
1320
- const action = btn.dataset.action;
1321
- const id = btn.dataset.id;
1322
- if (action === "keep" || action === "acknowledge") {
1323
- btn.dataset.state = "approved";
1324
- btn.textContent = action === "keep" ? "✓ Approved" : "✓ Acknowledged";
1325
- showToast("Saved locally - Sela noted your decision for " + id);
1326
- } else if (action === "rollback") {
1327
- const cmd = "npx sela dna refactor --rollback " + id;
1328
- navigator.clipboard?.writeText(cmd).catch(() => {});
1329
- showToast("Rollback command copied: " + cmd);
1330
- }
1331
- });
1332
-
1333
- // ── Filters + Search ──────────────────────────────────────────
1334
- let activeFilter = "ALL";
1335
- let searchTerm = "";
1336
- function applyFilters() {
1337
- let visible = 0;
1338
- $$(".card", $events).forEach(card => {
1339
- const matchKind = activeFilter === "ALL" || card.dataset.kind === activeFilter;
1340
- const matchSearch = !searchTerm || card.dataset.haystack.includes(searchTerm);
1341
- const show = matchKind && matchSearch;
1342
- card.style.display = show ? "" : "none";
1343
- if (show) visible++;
1344
- });
1345
- $("#empty-state").classList.toggle("hidden", visible !== 0 || DATA.events.length === 0);
1346
- }
1347
-
1348
- $("#filter-group").addEventListener("click", e => {
1349
- const chip = e.target.closest(".chip");
1350
- if (!chip) return;
1351
- $$(".chip", $("#filter-group")).forEach(c => c.classList.remove("active"));
1352
- chip.classList.add("active");
1353
- activeFilter = chip.dataset.filter;
1354
- applyFilters();
1355
- });
1356
- $("#search").addEventListener("input", e => {
1357
- searchTerm = e.target.value.toLowerCase();
1358
- applyFilters();
1359
- });
1360
-
1361
- // ── Keyboard niceties ─────────────────────────────────────────
1362
- document.addEventListener("keydown", (e) => {
1363
- if (e.key === "/" && document.activeElement !== $("#search")) {
1364
- e.preventDefault();
1365
- $("#search").focus();
1366
- }
1367
- });
1368
- })();
1369
- `;
228
+ // ─────────────────────────────────────────────────────────────────────────────
229
+ // HTML RENDERER
230
+ //
231
+ // Uses plain string concatenation throughout — no TypeScript template literals
232
+ // that could misinterpret the embedded CSS/JS content.
233
+ // ─────────────────────────────────────────────────────────────────────────────
234
+ function renderHtml(data) {
235
+ // Embed the JSON payload. Escape </script> sequences and lone < so the
236
+ // string is safe inside a <script> block regardless of content.
237
+ const payload = JSON.stringify(data)
238
+ .replace(/</g, "\\u003c")
239
+ .replace(/\//g, "\\/");
240
+ return ("<!DOCTYPE html>\n" +
241
+ '<html lang="en">\n' +
242
+ "<head>\n" +
243
+ '<meta charset="UTF-8"/>\n' +
244
+ '<meta name="viewport" content="width=device-width,initial-scale=1.0"/>\n' +
245
+ "<title>Sela Insights \u2014 Healing Report</title>\n" +
246
+ '<link rel="preconnect" href="https://fonts.googleapis.com"/>\n' +
247
+ '<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin/>\n' +
248
+ '<link href="https://fonts.googleapis.com/css2?family=Geist+Mono:wght@400;500;600&family=Geist:wght@300;400;500;600;700&display=swap" rel="stylesheet"/>\n' +
249
+ '<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.js"><\/script>\n' +
250
+ "<style>\n" +
251
+ REPORT_CSS +
252
+ "\n</style>\n" +
253
+ "</head>\n" +
254
+ "<body>\n" +
255
+ '<div class="bg-noise"></div>\n' +
256
+ '<div class="bg-grid"></div>\n' +
257
+ buildTopbar() +
258
+ buildOverviewView() +
259
+ buildDeepDiveView() +
260
+ buildFooter() +
261
+ '<div id="toast" class="toast hidden"></div>\n' +
262
+ "<script>window.__SELA_DATA__ = " +
263
+ payload +
264
+ ";<\/script>\n" +
265
+ "<script>\n" +
266
+ REPORT_JS +
267
+ "\n<\/script>\n" +
268
+ "</body>\n" +
269
+ "</html>");
270
+ }
271
+ function buildTopbar() {
272
+ return ('<nav class="topbar">\n' +
273
+ ' <div class="wrap topbar-inner">\n' +
274
+ ' <div class="brand">\n' +
275
+ ' <div class="brand-hex"></div>\n' +
276
+ ' <span class="brand-name">Sela</span>\n' +
277
+ ' <span class="brand-sep">/</span>\n' +
278
+ ' <span class="brand-sub">Healing Report</span>\n' +
279
+ " </div>\n" +
280
+ ' <div style="display:flex;align-items:center;gap:12px">\n' +
281
+ ' <div class="view-toggle">\n' +
282
+ ' <button class="vt-btn active" id="btn-overview" onclick="setView(\'overview\')">Overview</button>\n' +
283
+ ' <button class="vt-btn" id="btn-deepdive" onclick="setView(\'deepdive\')">Deep Dive</button>\n' +
284
+ " </div>\n" +
285
+ ' <div class="topbar-meta">\n' +
286
+ ' <div class="topbar-pill live"><span class="dot"></span>Run complete</div>\n' +
287
+ " </div>\n" +
288
+ " </div>\n" +
289
+ " </div>\n" +
290
+ "</nav>\n");
291
+ }
292
+ function buildOverviewView() {
293
+ return ('<div id="view-overview">\n' +
294
+ '<div class="wrap">\n' +
295
+ ' <div class="hero">\n' +
296
+ ' <div class="exec-row">\n' +
297
+ " <div>\n" +
298
+ ' <div class="exec-eyebrow"><span class="ei"></span>Sela Intelligence Run<span class="ei"></span></div>\n' +
299
+ ' <h1 class="exec-h1" id="exec-h1"></h1>\n' +
300
+ ' <p class="exec-sub" id="exec-sub"></p>\n' +
301
+ " </div>\n" +
302
+ ' <div style="text-align:right;font-size:12px;color:var(--text3)">\n' +
303
+ ' <div id="meta-gen" style="margin-bottom:3px"></div>\n' +
304
+ ' <div id="meta-cwd" style="font-family:var(--mono);font-size:11px;word-break:break-all;max-width:280px"></div>\n' +
305
+ " </div>\n" +
306
+ " </div>\n" +
307
+ ' <div class="kpi-row">\n' +
308
+ ' <div class="kpi k-time"><div class="kpi-accent"></div><div class="kpi-l">Time saved</div><div class="kpi-v" id="kpi-time"></div><div class="kpi-f">estimated dev time</div></div>\n' +
309
+ ' <div class="kpi k-healed"><div class="kpi-accent"></div><div class="kpi-l">Healed</div><div class="kpi-v" id="kpi-healed"></div><div class="kpi-f">auto-fixed &amp; verified</div></div>\n' +
310
+ ' <div class="kpi k-prot"><div class="kpi-accent"></div><div class="kpi-l">Protected</div><div class="kpi-v" id="kpi-prot"></div><div class="kpi-f">regressions blocked</div></div>\n' +
311
+ ' <div class="kpi k-fail"><div class="kpi-accent"></div><div class="kpi-l">Failed</div><div class="kpi-v" id="kpi-fail"></div><div class="kpi-f">needs your eyes</div></div>\n' +
312
+ " </div>\n" +
313
+ ' <div class="charts-row">\n' +
314
+ ' <div class="chart-card">\n' +
315
+ ' <div class="chart-label">Run Distribution</div>\n' +
316
+ ' <div class="donut-wrap">\n' +
317
+ ' <canvas id="donut-chart"></canvas>\n' +
318
+ ' <div class="donut-center"><div class="donut-total" id="donut-total"></div><div class="donut-total-l">Events</div></div>\n' +
319
+ " </div>\n" +
320
+ ' <div class="donut-legend" id="donut-legend"></div>\n' +
321
+ " </div>\n" +
322
+ ' <div class="chart-card">\n' +
323
+ ' <div class="trend-header">\n' +
324
+ ' <div class="chart-label" style="margin-bottom:0">Sela Impact \u2014 Last 14 Runs</div>\n' +
325
+ ' <div class="trend-badge" id="trend-badge"></div>\n' +
326
+ " </div>\n" +
327
+ ' <div class="trend-canvas-wrap"><canvas id="trend-chart"></canvas></div>\n' +
328
+ ' <div style="margin-top:16px">\n' +
329
+ ' <div class="chart-label">Recent Runs</div>\n' +
330
+ ' <div id="hist-table">\n' +
331
+ ' <div class="hist-row hdr">\n' +
332
+ ' <div class="hc">Run</div><div class="hc">Healed</div><div class="hc">Protected</div><div class="hc">Failed</div><div class="hc">Confidence</div><div class="hc">Time Saved</div>\n' +
333
+ " </div>\n" +
334
+ " </div>\n" +
335
+ " </div>\n" +
336
+ " </div>\n" +
337
+ " </div>\n" +
338
+ ' <div style="margin-bottom:32px;display:flex;gap:10px;flex-wrap:wrap">\n' +
339
+ ' <button class="btn btn-primary" onclick="setView(\'deepdive\')">&#x2192; Inspect all events</button>\n' +
340
+ ' <button class="btn" onclick="copyJson()">&#x2398; Copy JSON</button>\n' +
341
+ " </div>\n" +
342
+ " </div>\n" +
343
+ "</div>\n" +
344
+ "</div>\n");
345
+ }
346
+ function buildDeepDiveView() {
347
+ return ('<div id="view-deepdive">\n' +
348
+ '<div class="wrap" style="padding-top:24px">\n' +
349
+ ' <div class="section-hdr">\n' +
350
+ " <h2>Engineering Deep Dive</h2>\n" +
351
+ ' <div class="sh-count" id="dd-count"></div>\n' +
352
+ ' <div class="sh-line"></div>\n' +
353
+ ' <button class="btn" onclick="setView(\'overview\')" style="font-size:11.5px;padding:4px 10px">&#x2190; Overview</button>\n' +
354
+ " </div>\n" +
355
+ ' <div class="filter-bar">\n' +
356
+ ' <div class="fb-chips">\n' +
357
+ ' <div class="chip c-all on" data-filter="ALL"><span>All</span></div>\n' +
358
+ ' <div class="chip c-heal" data-filter="HEALED"><span class="chip-dot"></span>Healed</div>\n' +
359
+ ' <div class="chip c-prot" data-filter="PROTECTED"><span class="chip-dot"></span>Protected \xb7 Bug Caught</div>\n' +
360
+ ' <div class="chip c-fail" data-filter="FAILED"><span class="chip-dot"></span>Failed</div>\n' +
361
+ " </div>\n" +
362
+ ' <div class="fb-search">\n' +
363
+ ' <svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="7" cy="7" r="5"/><path d="M11 11l3 3"/><\/svg>\n' +
364
+ ' <input type="text" id="search-input" placeholder="selector, file, test name\u2026"/>\n' +
365
+ " </div>\n" +
366
+ " </div>\n" +
367
+ ' <div class="events" id="events-list"></div>\n' +
368
+ ' <div class="empty-state hidden" id="empty-state">\n' +
369
+ ' <div class="es-icon">\u2205</div>\n' +
370
+ ' <div class="es-title">Nothing matches</div>\n' +
371
+ " <div>Try a different filter or clear the search.</div>\n" +
372
+ " </div>\n" +
373
+ "</div>\n" +
374
+ "</div>\n");
375
+ }
376
+ function buildFooter() {
377
+ return ('<footer class="footer">\n' +
378
+ ' <div class="wrap footer-inner">\n' +
379
+ ' <div>Generated by <strong>sela-core</strong> \xb7 <span id="footer-cwd" style="font-family:var(--mono);font-size:11px"></span></div>\n' +
380
+ " <div>Sela is a transparent pair-programmer. Every decision is yours to keep, roll back, or report.</div>\n" +
381
+ " </div>\n" +
382
+ "</footer>\n");
383
+ }
384
+ // ─────────────────────────────────────────────────────────────────────────────
385
+ // CSS — stored as a plain string constant (no backtick template literal)
386
+ // so TypeScript has no chance to misparse it.
387
+ // ─────────────────────────────────────────────────────────────────────────────
388
+ /* eslint-disable prettier/prettier */
389
+ const REPORT_CSS = [
390
+ "*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}",
391
+ ":root{",
392
+ " --bg0:#060709;--bg1:#0b0d12;--bg2:#10131a;--bg3:#161923;--bg4:#1c2030;",
393
+ " --line:#1e2336;--line2:#262d42;--line3:#2e3650;",
394
+ " --text:#dde2f0;--text2:#8b93b0;--text3:#5a6180;",
395
+ " --cyan:#00d4ff;--cyan-dim:rgba(0,212,255,.12);--cyan-glow:rgba(0,212,255,.25);",
396
+ " --purple:#a259ff;--purple-dim:rgba(162,89,255,.12);--purple-glow:rgba(162,89,255,.25);",
397
+ " --red:#ff4d6a;--red-dim:rgba(255,77,106,.12);--red-glow:rgba(255,77,106,.25);",
398
+ " --green:#00e5a0;--green-dim:rgba(0,229,160,.1);",
399
+ " --amber:#ffb84d;--amber-dim:rgba(255,184,77,.1);",
400
+ " --radius:10px;--radius-lg:14px;--radius-xl:20px;",
401
+ " --mono:'Geist Mono',ui-monospace,monospace;",
402
+ " --sans:'Geist',system-ui,sans-serif;",
403
+ "}",
404
+ "html{background:var(--bg0);color:var(--text);font-family:var(--sans);font-size:14px;line-height:1.5;-webkit-font-smoothing:antialiased}",
405
+ "body{min-height:100vh;overflow-x:hidden}",
406
+ "a{color:var(--cyan);text-decoration:none}",
407
+ "button{font-family:var(--sans);cursor:pointer}",
408
+ "input{font-family:var(--sans)}",
409
+ "::-webkit-scrollbar{width:6px;height:6px}",
410
+ "::-webkit-scrollbar-track{background:var(--bg1)}",
411
+ "::-webkit-scrollbar-thumb{background:var(--line3);border-radius:3px}",
412
+ ".bg-noise{",
413
+ " position:fixed;inset:0;z-index:-2;",
414
+ " background:",
415
+ " radial-gradient(ellipse 80% 50% at 10% 10%,rgba(0,212,255,.07) 0%,transparent 60%),",
416
+ " radial-gradient(ellipse 60% 40% at 90% 20%,rgba(162,89,255,.08) 0%,transparent 60%),",
417
+ " radial-gradient(ellipse 50% 60% at 50% 90%,rgba(162,89,255,.05) 0%,transparent 60%),",
418
+ " var(--bg0);",
419
+ "}",
420
+ ".bg-grid{",
421
+ " position:fixed;inset:0;z-index:-1;pointer-events:none;",
422
+ " background-image:linear-gradient(rgba(255,255,255,.018) 1px,transparent 1px),linear-gradient(90deg,rgba(255,255,255,.018) 1px,transparent 1px);",
423
+ " background-size:40px 40px;",
424
+ " mask-image:radial-gradient(ellipse at 50% 0%,black 0%,transparent 65%);",
425
+ "}",
426
+ ".wrap{max-width:1320px;margin:0 auto;padding:0 24px}",
427
+ ".topbar{position:sticky;top:0;z-index:100;background:rgba(6,7,9,.85);backdrop-filter:blur(16px) saturate(1.4);border-bottom:1px solid var(--line);height:52px;}",
428
+ ".topbar-inner{height:52px;display:flex;align-items:center;justify-content:space-between}",
429
+ ".brand{display:flex;align-items:center;gap:10px}",
430
+ ".brand-hex{width:28px;height:28px;background:linear-gradient(135deg,rgba(0,212,255,.2),rgba(162,89,255,.2));border:1px solid rgba(0,212,255,.3);clip-path:polygon(50% 0%,100% 25%,100% 75%,50% 100%,0% 75%,0% 25%);position:relative;}",
431
+ ".brand-hex::after{content:'';position:absolute;inset:4px;background:linear-gradient(135deg,var(--cyan),var(--purple));clip-path:polygon(50% 0%,100% 25%,100% 75%,50% 100%,0% 75%,0% 25%);opacity:.7;}",
432
+ ".brand-name{font-weight:600;font-size:14px;letter-spacing:-.2px}",
433
+ ".brand-sep{color:var(--text3);font-size:13px}",
434
+ ".brand-sub{font-size:12px;color:var(--text3);letter-spacing:.6px;text-transform:uppercase}",
435
+ ".topbar-meta{font-size:12px;color:var(--text3);display:flex;align-items:center;gap:16px}",
436
+ ".topbar-pill{display:inline-flex;align-items:center;gap:5px;padding:3px 10px;border-radius:999px;font-size:11px;border:1px solid var(--line2);color:var(--text3);}",
437
+ ".topbar-pill .dot{width:5px;height:5px;border-radius:50%}",
438
+ ".topbar-pill.live .dot{background:var(--green);box-shadow:0 0 6px var(--green);animation:blink 2s ease-in-out infinite}",
439
+ "@keyframes blink{0%,100%{opacity:1}50%{opacity:.4}}",
440
+ ".view-toggle{display:flex;gap:2px;padding:3px;background:var(--bg2);border:1px solid var(--line);border-radius:var(--radius)}",
441
+ ".vt-btn{padding:5px 14px;border-radius:7px;font-size:12px;font-weight:500;border:none;background:transparent;color:var(--text3);transition:all .2s ease}",
442
+ ".vt-btn.active{background:var(--bg4);color:var(--text);box-shadow:0 1px 3px rgba(0,0,0,.4)}",
443
+ ".hero{padding:32px 0 0}",
444
+ ".exec-row{display:grid;grid-template-columns:1fr auto;gap:32px;align-items:start;padding-bottom:28px;border-bottom:1px solid var(--line);margin-bottom:28px}",
445
+ "@media(max-width:800px){.exec-row{grid-template-columns:1fr}}",
446
+ ".exec-eyebrow{display:inline-flex;align-items:center;gap:6px;font-size:11px;letter-spacing:1.8px;text-transform:uppercase;color:var(--cyan);margin-bottom:10px}",
447
+ ".exec-eyebrow .ei{width:14px;height:1px;background:var(--cyan);opacity:.6}",
448
+ ".exec-h1{font-size:32px;font-weight:700;line-height:1.1;letter-spacing:-.6px;background:linear-gradient(135deg,#fff 0%,rgba(221,226,240,.7) 100%);-webkit-background-clip:text;background-clip:text;color:transparent;margin-bottom:8px}",
449
+ ".exec-sub{font-size:14px;color:var(--text2);max-width:52ch;line-height:1.6}",
450
+ ".kpi-row{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;margin-bottom:28px}",
451
+ "@media(max-width:900px){.kpi-row{grid-template-columns:repeat(2,1fr)}}",
452
+ ".kpi{background:var(--bg2);border:1px solid var(--line);border-radius:var(--radius-lg);padding:16px 18px;position:relative;overflow:hidden;transition:border-color .25s ease,transform .2s ease}",
453
+ ".kpi::before{content:'';position:absolute;inset:0;opacity:.5;background:linear-gradient(180deg,rgba(255,255,255,.03) 0%,transparent 100%);pointer-events:none}",
454
+ ".kpi:hover{transform:translateY(-2px);border-color:var(--line3)}",
455
+ ".kpi-l{font-size:10.5px;letter-spacing:1.2px;text-transform:uppercase;color:var(--text3);margin-bottom:6px}",
456
+ ".kpi-v{font-size:28px;font-weight:700;line-height:1;font-variant-numeric:tabular-nums;margin-bottom:4px}",
457
+ ".kpi-f{font-size:11.5px;color:var(--text3)}",
458
+ ".kpi-accent{position:absolute;top:0;right:0;width:3px;height:100%;border-radius:0 var(--radius-lg) var(--radius-lg) 0}",
459
+ ".kpi.k-time .kpi-v{background:linear-gradient(135deg,var(--cyan),var(--purple));-webkit-background-clip:text;background-clip:text;color:transparent}",
460
+ ".kpi.k-time .kpi-accent{background:linear-gradient(180deg,var(--cyan),var(--purple))}",
461
+ ".kpi.k-healed .kpi-v{color:var(--cyan)}.kpi.k-healed .kpi-accent{background:var(--cyan)}",
462
+ ".kpi.k-prot .kpi-v{color:var(--purple)}.kpi.k-prot .kpi-accent{background:var(--purple)}",
463
+ ".kpi.k-fail .kpi-v{color:var(--red)}.kpi.k-fail .kpi-accent{background:var(--red)}",
464
+ ".charts-row{display:grid;grid-template-columns:280px 1fr;gap:18px;margin-bottom:32px}",
465
+ "@media(max-width:900px){.charts-row{grid-template-columns:1fr}}",
466
+ ".chart-card{background:var(--bg2);border:1px solid var(--line);border-radius:var(--radius-lg);padding:20px}",
467
+ ".chart-label{font-size:10.5px;letter-spacing:1.2px;text-transform:uppercase;color:var(--text3);margin-bottom:14px;display:flex;align-items:center;gap:8px}",
468
+ ".chart-label::after{content:'';flex:1;height:1px;background:var(--line)}",
469
+ ".donut-wrap{position:relative;width:160px;height:160px;margin:0 auto 16px}",
470
+ ".donut-center{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;pointer-events:none}",
471
+ ".donut-total{font-size:28px;font-weight:700;line-height:1}",
472
+ ".donut-total-l{font-size:10px;letter-spacing:1px;text-transform:uppercase;color:var(--text3);margin-top:2px}",
473
+ ".donut-legend{display:flex;flex-direction:column;gap:7px}",
474
+ ".dl-row{display:flex;align-items:center;gap:8px;padding:6px 10px;border-radius:7px;border:1px solid transparent;cursor:pointer;transition:all .2s ease}",
475
+ ".dl-row:hover,.dl-row.active{border-color:var(--line3);background:var(--bg3)}",
476
+ ".dl-swatch{width:8px;height:8px;border-radius:2px;flex-shrink:0}",
477
+ ".dl-name{font-size:12.5px;color:var(--text2);flex:1}",
478
+ ".dl-val{font-size:12.5px;font-weight:600;font-variant-numeric:tabular-nums}",
479
+ ".dl-pct{font-size:11px;color:var(--text3);margin-left:4px}",
480
+ ".trend-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:14px}",
481
+ ".trend-badge{font-size:11px;font-weight:600;padding:3px 9px;border-radius:999px;background:var(--green-dim);color:var(--green);border:1px solid rgba(0,229,160,.25)}",
482
+ ".trend-canvas-wrap{position:relative;width:100%;height:200px}",
483
+ ".section-hdr{display:flex;align-items:center;gap:10px;margin-bottom:14px}",
484
+ ".section-hdr h2{font-size:13px;font-weight:600;letter-spacing:.4px}",
485
+ ".section-hdr .sh-count{font-size:11px;padding:2px 8px;border-radius:999px;background:var(--bg3);color:var(--text3);border:1px solid var(--line);font-variant-numeric:tabular-nums}",
486
+ ".section-hdr .sh-line{flex:1;height:1px;background:var(--line)}",
487
+ ".filter-bar{display:flex;align-items:center;gap:10px;margin-bottom:16px;flex-wrap:wrap}",
488
+ ".fb-chips{display:flex;gap:6px;flex-wrap:wrap}",
489
+ ".chip{display:inline-flex;align-items:center;gap:5px;padding:5px 12px;border-radius:999px;font-size:12px;font-weight:500;border:1px solid var(--line2);background:var(--bg2);color:var(--text3);cursor:pointer;transition:all .2s ease}",
490
+ ".chip:hover{color:var(--text);border-color:var(--line3)}",
491
+ ".chip.c-all.on{border-color:rgba(255,255,255,.3);color:#fff;background:var(--bg3)}",
492
+ ".chip.c-heal.on{border-color:var(--cyan);color:var(--cyan);background:var(--cyan-dim)}",
493
+ ".chip.c-prot.on{border-color:var(--purple);color:var(--purple);background:var(--purple-dim)}",
494
+ ".chip.c-fail.on{border-color:var(--red);color:var(--red);background:var(--red-dim)}",
495
+ ".chip .chip-dot{width:5px;height:5px;border-radius:50%}",
496
+ ".c-heal .chip-dot,.chip.c-heal.on .chip-dot{background:var(--cyan)}",
497
+ ".c-prot .chip-dot,.chip.c-prot.on .chip-dot{background:var(--purple)}",
498
+ ".c-fail .chip-dot,.chip.c-fail.on .chip-dot{background:var(--red)}",
499
+ ".fb-search{margin-left:auto;display:flex;align-items:center;background:var(--bg2);border:1px solid var(--line2);border-radius:var(--radius);padding:0 12px;gap:7px;height:34px}",
500
+ ".fb-search input{background:none;border:none;outline:none;color:var(--text);font-size:12.5px;width:200px}",
501
+ ".fb-search input::placeholder{color:var(--text3)}",
502
+ ".events{display:flex;flex-direction:column;gap:8px}",
503
+ ".ecard{background:var(--bg2);border:1px solid var(--line);border-radius:var(--radius-lg);overflow:hidden;transition:border-color .2s ease;animation:cardIn .4s cubic-bezier(.18,.89,.32,1.15) both}",
504
+ "@keyframes cardIn{0%{opacity:0;transform:translateY(6px)}100%{opacity:1;transform:translateY(0)}}",
505
+ ".ecard:hover{border-color:var(--line3)}",
506
+ ".ecard.ec-healed{border-left:2px solid var(--cyan)}",
507
+ ".ecard.ec-protected{border-left:2px solid var(--purple)}",
508
+ ".ecard.ec-failed{border-left:2px solid var(--red)}",
509
+ ".ecard.ec-protected .ecard-head{background:linear-gradient(90deg,rgba(162,89,255,.04),transparent)}",
510
+ ".ecard-head{display:grid;grid-template-columns:auto 1fr auto auto;align-items:center;gap:12px;padding:13px 16px;cursor:pointer;user-select:none;transition:background .15s ease}",
511
+ ".ecard-head:hover{background:var(--bg3)}",
512
+ ".ecard.open .ecard-head{background:var(--bg3)}",
513
+ ".status-badge{display:inline-flex;align-items:center;gap:5px;padding:4px 9px;border-radius:999px;font-size:10.5px;font-weight:600;letter-spacing:.8px;text-transform:uppercase;white-space:nowrap;border:1px solid currentColor}",
514
+ ".sb-healed{color:var(--cyan);background:var(--cyan-dim)}",
515
+ ".sb-protected{color:var(--purple);background:var(--purple-dim);animation:pulseborder 2.5s ease-in-out infinite}",
516
+ "@keyframes pulseborder{0%,100%{box-shadow:0 0 0 0 rgba(162,89,255,0)}50%{box-shadow:0 0 0 4px rgba(162,89,255,.15)}}",
517
+ ".sb-failed{color:var(--red);background:var(--red-dim)}",
518
+ ".sb-dot{width:5px;height:5px;border-radius:50%;background:currentColor;box-shadow:0 0 5px currentColor}",
519
+ ".ecard-title{min-width:0}",
520
+ ".ecard-selector{font-family:var(--mono);font-size:12.5px;color:var(--text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-bottom:2px}",
521
+ ".ecard-meta{font-size:11.5px;color:var(--text3);display:flex;align-items:center;gap:5px;flex-wrap:wrap}",
522
+ ".ecard-meta a{color:var(--text2);font-family:var(--mono);font-size:11px}",
523
+ ".ecard-meta a:hover{color:var(--cyan)}",
524
+ ".ecard-tag{display:inline-block;font-size:10px;padding:1px 6px;border-radius:4px;background:var(--bg4);border:1px solid var(--line2);font-family:var(--mono);letter-spacing:.3px}",
525
+ ".ecard-tag.t-iframe{color:var(--cyan);border-color:rgba(0,212,255,.2)}",
526
+ ".ecard-tag.t-shadow{color:var(--purple);border-color:rgba(162,89,255,.2)}",
527
+ ".ecard-tag.t-dryrun{color:var(--amber);border-color:rgba(255,184,77,.2)}",
528
+ ".conf-badge{font-size:11px;font-weight:600;padding:3px 9px;border-radius:6px;font-variant-numeric:tabular-nums;white-space:nowrap}",
529
+ ".conf-high{background:rgba(0,229,160,.12);color:var(--green);border:1px solid rgba(0,229,160,.2)}",
530
+ ".conf-mid{background:rgba(255,184,77,.1);color:var(--amber);border:1px solid rgba(255,184,77,.2)}",
531
+ ".conf-low{background:rgba(255,77,106,.1);color:var(--red);border:1px solid rgba(255,77,106,.2)}",
532
+ ".chev{width:16px;height:16px;color:var(--text3);flex-shrink:0;transition:transform .25s ease}",
533
+ ".ecard.open .chev{transform:rotate(90deg)}",
534
+ ".ecard-body{display:none;padding:0 16px 18px;border-top:1px solid var(--line)}",
535
+ ".ecard.open .ecard-body{display:block;animation:bodyIn .3s ease both}",
536
+ "@keyframes bodyIn{0%{opacity:0;transform:translateY(-3px)}100%{opacity:1;transform:translateY(0)}}",
537
+ ".body-section{margin-top:16px}",
538
+ ".bs-title{font-size:10.5px;letter-spacing:1.4px;text-transform:uppercase;color:var(--text3);margin-bottom:10px;display:flex;align-items:center;gap:8px}",
539
+ ".bs-title .bst-dot{width:5px;height:5px;border-radius:50%;background:var(--purple);box-shadow:0 0 6px var(--purple)}",
540
+ ".diff-block{background:var(--bg1);border:1px solid var(--line);border-radius:var(--radius);overflow:hidden}",
541
+ ".diff-bar{display:flex;align-items:center;gap:8px;padding:7px 12px;background:var(--bg3);border-bottom:1px solid var(--line);font-size:11px;color:var(--text3);font-family:var(--mono)}",
542
+ ".diff-bar .sep{opacity:.3}",
543
+ ".diff-line{display:grid;grid-template-columns:22px 48px 1fr;font-family:var(--mono);font-size:12px;line-height:1.6}",
544
+ ".diff-line .gutter{text-align:center;color:var(--text3)}",
545
+ ".diff-line .ln{text-align:right;padding-right:10px;color:var(--text3)}",
546
+ ".diff-line .code{white-space:pre-wrap;word-break:break-word;padding-right:12px}",
547
+ ".diff-line.dl-add{background:rgba(0,229,160,.07)}",
548
+ ".diff-line.dl-add .gutter,.diff-line.dl-add .code{color:var(--green)}",
549
+ ".diff-line.dl-del{background:rgba(255,77,106,.07)}",
550
+ ".diff-line.dl-del .gutter,.diff-line.dl-del .code{color:var(--red)}",
551
+ ".diff-line.dl-ctx .code{color:var(--text3)}",
552
+ ".diff-line.dl-hunk{background:rgba(0,212,255,.04)}",
553
+ ".diff-line.dl-hunk .code{color:rgba(0,212,255,.6);font-size:11px}",
554
+ ".auditor-wrap{display:grid;grid-template-columns:160px 1fr;gap:16px;background:var(--bg1);border:1px solid var(--line);border-radius:var(--radius);padding:16px}",
555
+ "@media(max-width:700px){.auditor-wrap{grid-template-columns:1fr}}",
556
+ ".gauge-ring{position:relative;width:130px;height:130px;margin:0 auto}",
557
+ ".gauge-ring svg{width:100%;height:100%;transform:rotate(-90deg)}",
558
+ ".gauge-ring .gt{stroke:var(--line3);fill:none;stroke-width:9}",
559
+ ".gauge-ring .gb{fill:none;stroke-width:9;stroke-linecap:round;transition:stroke-dasharray 1.2s cubic-bezier(.4,0,.2,1)}",
560
+ ".gauge-center{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center}",
561
+ ".gauge-pct{font-size:26px;font-weight:700;font-variant-numeric:tabular-nums;line-height:1}",
562
+ ".gauge-sub{font-size:9.5px;letter-spacing:1px;text-transform:uppercase;color:var(--text3);margin-top:2px}",
563
+ ".gb-consistent{stroke:var(--green)}.gb-suspicious{stroke:var(--amber)}.gb-inconsistent{stroke:var(--red)}",
564
+ ".pct-consistent{color:var(--green)}.pct-suspicious{color:var(--amber)}.pct-inconsistent{color:var(--red)}",
565
+ ".audit-details{display:flex;flex-direction:column;gap:7px}",
566
+ ".ad-row{display:flex;gap:8px;font-size:12.5px;align-items:flex-start}",
567
+ ".ad-label{color:var(--text3);font-size:11px;letter-spacing:.5px;text-transform:uppercase;min-width:110px;flex-shrink:0;padding-top:1px}",
568
+ ".ad-val{color:var(--text)}",
569
+ ".ad-reason{margin-top:6px;padding:10px 12px;border-left:2px solid var(--purple);background:rgba(162,89,255,.06);font-size:12.5px;color:var(--text);line-height:1.55;border-radius:0 var(--radius) var(--radius) 0}",
570
+ ".verdict-pill{display:inline-block;padding:2px 8px;border-radius:5px;font-size:10.5px;font-weight:700;letter-spacing:.8px;text-transform:uppercase}",
571
+ ".vp-consistent{color:var(--green);background:var(--green-dim);border:1px solid rgba(0,229,160,.2)}",
572
+ ".vp-suspicious{color:var(--amber);background:var(--amber-dim);border:1px solid rgba(255,184,77,.2)}",
573
+ ".vp-inconsistent{color:var(--red);background:var(--red-dim);border:1px solid rgba(255,77,106,.2)}",
574
+ ".cot{display:flex;flex-direction:column;gap:7px}",
575
+ ".cot-step{display:flex;gap:10px;align-items:flex-start;padding:9px 12px;background:var(--bg1);border:1px solid var(--line);border-radius:var(--radius)}",
576
+ ".cot-n{flex-shrink:0;font-family:var(--mono);font-size:10.5px;color:var(--cyan);width:20px;height:20px;border-radius:50%;background:rgba(0,212,255,.1);border:1px solid rgba(0,212,255,.2);display:inline-flex;align-items:center;justify-content:center}",
577
+ ".cot-t{color:var(--text2);font-size:12.5px;line-height:1.5}",
578
+ ".dna-table{width:100%;border-collapse:separate;border-spacing:0;font-size:12px;background:var(--bg1);border:1px solid var(--line);border-radius:var(--radius);overflow:hidden}",
579
+ ".dna-table th,.dna-table td{padding:7px 11px;text-align:left;border-bottom:1px solid var(--line)}",
580
+ ".dna-table th{background:var(--bg3);font-weight:600;color:var(--text3);font-size:10.5px;letter-spacing:.6px;text-transform:uppercase}",
581
+ ".dna-table tr:last-child td{border-bottom:none}",
582
+ ".dna-table td.field{color:var(--text3);width:20%}",
583
+ ".dna-table td.val{font-family:var(--mono);font-size:11.5px}",
584
+ ".dna-table td.icon{width:32px;text-align:center}",
585
+ ".dna-table tr.matched td.icon{color:var(--green)}",
586
+ ".dna-table tr.changed td.val.new{color:var(--amber)}",
587
+ ".dna-table tr.changed td.val.old{color:var(--text3);text-decoration:line-through dotted}",
588
+ ".dna-table tr.changed td.icon{color:var(--amber)}",
589
+ ".dna-table tr.missing td.icon{color:var(--text3)}",
590
+ ".alts{display:flex;flex-direction:column;gap:5px}",
591
+ ".alt-row{font-family:var(--mono);font-size:12px;color:var(--text2);padding:7px 11px;background:var(--bg1);border:1px solid var(--line);border-radius:var(--radius);display:flex;align-items:center;justify-content:space-between;gap:8px}",
592
+ ".alt-copy{font-size:10px;padding:2px 8px;border-radius:5px;border:1px solid var(--line2);background:var(--bg3);color:var(--text3);cursor:pointer;transition:all .15s ease;white-space:nowrap}",
593
+ ".alt-copy:hover{color:var(--text);border-color:var(--line3)}",
594
+ ".actions{display:flex;gap:8px;flex-wrap:wrap;margin-top:16px;padding-top:14px;border-top:1px dashed var(--line2)}",
595
+ ".btn{display:inline-flex;align-items:center;gap:6px;padding:7px 14px;border-radius:var(--radius);font-size:12.5px;font-weight:500;border:1px solid var(--line2);background:var(--bg3);color:var(--text2);cursor:pointer;transition:all .2s ease}",
596
+ ".btn:hover{color:var(--text);border-color:var(--line3);transform:translateY(-1px)}",
597
+ ".btn.btn-primary{background:linear-gradient(135deg,rgba(0,212,255,.15),rgba(162,89,255,.15));border-color:rgba(0,212,255,.3);color:var(--cyan)}",
598
+ ".btn.btn-primary:hover{box-shadow:0 4px 20px rgba(0,212,255,.2)}",
599
+ ".btn.btn-danger{border-color:rgba(255,77,106,.3);color:var(--red)}",
600
+ ".btn.btn-danger:hover{background:var(--red-dim)}",
601
+ ".btn[data-done='true']{border-color:rgba(0,229,160,.3);color:var(--green);background:var(--green-dim)}",
602
+ ".fail-img-wrap{background:var(--bg1);border:1px solid var(--line);border-radius:var(--radius);overflow:hidden}",
603
+ ".fail-img-wrap img{display:block;max-width:100%;height:auto}",
604
+ ".fail-img-meta{padding:6px 11px;font-size:11px;color:var(--text3);font-family:var(--mono)}",
605
+ ".prot-notice{padding:12px 14px;background:rgba(162,89,255,.07);border:1px solid rgba(162,89,255,.2);border-radius:var(--radius);font-size:12.5px;color:var(--text2);line-height:1.55}",
606
+ ".prot-notice strong{color:var(--purple)}",
607
+ ".row2{display:grid;grid-template-columns:1fr 1fr;gap:14px}",
608
+ "@media(max-width:700px){.row2{grid-template-columns:1fr}}",
609
+ ".muted-block{padding:12px 14px;background:var(--bg1);border:1px dashed var(--line);border-radius:var(--radius);font-size:12px;color:var(--text3)}",
610
+ ".hist-row{display:grid;grid-template-columns:1fr repeat(5,80px);gap:0;font-size:12px}",
611
+ ".hist-row .hc{padding:6px 10px;border-bottom:1px solid var(--line)}",
612
+ ".hist-row.hdr .hc{color:var(--text3);font-size:10.5px;letter-spacing:.6px;text-transform:uppercase;background:var(--bg3)}",
613
+ ".hist-row:not(.hdr):hover .hc{background:var(--bg3)}",
614
+ ".hist-row .hc:first-child{color:var(--text2)}",
615
+ ".hist-saved{color:var(--cyan);font-weight:600}",
616
+ ".empty-state{text-align:center;padding:60px 20px;color:var(--text3);border:1px dashed var(--line2);border-radius:var(--radius-xl);margin-top:12px}",
617
+ ".es-icon{font-size:36px;margin-bottom:10px}",
618
+ ".es-title{font-size:15px;color:var(--text2);margin-bottom:4px}",
619
+ ".hidden{display:none!important}",
620
+ ".toast{position:fixed;bottom:24px;left:50%;transform:translateX(-50%);background:var(--bg4);border:1px solid var(--line3);color:var(--text);padding:10px 18px;border-radius:999px;font-size:12.5px;box-shadow:0 8px 30px rgba(0,0,0,.5);z-index:200;animation:toastIn .3s ease}",
621
+ "@keyframes toastIn{0%{opacity:0;transform:translate(-50%,8px)}100%{opacity:1;transform:translate(-50%,0)}}",
622
+ ".footer{border-top:1px solid var(--line);margin-top:60px;padding:20px 0;font-size:12px;color:var(--text3)}",
623
+ ".footer-inner{display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:10px}",
624
+ "#view-overview{display:block}",
625
+ "#view-deepdive{display:none}",
626
+ "body.view-deepdive #view-overview{display:none}",
627
+ "body.view-deepdive #view-deepdive{display:block}",
628
+ ].join("\n");
629
+ /* eslint-enable prettier/prettier */
630
+ // ─────────────────────────────────────────────────────────────────────────────
631
+ // FRONT-END JS
632
+ //
633
+ // Stored as a plain Array.join() string — no TypeScript template literal so
634
+ // the browser-side ${...} expressions and backtick strings are never
635
+ // interpreted by the TypeScript compiler.
636
+ // ─────────────────────────────────────────────────────────────────────────────
637
+ /* eslint-disable prettier/prettier */
638
+ const REPORT_JS = [
639
+ "(function(){",
640
+ "'use strict';",
641
+ "var DATA=window.__SELA_DATA__;",
642
+ "function esc(s){return String(s==null?'':s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/\"/g,'&quot;');}",
643
+ "function fmt(iso){try{return new Date(iso).toLocaleString();}catch(e){return iso;}}",
644
+ "function fmtPct(v){return(typeof v==='number'&&isFinite(v))?v+'%':'n/a';}",
645
+ "function fmtMin(m){var h=Math.floor(m/60),r=m%60;return h>0?h+'h '+r+'m':m+'m';}",
646
+ "function $(id){return document.getElementById(id);}",
647
+ "function qsa(sel,root){return Array.from((root||document).querySelectorAll(sel));}",
648
+ // ── View toggle
649
+ "function setView(v){",
650
+ " document.body.classList.toggle('view-deepdive',v==='deepdive');",
651
+ " $('btn-overview').classList.toggle('active',v==='overview');",
652
+ " $('btn-deepdive').classList.toggle('active',v==='deepdive');",
653
+ "}",
654
+ "window.setView=setView;",
655
+ // ── Toast
656
+ "var toastTimer;",
657
+ "function toast(msg){var t=$('toast');t.textContent=msg;t.classList.remove('hidden');clearTimeout(toastTimer);toastTimer=setTimeout(function(){t.classList.add('hidden');},2600);}",
658
+ "window.copyJson=function(){navigator.clipboard&&navigator.clipboard.writeText(JSON.stringify(DATA,null,2)).then(function(){toast('JSON copied to clipboard');});};",
659
+ // ── Meta + KPIs
660
+ "var s=DATA.stats;",
661
+ "$('meta-gen').textContent='Generated '+fmt(DATA.generatedAt);",
662
+ "$('meta-cwd').textContent=DATA.cwd;",
663
+ "$('footer-cwd').textContent=DATA.cwd;",
664
+ "$('kpi-time').textContent=fmtMin(s.timeSavedMin);",
665
+ "$('kpi-healed').textContent=s.healed;",
666
+ "$('kpi-prot').textContent=s.protectedCnt;",
667
+ "$('kpi-fail').textContent=s.failed;",
668
+ "$('donut-total').textContent=s.total;",
669
+ "$('dd-count').textContent=DATA.events.length+' events';",
670
+ // ── Headline
671
+ "var headline=(function(){",
672
+ " if(s.healed===0&&s.protectedCnt===0&&s.failed===0)return['Sela was idle this run.','No broken selectors were encountered.'];",
673
+ " if(s.protectedCnt>0&&s.healed===0)return['Sela caught '+s.protectedCnt+' masked regression'+(s.protectedCnt!==1?'s':'')+'.',",
674
+ " 'The Intent Auditor blocked auto-fixes that would have silently changed test semantics.'];",
675
+ " if(s.healed>0&&s.protectedCnt>0)return['Sela healed '+s.healed+' selector'+(s.healed!==1?'s':'')+' and caught '+s.protectedCnt+' regression'+(s.protectedCnt!==1?'s':'')+'.',",
676
+ " 'Every fix was verified by the Intent Auditor. Two would-be fixes were masked regressions \u2014 drill in below.'];",
677
+ " if(s.healed>0)return['Sela healed '+s.healed+' selector'+(s.healed!==1?'s':'')+' automatically.',",
678
+ " 'Every fix was double-checked by the Intent Auditor before touching source code.'];",
679
+ " return['Sela couldn\u2019t heal '+s.failed+' selector'+(s.failed!==1?'s':'')+'.',",
680
+ " 'These need a human hand \u2014 context, candidates, and direct links are ready below.'];",
681
+ "})();",
682
+ "$('exec-h1').textContent=headline[0];",
683
+ "$('exec-sub').textContent=headline[1];",
684
+ // ── Donut chart
685
+ "var total=s.healed+s.protectedCnt+s.failed||1;",
686
+ "var donutCtx=$('donut-chart').getContext('2d');",
687
+ "new Chart(donutCtx,{type:'doughnut',",
688
+ " data:{labels:['Healed','Protected','Failed'],",
689
+ " datasets:[{data:[s.healed,s.protectedCnt,s.failed],",
690
+ " backgroundColor:['rgba(0,212,255,.8)','rgba(162,89,255,.8)','rgba(255,77,106,.8)'],",
691
+ " borderColor:['#00d4ff','#a259ff','#ff4d6a'],borderWidth:1.5,hoverOffset:6}]},",
692
+ " options:{responsive:true,maintainAspectRatio:false,cutout:'72%',",
693
+ " plugins:{legend:{display:false},tooltip:{callbacks:{label:function(c){return' '+c.label+': '+c.parsed;}}}},",
694
+ " onClick:function(e,els){if(!els.length)return;applyFilter(['HEALED','PROTECTED','FAILED'][els[0].index]);setView('deepdive');}",
695
+ " }",
696
+ "});",
697
+ // ── Donut legend
698
+ "var legendData=[",
699
+ " {filter:'ALL',swatch:'rgba(255,255,255,.3)',name:'All events',val:s.total,pct:null},",
700
+ " {filter:'HEALED',swatch:'var(--cyan)',name:'Healed',val:s.healed,pct:Math.round(s.healed/total*100)+'%'},",
701
+ " {filter:'PROTECTED',swatch:'var(--purple)',name:'Protected',val:s.protectedCnt,pct:Math.round(s.protectedCnt/total*100)+'%'},",
702
+ " {filter:'FAILED',swatch:'var(--red)',name:'Failed',val:s.failed,pct:Math.round(s.failed/total*100)+'%'},",
703
+ "];",
704
+ "var $legend=$('donut-legend');",
705
+ "$legend.innerHTML=legendData.map(function(d,i){",
706
+ " return '<div class=\"dl-row'+(i===0?' active':'')+'\" data-filter=\"'+d.filter+'\">'",
707
+ " +'<div class=\"dl-swatch\" style=\"background:'+d.swatch+'\"></div>'",
708
+ " +'<div class=\"dl-name\">'+d.name+'</div>'",
709
+ " +'<div class=\"dl-val\"'+(d.filter!=='ALL'?' style=\"color:'+d.swatch+'\"':'')+'>'+d.val+'</div>'",
710
+ " +(d.pct?'<div class=\"dl-pct\">'+d.pct+'</div>':'')",
711
+ " +'</div>';",
712
+ "}).join('');",
713
+ "$legend.addEventListener('click',function(e){",
714
+ " var row=e.target.closest('.dl-row');if(!row)return;",
715
+ " qsa('.dl-row',$legend).forEach(function(r){r.classList.remove('active');});",
716
+ " row.classList.add('active');",
717
+ " applyFilter(row.dataset.filter);",
718
+ " setView('deepdive');",
719
+ "});",
720
+ // ── Trend badge
721
+ "var runs=DATA.historical_runs||[];",
722
+ "if(runs.length>1){",
723
+ " var last=runs[runs.length-1],prev=runs[runs.length-2];",
724
+ " if(prev&&prev.time_saved_min>0){",
725
+ " var delta=Math.round((last.time_saved_min-prev.time_saved_min)/prev.time_saved_min*100);",
726
+ " var badge=$('trend-badge');",
727
+ " if(badge){",
728
+ " badge.textContent=(delta>=0?'+':'')+delta+'% vs prev run';",
729
+ " badge.style.color=delta>=0?'var(--green)':'var(--red)';",
730
+ " badge.style.borderColor=delta>=0?'rgba(0,229,160,.25)':'rgba(255,77,106,.25)';",
731
+ " badge.style.background=delta>=0?'var(--green-dim)':'var(--red-dim)';",
732
+ " }",
733
+ " }",
734
+ "}",
735
+ // ── Trend chart
736
+ "var trendLabels=runs.map(function(r){return r.label;});",
737
+ "var cumSaved=runs.map(function(_,i){return Math.round(runs.slice(0,i+1).reduce(function(a,b){return a+(b.time_saved_min/60);},0)*10)/10;});",
738
+ "var healedSeries=runs.map(function(r){return r.healed;});",
739
+ "var maxHealed=Math.max.apply(null,healedSeries.concat([1]));",
740
+ "var trendCtx=$('trend-chart').getContext('2d');",
741
+ "new Chart(trendCtx,{type:'line',",
742
+ " data:{labels:trendLabels,datasets:[",
743
+ " {label:'Cumulative hours saved',data:cumSaved,borderColor:'#00d4ff',backgroundColor:'rgba(0,212,255,.06)',fill:true,tension:.35,borderWidth:1.5,pointRadius:2.5,pointBackgroundColor:'#00d4ff',yAxisID:'y'},",
744
+ " {label:'Healed per run',data:healedSeries,borderColor:'rgba(162,89,255,.7)',backgroundColor:'transparent',tension:.35,borderWidth:1,borderDash:[4,3],pointRadius:2,pointBackgroundColor:'#a259ff',yAxisID:'y1'}",
745
+ " ]},",
746
+ " options:{responsive:true,maintainAspectRatio:false,",
747
+ " interaction:{mode:'index',intersect:false},",
748
+ " plugins:{legend:{display:false},",
749
+ " tooltip:{backgroundColor:'rgba(16,19,26,.95)',borderColor:'rgba(30,35,54,.8)',borderWidth:1,titleColor:'#dde2f0',bodyColor:'#8b93b0',padding:10,",
750
+ " callbacks:{label:function(c){return c.datasetIndex===0?' '+c.parsed.y.toFixed(1)+'h saved':' '+c.parsed.y+' healed';}}}},",
751
+ " scales:{",
752
+ " x:{grid:{color:'rgba(30,35,54,.6)',lineWidth:.5},ticks:{color:'#5a6180',font:{size:10},maxRotation:45}},",
753
+ " y:{position:'left',grid:{color:'rgba(30,35,54,.6)',lineWidth:.5},ticks:{color:'#5a6180',font:{size:10},callback:function(v){return v+'h';}}},",
754
+ " y1:{position:'right',grid:{display:false},ticks:{color:'#5a6180',font:{size:10}},max:maxHealed+2}",
755
+ " }",
756
+ " }",
757
+ "});",
758
+ // ── Historical table (last 5)
759
+ "var htbl=$('hist-table');",
760
+ "runs.slice().reverse().slice(0,5).forEach(function(r){",
761
+ " var row=document.createElement('div');row.className='hist-row';",
762
+ " row.innerHTML='<div class=\"hc\">'+esc(r.label)+'</div>'",
763
+ " +'<div class=\"hc\" style=\"color:var(--cyan)\">'+r.healed+'</div>'",
764
+ " +'<div class=\"hc\" style=\"color:var(--purple)\">'+(r.protected||0)+'</div>'",
765
+ " +'<div class=\"hc\" style=\"color:var(--red)\">'+r.failed+'</div>'",
766
+ " +'<div class=\"hc\">'+(r.avg_confidence||0)+'%</div>'",
767
+ " +'<div class=\"hc hist-saved\">'+fmtMin(r.time_saved_min)+'</div>';",
768
+ " htbl.appendChild(row);",
769
+ "});",
770
+ // ── VS Code link
771
+ "function vscodeLink(file,line){",
772
+ " if(!file)return'#';",
773
+ " var abs=(file.startsWith('/')||/^[A-Za-z]:/.test(file))?file:DATA.cwd+'/'+file;",
774
+ " return'vscode://file/'+encodeURI(abs).replace(/#/g,'%23')+(line?':'+line:'');",
775
+ "}",
776
+ // ── Confidence badge
777
+ "function confBadge(pct){",
778
+ " if(typeof pct!=='number'||!isFinite(pct))return'';",
779
+ " var cls=pct>=80?'conf-high':pct>=60?'conf-mid':'conf-low';",
780
+ " return'<span class=\"conf-badge '+cls+'\">'+pct+'%</span>';",
781
+ "}",
782
+ // ── Diff renderer
783
+ "function renderDiff(ev){",
784
+ " if(ev.kind==='PROTECTED')",
785
+ " return'<div class=\"prot-notice\"><strong>Source code untouched.</strong> Sela blocked this auto-fix \u2014 the Intent Auditor detected a semantic mismatch'+(ev.auditor&&ev.auditor.inversionType?' ('+esc(ev.auditor.inversionType)+')':'')+'. This is a real regression surfacing in CI. Investigate <a href=\"'+vscodeLink(ev.sourceFile,ev.sourceLine)+'\">'+esc(ev.sourceFile)+':'+ev.sourceLine+'</a> manually.</div>';",
786
+ " if(ev.kind==='FAILED')",
787
+ " return'<div class=\"muted-block\">No source code was modified. The AI could not propose a confident fix \u2014 all candidates resolved to 0 elements. Open the file directly to investigate.</div>';",
788
+ " var raw=ev.gitUnifiedDiff||'';",
789
+ " var skipPfx=['diff ','index ','--- ','+++','new file','deleted','similarity','rename','Binary'];",
790
+ " var hunkRe=/^@@ -(\\d+)(?:,\\d+)? \\+(\\d+)(?:,\\d+)? @@/;",
791
+ " var body='',newLn=0,hasDiff=raw.split(/\\r?\\n/).some(function(l){return hunkRe.test(l);});",
792
+ " if(hasDiff){",
793
+ " raw.split(/\\r?\\n/).forEach(function(r){",
794
+ " if(skipPfx.some(function(p){return r.startsWith(p);}))return;",
795
+ " var hm=r.match(hunkRe);",
796
+ ' if(hm){newLn=parseInt(hm[2]);body+=\'<div class="diff-line dl-hunk"><span class="gutter">@</span><span class="ln"></span><span class="code">\'+esc(r)+\'</span></div>\';return;}',
797
+ " if(!r.length||r.startsWith('\\\\'))return;",
798
+ " var h=r[0],t=r.slice(1);",
799
+ " if(h==='+'){body+='<div class=\"diff-line dl-add\"><span class=\"gutter\">+</span><span class=\"ln\">'+newLn+'</span><span class=\"code\">'+esc(t)+'</span></div>';newLn++;}",
800
+ ' else if(h===\'-\'){body+=\'<div class="diff-line dl-del"><span class="gutter">-</span><span class="ln"></span><span class="code">\'+esc(t)+\'</span></div>\';}',
801
+ " else if(h===' '){body+='<div class=\"diff-line dl-ctx\"><span class=\"gutter\"> </span><span class=\"ln\">'+newLn+'</span><span class=\"code\">'+esc(t)+'</span></div>';newLn++;}",
802
+ " });",
803
+ " }else{",
804
+ " body='<div class=\"diff-line dl-del\"><span class=\"gutter\">-</span><span class=\"ln\">'+(ev.newLineNumber||'')+'</span><span class=\"code\">'+esc(ev.oldCodeLine||'')+'</span></div>'",
805
+ " +'<div class=\"diff-line dl-add\"><span class=\"gutter\">+</span><span class=\"ln\">'+(ev.newLineNumber||'')+'</span><span class=\"code\">'+esc(ev.newCodeLine||'')+'</span></div>';",
806
+ " }",
807
+ " return'<div class=\"diff-block\"><div class=\"diff-bar\"><span>'+esc(ev.sourceFile)+'</span><span class=\"sep\">\xb7</span><span>line '+ev.newLineNumber+'</span>'+(ev.diffStrategy?'<span class=\"sep\">\xb7</span><span>'+esc(ev.diffStrategy)+'</span>':'')+'<span class=\"sep\">\xb7</span><span>'+(hasDiff?'git diff':'fallback')+'</span></div><div>'+body+'</div></div>';",
808
+ "}",
809
+ // ── Auditor renderer
810
+ "function renderAuditor(a){",
811
+ " if(!a)return'<div class=\"muted-block\">Intent Auditor was not consulted for this event.</div>';",
812
+ " var vl=a.verdict.toLowerCase();",
813
+ " var hasPct=typeof a.confidence==='number'&&isFinite(a.confidence);",
814
+ " var pct=hasPct?Math.max(0,Math.min(100,a.confidence)):0;",
815
+ " var circ=2*Math.PI*50,dash=hasPct?(pct/100)*circ:0;",
816
+ " var adj=[];",
817
+ " if(a.penalty)adj.push('<span style=\"color:var(--red)\">'+a.penalty+'</span>');",
818
+ " if(a.bonus)adj.push('<span style=\"color:var(--green)\">+'+a.bonus+'</span>');",
819
+ " return'<div class=\"auditor-wrap\">'",
820
+ " +'<div class=\"gauge-ring\">'",
821
+ ' +\'<svg viewBox="0 0 120 120"><circle class="gt" cx="60" cy="60" r="50"/><circle class="gb gb-\'+vl+\'" cx="60" cy="60" r="50" stroke-dasharray="\'+dash.toFixed(1)+\' \'+circ.toFixed(1)+\'"/></svg>\'',
822
+ " +'<div class=\"gauge-center\"><div class=\"gauge-pct pct-'+vl+'\">'+(hasPct?pct+'%':'n/a')+'</div><div class=\"gauge-sub\">consistency</div></div>'",
823
+ " +'</div>'",
824
+ " +'<div class=\"audit-details\">'",
825
+ ' +\'<div class="ad-row"><span class="ad-label">Verdict</span><span class="ad-val"><span class="verdict-pill vp-\'+vl+\'">\'+esc(a.verdict)+\'</span></span></div>\'',
826
+ ' +(a.inversionType?\'<div class="ad-row"><span class="ad-label">Inversion</span><span class="ad-val" style="color:var(--red)">\'+esc(a.inversionType)+\'</span></div>\':\'\')',
827
+ " +(adj.length?'<div class=\"ad-row\"><span class=\"ad-label\">Score delta</span><span class=\"ad-val\">'+adj.join(' ')+' (raw: '+fmtPct(a.rawConfidence)+')</span></div>':'')",
828
+ " +'<div class=\"ad-reason\">'+esc(a.reason)+'</div>'",
829
+ " +'</div></div>';",
830
+ "}",
831
+ // ── CoT renderer
832
+ "function renderCoT(steps){",
833
+ " if(!steps||!steps.length)return'<div class=\"muted-block\">No reasoning trace captured.</div>';",
834
+ " return'<div class=\"cot\">'+steps.map(function(s,i){return'<div class=\"cot-step\"><span class=\"cot-n\">'+(i+1)+'</span><span class=\"cot-t\">'+esc(s)+'</span></div>';}).join('')+'</div>';",
835
+ "}",
836
+ // ── DNA table
837
+ "function renderDna(before,after){",
838
+ " if(!before)return'<div class=\"muted-block\">No DNA baseline available.</div>';",
839
+ " var rows=[];",
840
+ " function row(field,ov,nv){",
841
+ " var os=ov==null?'':Array.isArray(ov)?ov.join(' / '):String(ov);",
842
+ " var ns=nv==null?'':Array.isArray(nv)?nv.join(' / '):String(nv);",
843
+ " var same=os===ns,hasAfter=!!after;",
844
+ " var cls=!hasAfter?'missing':same?'matched':'changed';",
845
+ " var icon=!hasAfter?'\u2014':same?'\u2713':'\u25b3';",
846
+ " var vc;",
847
+ " if(!hasAfter||same)vc='<td class=\"val\">'+esc(os||'\u2014')+'</td>';",
848
+ " else vc='<td class=\"val\"><span class=\"val old\">'+esc(os||'\u2014')+'</span> \u2192 <span class=\"val new\">'+esc(ns||'\u2014')+'</span></td>';",
849
+ " rows.push('<tr class=\"'+cls+'\"><td class=\"field\">'+esc(field)+'</td>'+vc+'<td class=\"icon\">'+icon+'</td></tr>');",
850
+ " }",
851
+ " row('Tag',before.tagName,after&&after.tagName);",
852
+ " row('Text',before.text,after&&after.text);",
853
+ " row('Role',before.role,after&&after.role);",
854
+ " row('Classes',before.classes,after&&after.classes);",
855
+ " row('Ancestry',before.ancestry,after&&after.ancestry);",
856
+ " row('Closest label',before.closestLabel,after&&after.closestLabel);",
857
+ " var ab=Object.entries(before.attributes||{}).map(function(e){return e[0]+'='+e[1];}).join(' \xb7 ');",
858
+ " var aa=after?Object.entries(after.attributes||{}).map(function(e){return e[0]+'='+e[1];}).join(' \xb7 '):null;",
859
+ " row('Attributes',ab,aa);",
860
+ " return'<table class=\"dna-table\"><thead><tr><th>Field</th><th>Value</th><th></th></tr></thead><tbody>'+rows.join('')+'</tbody></table>';",
861
+ "}",
862
+ // ── Screenshot
863
+ "function renderScreenshot(ev){",
864
+ " if(ev.kind!=='FAILED'||!ev.failureScreenshotPng)return'';",
865
+ ' return\'<div class="body-section"><div class="bs-title"><span class="bst-dot"></span>Playwright Failure Screenshot</div>\'',
866
+ ' +\'<div class="fail-img-wrap"><img src="data:image/png;base64,\'+ev.failureScreenshotPng+\'" alt="Failure screenshot" loading="lazy"/>\'',
867
+ " +'<div class=\"fail-img-meta\">captured at test failure \xb7 base64-embedded</div></div></div>';",
868
+ "}",
869
+ // ── Card body
870
+ "function renderCardBody(ev){",
871
+ " var steps=ev.reasoningSteps||[];",
872
+ " if(ev.kind==='PROTECTED'&&!steps.length){",
873
+ " steps=['AI proposed a replacement selector for the broken locator.'];",
874
+ " if(ev.aiExplanation)steps.push('Reasoning: '+ev.aiExplanation);",
875
+ " if(ev.candidateNewSelector)steps.push('Candidate: '+ev.candidateNewSelector+' \u2014 but semantics changed.');",
876
+ " if(ev.auditor)steps.push('Intent Auditor: '+ev.auditor.verdict+' \u2014 '+ev.auditor.reason.slice(0,160));",
877
+ " steps.push('Sela blocked the auto-fix. Source code is untouched.');",
878
+ " }",
879
+ " if(ev.kind==='FAILED'&&!steps.length){",
880
+ " steps=['Locator failed to resolve at runtime \u2014 Sela attempted to heal it.'];",
881
+ " if(ev.aiExplanation)steps.push('AI: '+ev.aiExplanation);",
882
+ " steps.push('Reason: '+(ev.reason||'unknown'));",
883
+ " steps.push('Open the file in your editor to inspect the original locator.');",
884
+ " }",
885
+ " var issueUrl='https://github.com/sela-core/sela/issues/new?title='+encodeURIComponent('[Report] '+ev.kind+' '+ev.stableId)+'&body='+encodeURIComponent('Stable ID: '+ev.stableId+'\\nFile: '+ev.sourceFile+':'+ev.sourceLine);",
886
+ " var link=vscodeLink(ev.sourceFile,ev.sourceLine);",
887
+ " var actions='';",
888
+ " if(ev.kind==='HEALED'){",
889
+ " actions='<div class=\"actions\">'",
890
+ " +'<button class=\"btn btn-primary\" data-action=\"keep\" data-id=\"'+ev.stableId+'\" onclick=\"handleAction(this,\\'keep\\',\\''+ev.stableId+'\\')\">\u2713 Keep changes</button>'",
891
+ " +'<button class=\"btn btn-danger\" onclick=\"handleAction(this,\\'rollback\\',\\''+ev.stableId+'\\')\">&#x21B6; Rollback</button>'",
892
+ " +'<a class=\"btn\" href=\"'+link+'\">&#x2317; Open in VS Code</a>'",
893
+ ' +\'<a class="btn" href="\'+issueUrl+\'" target="_blank" rel="noopener">&#x2691; Report issue</a>\'',
894
+ " +'</div>';",
895
+ " }else if(ev.kind==='PROTECTED'){",
896
+ " actions='<div class=\"actions\">'",
897
+ " +'<a class=\"btn\" href=\"'+link+'\">\u2192 Open in VS Code</a>'",
898
+ " +'<button class=\"btn btn-primary\" onclick=\"handleAction(this,\\'ack\\',\\''+ev.stableId+'\\')\">&#x2713; Acknowledged \u2014 will investigate</button>'",
899
+ ' +\'<a class="btn" href="\'+issueUrl+\'" target="_blank" rel="noopener">&#x2691; File regression</a>\'',
900
+ " +'</div>';",
901
+ " }else{",
902
+ " actions='<div class=\"actions\">'",
903
+ " +'<a class=\"btn btn-primary\" href=\"'+link+'\">\u2192 Open in VS Code</a>'",
904
+ ' +\'<a class="btn" href="\'+issueUrl+\'" target="_blank" rel="noopener">&#x2691; Report issue</a>\'',
905
+ " +'</div>';",
906
+ " }",
907
+ " var alts=(ev.kind==='HEALED'&&ev.aiAlternatives&&ev.aiAlternatives.length)",
908
+ ' ?\'<div class="body-section"><div class="bs-title"><span class="bst-dot"></span>Alternative selectors considered</div><div class="alts">\'',
909
+ " +ev.aiAlternatives.map(function(a){return'<div class=\"alt-row\"><span>'+esc(a)+'</span><button class=\"alt-copy\" onclick=\"copyText(\\''+esc(a)+'\\',this)\">Copy</button></div>';}).join('')",
910
+ " +'</div></div>'",
911
+ " :'';",
912
+ ' return\'<div class="row2" style="margin-top:16px">\'',
913
+ " +'<div><div class=\"bs-title\"><span class=\"bst-dot\"></span>AI Reasoning \xb7 Chain of Thought</div>'+renderCoT(steps)+'</div>'",
914
+ " +'<div><div class=\"bs-title\"><span class=\"bst-dot\"></span>Intent Auditor</div>'+renderAuditor(ev.auditor||null)+'</div>'",
915
+ " +'</div>'",
916
+ ' +\'<div class="body-section"><div class="bs-title"><span class="bst-dot"></span>Source Code Diff</div>\'+renderDiff(ev)+\'</div>\'',
917
+ ' +\'<div class="body-section"><div class="bs-title"><span class="bst-dot"></span>Visual DNA Evidence</div>\'+renderDna(ev.dnaBefore||null,ev.dnaAfter||null)+\'</div>\'',
918
+ " +renderScreenshot(ev)",
919
+ " +alts",
920
+ " +actions;",
921
+ "}",
922
+ // ── Event card
923
+ "function renderCard(ev,idx){",
924
+ " var kindCls=ev.kind==='HEALED'?'ec-healed':ev.kind==='PROTECTED'?'ec-protected':'ec-failed';",
925
+ " var badgeCls=ev.kind==='HEALED'?'sb-healed':ev.kind==='PROTECTED'?'sb-protected':'sb-failed';",
926
+ " var badgeTxt=ev.kind==='HEALED'?'Healed':ev.kind==='PROTECTED'?'Protected \xb7 Bug Caught':'Failed';",
927
+ " var sel=ev.kind==='HEALED'?(ev.newSelector||ev.oldSelector):ev.oldSelector;",
928
+ " var link=vscodeLink(ev.sourceFile,ev.sourceLine);",
929
+ " var tags='';",
930
+ " if(ev.inIframe)tags+='<span class=\"ecard-tag t-iframe\">iframe</span>';",
931
+ " if(ev.inShadowDom)tags+='<span class=\"ecard-tag t-shadow\">shadow DOM</span>';",
932
+ " if(ev.dryRun)tags+='<span class=\"ecard-tag t-dryrun\">dry run</span>';",
933
+ " var haystack=(ev.oldSelector+' '+(ev.newSelector||'')+' '+ev.sourceFile+' '+(ev.testTitle||'')+' '+ev.stableId).toLowerCase();",
934
+ " return'<article class=\"ecard '+kindCls+'\" data-kind=\"'+ev.kind+'\" data-haystack=\"'+esc(haystack)+'\" style=\"animation-delay:'+(idx*0.04)+'s\">'",
935
+ " +'<div class=\"ecard-head\" onclick=\"this.parentElement.classList.toggle(\\'open\\')\">'",
936
+ " +'<span class=\"status-badge '+badgeCls+'\"><span class=\"sb-dot\"></span>'+badgeTxt+'</span>'",
937
+ " +'<div class=\"ecard-title\">'",
938
+ " +'<div class=\"ecard-selector\">'+esc(sel)+'</div>'",
939
+ " +'<div class=\"ecard-meta\">'+(ev.testTitle?esc(ev.testTitle)+' &nbsp;\xb7&nbsp; ':'')",
940
+ " +'<a href=\"'+link+'\">'+esc(ev.sourceFile)+':'+ev.sourceLine+'</a>'+tags+' &nbsp;\xb7&nbsp; '+fmt(ev.timestamp)+'</div>'",
941
+ " +'</div>'",
942
+ " +confBadge(ev.aiConfidence)",
943
+ ' +\'<svg class="chev" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2"><polyline points="5 3 11 8 5 13"/></svg>\'',
944
+ " +'</div>'",
945
+ " +'<div class=\"ecard-body\">'+renderCardBody(ev)+'</div>'",
946
+ " +'</article>';",
947
+ "}",
948
+ // ── Mount events
949
+ "var $el=$('events-list');",
950
+ "$el.innerHTML=DATA.events.map(renderCard).join('');",
951
+ "var firstProt=$el.querySelector('.ec-protected');",
952
+ "if(firstProt)firstProt.classList.add('open');",
953
+ "else{var f=$el.querySelector('.ecard');if(f)f.classList.add('open');}",
954
+ // ── Filter + Search
955
+ "var curFilter='ALL',searchTerm='';",
956
+ "function applyFilter(f){",
957
+ " curFilter=f;",
958
+ " qsa('.chip').forEach(function(c){c.classList.toggle('on',c.dataset.filter===f||(f==='ALL'&&c.dataset.filter==='ALL'));});",
959
+ " filterCards();",
960
+ "}",
961
+ "window.applyFilter=applyFilter;",
962
+ "function filterCards(){",
963
+ " var vis=0;",
964
+ " qsa('.ecard',$el).forEach(function(card){",
965
+ " var show=(curFilter==='ALL'||card.dataset.kind===curFilter)&&(!searchTerm||card.dataset.haystack.includes(searchTerm));",
966
+ " card.style.display=show?'':'none';",
967
+ " if(show)vis++;",
968
+ " });",
969
+ " $('empty-state').classList.toggle('hidden',vis>0||DATA.events.length===0);",
970
+ "}",
971
+ "var fbChips=document.querySelector('.fb-chips');",
972
+ "if(fbChips)fbChips.addEventListener('click',function(e){var chip=e.target.closest('.chip');if(chip&&chip.dataset.filter)applyFilter(chip.dataset.filter);});",
973
+ "$('search-input').addEventListener('input',function(e){searchTerm=e.target.value.toLowerCase().trim();filterCards();});",
974
+ // ── Actions
975
+ "window.handleAction=function(btn,action,id){",
976
+ " if(action==='keep'||action==='ack'){btn.dataset.done='true';btn.textContent=action==='keep'?'\u2713 Approved':'\u2713 Acknowledged';toast('Decision saved for '+id);}",
977
+ " else if(action==='rollback'){var cmd='npx sela rollback --id '+id;navigator.clipboard&&navigator.clipboard.writeText(cmd).then(function(){toast('Copied: '+cmd);});}",
978
+ "};",
979
+ "window.copyText=function(txt,btn){",
980
+ " navigator.clipboard&&navigator.clipboard.writeText(txt).then(function(){btn.textContent='Copied!';setTimeout(function(){btn.textContent='Copy';},1500);});",
981
+ "};",
982
+ // ── Keyboard shortcuts
983
+ "document.addEventListener('keydown',function(e){",
984
+ " if(e.key==='/'&&document.activeElement!==$('search-input')){e.preventDefault();$('search-input').focus();setView('deepdive');}",
985
+ " if(e.key==='Escape')$('search-input').blur();",
986
+ "});",
987
+ "})();",
988
+ ].join("\n");
989
+ /* eslint-enable prettier/prettier */