@xera-ai/core 0.9.8 → 0.11.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.
@@ -11,6 +11,10 @@
11
11
  <div class="logo"></div>
12
12
  <span class="title">xera graph</span>
13
13
  </div>
14
+ <nav class="toplevel-tabs">
15
+ <button data-tab="knowledge" class="active">Knowledge</button>
16
+ {{COVERAGE_TAB_BUTTON}}
17
+ </nav>
14
18
  <div id="stats-bar" data-stats="{{STATS}}"></div>
15
19
  <div class="topbar-controls">
16
20
  <input type="text" id="search" placeholder="search nodes…" autocomplete="off" />
@@ -23,21 +27,25 @@
23
27
  </div>
24
28
  </header>
25
29
  <div id="progress-wrap"><div id="progress-bar"></div></div>
26
- <main id="canvas"></main>
27
- <aside id="sidepanel" class="hidden">
28
- <div class="sp-header">
29
- <div id="sp-group" class="sp-group-badge"></div>
30
- <p id="sp-title" class="sp-title"></p>
31
- </div>
32
- <div id="sp-desc" class="sp-desc"></div>
33
- <div id="sp-actions" class="sp-actions"></div>
34
- </aside>
30
+ <section data-tab-panel="knowledge" class="active">
31
+ <main id="canvas"></main>
32
+ <aside id="sidepanel" class="hidden">
33
+ <div class="sp-header">
34
+ <div id="sp-group" class="sp-group-badge"></div>
35
+ <p id="sp-title" class="sp-title"></p>
36
+ </div>
37
+ <div id="sp-desc" class="sp-desc"></div>
38
+ <div id="sp-actions" class="sp-actions"></div>
39
+ </aside>
40
+ </section>
41
+ {{COVERAGE_TAB_PANEL}}
35
42
  <footer id="footer">
36
43
  generated {{GENERATED_AT}}
37
44
  <span>· scroll to zoom · drag to pan · click to inspect</span>
38
45
  </footer>
39
46
  <script>{{VIS_NETWORK_JS}}</script>
40
47
  <script>window.__GRAPH__ = {{GRAPH_DATA}};</script>
48
+ <script>window.__COVERAGE__ = {{COVERAGE_DATA}};</script>
41
49
  <script>{{INTERACTION_JS}}</script>
42
50
  </body>
43
51
  </html>
@@ -316,3 +316,188 @@
316
316
  };
317
317
  });
318
318
  })();
