@bughunters/vision 1.0.0

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,1039 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.BugHuntersVisionReporter = void 0;
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ const check_1 = require("./check");
40
+ // ─── CSS ─────────────────────────────────────────────────────────────────────
41
+ function getCss() {
42
+ return `
43
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
44
+
45
+ :root {
46
+ --bg: #f4f4f5;
47
+ --surface: #ffffff;
48
+ --surface-2: #f8f8f9;
49
+ --border: #e4e4e7;
50
+ --text: #18181b;
51
+ --text-2: #71717a;
52
+ --text-3: #a1a1aa;
53
+ --pass: #16a34a;
54
+ --pass-soft: #f0fdf4;
55
+ --pass-border: #bbf7d0;
56
+ --fail: #dc2626;
57
+ --fail-soft: #fef2f2;
58
+ --fail-border: #fca5a5;
59
+ --base: #2563eb;
60
+ --base-soft: #eff6ff;
61
+ --base-border: #bfdbfe;
62
+ --accent: #6366f1;
63
+ --accent-soft: #eef2ff;
64
+ --accent-border: rgba(99,102,241,.3);
65
+ --pixel: #d97706;
66
+ --pixel-soft: #fffbeb;
67
+ --pixel-border: #fde68a;
68
+ --radius: 8px;
69
+ --shadow: 0 1px 2px rgba(0,0,0,.06), 0 1px 3px rgba(0,0,0,.08);
70
+ --shadow-sm: 0 1px 2px rgba(0,0,0,.05);
71
+ }
72
+
73
+ [data-theme="dark"] {
74
+ --bg: #09090b;
75
+ --surface: #111113;
76
+ --surface-2: #18181b;
77
+ --border: #27272a;
78
+ --text: #fafafa;
79
+ --text-2: #a1a1aa;
80
+ --text-3: #52525b;
81
+ --pass: #4ade80;
82
+ --pass-soft: rgba(74,222,128,.07);
83
+ --pass-border: rgba(74,222,128,.25);
84
+ --fail: #f87171;
85
+ --fail-soft: rgba(248,113,113,.07);
86
+ --fail-border: rgba(248,113,113,.25);
87
+ --base: #60a5fa;
88
+ --base-soft: rgba(96,165,250,.07);
89
+ --base-border: rgba(96,165,250,.25);
90
+ --accent: #818cf8;
91
+ --accent-soft: rgba(129,140,248,.1);
92
+ --accent-border: rgba(129,140,248,.3);
93
+ --pixel: #fbbf24;
94
+ --pixel-soft: rgba(251,191,36,.1);
95
+ --pixel-border: rgba(251,191,36,.3);
96
+ --shadow: 0 1px 3px rgba(0,0,0,.4);
97
+ --shadow-sm: 0 1px 2px rgba(0,0,0,.3);
98
+ }
99
+
100
+ html { scroll-behavior: smooth; }
101
+
102
+ body {
103
+ font-family: -apple-system, BlinkMacSystemFont, 'Inter', 'Segoe UI', system-ui, sans-serif;
104
+ font-size: 13px;
105
+ line-height: 1.5;
106
+ background: var(--bg);
107
+ color: var(--text);
108
+ min-height: 100vh;
109
+ -webkit-font-smoothing: antialiased;
110
+ }
111
+
112
+ /* ── Layout ── */
113
+ .wrap { max-width: 900px; margin: 0 auto; padding: 0 20px; }
114
+
115
+ /* ── Header ── */
116
+ .hdr {
117
+ background: var(--surface);
118
+ border-bottom: 1px solid var(--border);
119
+ padding: 11px 0;
120
+ position: sticky; top: 0; z-index: 50;
121
+ box-shadow: var(--shadow-sm);
122
+ }
123
+ .hdr-inner { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
124
+ .logo { display: flex; align-items: center; gap: 7px; margin-right: auto; }
125
+ .logo-bug { font-size: 17px; line-height: 1; }
126
+ .logo-name { font-size: 14px; font-weight: 700; color: var(--accent); letter-spacing: -.3px; }
127
+ .run-at { font-size: 11px; color: var(--text-3); padding: 0 2px; }
128
+
129
+ /* theme toggle */
130
+ .t-toggle { display: flex; background: var(--surface-2); border: 1px solid var(--border); border-radius: 6px; padding: 2px; gap: 1px; }
131
+ .t-btn {
132
+ background: none; border: none; cursor: pointer;
133
+ padding: 4px 9px; border-radius: 4px;
134
+ font-size: 11px; font-family: inherit; color: var(--text-2);
135
+ transition: background .12s, color .12s;
136
+ white-space: nowrap;
137
+ }
138
+ .t-btn:hover { color: var(--text); }
139
+ .t-btn.on { background: var(--surface); color: var(--text); box-shadow: var(--shadow-sm); }
140
+
141
+ /* ── Summary bar ── */
142
+ .summary { display: flex; gap: 10px; margin: 22px 0 16px; flex-wrap: wrap; }
143
+ .stat {
144
+ background: var(--surface); border: 1px solid var(--border);
145
+ border-radius: var(--radius); padding: 14px 18px; min-width: 88px;
146
+ box-shadow: var(--shadow-sm);
147
+ }
148
+ .stat-lbl { font-size: 10px; text-transform: uppercase; letter-spacing: .9px; color: var(--text-3); font-weight: 500; margin-bottom: 7px; }
149
+ .stat-val { font-size: 28px; font-weight: 700; line-height: 1; color: var(--text); }
150
+ .stat.s-pass .stat-val { color: var(--pass); }
151
+ .stat.s-fail .stat-val { color: var(--fail); }
152
+ .stat.s-base .stat-val { color: var(--base); }
153
+ .stat.s-warn .stat-val { color: var(--pixel); }
154
+ .stat.s-neutral .stat-val { color: var(--text-3); }
155
+
156
+ /* ── Controls ── */
157
+ .controls { display: flex; align-items: center; gap: 8px; margin-bottom: 14px; flex-wrap: wrap; }
158
+
159
+ .f-group { display: flex; background: var(--surface-2); border: 1px solid var(--border); border-radius: 6px; padding: 2px; gap: 1px; }
160
+ .f-btn {
161
+ background: none; border: none; cursor: pointer;
162
+ padding: 5px 11px; border-radius: 4px;
163
+ font-size: 11px; font-family: inherit; color: var(--text-2);
164
+ transition: all .12s; white-space: nowrap;
165
+ }
166
+ .f-btn:hover { color: var(--text); }
167
+ .f-btn.on { background: var(--surface); color: var(--text); box-shadow: var(--shadow-sm); }
168
+
169
+ /* image toggle */
170
+ .img-btn {
171
+ display: flex; align-items: center; gap: 7px;
172
+ background: var(--surface); border: 1px solid var(--border);
173
+ border-radius: 6px; padding: 5px 12px; cursor: pointer;
174
+ font-size: 11px; font-family: inherit; color: var(--text-2);
175
+ transition: all .15s; white-space: nowrap;
176
+ }
177
+ .img-btn:hover { border-color: var(--text-3); color: var(--text); }
178
+ .img-btn.on { border-color: var(--accent); color: var(--accent); background: var(--accent-soft); }
179
+
180
+ .sw-track {
181
+ width: 26px; height: 14px; border-radius: 7px;
182
+ background: var(--border); position: relative; flex-shrink: 0;
183
+ transition: background .2s;
184
+ }
185
+ .img-btn.on .sw-track { background: var(--accent); }
186
+ .sw-thumb {
187
+ width: 10px; height: 10px; border-radius: 50%;
188
+ background: #fff; position: absolute; top: 2px; left: 2px;
189
+ transition: transform .2s; box-shadow: 0 1px 2px rgba(0,0,0,.2);
190
+ }
191
+ .img-btn.on .sw-thumb { transform: translateX(12px); }
192
+
193
+ /* ── Test list ── */
194
+ .test-list { display: flex; flex-direction: column; gap: 6px; padding-bottom: 48px; }
195
+ .test-item { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; box-shadow: var(--shadow-sm); }
196
+ .test-item.s-fail { border-left: 3px solid var(--fail); }
197
+ .test-item.s-base { border-left: 3px solid var(--base); }
198
+ .test-item.s-warn { border-left: 3px solid var(--pixel); }
199
+
200
+ .test-row {
201
+ display: flex; align-items: center; gap: 11px;
202
+ padding: 12px 14px; cursor: pointer;
203
+ transition: background .1s; user-select: none;
204
+ }
205
+ .test-row:hover { background: var(--surface-2); }
206
+
207
+ .s-dot {
208
+ width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0;
209
+ margin-top: 1px;
210
+ }
211
+ .s-pass .s-dot { background: var(--pass); }
212
+ .s-fail .s-dot { background: var(--fail); }
213
+ .s-base .s-dot { background: var(--base); }
214
+ .s-warn .s-dot { background: var(--pixel); }
215
+
216
+ .test-meta { flex: 1; min-width: 0; }
217
+ .test-name {
218
+ font-family: ui-monospace, 'SFMono-Regular', 'Fira Code', monospace;
219
+ font-size: 12.5px; font-weight: 600; color: var(--text);
220
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
221
+ }
222
+ .test-snip {
223
+ font-size: 11.5px; color: var(--text-3); margin-top: 2px;
224
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
225
+ }
226
+
227
+ .badges { display: flex; gap: 5px; align-items: center; flex-shrink: 0; }
228
+ .badge {
229
+ font-size: 10px; font-weight: 600; padding: 2px 7px; border-radius: 4px;
230
+ letter-spacing: .4px; font-family: ui-monospace, monospace; white-space: nowrap;
231
+ }
232
+ .badge.m-method { background: var(--surface-2); color: var(--text-3); border: 1px solid var(--border); }
233
+ .badge.m-ai { background: var(--accent-soft); color: var(--accent); border: 1px solid var(--accent-border); }
234
+ .badge.m-warn { background: var(--pixel-soft); color: var(--pixel); border: 1px solid var(--pixel-border); }
235
+ .badge.m-pixel { background: var(--pixel-soft); color: var(--pixel); border: 1px solid var(--pixel-border); }
236
+ .badge.m-pass { background: var(--pass-soft); color: var(--pass); border: 1px solid var(--pass-border); }
237
+ .badge.m-fail { background: var(--fail-soft); color: var(--fail); border: 1px solid var(--fail-border); }
238
+ .badge.m-base { background: var(--base-soft); color: var(--base); border: 1px solid var(--base-border); }
239
+
240
+ .chev { font-size: 16px; color: var(--text-3); flex-shrink: 0; line-height: 1; transition: transform .2s; display: inline-block; }
241
+ .chev.open { transform: rotate(90deg); }
242
+
243
+ /* ── Detail panel ── */
244
+ .test-detail { border-top: 1px solid var(--border); padding: 18px 14px 18px 33px; display: none; }
245
+
246
+ .verdict {
247
+ background: var(--accent-soft); border-left: 3px solid var(--accent);
248
+ border-radius: 0 6px 6px 0; padding: 12px 14px; margin-bottom: 18px;
249
+ }
250
+ .verdict-lbl { font-size: 10px; text-transform: uppercase; letter-spacing: 1.8px; color: var(--accent); font-weight: 600; margin-bottom: 7px; }
251
+ .verdict-txt { font-size: 12.5px; color: var(--text-2); line-height: 1.75; }
252
+
253
+ /* ── Visual comparison tabs ── */
254
+ .view-tabs {
255
+ display: flex; gap: 2px; margin-bottom: 12px;
256
+ background: var(--surface-2); border: 1px solid var(--border);
257
+ border-radius: 6px; padding: 2px; width: fit-content;
258
+ }
259
+ .view-tab {
260
+ background: none; border: none; cursor: pointer;
261
+ padding: 4px 13px; border-radius: 4px;
262
+ font-size: 11px; font-family: inherit; font-weight: 500;
263
+ color: var(--text-2); transition: all .12s; white-space: nowrap;
264
+ }
265
+ .view-tab:hover { color: var(--text); }
266
+ .view-tab.on { background: var(--surface); color: var(--text); box-shadow: var(--shadow-sm); }
267
+ .view-tab.diff-tab.on { color: var(--fail); }
268
+
269
+ /* ── Image views ── */
270
+ .img-section { }
271
+ .img-lbl { font-size: 10px; text-transform: uppercase; letter-spacing: 1.2px; color: var(--text-3); font-weight: 500; margin-bottom: 12px; }
272
+ .img-view { }
273
+ .view-img {
274
+ width: 100%; display: block; border-radius: 6px; border: 1px solid var(--border);
275
+ max-height: 360px; object-fit: contain; object-position: top center;
276
+ background: var(--surface-2); cursor: zoom-in;
277
+ transition: border-color .15s;
278
+ }
279
+ .view-img:hover { border-color: var(--text-3); }
280
+ canvas.view-img { image-rendering: pixelated; }
281
+
282
+ .diff-placeholder {
283
+ padding: 28px 20px; text-align: center;
284
+ color: var(--text-3); font-size: 12px;
285
+ background: var(--surface-2); border-radius: 6px;
286
+ border: 1px dashed var(--border);
287
+ }
288
+ .diff-generating {
289
+ padding: 28px 20px; text-align: center;
290
+ color: var(--text-3); font-size: 12px;
291
+ background: var(--surface-2); border-radius: 6px;
292
+ border: 1px solid var(--border);
293
+ }
294
+ .diff-unavailable {
295
+ padding: 18px 20px; font-size: 11.5px; line-height: 1.8;
296
+ color: var(--text-2); background: var(--surface-2);
297
+ border-radius: 6px; border: 1px solid var(--border);
298
+ }
299
+ .diff-unavailable code {
300
+ font-family: ui-monospace, monospace; font-size: 10.5px;
301
+ background: var(--bg); padding: 1px 5px; border-radius: 3px;
302
+ border: 1px solid var(--border);
303
+ }
304
+
305
+ /* pass-images: hidden by default, shown via body class */
306
+ .pass-imgs { display: none; }
307
+ body.show-pass .pass-imgs { display: block; }
308
+
309
+ /* ── Lightbox ── */
310
+ .lb-overlay {
311
+ position: fixed; inset: 0; z-index: 9000;
312
+ background: rgba(0,0,0,.93);
313
+ display: flex; align-items: center; justify-content: center;
314
+ opacity: 0; pointer-events: none;
315
+ transition: opacity .18s ease;
316
+ }
317
+ .lb-overlay.open { opacity: 1; pointer-events: all; }
318
+
319
+ .lb-inner {
320
+ position: absolute; inset: 0;
321
+ display: flex; align-items: center; justify-content: center;
322
+ overflow: hidden;
323
+ }
324
+ .lb-img-wrap {
325
+ display: flex; align-items: center; justify-content: center;
326
+ transform-origin: center center;
327
+ cursor: grab; user-select: none;
328
+ will-change: transform;
329
+ }
330
+ .lb-img-wrap.dragging { cursor: grabbing; }
331
+ .lb-img-wrap img,
332
+ .lb-img-wrap canvas {
333
+ display: block;
334
+ max-width: none;
335
+ pointer-events: none;
336
+ border-radius: 3px;
337
+ }
338
+
339
+ .lb-close {
340
+ position: absolute; top: 16px; right: 20px; z-index: 10;
341
+ width: 38px; height: 38px; border-radius: 50%;
342
+ background: rgba(255,255,255,.1); border: 1px solid rgba(255,255,255,.18);
343
+ color: #fff; font-size: 20px; line-height: 1;
344
+ cursor: pointer; display: flex; align-items: center; justify-content: center;
345
+ transition: background .15s; backdrop-filter: blur(8px);
346
+ }
347
+ .lb-close:hover { background: rgba(255,255,255,.22); }
348
+
349
+ .lb-zoom-label {
350
+ position: absolute; top: 20px; left: 20px; z-index: 10;
351
+ font-size: 11px; font-weight: 600; color: rgba(255,255,255,.7);
352
+ background: rgba(0,0,0,.45); padding: 3px 9px; border-radius: 4px;
353
+ backdrop-filter: blur(8px); pointer-events: none;
354
+ font-family: ui-monospace, monospace;
355
+ }
356
+ .lb-hint {
357
+ position: absolute; bottom: 18px; left: 50%; transform: translateX(-50%);
358
+ font-size: 11px; color: rgba(255,255,255,.32); pointer-events: none;
359
+ white-space: nowrap; letter-spacing: .2px;
360
+ }
361
+
362
+ /* ── Footer ── */
363
+ .ftr { border-top: 1px solid var(--border); padding: 18px 0; text-align: center; font-size: 11px; color: var(--text-3); }
364
+ .ftr strong { color: var(--accent); font-weight: 600; }
365
+
366
+ /* ── Responsive ── */
367
+ @media (max-width: 560px) {
368
+ .stat-val { font-size: 22px; }
369
+ .badges { display: none; }
370
+ .run-at { display: none; }
371
+ .lb-hint { display: none; }
372
+ }
373
+ `.trim();
374
+ }
375
+ // ─── JS App ──────────────────────────────────────────────────────────────────
376
+ // IMPORTANT: No ${...} template literals inside this string — TypeScript would interpolate them.
377
+ // Use string concatenation and unicode escapes instead.
378
+ function getJs() {
379
+ return `
380
+ (function () {
381
+ 'use strict';
382
+
383
+ var RESULTS = JSON.parse(document.getElementById('bv-r').textContent);
384
+ var META = JSON.parse(document.getElementById('bv-m').textContent);
385
+
386
+ /* ── State ── */
387
+ var state = {
388
+ theme: localStorage.getItem('bv-theme') || 'system',
389
+ showPassImg: localStorage.getItem('bv-pass-img') === 'true'
390
+ };
391
+
392
+ /* ── Lightbox state ── */
393
+ var lb = {
394
+ overlay: null, inner: null, wrap: null, zoomLbl: null,
395
+ scale: 1, tx: 0, ty: 0,
396
+ minScale: 0.05, maxScale: 20,
397
+ dragging: false, startX: 0, startY: 0, startTx: 0, startTy: 0,
398
+ pinchDist: 0, pinchScale: 1
399
+ };
400
+
401
+ /* ── Utils ── */
402
+ function esc(s) {
403
+ if (s == null) return '';
404
+ return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
405
+ }
406
+ function trunc(s, n) { return s.length > n ? s.slice(0, n) + '\\u2026' : s; }
407
+ function fmtDate(iso) {
408
+ return new Date(iso).toLocaleString(undefined, {
409
+ weekday:'short', year:'numeric', month:'short', day:'numeric',
410
+ hour:'2-digit', minute:'2-digit', second:'2-digit'
411
+ });
412
+ }
413
+
414
+ /* ── Theme ── */
415
+ function resolveTheme() {
416
+ return state.theme === 'system'
417
+ ? (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')
418
+ : state.theme;
419
+ }
420
+ function applyTheme() {
421
+ document.documentElement.setAttribute('data-theme', resolveTheme());
422
+ }
423
+
424
+ /* ── Pixel diff engine (100% client-side, zero server calls) ── */
425
+
426
+ /* Weighted squared RGB distance — perceptually accurate, fast (no sqrt needed) */
427
+ function colorDist(r1, g1, b1, r2, g2, b2) {
428
+ var rd = r1 - r2, gd = g1 - g2, bd = b1 - b2;
429
+ return rd * rd * 0.299 + gd * gd * 0.587 + bd * bd * 0.114;
430
+ }
431
+
432
+ /* Render diff image: changed pixels = neon red, unchanged = desaturated baseline */
433
+ function renderDiff(d1, d2, w, h) {
434
+ var n = w * h * 4;
435
+ var out = new Uint8ClampedArray(n);
436
+ var thr = 900; /* squared perceptual threshold */
437
+ for (var i = 0; i < n; i += 4) {
438
+ if (colorDist(d1[i], d1[i+1], d1[i+2], d2[i], d2[i+1], d2[i+2]) > thr) {
439
+ /* neon red highlight */
440
+ out[i] = 220; out[i+1] = 38; out[i+2] = 38; out[i+3] = 255;
441
+ } else {
442
+ /* desaturated baseline — guides the eye to red pixels */
443
+ var lum = (d1[i] * 77 + d1[i+1] * 150 + d1[i+2] * 29) >> 8;
444
+ out[i] = lum; out[i+1] = lum; out[i+2] = lum; out[i+3] = 210;
445
+ }
446
+ }
447
+ return new ImageData(out, w, h);
448
+ }
449
+
450
+ /* Load image and extract raw pixel data via canvas */
451
+ function loadImgPixels(url, cb) {
452
+ var img = new Image();
453
+ img.crossOrigin = 'anonymous';
454
+ img.onload = function () {
455
+ var c = document.createElement('canvas');
456
+ c.width = img.naturalWidth; c.height = img.naturalHeight;
457
+ var ctx = c.getContext('2d', { willReadFrequently: true });
458
+ ctx.drawImage(img, 0, 0);
459
+ try {
460
+ cb(null, ctx.getImageData(0, 0, c.width, c.height), c.width, c.height);
461
+ } catch (e) {
462
+ cb(e, null, 0, 0);
463
+ }
464
+ };
465
+ img.onerror = function () { cb(new Error('img-load'), null, 0, 0); };
466
+ img.src = url;
467
+ }
468
+
469
+ var diffCache = {};
470
+
471
+ function genDiff(idx, baseUrl, curUrl, container) {
472
+ /* Return cached result if already computed */
473
+ if (diffCache[idx]) {
474
+ var cached = diffCache[idx];
475
+ var clone = document.createElement('canvas');
476
+ clone.width = cached.width;
477
+ clone.height = cached.height;
478
+ clone.getContext('2d').drawImage(cached, 0, 0);
479
+ clone.className = 'view-img';
480
+ clone.setAttribute('data-lb', 'diff');
481
+ clone.setAttribute('data-lb-idx', String(idx));
482
+ container.innerHTML = '';
483
+ container.appendChild(clone);
484
+ return;
485
+ }
486
+
487
+ container.innerHTML = '<div class="diff-generating">Generating diff\u2026</div>';
488
+
489
+ loadImgPixels(baseUrl, function (e1, d1, w1, h1) {
490
+ if (e1) {
491
+ container.innerHTML = '<div class="diff-unavailable">'
492
+ + '<strong>\uD83D\uDD12 Diff unavailable</strong> \u2014 canvas pixel access is blocked when opening as a local file.<br>'
493
+ + 'Serve the report via HTTP to enable it:<br>'
494
+ + '<code>cd bhv-report &amp;&amp; npx serve .</code>'
495
+ + '</div>';
496
+ return;
497
+ }
498
+ loadImgPixels(curUrl, function (e2, d2, w2, h2) {
499
+ if (e2) {
500
+ container.innerHTML = '<div class="diff-unavailable">Could not load current screenshot.</div>';
501
+ return;
502
+ }
503
+ var fw = Math.min(w1, w2);
504
+ var fh = Math.min(h1, h2);
505
+ var diff = renderDiff(d1.data, d2.data, fw, fh);
506
+ var cv = document.createElement('canvas');
507
+ cv.width = fw; cv.height = fh;
508
+ cv.getContext('2d').putImageData(diff, 0, 0);
509
+ diffCache[idx] = cv;
510
+ cv.className = 'view-img';
511
+ cv.setAttribute('data-lb', 'diff');
512
+ cv.setAttribute('data-lb-idx', String(idx));
513
+ container.innerHTML = '';
514
+ container.appendChild(cv);
515
+ });
516
+ });
517
+ }
518
+
519
+ /* ── Lightbox ── */
520
+
521
+ function lbApply() {
522
+ lb.wrap.style.transform = 'translate(' + lb.tx + 'px,' + lb.ty + 'px) scale(' + lb.scale + ')';
523
+ if (lb.zoomLbl) lb.zoomLbl.textContent = Math.round(lb.scale * 100) + '%';
524
+ }
525
+
526
+ function lbReset() { lb.scale = 1; lb.tx = 0; lb.ty = 0; lbApply(); }
527
+
528
+ /* Zoom towards a viewport-center-relative point (mx, my) */
529
+ function lbZoom(factor, mx, my) {
530
+ var ns = Math.max(lb.minScale, Math.min(lb.maxScale, lb.scale * factor));
531
+ var r = ns / lb.scale;
532
+ lb.tx = mx * (1 - r) + lb.tx * r;
533
+ lb.ty = my * (1 - r) + lb.ty * r;
534
+ lb.scale = ns;
535
+ lbApply();
536
+ }
537
+
538
+ /* Auto-fit: scale image to fill viewport on first open */
539
+ function lbAutoFit(w, h) {
540
+ var vw = lb.inner.clientWidth - 48;
541
+ var vh = lb.inner.clientHeight - 48;
542
+ lb.scale = Math.min(1, vw / w, vh / h);
543
+ lb.tx = 0; lb.ty = 0;
544
+ lbApply();
545
+ }
546
+
547
+ function lbOpen(type, payload) {
548
+ lb.scale = 1; lb.tx = 0; lb.ty = 0;
549
+ lb.wrap.innerHTML = '';
550
+ var el;
551
+ if (type === 'img') {
552
+ el = new Image();
553
+ el.draggable = false;
554
+ el.style.maxWidth = 'none';
555
+ el.style.display = 'block';
556
+ el.onload = function () { lbAutoFit(el.naturalWidth, el.naturalHeight); };
557
+ el.src = payload;
558
+ } else {
559
+ /* diff canvas */
560
+ var src = diffCache[payload];
561
+ if (!src) return;
562
+ el = document.createElement('canvas');
563
+ el.width = src.width;
564
+ el.height = src.height;
565
+ el.style.maxWidth = 'none';
566
+ el.style.display = 'block';
567
+ el.getContext('2d').drawImage(src, 0, 0);
568
+ lb.wrap.appendChild(el);
569
+ lb.overlay.classList.add('open');
570
+ document.body.style.overflow = 'hidden';
571
+ lbAutoFit(src.width, src.height);
572
+ return;
573
+ }
574
+ lb.wrap.appendChild(el);
575
+ lb.overlay.classList.add('open');
576
+ document.body.style.overflow = 'hidden';
577
+ lbApply();
578
+ }
579
+
580
+ function lbClose() {
581
+ lb.overlay.classList.remove('open');
582
+ document.body.style.overflow = '';
583
+ setTimeout(function () { if (lb.wrap) lb.wrap.innerHTML = ''; }, 220);
584
+ }
585
+
586
+ function lbCenterPos(e) {
587
+ var r = lb.inner.getBoundingClientRect();
588
+ return { x: e.clientX - r.left - r.width / 2, y: e.clientY - r.top - r.height / 2 };
589
+ }
590
+
591
+ function initLightbox() {
592
+ var ov = document.createElement('div');
593
+ ov.id = 'lb-overlay';
594
+ ov.className = 'lb-overlay';
595
+ ov.innerHTML = '<div class="lb-inner"><div class="lb-img-wrap"></div></div>'
596
+ + '<button class="lb-close" title="Close (Esc)">\u00D7</button>'
597
+ + '<div class="lb-zoom-label">100%</div>'
598
+ + '<div class="lb-hint">Scroll / pinch to zoom \u00B7 Drag to pan \u00B7 Dbl-click to reset \u00B7 Esc to close</div>';
599
+ document.body.appendChild(ov);
600
+
601
+ lb.overlay = ov;
602
+ lb.inner = ov.querySelector('.lb-inner');
603
+ lb.wrap = ov.querySelector('.lb-img-wrap');
604
+ lb.zoomLbl = ov.querySelector('.lb-zoom-label');
605
+
606
+ /* close button */
607
+ ov.querySelector('.lb-close').addEventListener('click', lbClose);
608
+
609
+ /* click on backdrop (not on image) */
610
+ lb.inner.addEventListener('click', function (e) {
611
+ if (e.target === lb.inner) lbClose();
612
+ });
613
+
614
+ /* double-click: reset zoom */
615
+ lb.inner.addEventListener('dblclick', function (e) {
616
+ e.preventDefault();
617
+ lbReset();
618
+ });
619
+
620
+ /* scroll-to-zoom */
621
+ lb.inner.addEventListener('wheel', function (e) {
622
+ e.preventDefault();
623
+ var p = lbCenterPos(e);
624
+ lbZoom(e.deltaY < 0 ? 1.12 : 1 / 1.12, p.x, p.y);
625
+ }, { passive: false });
626
+
627
+ /* mouse drag: pan */
628
+ lb.inner.addEventListener('mousedown', function (e) {
629
+ if (e.button !== 0) return;
630
+ lb.dragging = true;
631
+ lb.startX = e.clientX; lb.startY = e.clientY;
632
+ lb.startTx = lb.tx; lb.startTy = lb.ty;
633
+ lb.wrap.classList.add('dragging');
634
+ e.preventDefault();
635
+ });
636
+ window.addEventListener('mousemove', function (e) {
637
+ if (!lb.dragging) return;
638
+ lb.tx = lb.startTx + (e.clientX - lb.startX);
639
+ lb.ty = lb.startTy + (e.clientY - lb.startY);
640
+ lbApply();
641
+ });
642
+ window.addEventListener('mouseup', function () {
643
+ if (!lb.dragging) return;
644
+ lb.dragging = false;
645
+ lb.wrap.classList.remove('dragging');
646
+ });
647
+
648
+ /* touch: pan + pinch-to-zoom */
649
+ lb.inner.addEventListener('touchstart', function (e) {
650
+ if (e.touches.length === 2) {
651
+ var dx = e.touches[0].clientX - e.touches[1].clientX;
652
+ var dy = e.touches[0].clientY - e.touches[1].clientY;
653
+ lb.pinchDist = Math.sqrt(dx * dx + dy * dy);
654
+ lb.pinchScale = lb.scale;
655
+ lb.dragging = false;
656
+ } else {
657
+ lb.dragging = true;
658
+ lb.startX = e.touches[0].clientX; lb.startY = e.touches[0].clientY;
659
+ lb.startTx = lb.tx; lb.startTy = lb.ty;
660
+ }
661
+ }, { passive: true });
662
+ lb.inner.addEventListener('touchmove', function (e) {
663
+ if (e.touches.length === 2) {
664
+ e.preventDefault();
665
+ var dx = e.touches[0].clientX - e.touches[1].clientX;
666
+ var dy = e.touches[0].clientY - e.touches[1].clientY;
667
+ var d = Math.sqrt(dx * dx + dy * dy);
668
+ lb.scale = Math.max(lb.minScale, Math.min(lb.maxScale, lb.pinchScale * d / lb.pinchDist));
669
+ lbApply();
670
+ } else if (lb.dragging) {
671
+ lb.tx = lb.startTx + (e.touches[0].clientX - lb.startX);
672
+ lb.ty = lb.startTy + (e.touches[0].clientY - lb.startY);
673
+ lbApply();
674
+ }
675
+ }, { passive: false });
676
+ lb.inner.addEventListener('touchend', function () { lb.dragging = false; }, { passive: true });
677
+
678
+ /* keyboard shortcuts */
679
+ document.addEventListener('keydown', function (e) {
680
+ if (!lb.overlay.classList.contains('open')) return;
681
+ if (e.key === 'Escape') lbClose();
682
+ if (e.key === '0') lbReset();
683
+ });
684
+ }
685
+
686
+ /* ── HTML builders ── */
687
+ function statusInfo(r) {
688
+ if (r.status === 'PASS') return { cls:'s-pass', badge:'m-pass', label:'PASS' };
689
+ if (r.status === 'FAIL') return { cls:'s-fail', badge:'m-fail', label:'FAIL' };
690
+ if (r.status === 'ERROR') return { cls:'s-warn', badge:'m-warn', label:'ERROR' };
691
+ return { cls:'s-base', badge:'m-base', label:'BASELINE' };
692
+ }
693
+ function methodLabel(r) {
694
+ if (r.status === 'BASELINE_CREATED') return 'NEW';
695
+ if (r.method === 'FAST_PIXEL_MATCH') return 'PIXEL';
696
+ return 'AI';
697
+ }
698
+ function methodBadgeCls(r) {
699
+ if (r.status === 'BASELINE_CREATED') return 'm-method';
700
+ if (r.status === 'ERROR') return 'm-warn';
701
+ if (r.method === 'FAST_PIXEL_MATCH') return 'm-pixel';
702
+ return 'm-ai';
703
+ }
704
+
705
+ function buildImages(r) {
706
+ if (r.status === 'BASELINE_CREATED') {
707
+ var bUrl = '../bhv-snapshots/' + esc(r.baselineFile);
708
+ return '<div class="img-lbl">Saved baseline</div>'
709
+ + '<div style="max-width:540px">'
710
+ + '<img class="view-img" src="' + bUrl + '" alt="Baseline"'
711
+ + ' data-lb="img" data-lb-src="' + bUrl + '" /></div>';
712
+ }
713
+
714
+ var baseUrl = '../bhv-snapshots/' + esc(r.baselineFile);
715
+ var curUrl = r.currentFile ? '../bhv-snapshots/' + esc(r.currentFile) : '';
716
+ var hasCur = !!r.currentFile;
717
+ var showDiff = hasCur && (r.status === 'FAIL' || r.status === 'ERROR');
718
+ var idx = r.index;
719
+
720
+ var diffTab = showDiff
721
+ ? '<button class="view-tab diff-tab" data-tab="diff" data-tidx="' + idx + '">\u26A1 Diff</button>'
722
+ : '';
723
+ var tabs = hasCur
724
+ ? '<div class="view-tabs">'
725
+ + '<button class="view-tab on" data-tab="base" data-tidx="' + idx + '">Baseline</button>'
726
+ + '<button class="view-tab" data-tab="cur" data-tidx="' + idx + '">Current</button>'
727
+ + diffTab
728
+ + '</div>'
729
+ : '';
730
+
731
+ var basePanel = '<div class="img-view" id="vbase-' + idx + '">'
732
+ + '<img class="view-img" src="' + baseUrl + '" alt="Baseline"'
733
+ + ' data-lb="img" data-lb-src="' + baseUrl + '" /></div>';
734
+
735
+ var curPanel = hasCur
736
+ ? '<div class="img-view" id="vcur-' + idx + '" style="display:none">'
737
+ + '<img class="view-img" src="' + curUrl + '" alt="Current"'
738
+ + ' data-lb="img" data-lb-src="' + curUrl + '" /></div>'
739
+ : '';
740
+
741
+ var diffPanel = showDiff
742
+ ? '<div class="img-view" id="vdiff-' + idx + '" style="display:none"'
743
+ + ' data-base="' + baseUrl + '" data-cur="' + curUrl + '" data-ready="0">'
744
+ + '<div class="diff-placeholder">\u26A1 Click the Diff tab to generate a pixel-level comparison in your browser.</div>'
745
+ + '</div>'
746
+ : '';
747
+
748
+ return '<div class="img-lbl">Visual comparison</div>'
749
+ + tabs + basePanel + curPanel + diffPanel;
750
+ }
751
+
752
+ function buildDetail(r) {
753
+ var imgWrap = r.status === 'PASS' ? 'img-section pass-imgs' : 'img-section';
754
+ var verdictLbl = r.method === 'FAST_PIXEL_MATCH'
755
+ ? '\u26A1 Fast Pixel Match'
756
+ : '\uD83E\uDD16 AI Verdict';
757
+ return '<div class="test-detail" id="d' + r.index + '">'
758
+ + '<div class="verdict">'
759
+ + '<div class="verdict-lbl">' + verdictLbl + '</div>'
760
+ + '<p class="verdict-txt">\u201C' + esc(r.reason) + '\u201D</p>'
761
+ + '</div>'
762
+ + '<div class="' + imgWrap + '">' + buildImages(r) + '</div>'
763
+ + '</div>';
764
+ }
765
+
766
+ function buildItem(r) {
767
+ var s = statusInfo(r);
768
+ var method = methodLabel(r);
769
+ var methodCls = methodBadgeCls(r);
770
+ var snip = trunc(r.reason, 88);
771
+ return '<div class="test-item ' + s.cls + '">'
772
+ + '<div class="test-row" data-t="' + r.index + '">'
773
+ + '<div class="s-dot"></div>'
774
+ + '<div class="test-meta">'
775
+ + '<div class="test-name">' + esc(r.testName) + '</div>'
776
+ + '<div class="test-snip">' + esc(snip) + '</div>'
777
+ + '</div>'
778
+ + '<div class="badges">'
779
+ + '<span class="badge ' + methodCls + '">' + method + '</span>'
780
+ + '<span class="badge ' + s.badge + '">' + s.label + '</span>'
781
+ + '</div>'
782
+ + '<span class="chev" id="c' + r.index + '">\u203A</span>'
783
+ + '</div>'
784
+ + buildDetail(r)
785
+ + '</div>';
786
+ }
787
+
788
+ function buildThemeBtns() {
789
+ var opts = [
790
+ { k:'system', lbl:'\u229E System' },
791
+ { k:'light', lbl:'\u2600\uFE0E Light' },
792
+ { k:'dark', lbl:'\u263D Dark' }
793
+ ];
794
+ return opts.map(function(o) {
795
+ return '<button class="t-btn' + (state.theme === o.k ? ' on' : '') + '" data-th="' + o.k + '">' + o.lbl + '</button>';
796
+ }).join('');
797
+ }
798
+
799
+ function buildApp() {
800
+ var ts = fmtDate(META.generatedAt);
801
+ var hasFail = META.failed > 0;
802
+ var hasBase = META.baselines > 0;
803
+ var hasErrors = META.errored > 0;
804
+ var imgOn = state.showPassImg ? ' on' : '';
805
+ var failCls = hasFail ? ' s-fail' : ' s-neutral';
806
+ var baseStat = hasBase
807
+ ? '<div class="stat s-base"><div class="stat-lbl">Baselines</div><div class="stat-val">' + META.baselines + '</div></div>'
808
+ : '';
809
+ var warnStat = hasErrors
810
+ ? '<div class="stat s-warn"><div class="stat-lbl">Errors</div><div class="stat-val">' + META.errored + '</div></div>'
811
+ : '';
812
+ var warnFilter = hasErrors
813
+ ? '<button class="f-btn" data-f="warn">Errors (' + META.errored + ')</button>'
814
+ : '';
815
+ var items = RESULTS.map(buildItem).join('');
816
+
817
+ return '<header class="hdr">'
818
+ + '<div class="wrap"><div class="hdr-inner">'
819
+ + '<div class="logo">'
820
+ + '<span class="logo-bug">\uD83D\uDC1E</span>'
821
+ + '<span class="logo-name">BugHunters Vision</span>'
822
+ + '<span class="run-at" id="run-at">' + esc(ts) + '</span>'
823
+ + '</div>'
824
+ + '<div class="t-toggle" id="t-toggle">' + buildThemeBtns() + '</div>'
825
+ + '</div></div>'
826
+ + '</header>'
827
+ + '<div class="wrap">'
828
+ + '<div class="summary">'
829
+ + '<div class="stat"><div class="stat-lbl">Total</div><div class="stat-val">' + META.total + '</div></div>'
830
+ + '<div class="stat s-pass"><div class="stat-lbl">Passed</div><div class="stat-val">' + META.passed + '</div></div>'
831
+ + '<div class="stat' + failCls + '"><div class="stat-lbl">Failed</div><div class="stat-val">' + META.failed + '</div></div>'
832
+ + warnStat
833
+ + baseStat
834
+ + '</div>'
835
+ + '<div class="controls">'
836
+ + '<div class="f-group">'
837
+ + '<button class="f-btn on" data-f="all">All (' + META.total + ')</button>'
838
+ + '<button class="f-btn" data-f="pass">Passed (' + META.passed + ')</button>'
839
+ + '<button class="f-btn" data-f="fail">Failed (' + META.failed + ')</button>'
840
+ + warnFilter
841
+ + '</div>'
842
+ + '<button id="img-btn" class="img-btn' + imgOn + '">'
843
+ + '<div class="sw-track"><div class="sw-thumb"></div></div>'
844
+ + 'Screenshots on pass'
845
+ + '</button>'
846
+ + '</div>'
847
+ + '<div class="test-list" id="tlist">' + items + '</div>'
848
+ + '</div>'
849
+ + '<footer class="ftr">Generated by <strong>BugHunters Vision</strong> \u00B7 AI-powered visual testing \u00B7 Images from local disk</footer>';
850
+ }
851
+
852
+ /* ── Events ── */
853
+ function bind() {
854
+ var tlist = document.getElementById('tlist');
855
+
856
+ /* accordion expand/collapse — only fires from .test-row[data-t] */
857
+ tlist.addEventListener('click', function (e) {
858
+ var row = e.target.closest('[data-t]');
859
+ if (!row) return;
860
+ var idx = row.getAttribute('data-t');
861
+ var detail = document.getElementById('d' + idx);
862
+ var chevron = document.getElementById('c' + idx);
863
+ if (!detail) return;
864
+ var open = detail.style.display !== 'none';
865
+ detail.style.display = open ? 'none' : 'block';
866
+ if (chevron) chevron.classList.toggle('open', !open);
867
+ });
868
+
869
+ /* view-tab switching: Baseline / Current / Diff */
870
+ tlist.addEventListener('click', function (e) {
871
+ var tab = e.target.closest('[data-tab]');
872
+ if (!tab) return;
873
+ var tidx = tab.getAttribute('data-tidx');
874
+ var tabName = tab.getAttribute('data-tab');
875
+ /* update active tab */
876
+ tab.closest('.view-tabs').querySelectorAll('.view-tab').forEach(function (t) {
877
+ t.classList.toggle('on', t === tab);
878
+ });
879
+ /* show correct panel, hide others */
880
+ ['base', 'cur', 'diff'].forEach(function (key) {
881
+ var panel = document.getElementById('v' + key + '-' + tidx);
882
+ if (panel) panel.style.display = key === tabName ? '' : 'none';
883
+ });
884
+ /* lazy diff generation — only on first click */
885
+ if (tabName === 'diff') {
886
+ var dp = document.getElementById('vdiff-' + tidx);
887
+ if (dp && dp.getAttribute('data-ready') === '0') {
888
+ dp.setAttribute('data-ready', '1');
889
+ genDiff(tidx, dp.getAttribute('data-base'), dp.getAttribute('data-cur'), dp);
890
+ }
891
+ }
892
+ });
893
+
894
+ /* image / diff-canvas click → open lightbox */
895
+ tlist.addEventListener('click', function (e) {
896
+ var el = e.target.closest('[data-lb]');
897
+ if (!el) return;
898
+ var type = el.getAttribute('data-lb');
899
+ if (type === 'img') lbOpen('img', el.getAttribute('data-lb-src'));
900
+ if (type === 'diff') lbOpen('diff', el.getAttribute('data-lb-idx'));
901
+ });
902
+
903
+ /* theme */
904
+ document.getElementById('t-toggle').addEventListener('click', function (e) {
905
+ var btn = e.target.closest('[data-th]');
906
+ if (!btn) return;
907
+ state.theme = btn.getAttribute('data-th');
908
+ localStorage.setItem('bv-theme', state.theme);
909
+ applyTheme();
910
+ document.querySelectorAll('[data-th]').forEach(function (b) {
911
+ b.classList.toggle('on', b.getAttribute('data-th') === state.theme);
912
+ });
913
+ });
914
+
915
+ /* screenshots on pass toggle */
916
+ document.getElementById('img-btn').addEventListener('click', function () {
917
+ state.showPassImg = !state.showPassImg;
918
+ localStorage.setItem('bv-pass-img', String(state.showPassImg));
919
+ document.body.classList.toggle('show-pass', state.showPassImg);
920
+ this.classList.toggle('on', state.showPassImg);
921
+ });
922
+
923
+ /* filter */
924
+ document.querySelectorAll('[data-f]').forEach(function (btn) {
925
+ btn.addEventListener('click', function () {
926
+ var f = btn.getAttribute('data-f');
927
+ document.querySelectorAll('[data-f]').forEach(function (b) {
928
+ b.classList.toggle('on', b.getAttribute('data-f') === f);
929
+ });
930
+ document.querySelectorAll('.test-item').forEach(function (el) {
931
+ var show = f === 'all'
932
+ || (f === 'pass' && el.classList.contains('s-pass'))
933
+ || (f === 'fail' && el.classList.contains('s-fail'))
934
+ || (f === 'warn' && el.classList.contains('s-warn'));
935
+ el.style.display = show ? '' : 'none';
936
+ });
937
+ });
938
+ });
939
+ }
940
+
941
+ /* ── Init ── */
942
+ applyTheme();
943
+ document.getElementById('root').innerHTML = buildApp();
944
+ if (state.showPassImg) document.body.classList.add('show-pass');
945
+ bind();
946
+ initLightbox();
947
+ window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function () {
948
+ if (state.theme === 'system') applyTheme();
949
+ });
950
+
951
+ })();
952
+ `.trim();
953
+ }
954
+ // ─── HTML Assembly ───────────────────────────────────────────────────────────
955
+ function generateHTML(results, meta) {
956
+ const resultsJson = JSON.stringify(results);
957
+ const metaJson = JSON.stringify(meta);
958
+ const svgPass = encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><rect width="32" height="32" rx="6" fill="#1a2e22"/><path d="M7 16.5L13 22L25 10" stroke="#73c991" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/></svg>');
959
+ const svgFail = encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><rect width="32" height="32" rx="6" fill="#2a1515"/><path d="M10 10L22 22M22 10L10 22" stroke="#f14c4c" stroke-width="3" stroke-linecap="round"/></svg>');
960
+ const faviconHref = `data:image/svg+xml,${meta.failed > 0 ? svgFail : svgPass}`;
961
+ return `<!DOCTYPE html>
962
+ <html lang="en">
963
+ <head>
964
+ <meta charset="UTF-8">
965
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
966
+ <title>BugHunters Vision Report</title>
967
+ <link rel="icon" type="image/svg+xml" href="${faviconHref}">
968
+ <!-- Flash prevention: apply saved theme before CSS paint -->
969
+ <script>
970
+ (function(){
971
+ var t = localStorage.getItem('bv-theme') || 'system';
972
+ var d = t === 'system' ? (matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light') : t;
973
+ document.documentElement.setAttribute('data-theme', d);
974
+ })();
975
+ </script>
976
+ <style>${getCss()}</style>
977
+ </head>
978
+ <body>
979
+ <div id="root"></div>
980
+ <script type="application/json" id="bv-r">${resultsJson}</script>
981
+ <script type="application/json" id="bv-m">${metaJson}</script>
982
+ <script>${getJs()}</script>
983
+ </body>
984
+ </html>`;
985
+ }
986
+ // ─── Reporter class ───────────────────────────────────────────────────────────
987
+ class BugHuntersVisionReporter {
988
+ snapshotsDir;
989
+ reportDir;
990
+ constructor(options) {
991
+ this.snapshotsDir = path.resolve(process.cwd(), options?.snapshotsDir ?? './bhv-snapshots');
992
+ this.reportDir = path.resolve(process.cwd(), options?.reportDir ?? './bhv-report');
993
+ }
994
+ onBegin() {
995
+ fs.mkdirSync(this.snapshotsDir, { recursive: true });
996
+ fs.writeFileSync(path.join(this.snapshotsDir, 'results.json'), '[]', 'utf-8');
997
+ }
998
+ onEnd() {
999
+ const resultsPath = path.join(this.snapshotsDir, 'results.json');
1000
+ if (!fs.existsSync(resultsPath)) {
1001
+ console.log('\n\u26A0\uFE0F [BugHunters Vision] No results — skipping report generation.\n');
1002
+ return;
1003
+ }
1004
+ const raw = fs.readFileSync(resultsPath, 'utf-8').trim();
1005
+ if (!raw || raw === '[]') {
1006
+ console.log('\n\u26A0\uFE0F [BugHunters Vision] No results — skipping report generation.\n');
1007
+ return;
1008
+ }
1009
+ const results = JSON.parse(raw);
1010
+ const passed = results.filter(r => r.status === 'PASS').length;
1011
+ const failed = results.filter(r => r.status === 'FAIL').length;
1012
+ const baselines = results.filter(r => r.status === 'BASELINE_CREATED').length;
1013
+ const errored = results.filter(r => r.status === 'ERROR').length;
1014
+ const meta = {
1015
+ generatedAt: new Date().toISOString(),
1016
+ total: results.length,
1017
+ passed,
1018
+ failed,
1019
+ baselines,
1020
+ errored,
1021
+ };
1022
+ const reportPath = path.join(this.reportDir, 'index.html');
1023
+ fs.mkdirSync(this.reportDir, { recursive: true });
1024
+ fs.writeFileSync(reportPath, generateHTML(results, meta), 'utf-8');
1025
+ console.log('\n\uD83D\uDC1E BugHunters Vision Report generated:');
1026
+ console.log(' \uD83D\uDCC4 ' + reportPath);
1027
+ const erroredStr = errored > 0 ? ' \u26A0\uFE0F ' + errored + ' errored' : '';
1028
+ console.log(' \u2705 ' + passed + ' passed \u274C ' + failed + ' failed' + erroredStr + ' (' + results.length + ' total)');
1029
+ // Show remaining API balance if an AI call was made this run
1030
+ const balance = (0, check_1.getLastRemainingBalance)();
1031
+ if (balance !== null) {
1032
+ const balanceStr = balance === 'Unlimited' ? '\x1b[32mUnlimited\x1b[0m' : '\x1b[36m' + balance + ' tokens\x1b[0m';
1033
+ console.log('\n\uD83D\uDC1E BugHunters Vision: AI Evaluation complete. Remaining API balance: ' + balanceStr);
1034
+ }
1035
+ console.log('');
1036
+ }
1037
+ }
1038
+ exports.BugHuntersVisionReporter = BugHuntersVisionReporter;
1039
+ exports.default = BugHuntersVisionReporter;