@balpal4495/quorum 3.3.2 → 3.4.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,676 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Quorum</title>
7
+ <style>
8
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
9
+
10
+ :root {
11
+ --bg: #0d0d0f;
12
+ --surface: #16161a;
13
+ --border: #2a2a30;
14
+ --text: #e8e8ec;
15
+ --muted: #6e6e7e;
16
+ --accent: #7c6eff;
17
+ --green: #34c97a;
18
+ --red: #e05252;
19
+ --yellow: #e0b952;
20
+ --blue: #52a8e0;
21
+ --radius: 8px;
22
+ --font: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
23
+ --mono: "JetBrains Mono", "Fira Code", "Cascadia Code", ui-monospace, monospace;
24
+ }
25
+
26
+ body {
27
+ font-family: var(--font);
28
+ background: var(--bg);
29
+ color: var(--text);
30
+ min-height: 100vh;
31
+ font-size: 14px;
32
+ line-height: 1.5;
33
+ }
34
+
35
+ /* ── Layout ── */
36
+ header {
37
+ display: flex;
38
+ align-items: center;
39
+ gap: 24px;
40
+ padding: 0 24px;
41
+ height: 52px;
42
+ background: var(--surface);
43
+ border-bottom: 1px solid var(--border);
44
+ position: sticky;
45
+ top: 0;
46
+ z-index: 100;
47
+ }
48
+
49
+ .logo {
50
+ font-weight: 700;
51
+ font-size: 16px;
52
+ color: var(--accent);
53
+ letter-spacing: -.3px;
54
+ }
55
+
56
+ nav {
57
+ display: flex;
58
+ gap: 2px;
59
+ flex: 1;
60
+ }
61
+
62
+ nav button {
63
+ padding: 6px 14px;
64
+ border: none;
65
+ background: none;
66
+ color: var(--muted);
67
+ font: inherit;
68
+ font-size: 13px;
69
+ cursor: pointer;
70
+ border-radius: var(--radius);
71
+ transition: background .15s, color .15s;
72
+ }
73
+ nav button:hover { background: rgba(255,255,255,.05); color: var(--text); }
74
+ nav button.active { background: rgba(124,110,255,.15); color: var(--accent); }
75
+
76
+ .badge {
77
+ display: inline-block;
78
+ padding: 1px 7px;
79
+ border-radius: 99px;
80
+ font-size: 11px;
81
+ font-weight: 600;
82
+ margin-left: 4px;
83
+ background: rgba(224,82,82,.2);
84
+ color: var(--red);
85
+ }
86
+
87
+ main {
88
+ max-width: 960px;
89
+ margin: 0 auto;
90
+ padding: 28px 24px 60px;
91
+ }
92
+
93
+ /* ── Tabs ── */
94
+ .tab { display: none; }
95
+ .tab.active { display: block; }
96
+
97
+ /* ── Search / toolbar ── */
98
+ .toolbar {
99
+ display: flex;
100
+ gap: 10px;
101
+ margin-bottom: 20px;
102
+ align-items: center;
103
+ }
104
+
105
+ input[type="search"], input[type="text"] {
106
+ flex: 1;
107
+ padding: 9px 14px;
108
+ background: var(--surface);
109
+ border: 1px solid var(--border);
110
+ border-radius: var(--radius);
111
+ color: var(--text);
112
+ font: inherit;
113
+ font-size: 13px;
114
+ outline: none;
115
+ transition: border-color .15s;
116
+ }
117
+ input:focus { border-color: var(--accent); }
118
+ input::placeholder { color: var(--muted); }
119
+
120
+ /* ── Cards ── */
121
+ .card {
122
+ background: var(--surface);
123
+ border: 1px solid var(--border);
124
+ border-radius: var(--radius);
125
+ padding: 16px 18px;
126
+ margin-bottom: 10px;
127
+ transition: border-color .15s;
128
+ }
129
+ .card:hover { border-color: rgba(124,110,255,.35); }
130
+
131
+ .card-header {
132
+ display: flex;
133
+ align-items: flex-start;
134
+ gap: 10px;
135
+ margin-bottom: 8px;
136
+ }
137
+
138
+ .card-title {
139
+ flex: 1;
140
+ font-weight: 600;
141
+ font-size: 13px;
142
+ line-height: 1.4;
143
+ }
144
+
145
+ .card-meta {
146
+ font-size: 12px;
147
+ color: var(--muted);
148
+ display: flex;
149
+ gap: 14px;
150
+ flex-wrap: wrap;
151
+ }
152
+
153
+ .card-body {
154
+ font-size: 13px;
155
+ color: var(--muted);
156
+ margin-top: 6px;
157
+ }
158
+
159
+ .areas {
160
+ margin-top: 8px;
161
+ display: flex;
162
+ flex-wrap: wrap;
163
+ gap: 4px;
164
+ }
165
+
166
+ .area-tag {
167
+ font-family: var(--mono);
168
+ font-size: 11px;
169
+ padding: 2px 7px;
170
+ background: rgba(255,255,255,.05);
171
+ border-radius: 4px;
172
+ color: var(--muted);
173
+ }
174
+
175
+ /* ── Status badges ── */
176
+ .status {
177
+ font-size: 11px;
178
+ font-weight: 600;
179
+ padding: 2px 8px;
180
+ border-radius: 99px;
181
+ white-space: nowrap;
182
+ }
183
+ .status-validated { background: rgba(52,201,122,.15); color: var(--green); }
184
+ .status-open { background: rgba(82,168,224,.15); color: var(--blue); }
185
+ .status-refuted { background: rgba(224,82,82,.15); color: var(--red); }
186
+ .status-pending { background: rgba(224,185,82,.15); color: var(--yellow); }
187
+
188
+ /* ── Confidence ── */
189
+ .confidence {
190
+ display: inline-flex;
191
+ align-items: center;
192
+ gap: 6px;
193
+ }
194
+ .conf-bar {
195
+ width: 48px;
196
+ height: 4px;
197
+ border-radius: 2px;
198
+ background: var(--border);
199
+ overflow: hidden;
200
+ }
201
+ .conf-fill { height: 100%; border-radius: 2px; background: var(--accent); }
202
+
203
+ /* ── Actions ── */
204
+ .actions { display: flex; gap: 8px; margin-top: 12px; }
205
+
206
+ button.btn {
207
+ padding: 7px 14px;
208
+ border-radius: var(--radius);
209
+ border: 1px solid var(--border);
210
+ background: none;
211
+ color: var(--text);
212
+ font: inherit;
213
+ font-size: 12px;
214
+ font-weight: 500;
215
+ cursor: pointer;
216
+ transition: background .15s, border-color .15s;
217
+ }
218
+ button.btn:hover { background: rgba(255,255,255,.06); }
219
+ button.btn-approve {
220
+ background: rgba(52,201,122,.12);
221
+ border-color: rgba(52,201,122,.3);
222
+ color: var(--green);
223
+ }
224
+ button.btn-approve:hover { background: rgba(52,201,122,.22); }
225
+ button.btn-reject {
226
+ background: rgba(224,82,82,.08);
227
+ border-color: rgba(224,82,82,.25);
228
+ color: var(--red);
229
+ }
230
+ button.btn-reject:hover { background: rgba(224,82,82,.16); }
231
+ button.btn:disabled { opacity: .4; cursor: not-allowed; }
232
+
233
+ /* ── Coverage ── */
234
+ .coverage-header {
235
+ display: flex;
236
+ align-items: center;
237
+ gap: 16px;
238
+ margin-bottom: 24px;
239
+ }
240
+
241
+ .pct-ring {
242
+ width: 72px;
243
+ height: 72px;
244
+ flex-shrink: 0;
245
+ }
246
+
247
+ .coverage-bar-wrap {
248
+ flex: 1;
249
+ }
250
+
251
+ .cov-bar {
252
+ height: 10px;
253
+ border-radius: 5px;
254
+ background: var(--border);
255
+ overflow: hidden;
256
+ margin-bottom: 8px;
257
+ }
258
+ .cov-fill {
259
+ height: 100%;
260
+ border-radius: 5px;
261
+ background: linear-gradient(90deg, var(--accent), var(--green));
262
+ transition: width .6s ease;
263
+ }
264
+
265
+ .cov-stats {
266
+ display: flex;
267
+ gap: 20px;
268
+ font-size: 13px;
269
+ color: var(--muted);
270
+ }
271
+ .cov-stats strong { color: var(--text); }
272
+
273
+ .cov-section { margin-bottom: 20px; }
274
+ .cov-section-title {
275
+ font-size: 12px;
276
+ font-weight: 600;
277
+ text-transform: uppercase;
278
+ letter-spacing: .06em;
279
+ color: var(--muted);
280
+ margin-bottom: 8px;
281
+ padding-bottom: 6px;
282
+ border-bottom: 1px solid var(--border);
283
+ }
284
+
285
+ .file-row {
286
+ display: flex;
287
+ align-items: center;
288
+ gap: 10px;
289
+ padding: 6px 0;
290
+ border-bottom: 1px solid rgba(255,255,255,.03);
291
+ font-family: var(--mono);
292
+ font-size: 12px;
293
+ }
294
+ .file-row:last-child { border-bottom: none; }
295
+ .file-dot {
296
+ width: 7px;
297
+ height: 7px;
298
+ border-radius: 50%;
299
+ flex-shrink: 0;
300
+ }
301
+ .dot-green { background: var(--green); }
302
+ .dot-red { background: var(--red); }
303
+ .file-name { flex: 1; color: var(--text); }
304
+ .file-entries { color: var(--muted); font-size: 11px; }
305
+
306
+ /* ── Empty / loading states ── */
307
+ .empty {
308
+ text-align: center;
309
+ color: var(--muted);
310
+ padding: 48px 0;
311
+ font-size: 14px;
312
+ }
313
+ .empty small { display: block; margin-top: 6px; font-size: 12px; }
314
+
315
+ .loading { color: var(--muted); padding: 32px 0; text-align: center; }
316
+
317
+ /* ── Section heading ── */
318
+ .section-heading {
319
+ font-size: 18px;
320
+ font-weight: 700;
321
+ margin-bottom: 20px;
322
+ }
323
+ .section-sub {
324
+ font-size: 13px;
325
+ color: var(--muted);
326
+ margin-top: -14px;
327
+ margin-bottom: 20px;
328
+ }
329
+
330
+ /* ── Toast ── */
331
+ #toast {
332
+ position: fixed;
333
+ bottom: 24px;
334
+ right: 24px;
335
+ padding: 10px 18px;
336
+ background: var(--surface);
337
+ border: 1px solid var(--border);
338
+ border-radius: var(--radius);
339
+ font-size: 13px;
340
+ transform: translateY(80px);
341
+ opacity: 0;
342
+ transition: transform .25s, opacity .25s;
343
+ z-index: 999;
344
+ max-width: 320px;
345
+ }
346
+ #toast.show { transform: translateY(0); opacity: 1; }
347
+ #toast.ok { border-color: rgba(52,201,122,.4); color: var(--green); }
348
+ #toast.err { border-color: rgba(224,82,82,.4); color: var(--red); }
349
+
350
+ /* ── Scrollbar ── */
351
+ ::-webkit-scrollbar { width: 6px; }
352
+ ::-webkit-scrollbar-track { background: transparent; }
353
+ ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
354
+ </style>
355
+ </head>
356
+ <body>
357
+
358
+ <header>
359
+ <span class="logo">Quorum</span>
360
+ <nav>
361
+ <button class="active" onclick="showTab('chronicle')">Chronicle</button>
362
+ <button onclick="showTab('proposals')">Proposals <span class="badge" id="proposalCount" style="display:none"></span></button>
363
+ <button onclick="showTab('coverage')">Coverage</button>
364
+ </nav>
365
+ </header>
366
+
367
+ <main>
368
+ <!-- ── Chronicle tab ────────────────────────────────────────────── -->
369
+ <div id="tab-chronicle" class="tab active">
370
+ <div class="toolbar">
371
+ <input type="search" id="chronicleSearch" placeholder="Search entries…" oninput="onSearch(this.value)" autocomplete="off">
372
+ </div>
373
+ <div id="chronicleList"><div class="loading">Loading…</div></div>
374
+ </div>
375
+
376
+ <!-- ── Proposals tab ────────────────────────────────────────────── -->
377
+ <div id="tab-proposals" class="tab">
378
+ <h2 class="section-heading">Proposals</h2>
379
+ <p class="section-sub">Review and approve Chronicle entries staged by AI agents.</p>
380
+ <div id="proposalList"><div class="loading">Loading…</div></div>
381
+ </div>
382
+
383
+ <!-- ── Coverage tab ─────────────────────────────────────────────── -->
384
+ <div id="tab-coverage" class="tab">
385
+ <h2 class="section-heading">Coverage</h2>
386
+ <p class="section-sub">Source files with Chronicle entries referencing them.</p>
387
+ <div id="coverageView"><div class="loading">Loading…</div></div>
388
+ </div>
389
+ </main>
390
+
391
+ <div id="toast"></div>
392
+
393
+ <script>
394
+ // ── State ──────────────────────────────────────────────────────────────────
395
+
396
+ let allEntries = []
397
+ let allProposals = []
398
+ let coverageData = null
399
+ let searchTimer = null
400
+ let activeTab = "chronicle"
401
+
402
+ // ── Bootstrap ─────────────────────────────────────────────────────────────
403
+
404
+ window.addEventListener("DOMContentLoaded", () => {
405
+ loadChronicle()
406
+ loadProposals()
407
+ // Lazy-load coverage when tab is first opened
408
+ })
409
+
410
+ // ── Tab switching ──────────────────────────────────────────────────────────
411
+
412
+ function showTab(name) {
413
+ document.querySelectorAll(".tab").forEach(t => t.classList.remove("active"))
414
+ document.querySelectorAll("nav button").forEach((b, i) => {
415
+ b.classList.toggle("active", ["chronicle","proposals","coverage"][i] === name)
416
+ })
417
+ document.getElementById(`tab-${name}`).classList.add("active")
418
+ activeTab = name
419
+ if (name === "coverage" && !coverageData) loadCoverage()
420
+ }
421
+
422
+ // ── Toast ──────────────────────────────────────────────────────────────────
423
+
424
+ let toastTimer
425
+ function toast(msg, type = "ok") {
426
+ const el = document.getElementById("toast")
427
+ el.textContent = msg
428
+ el.className = `show ${type}`
429
+ clearTimeout(toastTimer)
430
+ toastTimer = setTimeout(() => { el.className = "" }, 3200)
431
+ }
432
+
433
+ // ── Helpers ────────────────────────────────────────────────────────────────
434
+
435
+ function statusBadge(status) {
436
+ const cls = {
437
+ validated: "status-validated",
438
+ open: "status-open",
439
+ refuted: "status-refuted",
440
+ }[status] ?? "status-open"
441
+ return `<span class="status ${cls}">${status ?? "open"}</span>`
442
+ }
443
+
444
+ function confidenceBar(conf) {
445
+ const pct = Math.round((conf ?? 0.7) * 100)
446
+ const col = pct >= 80 ? "var(--green)" : pct >= 50 ? "var(--accent)" : "var(--yellow)"
447
+ return `<span class="confidence">
448
+ <span class="conf-bar"><span class="conf-fill" style="width:${pct}%;background:${col}"></span></span>
449
+ <span style="color:var(--muted);font-size:11px">${pct}%</span>
450
+ </span>`
451
+ }
452
+
453
+ function shortId(id) {
454
+ return (id ?? "").slice(0, 8)
455
+ }
456
+
457
+ function areas(arr) {
458
+ if (!arr?.length) return ""
459
+ return `<div class="areas">${arr.map(a => `<span class="area-tag">${esc(a)}</span>`).join("")}</div>`
460
+ }
461
+
462
+ function esc(s) {
463
+ return String(s ?? "").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;")
464
+ }
465
+
466
+ function timeAgo(iso) {
467
+ if (!iso) return ""
468
+ const diff = Date.now() - new Date(iso).getTime()
469
+ const d = Math.floor(diff / 86400000)
470
+ if (d > 30) return `${Math.floor(d/30)}mo ago`
471
+ if (d > 0) return `${d}d ago`
472
+ const h = Math.floor(diff / 3600000)
473
+ if (h > 0) return `${h}h ago`
474
+ return "just now"
475
+ }
476
+
477
+ // ── Chronicle ─────────────────────────────────────────────────────────────
478
+
479
+ async function loadChronicle(q = "") {
480
+ const url = q ? `/api/entries?q=${encodeURIComponent(q)}` : "/api/entries"
481
+ try {
482
+ const res = await fetch(url)
483
+ const data = await res.json()
484
+ allEntries = data
485
+ renderEntries(data, q)
486
+ } catch (err) {
487
+ document.getElementById("chronicleList").innerHTML =
488
+ `<div class="empty">Failed to load entries<small>${esc(err.message)}</small></div>`
489
+ }
490
+ }
491
+
492
+ function renderEntries(entries, q) {
493
+ const el = document.getElementById("chronicleList")
494
+ if (!entries.length) {
495
+ el.innerHTML = q
496
+ ? `<div class="empty">No entries matching "${esc(q)}"</div>`
497
+ : `<div class="empty">No Chronicle entries yet<small>Run <code>quorum commit</code> to index a proposal</small></div>`
498
+ return
499
+ }
500
+ el.innerHTML = entries.map(e => `
501
+ <div class="card">
502
+ <div class="card-header">
503
+ <div class="card-title">${esc(e.topic ?? e.decision ?? e.key_insight)}</div>
504
+ ${statusBadge(e.status)}
505
+ </div>
506
+ <div class="card-body">${esc(e.decision ?? e.key_insight ?? "")}</div>
507
+ ${areas(e.affected_areas)}
508
+ <div class="card-meta" style="margin-top:10px">
509
+ <span style="font-family:var(--mono);font-size:11px;color:var(--muted)">${shortId(e.id)}</span>
510
+ ${confidenceBar(e.confidence)}
511
+ <span>${timeAgo(e.timestamp)}</span>
512
+ ${e.work_ref?.ref ? `<span>${esc(e.work_ref.ref)}</span>` : ""}
513
+ </div>
514
+ </div>
515
+ `).join("")
516
+ }
517
+
518
+ function onSearch(val) {
519
+ clearTimeout(searchTimer)
520
+ searchTimer = setTimeout(() => loadChronicle(val.trim()), 240)
521
+ }
522
+
523
+ // ── Proposals ────────────────────────────────────────────────────────────
524
+
525
+ async function loadProposals() {
526
+ try {
527
+ const res = await fetch("/api/proposals")
528
+ allProposals = await res.json()
529
+ renderProposals(allProposals)
530
+ const count = allProposals.length
531
+ const badge = document.getElementById("proposalCount")
532
+ if (count > 0) {
533
+ badge.textContent = count
534
+ badge.style.display = "inline-block"
535
+ } else {
536
+ badge.style.display = "none"
537
+ }
538
+ } catch (err) {
539
+ document.getElementById("proposalList").innerHTML =
540
+ `<div class="empty">Failed to load proposals<small>${esc(err.message)}</small></div>`
541
+ }
542
+ }
543
+
544
+ function renderProposals(proposals) {
545
+ const el = document.getElementById("proposalList")
546
+ if (!proposals.length) {
547
+ el.innerHTML = `<div class="empty">No pending proposals<small>AI agents stage proposals via <code>chronicle_propose</code> or <code>quorum commit --list</code></small></div>`
548
+ return
549
+ }
550
+ el.innerHTML = proposals.map(p => `
551
+ <div class="card" id="proposal-${esc(p.proposalId)}">
552
+ <div class="card-header">
553
+ <div class="card-title">${esc(p.topic)}</div>
554
+ <span class="status status-pending">pending</span>
555
+ </div>
556
+ <div class="card-body">${esc(p.decision ?? p.key_insight ?? "")}</div>
557
+ ${areas(p.affected_areas)}
558
+ <div class="card-meta" style="margin-top:10px">
559
+ <span style="font-family:var(--mono);font-size:11px;color:var(--muted)">${esc(p.proposalId?.slice(0,8))}</span>
560
+ ${confidenceBar(p.confidence)}
561
+ </div>
562
+ <div class="actions">
563
+ <button class="btn btn-approve" onclick="approveProposal('${esc(p.proposalId)}', this)">✓ Approve</button>
564
+ <button class="btn btn-reject" onclick="rejectProposal('${esc(p.proposalId)}', this)">✕ Reject</button>
565
+ </div>
566
+ </div>
567
+ `).join("")
568
+ }
569
+
570
+ async function approveProposal(id, btn) {
571
+ btn.disabled = true
572
+ btn.textContent = "Approving…"
573
+ try {
574
+ const res = await fetch(`/api/proposals/${encodeURIComponent(id)}/commit`, { method: "POST" })
575
+ if (!res.ok) throw new Error((await res.json()).error)
576
+ const card = document.getElementById(`proposal-${id}`)
577
+ card.style.opacity = "0.4"
578
+ card.style.pointerEvents = "none"
579
+ setTimeout(() => { card.remove(); loadProposals() }, 600)
580
+ toast("Proposal approved and committed")
581
+ } catch (err) {
582
+ btn.disabled = false
583
+ btn.textContent = "✓ Approve"
584
+ toast(err.message, "err")
585
+ }
586
+ }
587
+
588
+ async function rejectProposal(id, btn) {
589
+ if (!confirm("Delete this proposal? This cannot be undone.")) return
590
+ btn.disabled = true
591
+ btn.textContent = "Deleting…"
592
+ try {
593
+ const res = await fetch(`/api/proposals/${encodeURIComponent(id)}`, { method: "DELETE" })
594
+ if (!res.ok) throw new Error((await res.json()).error)
595
+ const card = document.getElementById(`proposal-${id}`)
596
+ card.style.opacity = "0.4"
597
+ setTimeout(() => { card.remove(); loadProposals() }, 400)
598
+ toast("Proposal deleted")
599
+ } catch (err) {
600
+ btn.disabled = false
601
+ btn.textContent = "✕ Reject"
602
+ toast(err.message, "err")
603
+ }
604
+ }
605
+
606
+ // ── Coverage ──────────────────────────────────────────────────────────────
607
+
608
+ async function loadCoverage() {
609
+ document.getElementById("coverageView").innerHTML = `<div class="loading">Scanning files…</div>`
610
+ try {
611
+ const res = await fetch("/api/coverage")
612
+ coverageData = await res.json()
613
+ renderCoverage(coverageData)
614
+ } catch (err) {
615
+ document.getElementById("coverageView").innerHTML =
616
+ `<div class="empty">Failed to load coverage<small>${esc(err.message)}</small></div>`
617
+ }
618
+ }
619
+
620
+ function renderCoverage(data) {
621
+ const { percentage, totalFiles, coveredFiles, coverageByFile } = data
622
+ const covered = coverageByFile.filter(f => f.covered)
623
+ const uncovered = coverageByFile.filter(f => !f.covered)
624
+
625
+ const colorPct = percentage >= 70 ? "var(--green)" : percentage >= 40 ? "var(--accent)" : "var(--yellow)"
626
+
627
+ document.getElementById("coverageView").innerHTML = `
628
+ <div class="coverage-header">
629
+ <svg class="pct-ring" viewBox="0 0 72 72">
630
+ <circle cx="36" cy="36" r="28" fill="none" stroke="var(--border)" stroke-width="7"/>
631
+ <circle cx="36" cy="36" r="28" fill="none" stroke="${colorPct}" stroke-width="7"
632
+ stroke-dasharray="${2*Math.PI*28}"
633
+ stroke-dashoffset="${2*Math.PI*28 * (1 - percentage/100)}"
634
+ stroke-linecap="round"
635
+ transform="rotate(-90 36 36)"/>
636
+ <text x="36" y="41" text-anchor="middle" fill="${colorPct}" font-size="14" font-weight="700" font-family="var(--font)">${percentage}%</text>
637
+ </svg>
638
+ <div class="coverage-bar-wrap">
639
+ <div class="cov-bar"><div class="cov-fill" style="width:${percentage}%"></div></div>
640
+ <div class="cov-stats">
641
+ <span><strong>${coveredFiles}</strong> covered</span>
642
+ <span><strong>${totalFiles - coveredFiles}</strong> uncovered</span>
643
+ <span><strong>${totalFiles}</strong> total files</span>
644
+ </div>
645
+ </div>
646
+ </div>
647
+
648
+ ${covered.length ? `
649
+ <div class="cov-section">
650
+ <div class="cov-section-title">Covered — ${covered.length} files</div>
651
+ ${covered.map(f => `
652
+ <div class="file-row">
653
+ <span class="file-dot dot-green"></span>
654
+ <span class="file-name">${esc(f.file)}</span>
655
+ <span class="file-entries">${f.entryIds.length} ${f.entryIds.length === 1 ? "entry" : "entries"}</span>
656
+ </div>
657
+ `).join("")}
658
+ </div>` : ""}
659
+
660
+ ${uncovered.length ? `
661
+ <div class="cov-section">
662
+ <div class="cov-section-title">Uncovered — ${uncovered.length} files</div>
663
+ ${uncovered.map(f => `
664
+ <div class="file-row">
665
+ <span class="file-dot dot-red"></span>
666
+ <span class="file-name">${esc(f.file)}</span>
667
+ </div>
668
+ `).join("")}
669
+ </div>` : ""}
670
+
671
+ ${totalFiles === 0 ? `<div class="empty">No source files found</div>` : ""}
672
+ `
673
+ }
674
+ </script>
675
+ </body>
676
+ </html>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@balpal4495/quorum",
3
- "version": "3.3.2",
3
+ "version": "3.4.0",
4
4
  "description": "Git-backed memory and design review for AI coding agents",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -43,8 +43,8 @@
43
43
  },
44
44
  "scripts": {
45
45
  "build": "tsc -p tsconfig.build.json",
46
- "test": "vitest run modules/ evals/",
47
- "test:watch": "vitest modules/",
46
+ "test": "vitest run modules/ evals/ bin/",
47
+ "test:watch": "vitest modules/ bin/",
48
48
  "typecheck": "tsc --noEmit"
49
49
  },
50
50
  "dependencies": {