sela-core 0.1.0-alpha.1

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 (267) hide show
  1. package/README.md +103 -0
  2. package/bin/sela.js +3 -0
  3. package/dist/cli/ErrorHandler.d.ts +10 -0
  4. package/dist/cli/ErrorHandler.d.ts.map +1 -0
  5. package/dist/cli/ErrorHandler.js +70 -0
  6. package/dist/cli/commands/bulk.d.ts +3 -0
  7. package/dist/cli/commands/bulk.d.ts.map +1 -0
  8. package/dist/cli/commands/bulk.js +158 -0
  9. package/dist/cli/commands/find.d.ts +3 -0
  10. package/dist/cli/commands/find.d.ts.map +1 -0
  11. package/dist/cli/commands/find.js +51 -0
  12. package/dist/cli/commands/init.d.ts +3 -0
  13. package/dist/cli/commands/init.d.ts.map +1 -0
  14. package/dist/cli/commands/init.js +166 -0
  15. package/dist/cli/commands/list.d.ts +3 -0
  16. package/dist/cli/commands/list.d.ts.map +1 -0
  17. package/dist/cli/commands/list.js +56 -0
  18. package/dist/cli/commands/merge.d.ts +13 -0
  19. package/dist/cli/commands/merge.d.ts.map +1 -0
  20. package/dist/cli/commands/merge.js +119 -0
  21. package/dist/cli/commands/refactor.d.ts +3 -0
  22. package/dist/cli/commands/refactor.d.ts.map +1 -0
  23. package/dist/cli/commands/refactor.js +30 -0
  24. package/dist/cli/commands/showReport.d.ts +3 -0
  25. package/dist/cli/commands/showReport.d.ts.map +1 -0
  26. package/dist/cli/commands/showReport.js +82 -0
  27. package/dist/cli/commands/status.d.ts +3 -0
  28. package/dist/cli/commands/status.d.ts.map +1 -0
  29. package/dist/cli/commands/status.js +51 -0
  30. package/dist/cli/commands/sync.d.ts +3 -0
  31. package/dist/cli/commands/sync.d.ts.map +1 -0
  32. package/dist/cli/commands/sync.js +131 -0
  33. package/dist/cli/index.d.ts +2 -0
  34. package/dist/cli/index.d.ts.map +1 -0
  35. package/dist/cli/index.js +94 -0
  36. package/dist/cli/ui/DnaTable.d.ts +3 -0
  37. package/dist/cli/ui/DnaTable.d.ts.map +1 -0
  38. package/dist/cli/ui/DnaTable.js +77 -0
  39. package/dist/cli/ui/LocatorPicker.d.ts +8 -0
  40. package/dist/cli/ui/LocatorPicker.d.ts.map +1 -0
  41. package/dist/cli/ui/LocatorPicker.js +33 -0
  42. package/dist/cli/ui/ProgressReporter.d.ts +11 -0
  43. package/dist/cli/ui/ProgressReporter.d.ts.map +1 -0
  44. package/dist/cli/ui/ProgressReporter.js +33 -0
  45. package/dist/cli/ui/RefactorWizard.d.ts +26 -0
  46. package/dist/cli/ui/RefactorWizard.d.ts.map +1 -0
  47. package/dist/cli/ui/RefactorWizard.js +295 -0
  48. package/dist/config/ConfigLoader.d.ts +16 -0
  49. package/dist/config/ConfigLoader.d.ts.map +1 -0
  50. package/dist/config/ConfigLoader.js +185 -0
  51. package/dist/config/DryRunGuard.d.ts +14 -0
  52. package/dist/config/DryRunGuard.d.ts.map +1 -0
  53. package/dist/config/DryRunGuard.js +80 -0
  54. package/dist/config/PathScopeGuard.d.ts +42 -0
  55. package/dist/config/PathScopeGuard.d.ts.map +1 -0
  56. package/dist/config/PathScopeGuard.js +192 -0
  57. package/dist/config/SelaConfig.d.ts +178 -0
  58. package/dist/config/SelaConfig.d.ts.map +1 -0
  59. package/dist/config/SelaConfig.js +141 -0
  60. package/dist/config/defineConfig.d.ts +3 -0
  61. package/dist/config/defineConfig.d.ts.map +1 -0
  62. package/dist/config/defineConfig.js +9 -0
  63. package/dist/engine/FixwrightEngine.d.ts +24 -0
  64. package/dist/engine/FixwrightEngine.d.ts.map +1 -0
  65. package/dist/engine/FixwrightEngine.js +403 -0
  66. package/dist/engine/HealingRegistry.d.ts +40 -0
  67. package/dist/engine/HealingRegistry.d.ts.map +1 -0
  68. package/dist/engine/HealingRegistry.js +99 -0
  69. package/dist/engine/SelaEngine.d.ts +57 -0
  70. package/dist/engine/SelaEngine.d.ts.map +1 -0
  71. package/dist/engine/SelaEngine.js +1146 -0
  72. package/dist/engine/singleton.d.ts +3 -0
  73. package/dist/engine/singleton.d.ts.map +1 -0
  74. package/dist/engine/singleton.js +5 -0
  75. package/dist/errors/SelaError.d.ts +146 -0
  76. package/dist/errors/SelaError.d.ts.map +1 -0
  77. package/dist/errors/SelaError.js +168 -0
  78. package/dist/fixtures/expectProxy.d.ts +12 -0
  79. package/dist/fixtures/expectProxy.d.ts.map +1 -0
  80. package/dist/fixtures/expectProxy.js +246 -0
  81. package/dist/fixtures/index.d.ts +30 -0
  82. package/dist/fixtures/index.d.ts.map +1 -0
  83. package/dist/fixtures/index.js +765 -0
  84. package/dist/fixtures/moduleExpect.d.ts +2 -0
  85. package/dist/fixtures/moduleExpect.d.ts.map +1 -0
  86. package/dist/fixtures/moduleExpect.js +54 -0
  87. package/dist/fixtures/proxyTag.d.ts +12 -0
  88. package/dist/fixtures/proxyTag.d.ts.map +1 -0
  89. package/dist/fixtures/proxyTag.js +45 -0
  90. package/dist/index.d.ts +9 -0
  91. package/dist/index.d.ts.map +1 -0
  92. package/dist/index.js +31 -0
  93. package/dist/reporter/SelaReporter.d.ts +165 -0
  94. package/dist/reporter/SelaReporter.d.ts.map +1 -0
  95. package/dist/reporter/SelaReporter.js +517 -0
  96. package/dist/services/ASTSourceUpdater.d.ts +143 -0
  97. package/dist/services/ASTSourceUpdater.d.ts.map +1 -0
  98. package/dist/services/ASTSourceUpdater.js +3514 -0
  99. package/dist/services/AnchorResolver.d.ts +157 -0
  100. package/dist/services/AnchorResolver.d.ts.map +1 -0
  101. package/dist/services/AnchorResolver.js +289 -0
  102. package/dist/services/ArgumentTypeAnalyzer.d.ts +26 -0
  103. package/dist/services/ArgumentTypeAnalyzer.d.ts.map +1 -0
  104. package/dist/services/ArgumentTypeAnalyzer.js +92 -0
  105. package/dist/services/BlastRadiusAnalyzer.d.ts +15 -0
  106. package/dist/services/BlastRadiusAnalyzer.d.ts.map +1 -0
  107. package/dist/services/BlastRadiusAnalyzer.js +107 -0
  108. package/dist/services/ChainValidator.d.ts +76 -0
  109. package/dist/services/ChainValidator.d.ts.map +1 -0
  110. package/dist/services/ChainValidator.js +570 -0
  111. package/dist/services/CrossFileHealer.d.ts +6 -0
  112. package/dist/services/CrossFileHealer.d.ts.map +1 -0
  113. package/dist/services/CrossFileHealer.js +135 -0
  114. package/dist/services/DecisionEngine.d.ts +51 -0
  115. package/dist/services/DecisionEngine.d.ts.map +1 -0
  116. package/dist/services/DecisionEngine.js +260 -0
  117. package/dist/services/DefinitionTracer.d.ts +41 -0
  118. package/dist/services/DefinitionTracer.d.ts.map +1 -0
  119. package/dist/services/DefinitionTracer.js +366 -0
  120. package/dist/services/DnaEditorService.d.ts +31 -0
  121. package/dist/services/DnaEditorService.d.ts.map +1 -0
  122. package/dist/services/DnaEditorService.js +217 -0
  123. package/dist/services/DnaIndexService.d.ts +24 -0
  124. package/dist/services/DnaIndexService.d.ts.map +1 -0
  125. package/dist/services/DnaIndexService.js +131 -0
  126. package/dist/services/HealBudget.d.ts +32 -0
  127. package/dist/services/HealBudget.d.ts.map +1 -0
  128. package/dist/services/HealBudget.js +85 -0
  129. package/dist/services/HealReportService.d.ts +161 -0
  130. package/dist/services/HealReportService.d.ts.map +1 -0
  131. package/dist/services/HealReportService.js +1039 -0
  132. package/dist/services/HealingAdvisory.d.ts +30 -0
  133. package/dist/services/HealingAdvisory.d.ts.map +1 -0
  134. package/dist/services/HealingAdvisory.js +51 -0
  135. package/dist/services/HealingCacheService.d.ts +135 -0
  136. package/dist/services/HealingCacheService.d.ts.map +1 -0
  137. package/dist/services/HealingCacheService.js +404 -0
  138. package/dist/services/HealthReportService.d.ts +10 -0
  139. package/dist/services/HealthReportService.d.ts.map +1 -0
  140. package/dist/services/HealthReportService.js +84 -0
  141. package/dist/services/InitializerUpdater.d.ts +16 -0
  142. package/dist/services/InitializerUpdater.d.ts.map +1 -0
  143. package/dist/services/InitializerUpdater.js +73 -0
  144. package/dist/services/IntentAuditor.d.ts +56 -0
  145. package/dist/services/IntentAuditor.d.ts.map +1 -0
  146. package/dist/services/IntentAuditor.js +326 -0
  147. package/dist/services/InteractiveReview.d.ts +15 -0
  148. package/dist/services/InteractiveReview.d.ts.map +1 -0
  149. package/dist/services/InteractiveReview.js +128 -0
  150. package/dist/services/LLMService.d.ts +121 -0
  151. package/dist/services/LLMService.d.ts.map +1 -0
  152. package/dist/services/LLMService.js +475 -0
  153. package/dist/services/MutationApplier.d.ts +69 -0
  154. package/dist/services/MutationApplier.d.ts.map +1 -0
  155. package/dist/services/MutationApplier.js +360 -0
  156. package/dist/services/PRAutomationService.d.ts +50 -0
  157. package/dist/services/PRAutomationService.d.ts.map +1 -0
  158. package/dist/services/PRAutomationService.js +760 -0
  159. package/dist/services/PendingPromptLedger.d.ts +51 -0
  160. package/dist/services/PendingPromptLedger.d.ts.map +1 -0
  161. package/dist/services/PendingPromptLedger.js +205 -0
  162. package/dist/services/ReportGenerator.d.ts +156 -0
  163. package/dist/services/ReportGenerator.d.ts.map +1 -0
  164. package/dist/services/ReportGenerator.js +278 -0
  165. package/dist/services/ReportMergeService.d.ts +95 -0
  166. package/dist/services/ReportMergeService.d.ts.map +1 -0
  167. package/dist/services/ReportMergeService.js +0 -0
  168. package/dist/services/SafetyGuard.d.ts +112 -0
  169. package/dist/services/SafetyGuard.d.ts.map +1 -0
  170. package/dist/services/SafetyGuard.js +87 -0
  171. package/dist/services/Sanitizer.d.ts +28 -0
  172. package/dist/services/Sanitizer.d.ts.map +1 -0
  173. package/dist/services/Sanitizer.js +142 -0
  174. package/dist/services/SanitizingAnthropic.d.ts +19 -0
  175. package/dist/services/SanitizingAnthropic.d.ts.map +1 -0
  176. package/dist/services/SanitizingAnthropic.js +84 -0
  177. package/dist/services/SelectorSanitizer.d.ts +52 -0
  178. package/dist/services/SelectorSanitizer.d.ts.map +1 -0
  179. package/dist/services/SelectorSanitizer.js +318 -0
  180. package/dist/services/SnapshotService.d.ts +11 -0
  181. package/dist/services/SnapshotService.d.ts.map +1 -0
  182. package/dist/services/SnapshotService.js +352 -0
  183. package/dist/services/SourceLinkService.d.ts +26 -0
  184. package/dist/services/SourceLinkService.d.ts.map +1 -0
  185. package/dist/services/SourceLinkService.js +165 -0
  186. package/dist/services/SourceUpdater.d.ts +45 -0
  187. package/dist/services/SourceUpdater.d.ts.map +1 -0
  188. package/dist/services/SourceUpdater.js +1091 -0
  189. package/dist/services/TemplateDiffService.d.ts +25 -0
  190. package/dist/services/TemplateDiffService.d.ts.map +1 -0
  191. package/dist/services/TemplateDiffService.js +334 -0
  192. package/dist/services/TraceBackEngine.d.ts +67 -0
  193. package/dist/services/TraceBackEngine.d.ts.map +1 -0
  194. package/dist/services/TraceBackEngine.js +672 -0
  195. package/dist/services/WorkspaceSnapshotService.d.ts +71 -0
  196. package/dist/services/WorkspaceSnapshotService.d.ts.map +1 -0
  197. package/dist/services/WorkspaceSnapshotService.js +200 -0
  198. package/dist/services/safety/PipelineContext.d.ts +63 -0
  199. package/dist/services/safety/PipelineContext.d.ts.map +1 -0
  200. package/dist/services/safety/PipelineContext.js +34 -0
  201. package/dist/services/safety/roleClassifier.d.ts +4 -0
  202. package/dist/services/safety/roleClassifier.d.ts.map +1 -0
  203. package/dist/services/safety/roleClassifier.js +233 -0
  204. package/dist/services/safety/rules/ConfidenceTierRule.d.ts +3 -0
  205. package/dist/services/safety/rules/ConfidenceTierRule.d.ts.map +1 -0
  206. package/dist/services/safety/rules/ConfidenceTierRule.js +32 -0
  207. package/dist/services/safety/rules/DangerousSinkRule.d.ts +10 -0
  208. package/dist/services/safety/rules/DangerousSinkRule.d.ts.map +1 -0
  209. package/dist/services/safety/rules/DangerousSinkRule.js +53 -0
  210. package/dist/services/safety/rules/FunctionalRoleRule.d.ts +3 -0
  211. package/dist/services/safety/rules/FunctionalRoleRule.d.ts.map +1 -0
  212. package/dist/services/safety/rules/FunctionalRoleRule.js +64 -0
  213. package/dist/services/safety/rules/HardConfidenceRule.d.ts +3 -0
  214. package/dist/services/safety/rules/HardConfidenceRule.d.ts.map +1 -0
  215. package/dist/services/safety/rules/HardConfidenceRule.js +22 -0
  216. package/dist/services/safety/rules/IntentAuditorRule.d.ts +3 -0
  217. package/dist/services/safety/rules/IntentAuditorRule.d.ts.map +1 -0
  218. package/dist/services/safety/rules/IntentAuditorRule.js +97 -0
  219. package/dist/services/safety/rules/SemanticOppositeRule.d.ts +3 -0
  220. package/dist/services/safety/rules/SemanticOppositeRule.d.ts.map +1 -0
  221. package/dist/services/safety/rules/SemanticOppositeRule.js +30 -0
  222. package/dist/services/safety/rules/StructuralOnlyRule.d.ts +3 -0
  223. package/dist/services/safety/rules/StructuralOnlyRule.d.ts.map +1 -0
  224. package/dist/services/safety/rules/StructuralOnlyRule.js +38 -0
  225. package/dist/services/safety/rules/VisibilityRule.d.ts +3 -0
  226. package/dist/services/safety/rules/VisibilityRule.d.ts.map +1 -0
  227. package/dist/services/safety/rules/VisibilityRule.js +53 -0
  228. package/dist/services/safety/rules/ZeroTrustRule.d.ts +3 -0
  229. package/dist/services/safety/rules/ZeroTrustRule.d.ts.map +1 -0
  230. package/dist/services/safety/rules/ZeroTrustRule.js +37 -0
  231. package/dist/services/safety/rules/index.d.ts +3 -0
  232. package/dist/services/safety/rules/index.d.ts.map +1 -0
  233. package/dist/services/safety/rules/index.js +34 -0
  234. package/dist/services/types.d.ts +59 -0
  235. package/dist/services/types.d.ts.map +1 -0
  236. package/dist/services/types.js +2 -0
  237. package/dist/storage/SnapshotManager.d.ts +5 -0
  238. package/dist/storage/SnapshotManager.d.ts.map +1 -0
  239. package/dist/storage/SnapshotManager.js +32 -0
  240. package/dist/types/index.d.ts +95 -0
  241. package/dist/types/index.d.ts.map +1 -0
  242. package/dist/types/index.js +7 -0
  243. package/dist/utils/DOMUtils.d.ts +49 -0
  244. package/dist/utils/DOMUtils.d.ts.map +1 -0
  245. package/dist/utils/DOMUtils.js +466 -0
  246. package/dist/utils/FileLock.d.ts +44 -0
  247. package/dist/utils/FileLock.d.ts.map +1 -0
  248. package/dist/utils/FileLock.js +189 -0
  249. package/dist/utils/IsolatedDiff.d.ts +45 -0
  250. package/dist/utils/IsolatedDiff.d.ts.map +1 -0
  251. package/dist/utils/IsolatedDiff.js +382 -0
  252. package/dist/utils/StackUtils.d.ts +11 -0
  253. package/dist/utils/StackUtils.d.ts.map +1 -0
  254. package/dist/utils/StackUtils.js +120 -0
  255. package/dist/utils/SummaryTable.d.ts +17 -0
  256. package/dist/utils/SummaryTable.d.ts.map +1 -0
  257. package/dist/utils/SummaryTable.js +37 -0
  258. package/dist/utils/atomicWrite.d.ts +9 -0
  259. package/dist/utils/atomicWrite.d.ts.map +1 -0
  260. package/dist/utils/atomicWrite.js +83 -0
  261. package/dist/utils/logger.d.ts +59 -0
  262. package/dist/utils/logger.d.ts.map +1 -0
  263. package/dist/utils/logger.js +123 -0
  264. package/dist/vendor/enquirer.d.ts +33 -0
  265. package/dist/vendor/enquirer.d.ts.map +1 -0
  266. package/dist/vendor/enquirer.js +11 -0
  267. package/package.json +71 -0