319
+
320
+ // v0.8.1 — top-level tab switching
321
+ (function setupTabs() {
322
+ const tabButtons = document.querySelectorAll('.toplevel-tabs button');
323
+ if (!tabButtons.length) return;
324
+ tabButtons.forEach((btn) => {
325
+ btn.addEventListener('click', () => {
326
+ tabButtons.forEach((b) => {
327
+ b.classList.remove('active');
328
+ });
329
+ btn.classList.add('active');
330
+ const tab = btn.getAttribute('data-tab');
331
+ document.querySelectorAll('[data-tab-panel]').forEach((panel) => {
332
+ if (panel.getAttribute('data-tab-panel') === tab) {
333
+ panel.classList.add('active');
334
+ panel.removeAttribute('hidden');
335
+ } else {
336
+ panel.classList.remove('active');
337
+ }
338
+ });
339
+ if (tab === 'coverage' && window.__COVERAGE__) {
340
+ renderCoverageOnce();
341
+ }
342
+ });
343
+ });
344
+ })();
345
+
346
+ // v0.8.1 — coverage subtab switching
347
+ (function setupSubtabs() {
348
+ const subButtons = document.querySelectorAll('.subtabs button');
349
+ subButtons.forEach((btn) => {
350
+ btn.addEventListener('click', () => {
351
+ subButtons.forEach((b) => {
352
+ b.classList.remove('active');
353
+ });
354
+ btn.classList.add('active');
355
+ const sub = btn.getAttribute('data-subtab');
356
+ document.querySelectorAll('[data-subpanel]').forEach((panel) => {
357
+ if (panel.getAttribute('data-subpanel') === sub) {
358
+ panel.removeAttribute('hidden');
359
+ panel.classList.add('active');
360
+ } else {
361
+ panel.setAttribute('hidden', '');
362
+ panel.classList.remove('active');
363
+ }
364
+ });
365
+ });
366
+ });
367
+ })();
368
+
369
+ let _coverageRendered = false;
370
+ function renderCoverageOnce() {
371
+ if (_coverageRendered) return;
372
+ _coverageRendered = true;
373
+ renderCoverageList();
374
+ renderCoverageTrend();
375
+ renderCoverageMap();
376
+ }
377
+
378
+ // Task 27 — coverage map: area color overlay
379
+ function renderCoverageMap() {
380
+ const cov = window.__COVERAGE__;
381
+ if (!cov || !window.__GRAPH__) return;
382
+ const canvas = document.getElementById('coverage-map-canvas');
383
+ if (!canvas) return;
384
+
385
+ const STATUS_COLOR = {
386
+ UNCOVERED: { background: '#fca5a5', border: '#dc2626' },
387
+ STALE: { background: '#fcd34d', border: '#d97706' },
388
+ COVERED: { background: '#86efac', border: '#15803d' },
389
+ };
390
+ const NEUTRAL = { background: '#e5e7eb', border: '#9ca3af' };
391
+
392
+ const areaStatusById = {};
393
+ for (const a of cov.report.areas) {
394
+ areaStatusById[a.id] = a.status;
395
+ }
396
+
397
+ const mappedNodes = window.__GRAPH__.nodes.map((n) => {
398
+ if (n.group === 'SUTArea' && areaStatusById[n.id]) {
399
+ return Object.assign({}, n, { color: STATUS_COLOR[areaStatusById[n.id]] });
400
+ }
401
+ if (n.group !== 'SUTArea') return Object.assign({}, n, { color: NEUTRAL });
402
+ return n;
403
+ });
404
+
405
+ new vis.Network(
406
+ canvas,
407
+ { nodes: new vis.DataSet(mappedNodes), edges: new vis.DataSet(window.__GRAPH__.edges) },
408
+ {
409
+ physics: { enabled: true, stabilization: { iterations: 100 } },
410
+ nodes: { shape: 'dot', font: { size: 11 } },
411
+ },
412
+ );
413
+ }
414
+
415
+ // Task 28 — coverage list: sortable area + AC gap tables
416
+ function renderCoverageList() {
417
+ const cov = window.__COVERAGE__;
418
+ if (!cov) return;
419
+ const listBody = document.querySelector('#coverage-list-table tbody');
420
+ if (listBody) {
421
+ listBody.innerHTML = '';
422
+ for (const a of cov.report.areas) {
423
+ const tr = document.createElement('tr');
424
+ tr.classList.add(`status-${a.status.toLowerCase()}`);
425
+ const cells = [
426
+ a.status,
427
+ a.id,
428
+ String(a.risk),
429
+ String(a.breakdown.recentTickets),
430
+ String(a.breakdown.recentBugs),
431
+ ];
432
+ for (const c of cells) {
433
+ const td = document.createElement('td');
434
+ td.textContent = c;
435
+ tr.appendChild(td);
436
+ }
437
+ listBody.appendChild(tr);
438
+ }
439
+ }
440
+
441
+ const acBody = document.querySelector('#coverage-ac-table tbody');
442
+ if (acBody) {
443
+ acBody.innerHTML = '';
444
+ for (const t of cov.report.tickets) {
445
+ const tr = document.createElement('tr');
446
+ const cells = [
447
+ t.id,
448
+ `${t.satisfiedCount}/${t.acCount}`,
449
+ String(t.gapScore),
450
+ t.unsatisfiedAcs.map((ac) => `AC-${ac.index}`).join(', '),
451
+ ];
452
+ for (const c of cells) {
453
+ const td = document.createElement('td');
454
+ td.textContent = c;
455
+ tr.appendChild(td);
456
+ }
457
+ acBody.appendChild(tr);
458
+ }
459
+ }
460
+ }
461
+
462
+ // Task 29 — coverage trend: inline SVG line chart
463
+ function renderCoverageTrend() {
464
+ const cov = window.__COVERAGE__;
465
+ if (!cov) return;
466
+ const container = document.getElementById('coverage-trend-svg');
467
+ if (!container) return;
468
+
469
+ // Dedup by day (latest snapshot per day wins), sort asc.
470
+ const byDay = {};
471
+ for (const s of cov.snapshots) {
472
+ const day = s.ts.slice(0, 10);
473
+ byDay[day] = s;
474
+ }
475
+ const days = Object.keys(byDay).sort();
476
+ if (days.length === 0) {
477
+ container.innerHTML =
478
+ '<p class="subpanel-hint">No snapshots yet — run /xera-coverage on multiple days to build a trend.</p>';
479
+ return;
480
+ }
481
+
482
+ const points = days.map((d) => {
483
+ const snap = byDay[d];
484
+ const n = snap.areas.filter((a) => a.status === 'UNCOVERED' || a.status === 'STALE').length;
485
+ return { day: d, value: n };
486
+ });
487
+ const W = 800;
488
+ const H = 200;
489
+ const PAD = 30;
490
+ const maxValue = Math.max(...points.map((p) => p.value), 1);
491
+ const stepX = points.length > 1 ? (W - 2 * PAD) / (points.length - 1) : 0;
492
+ const path = points
493
+ .map((p, idx) => {
494
+ const x = PAD + idx * stepX;
495
+ const y = H - PAD - (p.value / maxValue) * (H - 2 * PAD);
496
+ return `${idx === 0 ? 'M' : 'L'}${x},${y}`;
497
+ })
498
+ .join(' ');
499
+
500
+ const labelFirst = points[0].day;
501
+ const labelLast = points[points.length - 1].day;
502
+ container.innerHTML = `<svg viewBox="0 0 ${W} ${H}" xmlns="http://www.w3.org/2000/svg"><path d="${path}" fill="none" stroke="#dc2626" stroke-width="2"/><text x="${PAD}" y="${H - 8}" font-size="11" fill="#6b7280">${labelFirst}</text><text x="${W - PAD - 60}" y="${H - 8}" font-size="11" fill="#6b7280">${labelLast}</text><text x="${PAD - 22}" y="${PAD - 4}" font-size="11" fill="#6b7280">${maxValue}</text></svg>`;
503
+ }
@@ -4,7 +4,15 @@ export const SCHEMA_VERSION = 1 as const;
4
4
 
