sela-core 1.0.3 → 1.0.4

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.
@@ -0,0 +1,1195 @@
1
+ "use strict";
2
+ // src/services/HealReportService.ts
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.
7
+ //
8
+ // Goal: explain Sela's "brain" to the developer. Maximum trust & observability.
9
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ var desc = Object.getOwnPropertyDescriptor(m, k);
12
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
13
+ desc = { enumerable: true, get: function() { return m[k]; } };
14
+ }
15
+ Object.defineProperty(o, k2, desc);
16
+ }) : (function(o, m, k, k2) {
17
+ if (k2 === undefined) k2 = k;
18
+ o[k2] = m[k];
19
+ }));
20
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
21
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
22
+ }) : function(o, v) {
23
+ o["default"] = v;
24
+ });
25
+ var __importStar = (this && this.__importStar) || (function () {
26
+ var ownKeys = function(o) {
27
+ ownKeys = Object.getOwnPropertyNames || function (o) {
28
+ var ar = [];
29
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
30
+ return ar;
31
+ };
32
+ return ownKeys(o);
33
+ };
34
+ return function (mod) {
35
+ if (mod && mod.__esModule) return mod;
36
+ var result = {};
37
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
38
+ __setModuleDefault(result, mod);
39
+ return result;
40
+ };
41
+ })();
42
+ Object.defineProperty(exports, "__esModule", { value: true });
43
+ exports.sharedHealReport = exports.HealReportService = void 0;
44
+ exports.summariseSnapshot = summariseSnapshot;
45
+ const fs = __importStar(require("fs"));
46
+ 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
+ // ═══════════════════════════════════════════════════════════════════
54
+ function summariseSnapshot(s) {
55
+ if (!s)
56
+ return null;
57
+ return {
58
+ tagName: s.tagName,
59
+ text: s.text,
60
+ role: s.role,
61
+ classes: s.classes ?? [],
62
+ attributes: s.attributes ?? {},
63
+ ancestry: (s.ancestry ?? []).map((a) => a.fingerprint),
64
+ closestLabel: s.anchors?.closestLabel ?? null,
65
+ rowAnchor: s.anchors?.rowAnchor ?? null,
66
+ isGhost: s.visibility?.isGhost ?? false,
67
+ ghostReason: s.visibility?.ghostReason ?? null,
68
+ isOccluded: s.visibility?.isOccluded ?? false,
69
+ isInShadowDom: s.isInShadowDom ?? false,
70
+ };
71
+ }
72
+ // ═══════════════════════════════════════════════════════════════════
73
+ // HealReportService
74
+ // ═══════════════════════════════════════════════════════════════════
75
+ class HealReportService {
76
+ events = [];
77
+ startedAt = new Date().toISOString();
78
+ record(event) {
79
+ this.events.push(event);
80
+ }
81
+ size() {
82
+ return this.events.length;
83
+ }
84
+ clear() {
85
+ this.events = [];
86
+ this.startedAt = new Date().toISOString();
87
+ }
88
+ hasContent() {
89
+ return this.events.length > 0;
90
+ }
91
+ /** All HEALED events accumulated in this session. */
92
+ getHealedEvents() {
93
+ return this.events.filter((e) => e.kind === "HEALED");
94
+ }
95
+ /** All PROTECTED events (SafetyGuard blocks) accumulated in this session. */
96
+ getProtectedEvents() {
97
+ return this.events.filter((e) => e.kind === "PROTECTED");
98
+ }
99
+ /**
100
+ * Write `sela-report.html` (+ `sela-report.json` debug dump) to cwd.
101
+ * Returns the absolute path of the HTML report, or null if nothing to write.
102
+ */
103
+ flushToDisk(targetDir = process.cwd()) {
104
+ if (this.events.length === 0)
105
+ return null;
106
+ const html = this.renderHtml();
107
+ const htmlPath = path.join(targetDir, "sela-report.html");
108
+ const jsonPath = path.join(targetDir, "sela-report.json");
109
+ fs.writeFileSync(htmlPath, html, "utf8");
110
+ fs.writeFileSync(jsonPath, JSON.stringify({ generatedAt: new Date().toISOString(), startedAt: this.startedAt, events: this.events }, null, 2), "utf8");
111
+ return htmlPath;
112
+ }
113
+ // ─────────────────────────────────────────────────────────────
114
+ // RENDERING
115
+ // ─────────────────────────────────────────────────────────────
116
+ renderHtml() {
117
+ const data = {
118
+ generatedAt: new Date().toISOString(),
119
+ startedAt: this.startedAt,
120
+ cwd: process.cwd(),
121
+ events: this.events,
122
+ stats: this.computeStats(),
123
+ };
124
+ return renderHtmlTemplate(data);
125
+ }
126
+ computeStats() {
127
+ let healed = 0, failed = 0, protectedCnt = 0;
128
+ let iframeCnt = 0, shadowCnt = 0;
129
+ for (const e of this.events) {
130
+ if (e.kind === "HEALED")
131
+ healed++;
132
+ else if (e.kind === "FAILED")
133
+ failed++;
134
+ else
135
+ protectedCnt++;
136
+ if (e.inIframe)
137
+ iframeCnt++;
138
+ if (e.inShadowDom)
139
+ shadowCnt++;
140
+ }
141
+ const timeSavedMin = healed * TIME_PER_HEAL +
142
+ protectedCnt * TIME_PER_PROTECT +
143
+ failed * TIME_PER_FAILED;
144
+ return {
145
+ total: this.events.length,
146
+ healed,
147
+ failed,
148
+ protectedCnt,
149
+ iframeCnt,
150
+ shadowCnt,
151
+ timeSavedMin,
152
+ };
153
+ }
154
+ }
155
+ exports.HealReportService = HealReportService;
156
+ // ═══════════════════════════════════════════════════════════════════
157
+ // MODULE-LEVEL SINGLETON
158
+ // ═══════════════════════════════════════════════════════════════════
159
+ exports.sharedHealReport = new HealReportService();
160
+ // ═══════════════════════════════════════════════════════════════════
161
+ // HTML TEMPLATE (single-file, dark mode, animated)
162
+ // ═══════════════════════════════════════════════════════════════════
163
+ function htmlEscape(s) {
164
+ return s
165
+ .replace(/&/g, "&amp;")
166
+ .replace(/</g, "&lt;")
167
+ .replace(/>/g, "&gt;")
168
+ .replace(/"/g, "&quot;")
169
+ .replace(/'/g, "&#39;");
170
+ }
171
+ function renderHtmlTemplate(input) {
172
+ // Embed structured data into the page so the front-end script renders it
173
+ // — keeps the template compact and avoids server-side template branching.
174
+ const payload = JSON.stringify(input).replace(/</g, "\\u003c");
175
+ return `<!DOCTYPE html>
176
+ <html lang="en" data-theme="dark">
177
+ <head>
178
+ <meta charset="UTF-8" />
179
+ <meta name="viewport" content="width=device-width,initial-scale=1.0" />
180
+ <title>Sela Insights — Healing Report</title>
181
+ <style>
182
+ ${REPORT_CSS}
183
+ </style>
184
+ </head>
185
+ <body>
186
+ <div class="bg-aurora"></div>
187
+ <div class="bg-grid"></div>
188
+
189
+ <header class="hero">
190
+ <div class="hero-inner">
191
+ <div class="brand">
192
+ <div class="brand-mark">
193
+ <svg viewBox="0 0 32 32" width="36" height="36" aria-hidden="true">
194
+ <defs>
195
+ <linearGradient id="g1" x1="0" y1="0" x2="1" y2="1">
196
+ <stop offset="0" stop-color="#7c5cff"/>
197
+ <stop offset="1" stop-color="#22d3ee"/>
198
+ </linearGradient>
199
+ </defs>
200
+ <path d="M16 2 L29 9 V23 L16 30 L3 23 V9 Z" fill="url(#g1)" opacity="0.18"/>
201
+ <path d="M16 2 L29 9 V23 L16 30 L3 23 V9 Z" fill="none" stroke="url(#g1)" stroke-width="1.2"/>
202
+ <circle cx="16" cy="16" r="4" fill="url(#g1)"/>
203
+ </svg>
204
+ </div>
205
+ <div>
206
+ <div class="brand-title">Sela Insights</div>
207
+ <div class="brand-sub">Transparent Healing Report</div>
208
+ </div>
209
+ </div>
210
+ <div class="hero-meta">
211
+ <span id="meta-generated"></span>
212
+ </div>
213
+ </div>
214
+
215
+ <section class="executive">
216
+ <div class="exec-headline">
217
+ <span id="exec-eyebrow" class="eyebrow">Run complete</span>
218
+ <h1 id="exec-title">Sela kept your suite alive.</h1>
219
+ <p id="exec-sub" class="exec-sub"></p>
220
+ </div>
221
+ <div class="kpi-row" id="kpi-row"></div>
222
+ </section>
223
+ </header>
224
+
225
+ <main class="container">
226
+ <div class="filter-bar">
227
+ <div class="filter-group" id="filter-group">
228
+ <button class="chip active" data-filter="ALL">All</button>
229
+ <button class="chip" data-filter="HEALED">Healed</button>
230
+ <button class="chip protect-chip" data-filter="PROTECTED">Protected · Bug Caught</button>
231
+ <button class="chip" data-filter="FAILED">Failed</button>
232
+ </div>
233
+ <div class="search-wrap">
234
+ <input id="search" type="search" placeholder="Filter by selector, file, or test…" />
235
+ </div>
236
+ </div>
237
+
238
+ <section id="events" class="events"></section>
239
+
240
+ <section id="empty-state" class="empty hidden">
241
+ <div class="empty-icon">∅</div>
242
+ <div class="empty-title">Nothing matches that filter</div>
243
+ <div class="empty-sub">Try a different filter or clear the search box.</div>
244
+ </section>
245
+ </main>
246
+
247
+ <footer class="footer">
248
+ <div class="footer-inner">
249
+ <div>
250
+ Generated by <strong>sela-core</strong> ·
251
+ <span class="muted" id="footer-cwd"></span>
252
+ </div>
253
+ <div class="muted">
254
+ Sela is a transparent pair-programmer. Every decision below is yours to keep, roll back, or report.
255
+ </div>
256
+ </div>
257
+ </footer>
258
+
259
+ <div id="toast" class="toast hidden"></div>
260
+
261
+ <script id="sela-data" type="application/json">${payload}</script>
262
+ <script>
263
+ ${REPORT_JS}
264
+ </script>
265
+ </body>
266
+ </html>`;
267
+ }
268
+ // ═══════════════════════════════════════════════════════════════════
269
+ // CSS — dark, immersive, animated
270
+ // ═══════════════════════════════════════════════════════════════════
271
+ const REPORT_CSS = `
272
+ :root {
273
+ color-scheme: dark;
274
+ --bg-0: #08090f;
275
+ --bg-1: #0d0f18;
276
+ --bg-2: #131623;
277
+ --bg-3: #1a1e2e;
278
+ --line: #1f2436;
279
+ --line-strong: #2a3148;
280
+ --text: #e6e8f2;
281
+ --text-dim: #9aa1bd;
282
+ --text-muted: #6b7393;
283
+ --brand: #7c5cff;
284
+ --brand-2: #22d3ee;
285
+ --good: #34d399;
286
+ --good-dim: rgba(52, 211, 153, 0.15);
287
+ --warn: #fbbf24;
288
+ --warn-dim: rgba(251, 191, 36, 0.15);
289
+ --bad: #f87171;
290
+ --bad-dim: rgba(248, 113, 113, 0.15);
291
+ --protect: #c084fc;
292
+ --protect-dim: rgba(192, 132, 252, 0.18);
293
+ --radius: 14px;
294
+ --radius-sm: 8px;
295
+ --shadow-card: 0 1px 0 rgba(255,255,255,0.04) inset, 0 8px 30px rgba(0,0,0,0.35);
296
+ --mono: ui-monospace, SFMono-Regular, "JetBrains Mono", Menlo, Consolas, monospace;
297
+ --sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Inter, system-ui, sans-serif;
298
+ }
299
+
300
+ * { box-sizing: border-box; }
301
+ html, body { margin: 0; padding: 0; }
302
+ body {
303
+ font-family: var(--sans);
304
+ color: var(--text);
305
+ background: var(--bg-0);
306
+ min-height: 100vh;
307
+ -webkit-font-smoothing: antialiased;
308
+ overflow-x: hidden;
309
+ }
310
+
311
+ a { color: var(--brand-2); text-decoration: none; }
312
+ a:hover { text-decoration: underline; }
313
+
314
+ /* ── Background flourishes ─────────────────────────────────────── */
315
+ .bg-aurora {
316
+ position: fixed; inset: -20% -10% auto -10%; height: 70vh; z-index: -2;
317
+ background:
318
+ radial-gradient(60% 60% at 20% 30%, rgba(124,92,255,0.25), transparent 70%),
319
+ radial-gradient(50% 50% at 80% 20%, rgba(34,211,238,0.18), transparent 70%),
320
+ radial-gradient(60% 50% at 50% 80%, rgba(192,132,252,0.12), transparent 70%);
321
+ filter: blur(40px);
322
+ animation: aurora 20s ease-in-out infinite alternate;
323
+ }
324
+ @keyframes aurora {
325
+ 0% { transform: translate3d(0, 0, 0) scale(1); }
326
+ 100% { transform: translate3d(-3%, 2%, 0) scale(1.08); }
327
+ }
328
+ .bg-grid {
329
+ position: fixed; inset: 0; z-index: -1; pointer-events: none;
330
+ background-image:
331
+ linear-gradient(rgba(124,92,255,0.05) 1px, transparent 1px),
332
+ linear-gradient(90deg, rgba(124,92,255,0.05) 1px, transparent 1px);
333
+ background-size: 48px 48px;
334
+ mask-image: radial-gradient(circle at 50% 0%, black 0%, transparent 70%);
335
+ }
336
+
337
+ /* ── Hero ──────────────────────────────────────────────────────── */
338
+ .hero {
339
+ padding: 28px 32px 12px;
340
+ max-width: 1280px; margin: 0 auto;
341
+ }
342
+ .hero-inner {
343
+ display: flex; align-items: center; justify-content: space-between;
344
+ margin-bottom: 36px;
345
+ }
346
+ .brand { display: flex; align-items: center; gap: 12px; }
347
+ .brand-mark {
348
+ display: flex; align-items: center; justify-content: center;
349
+ width: 44px; height: 44px; border-radius: 12px;
350
+ background: linear-gradient(145deg, rgba(124,92,255,0.18), rgba(34,211,238,0.10));
351
+ border: 1px solid var(--line-strong);
352
+ }
353
+ .brand-title { font-weight: 700; font-size: 16px; letter-spacing: 0.2px; }
354
+ .brand-sub { font-size: 12px; color: var(--text-muted); letter-spacing: 0.4px; text-transform: uppercase; }
355
+ .hero-meta { color: var(--text-muted); font-size: 13px; }
356
+
357
+ .executive {
358
+ margin-top: 24px;
359
+ display: grid; grid-template-columns: 1.4fr 1fr; gap: 36px;
360
+ align-items: end;
361
+ padding-bottom: 28px;
362
+ border-bottom: 1px solid var(--line);
363
+ }
364
+ @media (max-width: 900px) { .executive { grid-template-columns: 1fr; } }
365
+ .eyebrow {
366
+ display: inline-block; font-size: 11px; letter-spacing: 1.5px;
367
+ text-transform: uppercase; color: var(--brand-2);
368
+ padding: 4px 10px; border: 1px solid rgba(34,211,238,0.3); border-radius: 999px;
369
+ margin-bottom: 14px;
370
+ }
371
+ .exec-headline h1 {
372
+ margin: 0 0 10px; font-size: 38px; line-height: 1.1;
373
+ background: linear-gradient(135deg, #fff 0%, #cdd3f0 50%, #8c96c0 100%);
374
+ -webkit-background-clip: text; background-clip: text; color: transparent;
375
+ }
376
+ .exec-sub { color: var(--text-dim); font-size: 15px; max-width: 56ch; line-height: 1.55; }
377
+
378
+ .kpi-row {
379
+ display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
380
+ gap: 14px;
381
+ }
382
+ .kpi {
383
+ background: linear-gradient(180deg, var(--bg-2), var(--bg-1));
384
+ border: 1px solid var(--line);
385
+ border-radius: var(--radius);
386
+ padding: 16px 16px 14px;
387
+ position: relative;
388
+ overflow: hidden;
389
+ transition: transform .3s ease, border-color .3s ease;
390
+ }
391
+ .kpi:hover { transform: translateY(-2px); border-color: var(--line-strong); }
392
+ .kpi-label { font-size: 11px; letter-spacing: 1px; text-transform: uppercase; color: var(--text-muted); }
393
+ .kpi-value { font-size: 30px; font-weight: 700; margin-top: 6px; font-variant-numeric: tabular-nums; }
394
+ .kpi-foot { font-size: 12px; color: var(--text-dim); margin-top: 4px; }
395
+ .kpi.good .kpi-value { color: var(--good); }
396
+ .kpi.protect .kpi-value { color: var(--protect); }
397
+ .kpi.bad .kpi-value { color: var(--bad); }
398
+ .kpi.brand-accent .kpi-value { background: linear-gradient(135deg, var(--brand), var(--brand-2)); -webkit-background-clip: text; background-clip: text; color: transparent; }
399
+
400
+ /* ── Container & filter bar ────────────────────────────────────── */
401
+ .container { max-width: 1280px; margin: 0 auto; padding: 28px 32px 64px; }
402
+ .filter-bar {
403
+ display: flex; gap: 16px; align-items: center; justify-content: space-between;
404
+ margin-bottom: 18px; flex-wrap: wrap;
405
+ }
406
+ .filter-group { display: flex; gap: 8px; flex-wrap: wrap; }
407
+ .chip {
408
+ border: 1px solid var(--line-strong);
409
+ background: var(--bg-2); color: var(--text-dim);
410
+ padding: 7px 14px; border-radius: 999px;
411
+ font-size: 13px; cursor: pointer; transition: all .2s ease;
412
+ font-family: var(--sans);
413
+ }
414
+ .chip:hover { color: var(--text); border-color: var(--brand); }
415
+ .chip.active {
416
+ color: #fff; border-color: var(--brand);
417
+ background: linear-gradient(135deg, rgba(124,92,255,0.28), rgba(34,211,238,0.18));
418
+ box-shadow: 0 0 0 1px rgba(124,92,255,0.4), 0 4px 16px rgba(124,92,255,0.20);
419
+ }
420
+ .chip.protect-chip { border-color: rgba(192,132,252,0.5); color: var(--protect); }
421
+ .chip.protect-chip.active { background: linear-gradient(135deg, rgba(192,132,252,0.3), rgba(124,92,255,0.15)); color: #fff; }
422
+
423
+ .search-wrap input {
424
+ font-family: var(--sans); font-size: 13px;
425
+ padding: 9px 14px; min-width: 280px;
426
+ background: var(--bg-2); color: var(--text);
427
+ border: 1px solid var(--line-strong); border-radius: 999px;
428
+ outline: none; transition: border-color .2s ease, box-shadow .2s ease;
429
+ }
430
+ .search-wrap input:focus {
431
+ border-color: var(--brand);
432
+ box-shadow: 0 0 0 3px rgba(124,92,255,0.15);
433
+ }
434
+
435
+ /* ── Event cards ───────────────────────────────────────────────── */
436
+ .events { display: flex; flex-direction: column; gap: 18px; }
437
+ .card {
438
+ background: linear-gradient(180deg, var(--bg-2), var(--bg-1));
439
+ border: 1px solid var(--line);
440
+ border-radius: var(--radius);
441
+ overflow: hidden;
442
+ box-shadow: var(--shadow-card);
443
+ animation: cardIn .5s cubic-bezier(.18,.89,.32,1.28) both;
444
+ position: relative;
445
+ }
446
+ .card::before {
447
+ content: ""; position: absolute; inset: 0 auto 0 0; width: 3px;
448
+ background: var(--line-strong);
449
+ }
450
+ .card.healed::before { background: linear-gradient(180deg, var(--good), #14b8a6); }
451
+ .card.protected::before { background: linear-gradient(180deg, var(--protect), var(--brand)); }
452
+ .card.failed::before { background: linear-gradient(180deg, var(--bad), #ef4444); }
453
+
454
+ @keyframes cardIn {
455
+ 0% { opacity: 0; transform: translateY(8px) scale(.99); }
456
+ 100% { opacity: 1; transform: translateY(0) scale(1); }
457
+ }
458
+
459
+ .card-head {
460
+ display: flex; align-items: center; gap: 12px;
461
+ padding: 16px 18px;
462
+ border-bottom: 1px solid var(--line);
463
+ cursor: pointer;
464
+ user-select: none;
465
+ transition: background .2s ease;
466
+ }
467
+ .card-head:hover { background: var(--bg-3); }
468
+
469
+ .badge {
470
+ display: inline-flex; align-items: center; gap: 6px;
471
+ font-size: 11px; letter-spacing: 1.5px; text-transform: uppercase;
472
+ font-weight: 700;
473
+ padding: 5px 10px; border-radius: 999px;
474
+ border: 1px solid currentColor;
475
+ white-space: nowrap;
476
+ }
477
+ .badge .dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; box-shadow: 0 0 8px currentColor; }
478
+ .badge.healed { color: var(--good); background: var(--good-dim); }
479
+ .badge.protected { color: var(--protect); background: var(--protect-dim); animation: pulse 2.6s ease-in-out infinite; }
480
+ .badge.failed { color: var(--bad); background: var(--bad-dim); }
481
+
482
+ @keyframes pulse {
483
+ 0%,100% { box-shadow: 0 0 0 0 rgba(192,132,252,0); }
484
+ 50% { box-shadow: 0 0 0 6px rgba(192,132,252,0.18); }
485
+ }
486
+
487
+ .card-title {
488
+ flex: 1; min-width: 0;
489
+ display: flex; flex-direction: column; gap: 4px;
490
+ }
491
+ .card-title .selector {
492
+ font-family: var(--mono); font-size: 13px;
493
+ color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
494
+ }
495
+ .card-title .meta-line {
496
+ font-size: 12px; color: var(--text-muted);
497
+ }
498
+ .card-title .meta-line a { color: var(--text-dim); font-family: var(--mono); }
499
+ .card-title .meta-line a:hover { color: var(--brand-2); }
500
+
501
+ .card-head .chev { width: 16px; height: 16px; transition: transform .25s ease; color: var(--text-muted); }
502
+ .card.open .card-head .chev { transform: rotate(90deg); }
503
+
504
+ .card-body { padding: 0 18px 20px; display: none; }
505
+ .card.open .card-body { display: block; animation: bodyIn .35s ease both; }
506
+ @keyframes bodyIn {
507
+ 0% { opacity: 0; transform: translateY(-4px); }
508
+ 100% { opacity: 1; transform: translateY(0); }
509
+ }
510
+
511
+ /* ── Section blocks inside cards ───────────────────────────────── */
512
+ .section {
513
+ margin-top: 18px;
514
+ padding-top: 16px;
515
+ border-top: 1px dashed var(--line);
516
+ }
517
+ .section-title {
518
+ font-size: 11px; letter-spacing: 1.4px; text-transform: uppercase;
519
+ color: var(--text-muted); margin-bottom: 10px;
520
+ display: flex; align-items: center; gap: 8px;
521
+ }
522
+ .section-title .marker {
523
+ width: 6px; height: 6px; border-radius: 50%;
524
+ background: var(--brand); box-shadow: 0 0 8px var(--brand);
525
+ }
526
+
527
+ /* DNA evidence table */
528
+ .dna-table {
529
+ width: 100%; border-collapse: separate; border-spacing: 0;
530
+ font-size: 13px;
531
+ background: var(--bg-1); border: 1px solid var(--line); border-radius: var(--radius-sm);
532
+ overflow: hidden;
533
+ }
534
+ .dna-table th, .dna-table td {
535
+ padding: 9px 12px; text-align: left;
536
+ border-bottom: 1px solid var(--line);
537
+ vertical-align: top;
538
+ }
539
+ .dna-table th {
540
+ background: var(--bg-2); font-weight: 600; color: var(--text-dim);
541
+ font-size: 11px; letter-spacing: 0.8px; text-transform: uppercase;
542
+ }
543
+ .dna-table tr:last-child td { border-bottom: none; }
544
+ .dna-table td.field { color: var(--text-muted); width: 22%; }
545
+ .dna-table td.val { font-family: var(--mono); font-size: 12.5px; color: var(--text); }
546
+ .dna-table td.icon { width: 36px; text-align: center; font-size: 16px; }
547
+ .dna-table tr.changed td.val.new { color: var(--warn); }
548
+ .dna-table tr.changed td.val.old { color: var(--text-muted); text-decoration: line-through dotted; }
549
+ .dna-table tr.matched td.icon { color: var(--good); }
550
+ .dna-table tr.changed td.icon { color: var(--warn); }
551
+ .dna-table tr.missing td.icon { color: var(--text-muted); }
552
+
553
+ /* CoT bullets */
554
+ .cot { display: flex; flex-direction: column; gap: 8px; }
555
+ .cot .step {
556
+ display: flex; gap: 12px; align-items: flex-start;
557
+ padding: 10px 12px;
558
+ background: var(--bg-1); border: 1px solid var(--line); border-radius: var(--radius-sm);
559
+ }
560
+ .cot .step .step-no {
561
+ flex-shrink: 0;
562
+ font-family: var(--mono); font-size: 11px; color: var(--brand-2);
563
+ width: 22px; height: 22px; border-radius: 50%;
564
+ background: rgba(34,211,238,0.10);
565
+ display: inline-flex; align-items: center; justify-content: center;
566
+ border: 1px solid rgba(34,211,238,0.25);
567
+ }
568
+ .cot .step .step-text { color: var(--text-dim); font-size: 13px; line-height: 1.5; }
569
+
570
+ /* Auditor deep dive */
571
+ .auditor-grid {
572
+ display: grid; grid-template-columns: 200px 1fr; gap: 14px;
573
+ background: var(--bg-1); border: 1px solid var(--line); border-radius: var(--radius-sm);
574
+ padding: 16px;
575
+ }
576
+ @media (max-width: 700px) { .auditor-grid { grid-template-columns: 1fr; } }
577
+ .gauge {
578
+ position: relative; aspect-ratio: 1/1; width: 100%; max-width: 200px; margin: auto;
579
+ }
580
+ .gauge svg { width: 100%; height: 100%; transform: rotate(-90deg); }
581
+ .gauge .track { stroke: var(--line); fill: none; stroke-width: 10; }
582
+ .gauge .bar { stroke: var(--brand-2); fill: none; stroke-width: 10; stroke-linecap: round;
583
+ transition: stroke-dasharray 1s ease; }
584
+ .gauge.consistent .bar { stroke: var(--good); }
585
+ .gauge.suspicious .bar { stroke: var(--warn); }
586
+ .gauge.inconsistent .bar { stroke: var(--bad); }
587
+ .gauge .center {
588
+ position: absolute; inset: 0; display: flex; flex-direction: column;
589
+ align-items: center; justify-content: center;
590
+ }
591
+ .gauge .pct { font-size: 28px; font-weight: 700; font-variant-numeric: tabular-nums; }
592
+ .gauge .pct-label { font-size: 10px; letter-spacing: 1.2px; text-transform: uppercase; color: var(--text-muted); }
593
+
594
+ .audit-detail { display: flex; flex-direction: column; gap: 8px; }
595
+ .audit-row { display: flex; gap: 10px; font-size: 13px; }
596
+ .audit-row .label { color: var(--text-muted); min-width: 130px; font-size: 12px; text-transform: uppercase; letter-spacing: 0.8px; }
597
+ .audit-row .value { color: var(--text); }
598
+ .audit-reason {
599
+ margin-top: 6px;
600
+ padding: 10px 12px; border-left: 3px solid var(--brand);
601
+ background: rgba(124,92,255,0.08);
602
+ font-size: 13px; color: var(--text); line-height: 1.55;
603
+ border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
604
+ }
605
+
606
+ .verdict-chip {
607
+ display: inline-block; padding: 3px 9px; border-radius: 999px;
608
+ font-size: 11px; letter-spacing: 0.8px; font-weight: 700;
609
+ text-transform: uppercase;
610
+ }
611
+ .verdict-chip.consistent { color: var(--good); background: var(--good-dim); border: 1px solid var(--good); }
612
+ .verdict-chip.suspicious { color: var(--warn); background: var(--warn-dim); border: 1px solid var(--warn); }
613
+ .verdict-chip.inconsistent { color: var(--bad); background: var(--bad-dim); border: 1px solid var(--bad); }
614
+
615
+ /* Diff */
616
+ .diff {
617
+ background: var(--bg-1); border: 1px solid var(--line);
618
+ border-radius: var(--radius-sm); overflow: hidden;
619
+ }
620
+ .diff-head {
621
+ background: var(--bg-2); padding: 8px 12px;
622
+ font-family: var(--mono); font-size: 12px; color: var(--text-dim);
623
+ border-bottom: 1px solid var(--line);
624
+ display: flex; align-items: center; gap: 10px;
625
+ }
626
+ .diff-body { padding: 0; }
627
+ .diff-line {
628
+ display: grid; grid-template-columns: 28px 56px 1fr;
629
+ font-family: var(--mono); font-size: 12.5px; line-height: 1.55;
630
+ }
631
+ .diff-line .gutter { color: var(--text-muted); text-align: center; }
632
+ .diff-line .ln { color: var(--text-muted); text-align: right; padding-right: 12px; }
633
+ .diff-line .code { white-space: pre-wrap; word-break: break-word; padding-right: 12px; }
634
+ .diff-line.add { background: rgba(52,211,153,0.08); }
635
+ .diff-line.add .gutter, .diff-line.add .code { color: var(--good); }
636
+ .diff-line.del { background: rgba(248,113,113,0.08); }
637
+ .diff-line.del .gutter, .diff-line.del .code { color: var(--bad); }
638
+ .diff-line.ctx .code { color: var(--text-dim); }
639
+
640
+ /* Alternatives list */
641
+ .alts { display: flex; flex-direction: column; gap: 6px; }
642
+ .alts .alt {
643
+ font-family: var(--mono); font-size: 12.5px; color: var(--text-dim);
644
+ padding: 8px 12px; background: var(--bg-1); border: 1px solid var(--line); border-radius: var(--radius-sm);
645
+ }
646
+
647
+ /* Action bar */
648
+ .actions {
649
+ display: flex; gap: 10px; flex-wrap: wrap;
650
+ margin-top: 22px; padding-top: 16px; border-top: 1px dashed var(--line);
651
+ }
652
+ .btn {
653
+ font-family: var(--sans); font-size: 13px;
654
+ padding: 9px 16px; border-radius: var(--radius-sm);
655
+ border: 1px solid var(--line-strong); background: var(--bg-2);
656
+ color: var(--text); cursor: pointer;
657
+ transition: all .2s ease;
658
+ display: inline-flex; align-items: center; gap: 8px;
659
+ }
660
+ .btn:hover { border-color: var(--brand); transform: translateY(-1px); }
661
+ .btn.primary {
662
+ border-color: transparent;
663
+ background: linear-gradient(135deg, var(--brand), var(--brand-2));
664
+ color: #fff;
665
+ }
666
+ .btn.primary:hover { box-shadow: 0 6px 24px rgba(124,92,255,0.35); }
667
+ .btn.danger { border-color: rgba(248,113,113,0.4); color: var(--bad); }
668
+ .btn.danger:hover { background: var(--bad-dim); }
669
+ .btn.ghost { background: transparent; }
670
+ .btn[data-state="approved"] {
671
+ border-color: var(--good); color: var(--good); background: var(--good-dim);
672
+ }
673
+
674
+ /* Tag chips inside meta */
675
+ .tag {
676
+ display: inline-block; font-size: 10.5px; letter-spacing: 0.6px;
677
+ padding: 2px 8px; border-radius: 4px;
678
+ background: var(--bg-3); color: var(--text-dim);
679
+ margin-left: 6px; border: 1px solid var(--line);
680
+ font-family: var(--mono);
681
+ }
682
+ .tag.iframe { color: var(--brand-2); border-color: rgba(34,211,238,0.3); }
683
+ .tag.shadow { color: var(--protect); border-color: rgba(192,132,252,0.3); }
684
+ .tag.ghost { color: var(--warn); border-color: rgba(251,191,36,0.3); }
685
+ .tag.occlude { color: var(--bad); border-color: rgba(248,113,113,0.3); }
686
+
687
+ /* Empty state */
688
+ .empty {
689
+ margin-top: 32px; padding: 60px 20px; text-align: center;
690
+ border: 1px dashed var(--line-strong); border-radius: var(--radius);
691
+ color: var(--text-muted);
692
+ }
693
+ .empty-icon { font-size: 40px; color: var(--brand); margin-bottom: 12px; }
694
+ .empty-title { font-size: 16px; color: var(--text); margin-bottom: 4px; }
695
+ .empty-sub { font-size: 13px; }
696
+ .hidden { display: none !important; }
697
+
698
+ /* Toast */
699
+ .toast {
700
+ position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%);
701
+ background: var(--bg-3); border: 1px solid var(--line-strong);
702
+ color: var(--text); padding: 12px 18px; border-radius: 999px;
703
+ font-size: 13px; box-shadow: 0 8px 30px rgba(0,0,0,0.4);
704
+ animation: toastIn .35s ease;
705
+ z-index: 100;
706
+ }
707
+ @keyframes toastIn {
708
+ 0% { opacity: 0; transform: translate(-50%, 10px); }
709
+ 100% { opacity: 1; transform: translate(-50%, 0); }
710
+ }
711
+
712
+ /* Footer */
713
+ .footer { border-top: 1px solid var(--line); margin-top: 40px; }
714
+ .footer-inner {
715
+ max-width: 1280px; margin: 0 auto;
716
+ padding: 22px 32px; display: flex; justify-content: space-between;
717
+ flex-wrap: wrap; gap: 12px;
718
+ color: var(--text-dim); font-size: 12.5px;
719
+ }
720
+ .muted { color: var(--text-muted); }
721
+
722
+ /* Small helpers */
723
+ .row { display: flex; gap: 18px; flex-wrap: wrap; }
724
+ .col { flex: 1 1 320px; min-width: 0; }
725
+ .kbd {
726
+ font-family: var(--mono); font-size: 11px;
727
+ padding: 1px 5px; border: 1px solid var(--line-strong); border-radius: 4px;
728
+ background: var(--bg-2); color: var(--text-dim);
729
+ }
730
+ .muted-block {
731
+ font-size: 13px; color: var(--text-muted);
732
+ padding: 14px 16px; background: var(--bg-1);
733
+ border: 1px dashed var(--line); border-radius: var(--radius-sm);
734
+ }
735
+ `;
736
+ // ═══════════════════════════════════════════════════════════════════
737
+ // FRONT-END JS — renders the embedded payload
738
+ // ═══════════════════════════════════════════════════════════════════
739
+ const REPORT_JS = String.raw `
740
+ (function() {
741
+ const dataEl = document.getElementById("sela-data");
742
+ const DATA = JSON.parse(dataEl.textContent);
743
+ const $ = (sel, root = document) => root.querySelector(sel);
744
+ const $$ = (sel, root = document) => Array.from(root.querySelectorAll(sel));
745
+
746
+ function esc(s) {
747
+ return String(s == null ? "" : s)
748
+ .replace(/&/g, "&amp;").replace(/</g, "&lt;")
749
+ .replace(/>/g, "&gt;").replace(/"/g, "&quot;");
750
+ }
751
+ function fmtTime(iso) {
752
+ try { return new Date(iso).toLocaleString(); } catch { return iso; }
753
+ }
754
+ function pluralize(n, one, many) { return n === 1 ? one : (many || one + "s"); }
755
+
756
+ // ── Hero meta + KPIs ──────────────────────────────────────────
757
+ $("#meta-generated").textContent = "Generated " + fmtTime(DATA.generatedAt);
758
+ $("#footer-cwd").textContent = DATA.cwd;
759
+
760
+ const s = DATA.stats;
761
+ const minutes = s.timeSavedMin;
762
+ const hours = Math.floor(minutes / 60);
763
+ const remMin = minutes % 60;
764
+ const timeSavedText = hours > 0 ? hours + "h " + remMin + "m" : minutes + "m";
765
+
766
+ const headline = (() => {
767
+ if (s.healed === 0 && s.protectedCnt === 0 && s.failed === 0) {
768
+ return ["No healing events", "Sela was idle this run.", "Idle run"];
769
+ }
770
+ if (s.protectedCnt > 0 && s.healed === 0) {
771
+ return [
772
+ "Sela protected your suite from " + s.protectedCnt + " masked " + pluralize(s.protectedCnt, "regression") + ".",
773
+ "These changes looked safe — but the Intent Auditor caught semantic mismatch and blocked the auto-fix.",
774
+ "Bugs caught"
775
+ ];
776
+ }
777
+ if (s.healed > 0 && s.protectedCnt > 0) {
778
+ return [
779
+ "Sela healed " + s.healed + " " + pluralize(s.healed, "selector") + " and protected " + s.protectedCnt + " " + pluralize(s.protectedCnt, "regression") + ".",
780
+ "Your suite stayed green, but two of the would-be fixes were actually masked regressions. Drill in below.",
781
+ "Healed + Protected"
782
+ ];
783
+ }
784
+ if (s.healed > 0) {
785
+ return [
786
+ "Sela healed " + s.healed + " " + pluralize(s.healed, "selector") + " automatically.",
787
+ "Every fix below was double-checked by the Intent Auditor before it touched your source code.",
788
+ "Clean run"
789
+ ];
790
+ }
791
+ return [
792
+ "Sela couldn't heal " + s.failed + " " + pluralize(s.failed, "selector") + ".",
793
+ "These need a human hand — but you've got context, candidates, and direct links ready below.",
794
+ "Needs attention"
795
+ ];
796
+ })();
797
+ $("#exec-title").textContent = headline[0];
798
+ $("#exec-sub").textContent = headline[1];
799
+ $("#exec-eyebrow").textContent = headline[2];
800
+
801
+ const kpis = [
802
+ { label: "Time saved", value: timeSavedText, foot: "estimated developer time", cls: "brand-accent" },
803
+ { label: "Healed", value: String(s.healed), foot: "auto-fixed + verified", cls: "good" },
804
+ { label: "Protected", value: String(s.protectedCnt), foot: "bugs caught early", cls: "protect" },
805
+ { label: "Failed", value: String(s.failed), foot: "needs your eyes", cls: "bad" },
806
+ ];
807
+ if (s.iframeCnt > 0 || s.shadowCnt > 0) {
808
+ kpis.push({
809
+ label: "Complex contexts",
810
+ value: String(s.iframeCnt + s.shadowCnt),
811
+ foot: (s.iframeCnt ? s.iframeCnt + " iframe " : "") + (s.shadowCnt ? s.shadowCnt + " shadow DOM" : ""),
812
+ cls: ""
813
+ });
814
+ }
815
+ $("#kpi-row").innerHTML = kpis.map(k => (
816
+ '<div class="kpi ' + k.cls + '">' +
817
+ '<div class="kpi-label">' + esc(k.label) + '</div>' +
818
+ '<div class="kpi-value">' + esc(k.value) + '</div>' +
819
+ '<div class="kpi-foot">' + esc(k.foot) + '</div>' +
820
+ '</div>'
821
+ )).join("");
822
+
823
+ // ── Event cards ───────────────────────────────────────────────
824
+ function vscodeLink(file, line) {
825
+ if (!file) return "";
826
+ const abs = file.startsWith("/") || /^[A-Za-z]:/.test(file) ? file : (DATA.cwd + "/" + file);
827
+ return "vscode://file/" + encodeURI(abs).replace(/#/g, "%23") + (line ? ":" + line : "");
828
+ }
829
+
830
+ function badge(kind) {
831
+ const map = {
832
+ HEALED: { cls: "healed", label: "Healed" },
833
+ PROTECTED: { cls: "protected", label: "Protected · Bug Caught" },
834
+ FAILED: { cls: "failed", label: "Failed" },
835
+ };
836
+ const m = map[kind];
837
+ return '<span class="badge ' + m.cls + '"><span class="dot"></span>' + m.label + '</span>';
838
+ }
839
+
840
+ function contextTags(ev) {
841
+ const parts = [];
842
+ if (ev.inIframe) parts.push('<span class="tag iframe">iframe</span>');
843
+ if (ev.inShadowDom) parts.push('<span class="tag shadow">shadow DOM</span>');
844
+ const dna = ev.dnaBefore;
845
+ if (dna && dna.isGhost) parts.push('<span class="tag ghost">ghost · ' + esc(dna.ghostReason || "") + '</span>');
846
+ if (dna && dna.isOccluded) parts.push('<span class="tag occlude">occluded</span>');
847
+ return parts.join("");
848
+ }
849
+
850
+ function renderDnaTable(before, after) {
851
+ if (!before) return '<div class="muted-block">No DNA baseline available for this event.</div>';
852
+ const rows = [];
853
+ function row(field, oldV, newV) {
854
+ const oldStr = oldV == null ? "" : (Array.isArray(oldV) ? oldV.join(" / ") : String(oldV));
855
+ const newStr = newV == null ? "" : (Array.isArray(newV) ? newV.join(" / ") : String(newV));
856
+ const same = oldStr === newStr;
857
+ const hasAfter = !!after;
858
+ let cls, icon;
859
+ if (!hasAfter) { cls = "missing"; icon = "—"; }
860
+ else if (same) { cls = "matched"; icon = "✅"; }
861
+ else { cls = "changed"; icon = "⚠️"; }
862
+ const valCell = !hasAfter
863
+ ? '<td class="val">' + esc(oldStr || "—") + '</td>'
864
+ : same
865
+ ? '<td class="val" colspan="1">' + esc(oldStr || "—") + '</td>'
866
+ : '<td class="val"><span class="val old">' + esc(oldStr || "—") + '</span>' +
867
+ '<br><span class="val new">' + esc(newStr || "—") + '</span></td>';
868
+ rows.push(
869
+ '<tr class="' + cls + '">' +
870
+ '<td class="field">' + esc(field) + '</td>' +
871
+ valCell +
872
+ '<td class="icon">' + icon + '</td>' +
873
+ '</tr>'
874
+ );
875
+ }
876
+ row("Tag", before.tagName, after && after.tagName);
877
+ row("Text", before.text, after && after.text);
878
+ row("Role", before.role, after && after.role);
879
+ row("Classes", before.classes, after && after.classes);
880
+ row("Ancestry", before.ancestry, after && after.ancestry);
881
+ row("Closest Label", before.closestLabel, after && after.closestLabel);
882
+ row("Row Anchor", before.rowAnchor, after && after.rowAnchor);
883
+ const attrsBefore = Object.entries(before.attributes || {}).map(([k,v]) => k + "=" + v).join(" · ");
884
+ const attrsAfter = after ? Object.entries(after.attributes || {}).map(([k,v]) => k + "=" + v).join(" · ") : null;
885
+ row("Attributes", attrsBefore, attrsAfter);
886
+
887
+ return (
888
+ '<table class="dna-table">' +
889
+ '<thead><tr><th>Field</th><th>Value</th><th>Match</th></tr></thead>' +
890
+ '<tbody>' + rows.join("") + '</tbody>' +
891
+ '</table>'
892
+ );
893
+ }
894
+
895
+ function renderCoT(steps) {
896
+ if (!steps || !steps.length) return '<div class="muted-block">No reasoning trace captured for this event.</div>';
897
+ return '<div class="cot">' + steps.map((s, i) =>
898
+ '<div class="step">' +
899
+ '<span class="step-no">' + (i + 1) + '</span>' +
900
+ '<span class="step-text">' + esc(s) + '</span>' +
901
+ '</div>'
902
+ ).join("") + '</div>';
903
+ }
904
+
905
+ function renderAuditor(a) {
906
+ if (!a) return '<div class="muted-block">The Intent Auditor was not consulted for this event (no AI fix attempted, or auditor unavailable).</div>';
907
+ const verdictCls = a.verdict.toLowerCase();
908
+ const pct = Math.max(0, Math.min(100, a.confidence));
909
+ const radius = 56;
910
+ const circ = 2 * Math.PI * radius;
911
+ const dash = (pct / 100) * circ;
912
+ const adjustments = [];
913
+ if (a.penalty != null && a.penalty !== 0) adjustments.push("ancestry drift " + a.penalty);
914
+ if (a.bonus != null && a.bonus !== 0) adjustments.push("anchor +" + a.bonus);
915
+ const adjLine = adjustments.length
916
+ ? '<div class="audit-row"><span class="label">Score adjustments</span><span class="value">raw ' + a.rawConfidence + '% → ' + pct + '% (' + adjustments.join(", ") + ')</span></div>'
917
+ : "";
918
+ return (
919
+ '<div class="auditor-grid">' +
920
+ '<div class="gauge ' + verdictCls + '">' +
921
+ '<svg viewBox="0 0 140 140">' +
922
+ '<circle class="track" cx="70" cy="70" r="' + radius + '"/>' +
923
+ '<circle class="bar" cx="70" cy="70" r="' + radius + '" ' +
924
+ 'stroke-dasharray="' + dash.toFixed(2) + ' ' + circ.toFixed(2) + '"/>' +
925
+ '</svg>' +
926
+ '<div class="center">' +
927
+ '<div class="pct">' + pct + '%</div>' +
928
+ '<div class="pct-label">Consistency</div>' +
929
+ '</div>' +
930
+ '</div>' +
931
+ '<div class="audit-detail">' +
932
+ '<div class="audit-row"><span class="label">Verdict</span><span class="value"><span class="verdict-chip ' + verdictCls + '">' + a.verdict + '</span></span></div>' +
933
+ (a.inversionType ? '<div class="audit-row"><span class="label">Inversion type</span><span class="value">' + esc(a.inversionType) + '</span></div>' : "") +
934
+ adjLine +
935
+ '<div class="audit-reason">' + esc(a.reason) + '</div>' +
936
+ '</div>' +
937
+ '</div>'
938
+ );
939
+ }
940
+
941
+ function renderDiff(ev) {
942
+ if (ev.kind !== "HEALED") {
943
+ if (ev.kind === "PROTECTED") {
944
+ return (
945
+ '<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>'
946
+ );
947
+ }
948
+ return '<div class="muted-block">No source code was modified — the AI could not propose a confident fix for this selector.</div>';
949
+ }
950
+ const oldLine = ev.oldCodeLine || "";
951
+ const newLine = ev.newCodeLine || "";
952
+ return (
953
+ '<div class="diff">' +
954
+ '<div class="diff-head">' +
955
+ '<span>' + esc(ev.sourceFile) + '</span>' +
956
+ '<span class="muted">·</span>' +
957
+ '<span>line ' + ev.newLineNumber + '</span>' +
958
+ (ev.diffStrategy ? '<span class="muted">·</span><span>strategy: ' + esc(ev.diffStrategy) + '</span>' : "") +
959
+ (ev.blastRadius != null ? '<span class="muted">·</span><span>blast radius: ' + ev.blastRadius + '</span>' : "") +
960
+ '</div>' +
961
+ '<div class="diff-body">' +
962
+ '<div class="diff-line del">' +
963
+ '<span class="gutter">-</span><span class="ln">' + ev.newLineNumber + '</span><span class="code">' + esc(oldLine) + '</span>' +
964
+ '</div>' +
965
+ '<div class="diff-line add">' +
966
+ '<span class="gutter">+</span><span class="ln">' + ev.newLineNumber + '</span><span class="code">' + esc(newLine) + '</span>' +
967
+ '</div>' +
968
+ '</div>' +
969
+ '</div>'
970
+ );
971
+ }
972
+
973
+ function renderAlts(ev) {
974
+ if (ev.kind !== "HEALED" || !ev.aiAlternatives || !ev.aiAlternatives.length) return "";
975
+ return (
976
+ '<div class="section">' +
977
+ '<div class="section-title"><span class="marker"></span>Alternative selectors the AI considered</div>' +
978
+ '<div class="alts">' +
979
+ ev.aiAlternatives.map(a => '<div class="alt">' + esc(a) + '</div>').join("") +
980
+ '</div>' +
981
+ '</div>'
982
+ );
983
+ }
984
+
985
+ function renderActions(ev) {
986
+ const issueUrl = "https://github.com/anthropics/sela-core/issues/new?title=" +
987
+ encodeURIComponent("[Sela Report] " + ev.kind + " — " + ev.stableId) +
988
+ "&body=" + encodeURIComponent(
989
+ "Stable ID: " + ev.stableId + "\n" +
990
+ "File: " + ev.sourceFile + ":" + ev.sourceLine + "\n" +
991
+ "Kind: " + ev.kind + "\n" +
992
+ "Generated: " + DATA.generatedAt
993
+ );
994
+
995
+ if (ev.kind === "HEALED") {
996
+ return (
997
+ '<div class="actions">' +
998
+ '<button class="btn primary" data-action="keep" data-id="' + ev.stableId + '">✓ Keep changes</button>' +
999
+ '<button class="btn danger" data-action="rollback" data-id="' + ev.stableId + '">↶ Rollback</button>' +
1000
+ '<a class="btn ghost" target="_blank" rel="noopener" href="' + issueUrl + '">⚑ Report issue</a>' +
1001
+ '</div>'
1002
+ );
1003
+ }
1004
+ if (ev.kind === "PROTECTED") {
1005
+ return (
1006
+ '<div class="actions">' +
1007
+ '<button class="btn primary" data-action="acknowledge" data-id="' + ev.stableId + '">✓ Acknowledge — I will investigate</button>' +
1008
+ '<a class="btn ghost" target="_blank" rel="noopener" href="' + issueUrl + '">⚑ File regression</a>' +
1009
+ '</div>'
1010
+ );
1011
+ }
1012
+ return (
1013
+ '<div class="actions">' +
1014
+ '<a class="btn primary" href="' + vscodeLink(ev.sourceFile, ev.sourceLine) + '">→ Open in editor</a>' +
1015
+ '<a class="btn ghost" target="_blank" rel="noopener" href="' + issueUrl + '">⚑ Report issue</a>' +
1016
+ '</div>'
1017
+ );
1018
+ }
1019
+
1020
+ function renderCardBody(ev) {
1021
+ const blocks = [];
1022
+
1023
+ // 1. AI Reasoning (CoT)
1024
+ if (ev.kind !== "FAILED" || ev.aiExplanation) {
1025
+ const steps = ev.kind === "HEALED" ? ev.reasoningSteps :
1026
+ ev.kind === "PROTECTED" ? buildProtectedSteps(ev) :
1027
+ buildFailedSteps(ev);
1028
+ blocks.push(
1029
+ '<div class="section">' +
1030
+ '<div class="section-title"><span class="marker"></span>AI Reasoning · Chain of Thought</div>' +
1031
+ renderCoT(steps) +
1032
+ '</div>'
1033
+ );
1034
+ }
1035
+
1036
+ // 2. Intent Auditor deep dive
1037
+ if (ev.auditor !== undefined) {
1038
+ blocks.push(
1039
+ '<div class="section">' +
1040
+ '<div class="section-title"><span class="marker"></span>Intent Auditor · Deep Dive</div>' +
1041
+ renderAuditor(ev.auditor) +
1042
+ '</div>'
1043
+ );
1044
+ }
1045
+
1046
+ // 3. Visual DNA Evidence
1047
+ blocks.push(
1048
+ '<div class="section">' +
1049
+ '<div class="section-title"><span class="marker"></span>Visual DNA Evidence · Old vs New</div>' +
1050
+ renderDnaTable(ev.dnaBefore || null, ev.dnaAfter || null) +
1051
+ '</div>'
1052
+ );
1053
+
1054
+ // 4. Diff
1055
+ blocks.push(
1056
+ '<div class="section">' +
1057
+ '<div class="section-title"><span class="marker"></span>Source Code Diff</div>' +
1058
+ renderDiff(ev) +
1059
+ '</div>'
1060
+ );
1061
+
1062
+ // 5. Alternatives
1063
+ blocks.push(renderAlts(ev));
1064
+
1065
+ // 6. Actions
1066
+ blocks.push(renderActions(ev));
1067
+
1068
+ return blocks.join("");
1069
+ }
1070
+
1071
+ function buildProtectedSteps(ev) {
1072
+ const steps = [];
1073
+ steps.push("AI proposed a replacement selector for the broken locator.");
1074
+ if (ev.aiExplanation) steps.push("Reasoning given: " + ev.aiExplanation);
1075
+ if (ev.candidateNewSelector) steps.push("Candidate selector resolved live — but its semantics changed.");
1076
+ if (ev.auditor) steps.push("Intent Auditor verdict: " + ev.auditor.verdict + " · " + ev.auditor.reason);
1077
+ steps.push("Sela blocked the auto-fix. Source code untouched. Investigate this as a real regression.");
1078
+ return steps;
1079
+ }
1080
+ function buildFailedSteps(ev) {
1081
+ const steps = [];
1082
+ steps.push("Locator failed to resolve at runtime — Sela attempted to heal it.");
1083
+ if (ev.aiExplanation) steps.push("AI response: " + ev.aiExplanation);
1084
+ steps.push("No confident replacement was produced. Reason: " + (ev.reason || "unknown"));
1085
+ steps.push("Open the file directly in your editor to inspect the original locator.");
1086
+ return steps;
1087
+ }
1088
+
1089
+ function eventCard(ev) {
1090
+ const link = vscodeLink(ev.sourceFile, ev.sourceLine);
1091
+ const cardCls = ev.kind === "HEALED" ? "healed" : ev.kind === "PROTECTED" ? "protected" : "failed";
1092
+ return (
1093
+ '<article class="card ' + cardCls + '" data-kind="' + ev.kind + '" data-haystack="' + esc((ev.oldSelector + " " + (ev.newSelector || "") + " " + ev.sourceFile + " " + (ev.testTitle || "")).toLowerCase()) + '">' +
1094
+ '<div class="card-head">' +
1095
+ badge(ev.kind) +
1096
+ '<div class="card-title">' +
1097
+ '<div class="selector">' + esc(ev.kind === "HEALED" ? (ev.newSelector || ev.oldSelector) : ev.oldSelector) + '</div>' +
1098
+ '<div class="meta-line">' +
1099
+ (ev.testTitle ? esc(ev.testTitle) + ' · ' : "") +
1100
+ '<a href="' + link + '">' + esc(ev.sourceFile) + ':' + ev.sourceLine + '</a>' +
1101
+ contextTags(ev) +
1102
+ ' · ' + fmtTime(ev.timestamp) +
1103
+ '</div>' +
1104
+ '</div>' +
1105
+ '<svg class="chev" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2"><polyline points="5 3 11 8 5 13"/></svg>' +
1106
+ '</div>' +
1107
+ '<div class="card-body">' + renderCardBody(ev) + '</div>' +
1108
+ '</article>'
1109
+ );
1110
+ }
1111
+
1112
+ // ── Mount ─────────────────────────────────────────────────────
1113
+ const $events = $("#events");
1114
+ if (DATA.events.length === 0) {
1115
+ $events.innerHTML = '<div class="muted-block">Sela was idle on this run — no broken selectors encountered.</div>';
1116
+ } else {
1117
+ $events.innerHTML = DATA.events.map(eventCard).join("");
1118
+
1119
+ // Auto-open the first PROTECTED card to surface bugs immediately.
1120
+ const firstProtected = $events.querySelector('.card.protected');
1121
+ if (firstProtected) firstProtected.classList.add("open");
1122
+ else {
1123
+ const firstCard = $events.querySelector('.card');
1124
+ if (firstCard) firstCard.classList.add("open");
1125
+ }
1126
+ }
1127
+
1128
+ // ── Interactions ──────────────────────────────────────────────
1129
+ function showToast(text) {
1130
+ const t = $("#toast");
1131
+ t.textContent = text;
1132
+ t.classList.remove("hidden");
1133
+ clearTimeout(showToast._t);
1134
+ showToast._t = setTimeout(() => t.classList.add("hidden"), 2400);
1135
+ }
1136
+
1137
+ $events.addEventListener("click", (e) => {
1138
+ const head = e.target.closest(".card-head");
1139
+ if (head) {
1140
+ const card = head.parentElement;
1141
+ card.classList.toggle("open");
1142
+ return;
1143
+ }
1144
+ const btn = e.target.closest("button[data-action]");
1145
+ if (!btn) return;
1146
+ const action = btn.dataset.action;
1147
+ const id = btn.dataset.id;
1148
+ if (action === "keep" || action === "acknowledge") {
1149
+ btn.dataset.state = "approved";
1150
+ btn.textContent = action === "keep" ? "✓ Approved" : "✓ Acknowledged";
1151
+ showToast("Saved locally — Sela noted your decision for " + id);
1152
+ } else if (action === "rollback") {
1153
+ const cmd = "npx sela dna refactor --rollback " + id;
1154
+ navigator.clipboard?.writeText(cmd).catch(() => {});
1155
+ showToast("Rollback command copied: " + cmd);
1156
+ }
1157
+ });
1158
+
1159
+ // ── Filters + Search ──────────────────────────────────────────
1160
+ let activeFilter = "ALL";
1161
+ let searchTerm = "";
1162
+ function applyFilters() {
1163
+ let visible = 0;
1164
+ $$(".card", $events).forEach(card => {
1165
+ const matchKind = activeFilter === "ALL" || card.dataset.kind === activeFilter;
1166
+ const matchSearch = !searchTerm || card.dataset.haystack.includes(searchTerm);
1167
+ const show = matchKind && matchSearch;
1168
+ card.style.display = show ? "" : "none";
1169
+ if (show) visible++;
1170
+ });
1171
+ $("#empty-state").classList.toggle("hidden", visible !== 0 || DATA.events.length === 0);
1172
+ }
1173
+
1174
+ $("#filter-group").addEventListener("click", e => {
1175
+ const chip = e.target.closest(".chip");
1176
+ if (!chip) return;
1177
+ $$(".chip", $("#filter-group")).forEach(c => c.classList.remove("active"));
1178
+ chip.classList.add("active");
1179
+ activeFilter = chip.dataset.filter;
1180
+ applyFilters();
1181
+ });
1182
+ $("#search").addEventListener("input", e => {
1183
+ searchTerm = e.target.value.toLowerCase();
1184
+ applyFilters();
1185
+ });
1186
+
1187
+ // ── Keyboard niceties ─────────────────────────────────────────
1188
+ document.addEventListener("keydown", (e) => {
1189
+ if (e.key === "/" && document.activeElement !== $("#search")) {
1190
+ e.preventDefault();
1191
+ $("#search").focus();
1192
+ }
1193
+ });
1194
+ })();
1195
+ `;