@@ -0,0 +1,1039 @@
1
+ "use strict";
2
+ // src/services/HealReportService.ts
3
+ //
4
+ // Sela Insights — Transparent Healing Report.
5
+ //
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.
22
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
23
+ if (k2 === undefined) k2 = k;
24
+ var desc = Object.getOwnPropertyDescriptor(m, k);
25
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
26
+ desc = { enumerable: true, get: function() { return m[k]; } };
27
+ }
28
+ Object.defineProperty(o, k2, desc);
29
+ }) : (function(o, m, k, k2) {
30
+ if (k2 === undefined) k2 = k;
31
+ o[k2] = m[k];
32
+ }));
33
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
34
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
35
+ }) : function(o, v) {
36
+ o["default"] = v;
37
+ });
38
+ var __importStar = (this && this.__importStar) || (function () {
39
+ var ownKeys = function(o) {
40
+ ownKeys = Object.getOwnPropertyNames || function (o) {
41
+ var ar = [];
42
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
43
+ return ar;
44
+ };
45
+ return ownKeys(o);
46
+ };
47
+ return function (mod) {
48
+ if (mod && mod.__esModule) return mod;
49
+ var result = {};
50
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
51
+ __setModuleDefault(result, mod);
52
+ return result;
53
+ };
54
+ })();
55
+ Object.defineProperty(exports, "__esModule", { value: true });
56
+ exports.sharedHealReport = exports.HealReportService = void 0;
57
+ exports.reportShardDir = reportShardDir;
58
+ exports.clearReportShards = clearReportShards;
59
+ exports.summariseSnapshot = summariseSnapshot;
60
+ const fs = __importStar(require("fs"));
61
+ const path = __importStar(require("path"));
62
+ const atomicWrite_1 = require("../utils/atomicWrite");
63
+ const logger_1 = require("../utils/logger");
64
+ const ReportGenerator_1 = require("./ReportGenerator");
65
+ // ─────────────────────────────────────────────────────────────────────────────
66
+ // SIDECAR CONFIG
67
+ // ─────────────────────────────────────────────────────────────────────────────
68
+ const HISTORY_FILE = ".sela-history.json";
69
+ // ─────────────────────────────────────────────────────────────────────────────
70
+ // PER-WORKER REPORT SHARDS (SELA-5 concurrency safety)
71
+ //
72
+ // Local multi-worker runs (`playwright test --workers=N`) all share one cwd, so
73
+ // having every worker write `sela-report.json` directly is a last-writer-wins
74
+ // race — the final file keeps only one worker's events. Instead each worker
75
+ // writes its events to a private shard file under this dir (mirroring the
76
+ // pending-prompts ledger), and the single main-process reporter fans them in
77
+ // via ReportMergeService at onEnd. CI already shards across machines + merges,
78
+ // so it keeps the direct flushToDisk path.
79
+ // ─────────────────────────────────────────────────────────────────────────────
80
+ const SHARD_SUBDIR = path.join(".sela", "report-shards");
81
+ /** Directory holding this run's per-worker report shards. */
82
+ function reportShardDir(cwd) {
83
+ return path.join(cwd, SHARD_SUBDIR);
84
+ }
85
+ /** Remove the shard directory and every shard beneath it. Idempotent. */
86
+ function clearReportShards(cwd) {
87
+ const dir = reportShardDir(cwd);
88
+ if (!fs.existsSync(dir))
89
+ return;
90
+ fs.rmSync(dir, { recursive: true, force: true });
91
+ }
92
+ function shardRandomTag() {
93
+ return Math.random().toString(36).slice(2, 8);
94
+ }
95
+ // ─────────────────────────────────────────────────────────────────────────────
96
+ // SNAPSHOT SUMMARISER
97
+ // ─────────────────────────────────────────────────────────────────────────────
98
+ function summariseSnapshot(s) {
99
+ if (!s)
100
+ return null;
101
+ return {
102
+ tagName: s.tagName,
103
+ text: s.text,
104
+ role: s.role,
105
+ classes: s.classes ?? [],
106
+ attributes: s.attributes ?? {},
107
+ ancestry: (s.ancestry ?? []).map((a) => a.fingerprint),
108
+ closestLabel: s.anchors?.closestLabel ?? null,
109
+ rowAnchor: s.anchors?.rowAnchor ?? null,
110
+ isGhost: s.visibility?.isGhost ?? false,
111
+ ghostReason: s.visibility?.ghostReason ?? null,
112
+ isOccluded: s.visibility?.isOccluded ?? false,
113
+ isInShadowDom: s.isInShadowDom ?? false,
114
+ };
115
+ }
116
+ // ─────────────────────────────────────────────────────────────────────────────
117
+ // HealReportService
118
+ // ─────────────────────────────────────────────────────────────────────────────
119
+ class HealReportService {
120
+ events = [];
121
+ startedAt = new Date().toISOString();
122
+ record(event) {
123
+ this.events.push(event);
124
+ }
125
+ size() {
126
+ return this.events.length;
127
+ }
128
+ clear() {
129
+ this.events = [];
130
+ this.startedAt = new Date().toISOString();
131
+ }
132
+ hasContent() {
133
+ return this.events.length > 0;
134
+ }
135
+ getHealedEvents() {
136
+ return this.events.filter((e) => e.kind === "HEALED");
137
+ }
138
+ getProtectedEvents() {
139
+ return this.events.filter((e) => e.kind === "PROTECTED");
140
+ }
141
+ getFailedEvents() {
142
+ return this.events.filter((e) => e.kind === "FAILED");
143
+ }
144
+ /**
145
+ * Attach a Playwright failure screenshot (Base64 PNG, no data: prefix)
146
+ * onto the FAILED event(s) matching the supplied identity hints.
147
+ * Returns the number of events updated.
148
+ */
149
+ attachFailureScreenshot(match, base64Png) {
150
+ if (!base64Png)
151
+ return 0;
152
+ const norm = (p) => (p ?? "").replace(/\\/g, "/").toLowerCase();
153
+ const targetFile = norm(match.sourceFile);
154
+ let touched = 0;
155
+ for (const ev of this.events) {
156
+ if (ev.kind !== "FAILED")
157
+ continue;
158
+ const evFile = norm(ev.sourceFile);
159
+ const fileMatch = !targetFile ||
160
+ !evFile ||
161
+ evFile === targetFile ||
162
+ evFile.endsWith(targetFile) ||
163
+ targetFile.endsWith(evFile);
164
+ const titleMatch = !match.testTitle || ev.testTitle === match.testTitle;
165
+ const idMatch = !match.stableId || ev.stableId === match.stableId;
166
+ if (fileMatch && titleMatch && idMatch) {
167
+ ev.failureScreenshotPng = base64Png;
168
+ touched++;
169
+ }
170
+ }
171
+ return touched;
172
+ }
173
+ // ── History sidecar ─────────────────────────────────────────────────────
174
+ readHistory(targetDir) {
175
+ const histPath = path.join(targetDir, HISTORY_FILE);
176
+ try {
177
+ const raw = fs.readFileSync(histPath, "utf8");
178
+ const parsed = JSON.parse(raw);
179
+ if (!Array.isArray(parsed))
180
+ return [];
181
+ return parsed;
182
+ }
183
+ catch {
184
+ return [];
185
+ }
186
+ }
187
+ writeHistory(targetDir, runs) {
188
+ const histPath = path.join(targetDir, HISTORY_FILE);
189
+ try {
190
+ fs.writeFileSync(histPath, JSON.stringify(runs, null, 2), "utf8");
191
+ }
192
+ catch (err) {
193
+ logger_1.logger.warn(`Could not write history sidecar: ${err}`);
194
+ }
195
+ }
196
+ // ── Main flush ──────────────────────────────────────────────────────────
197
+ /**
198
+ * Generate and write `sela-report.html` + `sela-report.json` to targetDir.
199
+ * Also updates `.sela-history.json` with the current run's stats.
200
+ * Returns the absolute path of the HTML report, or null when there is
201
+ * nothing to write.
202
+ */
203
+ flushToDisk(targetDir = process.cwd(), opts = {}) {
204
+ if (this.events.length === 0)
205
+ return null;
206
+ const priorHistory = this.readHistory(targetDir);
207
+ const data = ReportGenerator_1.ReportGenerator.generate(this.events, {
208
+ cwd: targetDir,
209
+ startedAt: this.startedAt,
210
+ historicalRuns: priorHistory,
211
+ runId: opts.runId ??
212
+ process.env["GITHUB_RUN_ID"] ??
213
+ process.env["CI_PIPELINE_ID"] ??
214
+ undefined,
215
+ });
216
+ return this.writeReportData(targetDir, data);
217
+ }
218
+ /**
219
+ * Render and persist a pre-computed {@link ReportData} payload to `targetDir`:
220
+ * • `.sela-history.json` (the rolling window from `data.historical_runs`)
221
+ * • `sela-report.html` (the embedded dashboard)
222
+ * • `sela-report.json` (the raw payload for CI consumers)
223
+ *
224
+ * Extracted from {@link flushToDisk} so the CI merge path
225
+ * (`ReportMergeService`) writes a shard-aggregated report through the exact
226
+ * same rendering / serialisation logic — no duplication.
227
+ *
228
+ * Unlike `flushToDisk`, this writes unconditionally: an empty-event payload
229
+ * still produces a report and a `healed:0` history row. That is the desired
230
+ * CI behaviour — a clean, zero-heal pipeline is a healthy data point that
231
+ * must keep the trend chart's x-axis cadence intact.
232
+ *
233
+ * Returns the absolute path of the written HTML report.
234
+ */
235
+ writeReportData(targetDir, data) {
236
+ this.writeHistory(targetDir, data.historical_runs);
237
+ const html = renderHtml(data);
238
+ const htmlPath = path.join(targetDir, "sela-report.html");
239
+ fs.writeFileSync(htmlPath, html, "utf8");
240
+ const jsonPath = path.join(targetDir, "sela-report.json");
241
+ fs.writeFileSync(jsonPath, JSON.stringify({
242
+ generatedAt: data.generatedAt,
243
+ startedAt: data.startedAt,
244
+ cwd: data.cwd,
245
+ stats: data.stats,
246
+ roi: data.roi,
247
+ historical_runs: data.historical_runs,
248
+ events: data.events,
249
+ }, null, 2), "utf8");
250
+ return htmlPath;
251
+ }
252
+ /**
253
+ * Write THIS worker's buffered events to a private shard file under
254
+ * `<cwd>/.sela/report-shards/` — one file per worker process (pid +
255
+ * timestamp + random tag), so concurrent workers never contend on a shared
256
+ * report path. The main-process reporter fans them in via
257
+ * {@link ReportMergeService} at onEnd. Returns the shard path, or null when
258
+ * there is nothing to flush.
259
+ *
260
+ * Shard shape is `{ startedAt, events }`, which ReportMergeService.readShard
261
+ * Events accepts (it also tolerates a bare event array).
262
+ */
263
+ flushShard(cwd = process.cwd()) {
264
+ if (this.events.length === 0)
265
+ return null;
266
+ const dir = reportShardDir(cwd);
267
+ fs.mkdirSync(dir, { recursive: true });
268
+ const file = path.join(dir, `sela-shard-${process.pid}-${Date.now()}-${shardRandomTag()}.json`);
269
+ (0, atomicWrite_1.atomicWriteFile)(file, JSON.stringify({ startedAt: this.startedAt, events: this.events }, null, 2));
270
+ return file;
271
+ }
272
+ }
273
+ exports.HealReportService = HealReportService;
274
+ // ─────────────────────────────────────────────────────────────────────────────
275
+ // MODULE-LEVEL SINGLETON
276
+ // ─────────────────────────────────────────────────────────────────────────────
277
+ exports.sharedHealReport = new HealReportService();
278
+ // ─────────────────────────────────────────────────────────────────────────────
279
+ // HTML RENDERER
280
+ //
281
+ // Uses plain string concatenation throughout — no TypeScript template literals
282
+ // that could misinterpret the embedded CSS/JS content.
283
+ // ─────────────────────────────────────────────────────────────────────────────
284
+ function renderHtml(data) {
285
+ // Embed the JSON payload. Escape </script> sequences and lone < so the
286
+ // string is safe inside a <script> block regardless of content.
287
+ const payload = JSON.stringify(data)
288
+ .replace(/</g, "\\u003c")
289
+ .replace(/\//g, "\\/");
290
+ return ("<!DOCTYPE html>\n" +
291
+ '<html lang="en">\n' +
292
+ "<head>\n" +
293
+ '<meta charset="UTF-8"/>\n' +
294
+ '<meta name="viewport" content="width=device-width,initial-scale=1.0"/>\n' +
295
+ "<title>Sela Insights \u2014 Healing Report</title>\n" +
296
+ '<link rel="preconnect" href="https://fonts.googleapis.com"/>\n' +
297
+ '<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin/>\n' +
298
+ '<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' +
299
+ '<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.js"><\/script>\n' +
300
+ "<style>\n" +
301
+ REPORT_CSS +
302
+ "\n</style>\n" +
303
+ "</head>\n" +
304
+ "<body>\n" +
305
+ '<div class="bg-noise"></div>\n' +
306
+ '<div class="bg-grid"></div>\n' +
307
+ buildTopbar() +
308
+ buildOverviewView() +
309
+ buildDeepDiveView() +
310
+ buildFooter() +
311
+ '<div id="toast" class="toast hidden"></div>\n' +
312
+ "<script>window.__SELA_DATA__ = " +
313
+ payload +
314
+ ";<\/script>\n" +
315
+ "<script>\n" +
316
+ REPORT_JS +
317
+ "\n<\/script>\n" +
318
+ "</body>\n" +
319
+ "</html>");
320
+ }
321
+ function buildTopbar() {
322
+ return ('<nav class="topbar">\n' +
323
+ ' <div class="wrap topbar-inner">\n' +
324
+ ' <div class="brand">\n' +
325
+ ' <div class="brand-hex"></div>\n' +
326
+ ' <span class="brand-name">Sela</span>\n' +
327
+ ' <span class="brand-sep">/</span>\n' +
328
+ ' <span class="brand-sub">Healing Report</span>\n' +
329
+ " </div>\n" +
330
+ ' <div style="display:flex;align-items:center;gap:12px">\n' +
331
+ ' <div class="view-toggle">\n' +
332
+ ' <button class="vt-btn active" id="btn-overview" onclick="setView(\'overview\')">Overview</button>\n' +
333
+ ' <button class="vt-btn" id="btn-deepdive" onclick="setView(\'deepdive\')">Deep Dive</button>\n' +
334
+ " </div>\n" +
335
+ ' <div class="topbar-meta">\n' +
336
+ ' <div class="topbar-pill live"><span class="dot"></span>Run complete</div>\n' +
337
+ " </div>\n" +
338
+ " </div>\n" +
339
+ " </div>\n" +
340
+ "</nav>\n");
341
+ }
342
+ function buildOverviewView() {
343
+ return ('<div id="view-overview">\n' +
344
+ '<div class="wrap">\n' +
345
+ ' <div class="hero">\n' +
346
+ ' <div class="exec-row">\n' +
347
+ " <div>\n" +
348
+ ' <div class="exec-eyebrow"><span class="ei"></span>Sela Intelligence Run<span class="ei"></span></div>\n' +
349
+ ' <h1 class="exec-h1" id="exec-h1"></h1>\n' +
350
+ ' <p class="exec-sub" id="exec-sub"></p>\n' +
351
+ " </div>\n" +
352
+ ' <div style="text-align:right;font-size:12px;color:var(--text3)">\n' +
353
+ ' <div id="meta-gen" style="margin-bottom:3px"></div>\n' +
354
+ ' <div id="meta-cwd" style="font-family:var(--mono);font-size:11px;word-break:break-all;max-width:280px"></div>\n' +
355
+ " </div>\n" +
356
+ " </div>\n" +
357
+ ' <div class="kpi-row">\n' +
358
+ ' <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' +
359
+ ' <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' +
360
+ ' <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' +
361
+ ' <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' +
362
+ " </div>\n" +
363
+ ' <div class="charts-row">\n' +
364
+ ' <div class="chart-card">\n' +
365
+ ' <div class="chart-label">Run Distribution</div>\n' +
366
+ ' <div class="donut-wrap">\n' +
367
+ ' <canvas id="donut-chart"></canvas>\n' +
368
+ ' <div class="donut-center"><div class="donut-total" id="donut-total"></div><div class="donut-total-l">Events</div></div>\n' +
369
+ " </div>\n" +
370
+ ' <div class="donut-legend" id="donut-legend"></div>\n' +
371
+ " </div>\n" +
372
+ ' <div class="chart-card">\n' +
373
+ ' <div class="trend-header">\n' +
374
+ ' <div class="chart-label" style="margin-bottom:0">Sela Impact \u2014 Last 14 Runs</div>\n' +
375
+ ' <div class="trend-badge" id="trend-badge"></div>\n' +
376
+ " </div>\n" +
377
+ ' <div class="trend-canvas-wrap"><canvas id="trend-chart"></canvas></div>\n' +
378
+ ' <div style="margin-top:16px">\n' +
379
+ ' <div class="chart-label">Recent Runs</div>\n' +
380
+ ' <div id="hist-table">\n' +
381
+ ' <div class="hist-row hdr">\n' +
382
+ ' <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' +
383
+ " </div>\n" +
384
+ " </div>\n" +
385
+ " </div>\n" +
386
+ " </div>\n" +
387
+ " </div>\n" +
388
+ ' <div style="margin-bottom:32px;display:flex;gap:10px;flex-wrap:wrap">\n' +
389
+ ' <button class="btn btn-primary" onclick="setView(\'deepdive\')">&#x2192; Inspect all events</button>\n' +
390
+ ' <button class="btn" onclick="copyJson()">&#x2398; Copy JSON</button>\n' +
391
+ " </div>\n" +
392
+ " </div>\n" +
393
+ "</div>\n" +
394
+ "</div>\n");
395
+ }
396
+ function buildDeepDiveView() {
397
+ return ('<div id="view-deepdive">\n' +
398
+ '<div class="wrap" style="padding-top:24px">\n' +
399
+ ' <div class="section-hdr">\n' +
400
+ " <h2>Engineering Deep Dive</h2>\n" +
401
+ ' <div class="sh-count" id="dd-count"></div>\n' +
402
+ ' <div class="sh-line"></div>\n' +
403
+ ' <button class="btn" onclick="setView(\'overview\')" style="font-size:11.5px;padding:4px 10px">&#x2190; Overview</button>\n' +
404
+ " </div>\n" +
405
+ ' <div class="filter-bar">\n' +
406
+ ' <div class="fb-chips">\n' +
407
+ ' <div class="chip c-all on" data-filter="ALL"><span>All</span></div>\n' +
408
+ ' <div class="chip c-heal" data-filter="HEALED"><span class="chip-dot"></span>Healed</div>\n' +
409
+ ' <div class="chip c-prot" data-filter="PROTECTED"><span class="chip-dot"></span>Protected \xb7 Bug Caught</div>\n' +
410
+ ' <div class="chip c-fail" data-filter="FAILED"><span class="chip-dot"></span>Failed</div>\n' +
411
+ " </div>\n" +
412
+ ' <div class="fb-search">\n' +
413
+ ' <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' +
414
+ ' <input type="text" id="search-input" placeholder="selector, file, test name\u2026"/>\n' +
415
+ " </div>\n" +
416
+ " </div>\n" +
417
+ ' <div class="events" id="events-list"></div>\n' +
418
+ ' <div class="empty-state hidden" id="empty-state">\n' +
419
+ ' <div class="es-icon">\u2205</div>\n' +
420
+ ' <div class="es-title">Nothing matches</div>\n' +
421
+ " <div>Try a different filter or clear the search.</div>\n" +
422
+ " </div>\n" +
423
+ "</div>\n" +
424
+ "</div>\n");
425
+ }
426
+ function buildFooter() {
427
+ return ('<footer class="footer">\n' +
428
+ ' <div class="wrap footer-inner">\n' +
429
+ ' <div>Generated by <strong>sela-core</strong> \xb7 <span id="footer-cwd" style="font-family:var(--mono);font-size:11px"></span></div>\n' +
430
+ " <div>Sela is a transparent pair-programmer. Every decision is yours to keep, roll back, or report.</div>\n" +
431
+ " </div>\n" +
432
+ "</footer>\n");
433
+ }
434
+ // ─────────────────────────────────────────────────────────────────────────────
435
+ // CSS — stored as a plain string constant (no backtick template literal)
436
+ // so TypeScript has no chance to misparse it.
437
+ // ─────────────────────────────────────────────────────────────────────────────
438
+ /* eslint-disable prettier/prettier */
439
+ const REPORT_CSS = [
440
+ "*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}",
441
+ ":root{",
442
+ " --bg0:#060709;--bg1:#0b0d12;--bg2:#10131a;--bg3:#161923;--bg4:#1c2030;",
443
+ " --line:#1e2336;--line2:#262d42;--line3:#2e3650;",
444
+ " --text:#dde2f0;--text2:#8b93b0;--text3:#5a6180;",
445
+ " --cyan:#00d4ff;--cyan-dim:rgba(0,212,255,.12);--cyan-glow:rgba(0,212,255,.25);",
446
+ " --purple:#a259ff;--purple-dim:rgba(162,89,255,.12);--purple-glow:rgba(162,89,255,.25);",
447
+ " --red:#ff4d6a;--red-dim:rgba(255,77,106,.12);--red-glow:rgba(255,77,106,.25);",
448
+ " --green:#00e5a0;--green-dim:rgba(0,229,160,.1);",
449
+ " --amber:#ffb84d;--amber-dim:rgba(255,184,77,.1);",
450
+ " --radius:10px;--radius-lg:14px;--radius-xl:20px;",
451
+ " --mono:'Geist Mono',ui-monospace,monospace;",
452
+ " --sans:'Geist',system-ui,sans-serif;",
453
+ "}",
454
+ "html{background:var(--bg0);color:var(--text);font-family:var(--sans);font-size:14px;line-height:1.5;-webkit-font-smoothing:antialiased}",
455
+ "body{min-height:100vh;overflow-x:hidden}",
456
+ "a{color:var(--cyan);text-decoration:none}",
457
+ "button{font-family:var(--sans);cursor:pointer}",
458
+ "input{font-family:var(--sans)}",
459
+ "::-webkit-scrollbar{width:6px;height:6px}",
460
+ "::-webkit-scrollbar-track{background:var(--bg1)}",
461
+ "::-webkit-scrollbar-thumb{background:var(--line3);border-radius:3px}",
462
+ ".bg-noise{",
463
+ " position:fixed;inset:0;z-index:-2;",
464
+ " background:",
465
+ " radial-gradient(ellipse 80% 50% at 10% 10%,rgba(0,212,255,.07) 0%,transparent 60%),",
466
+ " radial-gradient(ellipse 60% 40% at 90% 20%,rgba(162,89,255,.08) 0%,transparent 60%),",
467
+ " radial-gradient(ellipse 50% 60% at 50% 90%,rgba(162,89,255,.05) 0%,transparent 60%),",
468
+ " var(--bg0);",
469
+ "}",
470
+ ".bg-grid{",
471
+ " position:fixed;inset:0;z-index:-1;pointer-events:none;",
472
+ " background-image:linear-gradient(rgba(255,255,255,.018) 1px,transparent 1px),linear-gradient(90deg,rgba(255,255,255,.018) 1px,transparent 1px);",
473
+ " background-size:40px 40px;",
474
+ " mask-image:radial-gradient(ellipse at 50% 0%,black 0%,transparent 65%);",
475
+ "}",
476
+ ".wrap{max-width:1320px;margin:0 auto;padding:0 24px}",
477
+ ".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;}",
478
+ ".topbar-inner{height:52px;display:flex;align-items:center;justify-content:space-between}",
479
+ ".brand{display:flex;align-items:center;gap:10px}",
480
+ ".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;}",
481
+ ".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;}",
482
+ ".brand-name{font-weight:600;font-size:14px;letter-spacing:-.2px}",
483
+ ".brand-sep{color:var(--text3);font-size:13px}",
484
+ ".brand-sub{font-size:12px;color:var(--text3);letter-spacing:.6px;text-transform:uppercase}",
485
+ ".topbar-meta{font-size:12px;color:var(--text3);display:flex;align-items:center;gap:16px}",
486
+ ".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);}",
487
+ ".topbar-pill .dot{width:5px;height:5px;border-radius:50%}",
488
+ ".topbar-pill.live .dot{background:var(--green);box-shadow:0 0 6px var(--green);animation:blink 2s ease-in-out infinite}",
489
+ "@keyframes blink{0%,100%{opacity:1}50%{opacity:.4}}",
490
+ ".view-toggle{display:flex;gap:2px;padding:3px;background:var(--bg2);border:1px solid var(--line);border-radius:var(--radius)}",
491
+ ".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}",
492
+ ".vt-btn.active{background:var(--bg4);color:var(--text);box-shadow:0 1px 3px rgba(0,0,0,.4)}",
493
+ ".hero{padding:32px 0 0}",
494
+ ".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}",
495
+ "@media(max-width:800px){.exec-row{grid-template-columns:1fr}}",
496
+ ".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}",
497
+ ".exec-eyebrow .ei{width:14px;height:1px;background:var(--cyan);opacity:.6}",
498
+ ".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}",
499
+ ".exec-sub{font-size:14px;color:var(--text2);max-width:52ch;line-height:1.6}",
500
+ ".kpi-row{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;margin-bottom:28px}",
501
+ "@media(max-width:900px){.kpi-row{grid-template-columns:repeat(2,1fr)}}",
502
+ ".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}",
503
+ ".kpi::before{content:'';position:absolute;inset:0;opacity:.5;background:linear-gradient(180deg,rgba(255,255,255,.03) 0%,transparent 100%);pointer-events:none}",
504
+ ".kpi:hover{transform:translateY(-2px);border-color:var(--line3)}",
505
+ ".kpi-l{font-size:10.5px;letter-spacing:1.2px;text-transform:uppercase;color:var(--text3);margin-bottom:6px}",
506
+ ".kpi-v{font-size:28px;font-weight:700;line-height:1;font-variant-numeric:tabular-nums;margin-bottom:4px}",
507
+ ".kpi-f{font-size:11.5px;color:var(--text3)}",
508
+ ".kpi-accent{position:absolute;top:0;right:0;width:3px;height:100%;border-radius:0 var(--radius-lg) var(--radius-lg) 0}",
509
+ ".kpi.k-time .kpi-v{background:linear-gradient(135deg,var(--cyan),var(--purple));-webkit-background-clip:text;background-clip:text;color:transparent}",
510
+ ".kpi.k-time .kpi-accent{background:linear-gradient(180deg,var(--cyan),var(--purple))}",
511
+ ".kpi.k-healed .kpi-v{color:var(--cyan)}.kpi.k-healed .kpi-accent{background:var(--cyan)}",
512
+ ".kpi.k-prot .kpi-v{color:var(--purple)}.kpi.k-prot .kpi-accent{background:var(--purple)}",
513
+ ".kpi.k-fail .kpi-v{color:var(--red)}.kpi.k-fail .kpi-accent{background:var(--red)}",
514
+ ".charts-row{display:grid;grid-template-columns:280px 1fr;gap:18px;margin-bottom:32px}",
515
+ "@media(max-width:900px){.charts-row{grid-template-columns:1fr}}",
516
+ ".chart-card{background:var(--bg2);border:1px solid var(--line);border-radius:var(--radius-lg);padding:20px}",
517
+ ".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}",
518
+ ".chart-label::after{content:'';flex:1;height:1px;background:var(--line)}",
519
+ ".donut-wrap{position:relative;width:160px;height:160px;margin:0 auto 16px}",
520
+ ".donut-center{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;pointer-events:none}",
521
+ ".donut-total{font-size:28px;font-weight:700;line-height:1}",
522
+ ".donut-total-l{font-size:10px;letter-spacing:1px;text-transform:uppercase;color:var(--text3);margin-top:2px}",
523
+ ".donut-legend{display:flex;flex-direction:column;gap:7px}",
524
+ ".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}",
525
+ ".dl-row:hover,.dl-row.active{border-color:var(--line3);background:var(--bg3)}",
526
+ ".dl-swatch{width:8px;height:8px;border-radius:2px;flex-shrink:0}",
527
+ ".dl-name{font-size:12.5px;color:var(--text2);flex:1}",
528
+ ".dl-val{font-size:12.5px;font-weight:600;font-variant-numeric:tabular-nums}",
529
+ ".dl-pct{font-size:11px;color:var(--text3);margin-left:4px}",
530
+ ".trend-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:14px}",
531
+ ".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)}",
532
+ ".trend-canvas-wrap{position:relative;width:100%;height:200px}",
533
+ ".section-hdr{display:flex;align-items:center;gap:10px;margin-bottom:14px}",
534
+ ".section-hdr h2{font-size:13px;font-weight:600;letter-spacing:.4px}",
535
+ ".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}",
536
+ ".section-hdr .sh-line{flex:1;height:1px;background:var(--line)}",
537
+ ".filter-bar{display:flex;align-items:center;gap:10px;margin-bottom:16px;flex-wrap:wrap}",
538
+ ".fb-chips{display:flex;gap:6px;flex-wrap:wrap}",
539
+ ".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}",
540
+ ".chip:hover{color:var(--text);border-color:var(--line3)}",
541
+ ".chip.c-all.on{border-color:rgba(255,255,255,.3);color:#fff;background:var(--bg3)}",
542
+ ".chip.c-heal.on{border-color:var(--cyan);color:var(--cyan);background:var(--cyan-dim)}",
543
+ ".chip.c-prot.on{border-color:var(--purple);color:var(--purple);background:var(--purple-dim)}",
544
+ ".chip.c-fail.on{border-color:var(--red);color:var(--red);background:var(--red-dim)}",
545
+ ".chip .chip-dot{width:5px;height:5px;border-radius:50%}",
546
+ ".c-heal .chip-dot,.chip.c-heal.on .chip-dot{background:var(--cyan)}",
547
+ ".c-prot .chip-dot,.chip.c-prot.on .chip-dot{background:var(--purple)}",
548
+ ".c-fail .chip-dot,.chip.c-fail.on .chip-dot{background:var(--red)}",
549
+ ".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}",
550
+ ".fb-search input{background:none;border:none;outline:none;color:var(--text);font-size:12.5px;width:200px}",
551
+ ".fb-search input::placeholder{color:var(--text3)}",
552
+ ".events{display:flex;flex-direction:column;gap:8px}",
553
+ ".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}",
554
+ "@keyframes cardIn{0%{opacity:0;transform:translateY(6px)}100%{opacity:1;transform:translateY(0)}}",
555
+ ".ecard:hover{border-color:var(--line3)}",
556
+ ".ecard.ec-healed{border-left:2px solid var(--cyan)}",
557
+ ".ecard.ec-protected{border-left:2px solid var(--purple)}",
558
+ ".ecard.ec-failed{border-left:2px solid var(--red)}",
559
+ ".ecard.ec-protected .ecard-head{background:linear-gradient(90deg,rgba(162,89,255,.04),transparent)}",
560
+ ".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}",
561
+ ".ecard-head:hover{background:var(--bg3)}",
562
+ ".ecard.open .ecard-head{background:var(--bg3)}",
563
+ ".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}",
564
+ ".sb-healed{color:var(--cyan);background:var(--cyan-dim)}",
565
+ ".sb-protected{color:var(--purple);background:var(--purple-dim);animation:pulseborder 2.5s ease-in-out infinite}",
566
+ "@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)}}",
567
+ ".sb-failed{color:var(--red);background:var(--red-dim)}",
568
+ ".sb-dot{width:5px;height:5px;border-radius:50%;background:currentColor;box-shadow:0 0 5px currentColor}",
569
+ ".ecard-title{min-width:0}",
570
+ ".ecard-selector{font-family:var(--mono);font-size:12.5px;color:var(--text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-bottom:2px}",
571
+ ".ecard-meta{font-size:11.5px;color:var(--text3);display:flex;align-items:center;gap:5px;flex-wrap:wrap}",
572
+ ".ecard-meta a{color:var(--text2);font-family:var(--mono);font-size:11px}",
573
+ ".ecard-meta a:hover{color:var(--cyan)}",
574
+ ".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}",
575
+ ".ecard-tag.t-iframe{color:var(--cyan);border-color:rgba(0,212,255,.2)}",
576
+ ".ecard-tag.t-shadow{color:var(--purple);border-color:rgba(162,89,255,.2)}",
577
+ ".ecard-tag.t-dryrun{color:var(--amber);border-color:rgba(255,184,77,.2)}",
578
+ ".conf-badge{font-size:11px;font-weight:600;padding:3px 9px;border-radius:6px;font-variant-numeric:tabular-nums;white-space:nowrap}",
579
+ ".conf-high{background:rgba(0,229,160,.12);color:var(--green);border:1px solid rgba(0,229,160,.2)}",
580
+ ".conf-mid{background:rgba(255,184,77,.1);color:var(--amber);border:1px solid rgba(255,184,77,.2)}",
581
+ ".conf-low{background:rgba(255,77,106,.1);color:var(--red);border:1px solid rgba(255,77,106,.2)}",
582
+ ".chev{width:16px;height:16px;color:var(--text3);flex-shrink:0;transition:transform .25s ease}",
583
+ ".ecard.open .chev{transform:rotate(90deg)}",
584
+ ".ecard-body{display:none;padding:0 16px 18px;border-top:1px solid var(--line)}",
585
+ ".ecard.open .ecard-body{display:block;animation:bodyIn .3s ease both}",
586
+ "@keyframes bodyIn{0%{opacity:0;transform:translateY(-3px)}100%{opacity:1;transform:translateY(0)}}",
587
+ ".body-section{margin-top:16px}",
588
+ ".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}",
589
+ ".bs-title .bst-dot{width:5px;height:5px;border-radius:50%;background:var(--purple);box-shadow:0 0 6px var(--purple)}",
590
+ ".diff-block{background:var(--bg1);border:1px solid var(--line);border-radius:var(--radius);overflow:hidden}",
591
+ ".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)}",
592
+ ".diff-bar .sep{opacity:.3}",
593
+ ".diff-line{display:grid;grid-template-columns:22px 48px 1fr;font-family:var(--mono);font-size:12px;line-height:1.6}",
594
+ ".diff-line .gutter{text-align:center;color:var(--text3)}",
595
+ ".diff-line .ln{text-align:right;padding-right:10px;color:var(--text3)}",
596
+ ".diff-line .code{white-space:pre-wrap;word-break:break-word;padding-right:12px}",
597
+ ".diff-line.dl-add{background:rgba(0,229,160,.07)}",
598
+ ".diff-line.dl-add .gutter,.diff-line.dl-add .code{color:var(--green)}",
599
+ ".diff-line.dl-del{background:rgba(255,77,106,.07)}",
600
+ ".diff-line.dl-del .gutter,.diff-line.dl-del .code{color:var(--red)}",
601
+ ".diff-line.dl-ctx .code{color:var(--text3)}",
602
+ ".diff-line.dl-hunk{background:rgba(0,212,255,.04)}",
603
+ ".diff-line.dl-hunk .code{color:rgba(0,212,255,.6);font-size:11px}",
604
+ ".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}",
605
+ "@media(max-width:700px){.auditor-wrap{grid-template-columns:1fr}}",
606
+ ".gauge-ring{position:relative;width:130px;height:130px;margin:0 auto}",
607
+ ".gauge-ring svg{width:100%;height:100%;transform:rotate(-90deg)}",
608
+ ".gauge-ring .gt{stroke:var(--line3);fill:none;stroke-width:9}",
609
+ ".gauge-ring .gb{fill:none;stroke-width:9;stroke-linecap:round;transition:stroke-dasharray 1.2s cubic-bezier(.4,0,.2,1)}",
610
+ ".gauge-center{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center}",
611
+ ".gauge-pct{font-size:26px;font-weight:700;font-variant-numeric:tabular-nums;line-height:1}",
612
+ ".gauge-sub{font-size:9.5px;letter-spacing:1px;text-transform:uppercase;color:var(--text3);margin-top:2px}",
613
+ ".gb-consistent{stroke:var(--green)}.gb-suspicious{stroke:var(--amber)}.gb-inconsistent{stroke:var(--red)}",
614
+ ".pct-consistent{color:var(--green)}.pct-suspicious{color:var(--amber)}.pct-inconsistent{color:var(--red)}",
615
+ ".audit-details{display:flex;flex-direction:column;gap:7px}",
616
+ ".ad-row{display:flex;gap:8px;font-size:12.5px;align-items:flex-start}",
617
+ ".ad-label{color:var(--text3);font-size:11px;letter-spacing:.5px;text-transform:uppercase;min-width:110px;flex-shrink:0;padding-top:1px}",
618
+ ".ad-val{color:var(--text)}",
619
+ ".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}",
620
+ ".verdict-pill{display:inline-block;padding:2px 8px;border-radius:5px;font-size:10.5px;font-weight:700;letter-spacing:.8px;text-transform:uppercase}",
621
+ ".vp-consistent{color:var(--green);background:var(--green-dim);border:1px solid rgba(0,229,160,.2)}",
622
+ ".vp-suspicious{color:var(--amber);background:var(--amber-dim);border:1px solid rgba(255,184,77,.2)}",
623
+ ".vp-inconsistent{color:var(--red);background:var(--red-dim);border:1px solid rgba(255,77,106,.2)}",
624
+ ".cot{display:flex;flex-direction:column;gap:7px}",
625
+ ".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)}",
626
+ ".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}",
627
+ ".cot-t{color:var(--text2);font-size:12.5px;line-height:1.5}",
628
+ ".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}",
629
+ ".dna-table th,.dna-table td{padding:7px 11px;text-align:left;border-bottom:1px solid var(--line)}",
630
+ ".dna-table th{background:var(--bg3);font-weight:600;color:var(--text3);font-size:10.5px;letter-spacing:.6px;text-transform:uppercase}",
631
+ ".dna-table tr:last-child td{border-bottom:none}",
632
+ ".dna-table td.field{color:var(--text3);width:20%}",
633
+ ".dna-table td.val{font-family:var(--mono);font-size:11.5px}",
634
+ ".dna-table td.icon{width:32px;text-align:center}",
635
+ ".dna-table tr.matched td.icon{color:var(--green)}",
636
+ ".dna-table tr.changed td.val.new{color:var(--amber)}",
637
+ ".dna-table tr.changed td.val.old{color:var(--text3);text-decoration:line-through dotted}",
638
+ ".dna-table tr.changed td.icon{color:var(--amber)}",
639
+ ".dna-table tr.missing td.icon{color:var(--text3)}",
640
+ ".alts{display:flex;flex-direction:column;gap:5px}",
641
+ ".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}",
642
+ ".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}",
643
+ ".alt-copy:hover{color:var(--text);border-color:var(--line3)}",
644
+ ".actions{display:flex;gap:8px;flex-wrap:wrap;margin-top:16px;padding-top:14px;border-top:1px dashed var(--line2)}",
645
+ ".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}",
646
+ ".btn:hover{color:var(--text);border-color:var(--line3);transform:translateY(-1px)}",
647
+ ".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)}",
648
+ ".btn.btn-primary:hover{box-shadow:0 4px 20px rgba(0,212,255,.2)}",
649
+ ".btn.btn-danger{border-color:rgba(255,77,106,.3);color:var(--red)}",
650
+ ".btn.btn-danger:hover{background:var(--red-dim)}",
651
+ ".btn[data-done='true']{border-color:rgba(0,229,160,.3);color:var(--green);background:var(--green-dim)}",
652
+ ".fail-img-wrap{background:var(--bg1);border:1px solid var(--line);border-radius:var(--radius);overflow:hidden}",
653
+ ".fail-img-wrap img{display:block;max-width:100%;height:auto}",
654
+ ".fail-img-meta{padding:6px 11px;font-size:11px;color:var(--text3);font-family:var(--mono)}",
655
+ ".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}",
656
+ ".prot-notice strong{color:var(--purple)}",
657
+ ".row2{display:grid;grid-template-columns:1fr 1fr;gap:14px}",
658
+ "@media(max-width:700px){.row2{grid-template-columns:1fr}}",
659
+ ".muted-block{padding:12px 14px;background:var(--bg1);border:1px dashed var(--line);border-radius:var(--radius);font-size:12px;color:var(--text3)}",
660
+ ".hist-row{display:grid;grid-template-columns:1fr repeat(5,80px);gap:0;font-size:12px}",
661
+ ".hist-row .hc{padding:6px 10px;border-bottom:1px solid var(--line)}",
662
+ ".hist-row.hdr .hc{color:var(--text3);font-size:10.5px;letter-spacing:.6px;text-transform:uppercase;background:var(--bg3)}",
663
+ ".hist-row:not(.hdr):hover .hc{background:var(--bg3)}",
664
+ ".hist-row .hc:first-child{color:var(--text2)}",
665
+ ".hist-saved{color:var(--cyan);font-weight:600}",
666
+ ".empty-state{text-align:center;padding:60px 20px;color:var(--text3);border:1px dashed var(--line2);border-radius:var(--radius-xl);margin-top:12px}",
667
+ ".es-icon{font-size:36px;margin-bottom:10px}",
668
+ ".es-title{font-size:15px;color:var(--text2);margin-bottom:4px}",
669
+ ".hidden{display:none!important}",
670
+ ".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}",
671
+ "@keyframes toastIn{0%{opacity:0;transform:translate(-50%,8px)}100%{opacity:1;transform:translate(-50%,0)}}",
672
+ ".footer{border-top:1px solid var(--line);margin-top:60px;padding:20px 0;font-size:12px;color:var(--text3)}",
673
+ ".footer-inner{display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:10px}",
674
+ "#view-overview{display:block}",
675
+ "#view-deepdive{display:none}",
676
+ "body.view-deepdive #view-overview{display:none}",
677
+ "body.view-deepdive #view-deepdive{display:block}",
678
+ ].join("\n");
679
+ /* eslint-enable prettier/prettier */
680
+ // ─────────────────────────────────────────────────────────────────────────────
681
+ // FRONT-END JS
682
+ //
683
+ // Stored as a plain Array.join() string — no TypeScript template literal so
684
+ // the browser-side ${...} expressions and backtick strings are never
685
+ // interpreted by the TypeScript compiler.
686
+ // ─────────────────────────────────────────────────────────────────────────────
687
+ /* eslint-disable prettier/prettier */
688
+ const REPORT_JS = [
689
+ "(function(){",
690
+ "'use strict';",
691
+ "var DATA=window.__SELA_DATA__;",
692
+ "function esc(s){return String(s==null?'':s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/\"/g,'&quot;');}",
693
+ "function fmt(iso){try{return new Date(iso).toLocaleString();}catch(e){return iso;}}",
694
+ "function fmtPct(v){return(typeof v==='number'&&isFinite(v))?v+'%':'n/a';}",
695
+ "function fmtMin(m){var h=Math.floor(m/60),r=m%60;return h>0?h+'h '+r+'m':m+'m';}",
696
+ "function $(id){return document.getElementById(id);}",
697
+ "function qsa(sel,root){return Array.from((root||document).querySelectorAll(sel));}",
698
+ // ── View toggle
699
+ "function setView(v){",
700
+ " document.body.classList.toggle('view-deepdive',v==='deepdive');",
701
+ " $('btn-overview').classList.toggle('active',v==='overview');",
702
+ " $('btn-deepdive').classList.toggle('active',v==='deepdive');",
703
+ "}",
704
+ "window.setView=setView;",
705
+ // ── Toast
706
+ "var toastTimer;",
707
+ "function toast(msg){var t=$('toast');t.textContent=msg;t.classList.remove('hidden');clearTimeout(toastTimer);toastTimer=setTimeout(function(){t.classList.add('hidden');},2600);}",
708
+ "window.copyJson=function(){navigator.clipboard&&navigator.clipboard.writeText(JSON.stringify(DATA,null,2)).then(function(){toast('JSON copied to clipboard');});};",
709
+ // ── Meta + KPIs
710
+ "var s=DATA.stats;",
711
+ "$('meta-gen').textContent='Generated '+fmt(DATA.generatedAt);",
712
+ "$('meta-cwd').textContent=DATA.cwd;",
713
+ "$('footer-cwd').textContent=DATA.cwd;",
714
+ "$('kpi-time').textContent=fmtMin(s.timeSavedMin);",
715
+ "$('kpi-healed').textContent=s.healed;",
716
+ "$('kpi-prot').textContent=s.protectedCnt;",
717
+ "$('kpi-fail').textContent=s.failed;",
718
+ "$('donut-total').textContent=s.total;",
719
+ "$('dd-count').textContent=DATA.events.length+' events';",
720
+ // ── Headline
721
+ "var headline=(function(){",
722
+ " if(s.healed===0&&s.protectedCnt===0&&s.failed===0)return['Sela was idle this run.','No broken selectors were encountered.'];",
723
+ " if(s.protectedCnt>0&&s.healed===0)return['Sela caught '+s.protectedCnt+' masked regression'+(s.protectedCnt!==1?'s':'')+'.',",
724
+ " 'The Intent Auditor blocked auto-fixes that would have silently changed test semantics.'];",
725
+ " 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':'')+'.',",
726
+ " 'Every fix was verified by the Intent Auditor. Two would-be fixes were masked regressions \u2014 drill in below.'];",
727
+ " if(s.healed>0)return['Sela healed '+s.healed+' selector'+(s.healed!==1?'s':'')+' automatically.',",
728
+ " 'Every fix was double-checked by the Intent Auditor before touching source code.'];",
729
+ " return['Sela couldn\u2019t heal '+s.failed+' selector'+(s.failed!==1?'s':'')+'.',",
730
+ " 'These need a human hand \u2014 context, candidates, and direct links are ready below.'];",
731
+ "})();",
732
+ "$('exec-h1').textContent=headline[0];",
733
+ "$('exec-sub').textContent=headline[1];",
734
+ // ── Donut chart
735
+ "var total=s.healed+s.protectedCnt+s.failed||1;",
736
+ "var donutCtx=$('donut-chart').getContext('2d');",
737
+ "new Chart(donutCtx,{type:'doughnut',",
738
+ " data:{labels:['Healed','Protected','Failed'],",
739
+ " datasets:[{data:[s.healed,s.protectedCnt,s.failed],",
740
+ " backgroundColor:['rgba(0,212,255,.8)','rgba(162,89,255,.8)','rgba(255,77,106,.8)'],",
741
+ " borderColor:['#00d4ff','#a259ff','#ff4d6a'],borderWidth:1.5,hoverOffset:6}]},",
742
+ " options:{responsive:true,maintainAspectRatio:false,cutout:'72%',",
743
+ " plugins:{legend:{display:false},tooltip:{callbacks:{label:function(c){return' '+c.label+': '+c.parsed;}}}},",
744
+ " onClick:function(e,els){if(!els.length)return;applyFilter(['HEALED','PROTECTED','FAILED'][els[0].index]);setView('deepdive');}",
745
+ " }",
746
+ "});",
747
+ // ── Donut legend
748
+ "var legendData=[",
749
+ " {filter:'ALL',swatch:'rgba(255,255,255,.3)',name:'All events',val:s.total,pct:null},",
750
+ " {filter:'HEALED',swatch:'var(--cyan)',name:'Healed',val:s.healed,pct:Math.round(s.healed/total*100)+'%'},",
751
+ " {filter:'PROTECTED',swatch:'var(--purple)',name:'Protected',val:s.protectedCnt,pct:Math.round(s.protectedCnt/total*100)+'%'},",
752
+ " {filter:'FAILED',swatch:'var(--red)',name:'Failed',val:s.failed,pct:Math.round(s.failed/total*100)+'%'},",
753
+ "];",
754
+ "var $legend=$('donut-legend');",
755
+ "$legend.innerHTML=legendData.map(function(d,i){",
756
+ " return '<div class=\"dl-row'+(i===0?' active':'')+'\" data-filter=\"'+d.filter+'\">'",
757
+ " +'<div class=\"dl-swatch\" style=\"background:'+d.swatch+'\"></div>'",
758
+ " +'<div class=\"dl-name\">'+d.name+'</div>'",
759
+ " +'<div class=\"dl-val\"'+(d.filter!=='ALL'?' style=\"color:'+d.swatch+'\"':'')+'>'+d.val+'</div>'",
760
+ " +(d.pct?'<div class=\"dl-pct\">'+d.pct+'</div>':'')",
761
+ " +'</div>';",
762
+ "}).join('');",
763
+ "$legend.addEventListener('click',function(e){",
764
+ " var row=e.target.closest('.dl-row');if(!row)return;",
765
+ " qsa('.dl-row',$legend).forEach(function(r){r.classList.remove('active');});",
766
+ " row.classList.add('active');",
767
+ " applyFilter(row.dataset.filter);",
768
+ " setView('deepdive');",
769
+ "});",
770
+ // ── Trend badge
771
+ "var runs=DATA.historical_runs||[];",
772
+ "if(runs.length>1){",
773
+ " var last=runs[runs.length-1],prev=runs[runs.length-2];",
774
+ " if(prev&&prev.time_saved_min>0){",
775
+ " var delta=Math.round((last.time_saved_min-prev.time_saved_min)/prev.time_saved_min*100);",
776
+ " var badge=$('trend-badge');",
777
+ " if(badge){",
778
+ " badge.textContent=(delta>=0?'+':'')+delta+'% vs prev run';",
779
+ " badge.style.color=delta>=0?'var(--green)':'var(--red)';",
780
+ " badge.style.borderColor=delta>=0?'rgba(0,229,160,.25)':'rgba(255,77,106,.25)';",
781
+ " badge.style.background=delta>=0?'var(--green-dim)':'var(--red-dim)';",
782
+ " }",
783
+ " }",
784
+ "}",
785
+ // ── Trend chart
786
+ "var trendLabels=runs.map(function(r){return r.label;});",
787
+ "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;});",
788
+ "var healedSeries=runs.map(function(r){return r.healed;});",
789
+ "var maxHealed=Math.max.apply(null,healedSeries.concat([1]));",
790
+ "var trendCtx=$('trend-chart').getContext('2d');",
791
+ "new Chart(trendCtx,{type:'line',",
792
+ " data:{labels:trendLabels,datasets:[",
793
+ " {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'},",
794
+ " {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'}",
795
+ " ]},",
796
+ " options:{responsive:true,maintainAspectRatio:false,",
797
+ " interaction:{mode:'index',intersect:false},",
798
+ " plugins:{legend:{display:false},",
799
+ " tooltip:{backgroundColor:'rgba(16,19,26,.95)',borderColor:'rgba(30,35,54,.8)',borderWidth:1,titleColor:'#dde2f0',bodyColor:'#8b93b0',padding:10,",
800
+ " callbacks:{label:function(c){return c.datasetIndex===0?' '+c.parsed.y.toFixed(1)+'h saved':' '+c.parsed.y+' healed';}}}},",
801
+ " scales:{",
802
+ " x:{grid:{color:'rgba(30,35,54,.6)',lineWidth:.5},ticks:{color:'#5a6180',font:{size:10},maxRotation:45}},",
803
+ " y:{position:'left',grid:{color:'rgba(30,35,54,.6)',lineWidth:.5},ticks:{color:'#5a6180',font:{size:10},callback:function(v){return v+'h';}}},",
804
+ " y1:{position:'right',grid:{display:false},ticks:{color:'#5a6180',font:{size:10}},max:maxHealed+2}",
805
+ " }",
806
+ " }",
807
+ "});",
808
+ // ── Historical table (last 5)
809
+ "var htbl=$('hist-table');",
810
+ "runs.slice().reverse().slice(0,5).forEach(function(r){",
811
+ " var row=document.createElement('div');row.className='hist-row';",
812
+ " row.innerHTML='<div class=\"hc\">'+esc(r.label)+'</div>'",
813
+ " +'<div class=\"hc\" style=\"color:var(--cyan)\">'+r.healed+'</div>'",
814
+ " +'<div class=\"hc\" style=\"color:var(--purple)\">'+(r.protected||0)+'</div>'",
815
+ " +'<div class=\"hc\" style=\"color:var(--red)\">'+r.failed+'</div>'",
816
+ " +'<div class=\"hc\">'+(r.avg_confidence||0)+'%</div>'",
817
+ " +'<div class=\"hc hist-saved\">'+fmtMin(r.time_saved_min)+'</div>';",
818
+ " htbl.appendChild(row);",
819
+ "});",
820
+ // ── VS Code link
821
+ "function vscodeLink(file,line){",
822
+ " if(!file)return'#';",
823
+ " var abs=(file.startsWith('/')||/^[A-Za-z]:/.test(file))?file:DATA.cwd+'/'+file;",
824
+ " return'vscode://file/'+encodeURI(abs).replace(/#/g,'%23')+(line?':'+line:'');",
825
+ "}",
826
+ // ── Confidence badge
827
+ "function confBadge(pct){",
828
+ " if(typeof pct!=='number'||!isFinite(pct))return'';",
829
+ " var cls=pct>=80?'conf-high':pct>=60?'conf-mid':'conf-low';",
830
+ " return'<span class=\"conf-badge '+cls+'\">'+pct+'%</span>';",
831
+ "}",
832
+ // ── Diff renderer
833
+ "function renderDiff(ev){",
834
+ " if(ev.kind==='PROTECTED')",
835
+ " 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>';",
836
+ " if(ev.kind==='FAILED')",
837
+ " 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>';",
838
+ " var raw=ev.gitUnifiedDiff||'';",
839
+ " var skipPfx=['diff ','index ','--- ','+++','new file','deleted','similarity','rename','Binary'];",
840
+ " var hunkRe=/^@@ -(\\d+)(?:,\\d+)? \\+(\\d+)(?:,\\d+)? @@/;",
841
+ " var body='',newLn=0,hasDiff=raw.split(/\\r?\\n/).some(function(l){return hunkRe.test(l);});",
842
+ " if(hasDiff){",
843
+ " raw.split(/\\r?\\n/).forEach(function(r){",
844
+ " if(skipPfx.some(function(p){return r.startsWith(p);}))return;",
845
+ " var hm=r.match(hunkRe);",
846
+ ' 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;}',
847
+ " if(!r.length||r.startsWith('\\\\'))return;",
848
+ " var h=r[0],t=r.slice(1);",
849
+ " 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++;}",
850
+ ' 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>\';}',
851
+ " 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++;}",
852
+ " });",
853
+ " }else{",
854
+ " 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>'",
855
+ " +'<div class=\"diff-line dl-add\"><span class=\"gutter\">+</span><span class=\"ln\">'+(ev.newLineNumber||'')+'</span><span class=\"code\">'+esc(ev.newCodeLine||'')+'</span></div>';",
856
+ " }",
857
+ " 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>';",
858
+ "}",
859
+ // ── Auditor renderer
860
+ "function renderAuditor(a){",
861
+ " if(!a)return'<div class=\"muted-block\">Intent Auditor was not consulted for this event.</div>';",
862
+ " var vl=a.verdict.toLowerCase();",
863
+ " var hasPct=typeof a.confidence==='number'&&isFinite(a.confidence);",
864
+ " var pct=hasPct?Math.max(0,Math.min(100,a.confidence)):0;",
865
+ " var circ=2*Math.PI*50,dash=hasPct?(pct/100)*circ:0;",
866
+ " var adj=[];",
867
+ " if(a.penalty)adj.push('<span style=\"color:var(--red)\">'+a.penalty+'</span>');",
868
+ " if(a.bonus)adj.push('<span style=\"color:var(--green)\">+'+a.bonus+'</span>');",
869
+ " return'<div class=\"auditor-wrap\">'",
870
+ " +'<div class=\"gauge-ring\">'",
871
+ ' +\'<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>\'',
872
+ " +'<div class=\"gauge-center\"><div class=\"gauge-pct pct-'+vl+'\">'+(hasPct?pct+'%':'n/a')+'</div><div class=\"gauge-sub\">consistency</div></div>'",
873
+ " +'</div>'",
874
+ " +'<div class=\"audit-details\">'",
875
+ ' +\'<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>\'',
876
+ ' +(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>\':\'\')',
877
+ " +(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>':'')",
878
+ " +'<div class=\"ad-reason\">'+esc(a.reason)+'</div>'",
879
+ " +'</div></div>';",
880
+ "}",
881
+ // ── CoT renderer
882
+ "function renderCoT(steps){",
883
+ " if(!steps||!steps.length)return'<div class=\"muted-block\">No reasoning trace captured.</div>';",
884
+ " 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>';",
885
+ "}",
886
+ // ── DNA table
887
+ "function renderDna(before,after){",
888
+ " if(!before)return'<div class=\"muted-block\">No DNA baseline available.</div>';",
889
+ " var rows=[];",
890
+ " function row(field,ov,nv){",
891
+ " var os=ov==null?'':Array.isArray(ov)?ov.join(' / '):String(ov);",
892
+ " var ns=nv==null?'':Array.isArray(nv)?nv.join(' / '):String(nv);",
893
+ " var same=os===ns,hasAfter=!!after;",
894
+ " var cls=!hasAfter?'missing':same?'matched':'changed';",
895
+ " var icon=!hasAfter?'\u2014':same?'\u2713':'\u25b3';",
896
+ " var vc;",
897
+ " if(!hasAfter||same)vc='<td class=\"val\">'+esc(os||'\u2014')+'</td>';",
898
+ " else vc='<td class=\"val\"><span class=\"val old\">'+esc(os||'\u2014')+'</span> \u2192 <span class=\"val new\">'+esc(ns||'\u2014')+'</span></td>';",
899
+ " rows.push('<tr class=\"'+cls+'\"><td class=\"field\">'+esc(field)+'</td>'+vc+'<td class=\"icon\">'+icon+'</td></tr>');",
900
+ " }",
901
+ " row('Tag',before.tagName,after&&after.tagName);",
902
+ " row('Text',before.text,after&&after.text);",
903
+ " row('Role',before.role,after&&after.role);",
904
+ " row('Classes',before.classes,after&&after.classes);",
905
+ " row('Ancestry',before.ancestry,after&&after.ancestry);",
906
+ " row('Closest label',before.closestLabel,after&&after.closestLabel);",
907
+ " var ab=Object.entries(before.attributes||{}).map(function(e){return e[0]+'='+e[1];}).join(' \xb7 ');",
908
+ " var aa=after?Object.entries(after.attributes||{}).map(function(e){return e[0]+'='+e[1];}).join(' \xb7 '):null;",
909
+ " row('Attributes',ab,aa);",
910
+ " return'<table class=\"dna-table\"><thead><tr><th>Field</th><th>Value</th><th></th></tr></thead><tbody>'+rows.join('')+'</tbody></table>';",
911
+ "}",
912
+ // ── Screenshot
913
+ "function renderScreenshot(ev){",
914
+ " if(ev.kind!=='FAILED'||!ev.failureScreenshotPng)return'';",
915
+ ' return\'<div class="body-section"><div class="bs-title"><span class="bst-dot"></span>Playwright Failure Screenshot</div>\'',
916
+ ' +\'<div class="fail-img-wrap"><img src="data:image/png;base64,\'+ev.failureScreenshotPng+\'" alt="Failure screenshot" loading="lazy"/>\'',
917
+ " +'<div class=\"fail-img-meta\">captured at test failure \xb7 base64-embedded</div></div></div>';",
918
+ "}",
919
+ // ── Card body
920
+ "function renderCardBody(ev){",
921
+ " var steps=ev.reasoningSteps||[];",
922
+ " if(ev.kind==='PROTECTED'&&!steps.length){",
923
+ " steps=['AI proposed a replacement selector for the broken locator.'];",
924
+ " if(ev.aiExplanation)steps.push('Reasoning: '+ev.aiExplanation);",
925
+ " if(ev.candidateNewSelector)steps.push('Candidate: '+ev.candidateNewSelector+' \u2014 but semantics changed.');",
926
+ " if(ev.auditor)steps.push('Intent Auditor: '+ev.auditor.verdict+' \u2014 '+ev.auditor.reason.slice(0,160));",
927
+ " steps.push('Sela blocked the auto-fix. Source code is untouched.');",
928
+ " }",
929
+ " if(ev.kind==='FAILED'&&!steps.length){",
930
+ " steps=['Locator failed to resolve at runtime \u2014 Sela attempted to heal it.'];",
931
+ " if(ev.aiExplanation)steps.push('AI: '+ev.aiExplanation);",
932
+ " steps.push('Reason: '+(ev.reason||'unknown'));",
933
+ " steps.push('Open the file in your editor to inspect the original locator.');",
934
+ " }",
935
+ " 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);",
936
+ " var link=vscodeLink(ev.sourceFile,ev.sourceLine);",
937
+ " var actions='';",
938
+ " if(ev.kind==='HEALED'){",
939
+ " actions='<div class=\"actions\">'",
940
+ " +'<button class=\"btn btn-primary\" data-action=\"keep\" data-id=\"'+ev.stableId+'\" onclick=\"handleAction(this,\\'keep\\',\\''+ev.stableId+'\\')\">\u2713 Keep changes</button>'",
941
+ " +'<button class=\"btn btn-danger\" onclick=\"handleAction(this,\\'rollback\\',\\''+ev.stableId+'\\')\">&#x21B6; Rollback</button>'",
942
+ " +'<a class=\"btn\" href=\"'+link+'\">&#x2317; Open in VS Code</a>'",
943
+ ' +\'<a class="btn" href="\'+issueUrl+\'" target="_blank" rel="noopener">&#x2691; Report issue</a>\'',
944
+ " +'</div>';",
945
+ " }else if(ev.kind==='PROTECTED'){",
946
+ " actions='<div class=\"actions\">'",
947
+ " +'<a class=\"btn\" href=\"'+link+'\">\u2192 Open in VS Code</a>'",
948
+ " +'<button class=\"btn btn-primary\" onclick=\"handleAction(this,\\'ack\\',\\''+ev.stableId+'\\')\">&#x2713; Acknowledged \u2014 will investigate</button>'",
949
+ ' +\'<a class="btn" href="\'+issueUrl+\'" target="_blank" rel="noopener">&#x2691; File regression</a>\'',
950
+ " +'</div>';",
951
+ " }else{",
952
+ " actions='<div class=\"actions\">'",
953
+ " +'<a class=\"btn btn-primary\" href=\"'+link+'\">\u2192 Open in VS Code</a>'",
954
+ ' +\'<a class="btn" href="\'+issueUrl+\'" target="_blank" rel="noopener">&#x2691; Report issue</a>\'',
955
+ " +'</div>';",
956
+ " }",
957
+ " var alts=(ev.kind==='HEALED'&&ev.aiAlternatives&&ev.aiAlternatives.length)",
958
+ ' ?\'<div class="body-section"><div class="bs-title"><span class="bst-dot"></span>Alternative selectors considered</div><div class="alts">\'',
959
+ " +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('')",
960
+ " +'</div></div>'",
961
+ " :'';",
962
+ ' return\'<div class="row2" style="margin-top:16px">\'',
963
+ " +'<div><div class=\"bs-title\"><span class=\"bst-dot\"></span>AI Reasoning \xb7 Chain of Thought</div>'+renderCoT(steps)+'</div>'",
964
+ " +'<div><div class=\"bs-title\"><span class=\"bst-dot\"></span>Intent Auditor</div>'+renderAuditor(ev.auditor||null)+'</div>'",
965
+ " +'</div>'",
966
+ ' +\'<div class="body-section"><div class="bs-title"><span class="bst-dot"></span>Source Code Diff</div>\'+renderDiff(ev)+\'</div>\'',
967
+ ' +\'<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>\'',
968
+ " +renderScreenshot(ev)",
969
+ " +alts",
970
+ " +actions;",
971
+ "}",
972
+ // ── Event card
973
+ "function renderCard(ev,idx){",
974
+ " var kindCls=ev.kind==='HEALED'?'ec-healed':ev.kind==='PROTECTED'?'ec-protected':'ec-failed';",
975
+ " var badgeCls=ev.kind==='HEALED'?'sb-healed':ev.kind==='PROTECTED'?'sb-protected':'sb-failed';",
976
+ " var badgeTxt=ev.kind==='HEALED'?'Healed':ev.kind==='PROTECTED'?'Protected \xb7 Bug Caught':'Failed';",
977
+ " var sel=ev.kind==='HEALED'?(ev.newSelector||ev.oldSelector):ev.oldSelector;",
978
+ " var link=vscodeLink(ev.sourceFile,ev.sourceLine);",
979
+ " var tags='';",
980
+ " if(ev.inIframe)tags+='<span class=\"ecard-tag t-iframe\">iframe</span>';",
981
+ " if(ev.inShadowDom)tags+='<span class=\"ecard-tag t-shadow\">shadow DOM</span>';",
982
+ " if(ev.dryRun)tags+='<span class=\"ecard-tag t-dryrun\">dry run</span>';",
983
+ " var haystack=(ev.oldSelector+' '+(ev.newSelector||'')+' '+ev.sourceFile+' '+(ev.testTitle||'')+' '+ev.stableId).toLowerCase();",
984
+ " return'<article class=\"ecard '+kindCls+'\" data-kind=\"'+ev.kind+'\" data-haystack=\"'+esc(haystack)+'\" style=\"animation-delay:'+(idx*0.04)+'s\">'",
985
+ " +'<div class=\"ecard-head\" onclick=\"this.parentElement.classList.toggle(\\'open\\')\">'",
986
+ " +'<span class=\"status-badge '+badgeCls+'\"><span class=\"sb-dot\"></span>'+badgeTxt+'</span>'",
987
+ " +'<div class=\"ecard-title\">'",
988
+ " +'<div class=\"ecard-selector\">'+esc(sel)+'</div>'",
989
+ " +'<div class=\"ecard-meta\">'+(ev.testTitle?esc(ev.testTitle)+' &nbsp;\xb7&nbsp; ':'')",
990
+ " +'<a href=\"'+link+'\">'+esc(ev.sourceFile)+':'+ev.sourceLine+'</a>'+tags+' &nbsp;\xb7&nbsp; '+fmt(ev.timestamp)+'</div>'",
991
+ " +'</div>'",
992
+ " +confBadge(ev.aiConfidence)",
993
+ ' +\'<svg class="chev" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2"><polyline points="5 3 11 8 5 13"/></svg>\'',
994
+ " +'</div>'",
995
+ " +'<div class=\"ecard-body\">'+renderCardBody(ev)+'</div>'",
996
+ " +'</article>';",
997
+ "}",
998
+ // ── Mount events
999
+ "var $el=$('events-list');",
1000
+ "$el.innerHTML=DATA.events.map(renderCard).join('');",
1001
+ "var firstProt=$el.querySelector('.ec-protected');",
1002
+ "if(firstProt)firstProt.classList.add('open');",
1003
+ "else{var f=$el.querySelector('.ecard');if(f)f.classList.add('open');}",
1004
+ // ── Filter + Search
1005
+ "var curFilter='ALL',searchTerm='';",
1006
+ "function applyFilter(f){",
1007
+ " curFilter=f;",
1008
+ " qsa('.chip').forEach(function(c){c.classList.toggle('on',c.dataset.filter===f||(f==='ALL'&&c.dataset.filter==='ALL'));});",
1009
+ " filterCards();",
1010
+ "}",
1011
+ "window.applyFilter=applyFilter;",
1012
+ "function filterCards(){",
1013
+ " var vis=0;",
1014
+ " qsa('.ecard',$el).forEach(function(card){",
1015
+ " var show=(curFilter==='ALL'||card.dataset.kind===curFilter)&&(!searchTerm||card.dataset.haystack.includes(searchTerm));",
1016
+ " card.style.display=show?'':'none';",
1017
+ " if(show)vis++;",
1018
+ " });",
1019
+ " $('empty-state').classList.toggle('hidden',vis>0||DATA.events.length===0);",
1020
+ "}",
1021
+ "var fbChips=document.querySelector('.fb-chips');",
1022
+ "if(fbChips)fbChips.addEventListener('click',function(e){var chip=e.target.closest('.chip');if(chip&&chip.dataset.filter)applyFilter(chip.dataset.filter);});",
1023
+ "$('search-input').addEventListener('input',function(e){searchTerm=e.target.value.toLowerCase().trim();filterCards();});",
1024
+ // ── Actions
1025
+ "window.handleAction=function(btn,action,id){",
1026
+ " if(action==='keep'||action==='ack'){btn.dataset.done='true';btn.textContent=action==='keep'?'\u2713 Approved':'\u2713 Acknowledged';toast('Decision saved for '+id);}",
1027
+ " else if(action==='rollback'){var cmd='npx sela rollback --id '+id;navigator.clipboard&&navigator.clipboard.writeText(cmd).then(function(){toast('Copied: '+cmd);});}",
1028
+ "};",
1029
+ "window.copyText=function(txt,btn){",
1030
+ " navigator.clipboard&&navigator.clipboard.writeText(txt).then(function(){btn.textContent='Copied!';setTimeout(function(){btn.textContent='Copy';},1500);});",
1031
+ "};",
1032
+ // ── Keyboard shortcuts
1033
+ "document.addEventListener('keydown',function(e){",
1034
+ " if(e.key==='/'&&document.activeElement!==$('search-input')){e.preventDefault();$('search-input').focus();setView('deepdive');}",
1035
+ " if(e.key==='Escape')$('search-input').blur();",
1036
+ "});",
1037
+ "})();",
1038
+ ].join("\n");
1039
+ /* eslint-enable prettier/prettier */