5
5
  export type Priority = 'p0' | 'p1' | 'p2';
6
6
  export type ScenarioStatus = 'pass' | 'fail';
7
- export type EdgeKind = 'tests' | 'uses' | 'covers' | 'modifies' | 'jira-linked' | 'similar' | 'ran';
7
+ export type EdgeKind =
8
+ | 'tests'
9
+ | 'uses'
10
+ | 'covers'
11
+ | 'modifies'
12
+ | 'jira-linked'
13
+ | 'similar'
14
+ | 'ran'
15
+ | 'satisfies';
8
16
 
9
17
  export type Classification =
10
18
  | 'REAL_BUG'
@@ -43,6 +51,7 @@ export interface ScenarioGeneratedPayload {
43
51
  priority: Priority;
44
52
  featureHash: string;
45
53
  generatedAt: string;
54
+ satisfiesAcs?: number[]; // NEW v0.8: AC indices (0-based) this scenario asserts
46
55
  }
47
56
 
48
57
  export interface PomGeneratedPayload {
@@ -93,6 +102,37 @@ export interface EdgeDiscoveredPayload {
93
102
  source: string;
94
103
  }
95
104
 
105
+ export interface CoverageSnapshotPayload {
106
+ ts: string; // ISO8601
107
+ windowDays: number;
108
+ areas: Array<{
109
+ id: string;
110
+ status: 'UNCOVERED' | 'STALE' | 'COVERED';
111
+ risk: number;
112
+ breakdown: {
113
+ recentTickets: number;
114
+ recentBugs: number;
115
+ criticalBoost: 1 | 2;
116
+ };
117
+ }>;
118
+ tickets: Array<{
119
+ id: string;
120
+ acCount: number;
121
+ satisfiedCount: number;
122
+ gapScore: number;
123
+ }>;
124
+ }
125
+
126
+ export interface AcCoverageBackfilledPayload {
127
+ ts: string;
128
+ ticketId: string;
129
+ mappings: Array<{
130
+ scenarioId: string;
131
+ satisfiesAcs: number[];
132
+ confidence: number;
133
+ }>;
134
+ }
135
+
96
136
  export type EventPayloadMap = {
97
137
  'ticket.fetched': TicketFetchedPayload;
98
138
  'ticket.enriched': TicketEnrichedPayload;
@@ -103,6 +143,8 @@ export type EventPayloadMap = {
103
143
  'run.classified': RunClassifiedPayload;
104
144
  'classification.disputed': ClassificationDisputedPayload;
105
145
  'edge.discovered': EdgeDiscoveredPayload;
146
+ 'coverage.snapshot': CoverageSnapshotPayload; // NEW
147
+ 'ac-coverage.backfilled': AcCoverageBackfilledPayload; // NEW
106
148
  };
107
149
 
108
150
  export type EventType = keyof EventPayloadMap;
@@ -151,6 +193,13 @@ export interface AreaNode {
151
193
  id: string;
152
194
  }
153
195
 
196
+ export interface ACNode {
197
+ id: string; // `${ticketId}#ac-${index}` (0-based)
198
+ ticketId: string;
199
+ index: number;
200
+ text: string;
201
+ }
202
+
154
203
  export interface FailureNode {
155
204
  id: string;
156
205
  scenarioId: string;
@@ -180,4 +229,10 @@ export interface Snapshot {
180
229
  areas: Record<string, AreaNode>;
181
230
  edges: EdgeRecord[];
182
231
  latest_failures: Record<string, FailureNode>;
232
+ acNodes: Record<string, ACNode>; // NEW v0.8
233
+ classifications: Array<{
234
+ scenarioId: string;
235
+ classification: Classification;
236
+ ts: string;
237
+ }>; // NEW v0.8
183
238
  }