@xera-ai/core 0.9.7 → 0.10.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.
Files changed (34) hide show
  1. package/dist/bin/internal.js +1415 -534
  2. package/dist/bin/templates/LICENSE-vis-network.txt +2 -0
  3. package/dist/bin/templates/coverage-panel.html.fragment +20 -0
  4. package/dist/bin/templates/graph.css +379 -43
  5. package/dist/bin/templates/graph.html.template +35 -15
  6. package/dist/bin/templates/graph.js +458 -56
  7. package/dist/bin/templates/vis-network.min.js +3 -24976
  8. package/dist/src/index.js +6 -0
  9. package/package.json +3 -3
  10. package/src/bin-internal/ac-coverage-backfill-finalize.ts +90 -0
  11. package/src/bin-internal/ac-coverage-backfill-prepare.ts +72 -0
  12. package/src/bin-internal/coverage-prepare.ts +123 -0
  13. package/src/bin-internal/fill-gap-finalize.ts +115 -0
  14. package/src/bin-internal/fill-gap-prepare.ts +150 -0
  15. package/src/bin-internal/graph-render.ts +32 -4
  16. package/src/bin-internal/index.ts +10 -0
  17. package/src/bin-internal/verify-prompts.ts +2 -0
  18. package/src/config/schema.ts +9 -0
  19. package/src/coverage/index.ts +29 -0
  20. package/src/coverage/report.ts +206 -0
  21. package/src/coverage/risk.ts +69 -0
  22. package/src/coverage/status.ts +76 -0
  23. package/src/coverage/types.ts +11 -0
  24. package/src/coverage/why.ts +122 -0
  25. package/src/graph/render.ts +16 -2
  26. package/src/graph/schema.ts +54 -1
  27. package/src/graph/store.ts +96 -6
  28. package/src/graph/templates/LICENSE-vis-network.txt +2 -0
  29. package/src/graph/templates/coverage-panel.html.fragment +20 -0
  30. package/src/graph/templates/graph.css +379 -43
  31. package/src/graph/templates/graph.html.template +35 -15
  32. package/src/graph/templates/graph.js +458 -56
  33. package/src/graph/templates/vis-network.min.js +3 -24976
  34. package/src/graph/types.ts +56 -1
@@ -11,6 +11,8 @@ import { dirname } from 'node:path';
11
11
  import { currentYyyyMm, graphPaths } from './paths';
12
12
  import { safeParseEvent } from './schema';
13
13
  import type {
14
+ ACNode,
15
+ Classification,
14
16
  EdgeRecord,
15
17
  Event,
16
18
  FailureNode,
@@ -98,12 +100,19 @@ export function deriveSnapshot(events: Event[]): Snapshot {
98
100
  const areas: Record<string, { id: string }> = {};
99
101
  const edges: EdgeRecord[] = [];
100
102
  const latestFailures: Record<string, FailureNode> = {};
103
+ const acNodes: Record<string, ACNode> = {};
104
+ const classifications: Array<{
105
+ scenarioId: string;
106
+ classification: Classification;
107
+ ts: string;
108
+ }> = [];
101
109
 
102
110
  for (const e of events) {
103
111
  switch (e.type) {
104
- case 'ticket.fetched':
105
- tickets[e.payload.ticketId] = {
106
- id: e.payload.ticketId,
112
+ case 'ticket.fetched': {
113
+ const tid = e.payload.ticketId;
114
+ tickets[tid] = {
115
+ id: tid,
107
116
  summary: e.payload.summary,
108
117
  ac: e.payload.ac,
109
118
  storyHash: e.payload.storyHash,
@@ -114,18 +123,33 @@ export function deriveSnapshot(events: Event[]): Snapshot {
114
123
  for (const link of e.payload.jiraLinks) {
115
124
  edges.push({
116
125
  kind: 'jira-linked',
117
- from: e.payload.ticketId,
126
+ from: tid,
118
127
  to: link.ticketId,
119
128
  source: `jira:${link.relation}`,
120
129
  discoveredAt: e.ts,
121
130
  });
122
131
  }
132
+ // NEW v0.8: drop prior ACNodes for this ticket, materialize fresh
133
+ for (const acId of Object.keys(acNodes)) {
134
+ if (acNodes[acId]?.ticketId === tid) delete acNodes[acId];
135
+ }
136
+ e.payload.ac.forEach((text, index) => {
137
+ const acId = `${tid}#ac-${index}`;
138
+ acNodes[acId] = { id: acId, ticketId: tid, index, text };
139
+ });
140
+ // Prune satisfies edges that target ACNodes no longer present
141
+ for (let i = edges.length - 1; i >= 0; i--) {
142
+ const ed = edges[i]!;
143
+ if (ed.kind !== 'satisfies') continue;
144
+ if (acNodes[ed.to] === undefined) edges.splice(i, 1);
145
+ }
123
146
  break;
147
+ }
124
148
  case 'ticket.enriched':
125
149
  if (tickets[e.payload.ticketId])
126
150
  tickets[e.payload.ticketId]!.enrichedAt = e.payload.enrichedAt;
127
151
  break;
128
- case 'scenario.generated':
152
+ case 'scenario.generated': {
129
153
  scenarios[e.payload.scenarioId] = {
130
154
  id: e.payload.scenarioId,
131
155
  ticketId: e.payload.ticketId,
@@ -142,7 +166,33 @@ export function deriveSnapshot(events: Event[]): Snapshot {
142
166
  source: 'xera-script',
143
167
  discoveredAt: e.ts,
144
168
  });
169
+ // NEW v0.8: drop prior eager satisfies edges for this scenario, then emit fresh
170
+ if (e.payload.satisfiesAcs && e.payload.satisfiesAcs.length > 0) {
171
+ for (let i = edges.length - 1; i >= 0; i--) {
172
+ const ed = edges[i]!;
173
+ if (
174
+ ed.kind === 'satisfies' &&
175
+ ed.from === e.payload.scenarioId &&
176
+ ed.source === 'xera-script'
177
+ ) {
178
+ edges.splice(i, 1);
179
+ }
180
+ }
181
+ for (const acIdx of e.payload.satisfiesAcs) {
182
+ const acId = `${e.payload.ticketId}#ac-${acIdx}`;
183
+ if (acNodes[acId] === undefined) continue;
184
+ edges.push({
185
+ kind: 'satisfies',
186
+ from: e.payload.scenarioId,
187
+ to: acId,
188
+ confidence: 1.0,
189
+ source: 'xera-script',
190
+ discoveredAt: e.ts,
191
+ });
192
+ }
193
+ }
145
194
  break;
195
+ }
146
196
  case 'pom.generated':
147
197
  poms[e.payload.pomId] = {
148
198
  id: e.payload.pomId,
@@ -192,7 +242,45 @@ export function deriveSnapshot(events: Event[]): Snapshot {
192
242
  }
193
243
  break;
194
244
  }
195
- // run.classified: not materialized in snapshot
245
+ case 'run.classified':
246
+ classifications.push({
247
+ scenarioId: e.payload.scenarioId,
248
+ classification: e.payload.classification,
249
+ ts: e.ts,
250
+ });
251
+ break;
252
+ case 'ac-coverage.backfilled': {
253
+ const { ts, ticketId, mappings } = e.payload;
254
+ // Remove prior backfill edges for this ticket (idempotent)
255
+ for (let i = edges.length - 1; i >= 0; i--) {
256
+ const ed = edges[i]!;
257
+ if (
258
+ ed.kind === 'satisfies' &&
259
+ ed.source === 'ac-coverage' &&
260
+ ed.to.startsWith(`${ticketId}#ac-`)
261
+ ) {
262
+ edges.splice(i, 1);
263
+ }
264
+ }
265
+ for (const m of mappings) {
266
+ for (const acIdx of m.satisfiesAcs) {
267
+ const acId = `${ticketId}#ac-${acIdx}`;
268
+ if (acNodes[acId] === undefined) continue;
269
+ edges.push({
270
+ kind: 'satisfies',
271
+ from: m.scenarioId,
272
+ to: acId,
273
+ confidence: m.confidence,
274
+ source: 'ac-coverage',
275
+ discoveredAt: ts,
276
+ });
277
+ }
278
+ }
279
+ break;
280
+ }
281
+ case 'coverage.snapshot':
282
+ // Read-side only — Trend tab queries these events directly from JSONL.
283
+ break;
196
284
  default:
197
285
  break;
198
286
  }
@@ -209,6 +297,8 @@ export function deriveSnapshot(events: Event[]): Snapshot {
209
297
  areas,
210
298
  edges,
211
299
  latest_failures: latestFailures,
300
+ acNodes,
301
+ classifications,
212
302
  };
213
303
  }
214
304
 
@@ -1,3 +1,5 @@
1
+ vis-network v9.1.6 — https://visjs.github.io/vis-network/
2
+
1
3
  Apache License
2
4
  Version 2.0, January 2004
3
5
  http://www.apache.org/licenses/
@@ -0,0 +1,20 @@
1
+ <section data-tab-panel="coverage" hidden>
2
+ <nav class="subtabs">
3
+ <button data-subtab="map" class="active">Map</button>
4
+ <button data-subtab="list">List</button>
5
+ <button data-subtab="trend">Trend</button>
6
+ </nav>
7
+ <div data-subpanel="map" class="active">
8
+ <p class="subpanel-hint">Area nodes are colored by status. Red = UNCOVERED, amber = STALE, green = COVERED. Other nodes neutral.</p>
9
+ <main id="coverage-map-canvas"></main>
10
+ </div>
11
+ <div data-subpanel="list" hidden>
12
+ <table id="coverage-list-table"><thead><tr><th>Status</th><th>Area</th><th>Risk</th><th>Recent tickets</th><th>Recent bugs</th></tr></thead><tbody></tbody></table>
13
+ <h3>AC Gaps</h3>
14
+ <table id="coverage-ac-table"><thead><tr><th>Ticket</th><th>Coverage</th><th>Gap</th><th>Unsatisfied</th></tr></thead><tbody></tbody></table>
15
+ </div>
16
+ <div data-subpanel="trend" hidden>
17
+ <p class="subpanel-hint">UNCOVERED + STALE area count over time (one point per day, latest snapshot wins).</p>
18
+ <div id="coverage-trend-svg"></div>
19
+ </div>
20
+ </section>
@@ -1,88 +1,424 @@
1
1
  * {
2
2
  box-sizing: border-box;
3
3
  }
4
+
4
5
  body {
5
6
  margin: 0;
6
7
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
7
8
  height: 100vh;
9
+ background: #050810;
8
10
  display: grid;
9
- grid-template-rows: 56px 1fr 32px;
10
- grid-template-columns: 1fr 320px;
11
+ grid-template-rows: 52px 1fr 26px;
12
+ grid-template-columns: 1fr 280px;
11
13
  }
14
+
15
+ /* ─── Topbar ─────────────────────────────────────── */
12
16
  #topbar {
13
17
  grid-column: 1 / -1;
14
18
  display: flex;
15
19
  align-items: center;
16
- gap: 16px;
20
+ gap: 0;
17
21
  padding: 0 16px;
18
- background: #1f2937;
19
- color: #f9fafb;
20
- border-bottom: 1px solid #374151;
22
+ background: #080c14;
23
+ border-bottom: 1px solid #1a2035;
21
24
  }
22
- #topbar .title {
23
- font-weight: 600;
24
- font-size: 16px;
25
+
26
+ .topbar-brand {
27
+ display: flex;
28
+ align-items: center;
29
+ gap: 8px;
30
+ padding-right: 20px;
31
+ border-right: 1px solid #1a2035;
32
+ margin-right: 16px;
25
33
  }
26
- #topbar .stats {
27
- color: #9ca3af;
34
+
35
+ .topbar-brand .logo {
36
+ width: 18px;
37
+ height: 18px;
38
+ border-radius: 4px;
39
+ background: linear-gradient(135deg, #3b82f6 0%, #6366f1 100%);
40
+ display: flex;
41
+ align-items: center;
42
+ justify-content: center;
43
+ flex-shrink: 0;
44
+ }
45
+
46
+ .topbar-brand .logo::after {
47
+ content: "";
48
+ display: block;
49
+ width: 6px;
50
+ height: 6px;
51
+ border-radius: 50%;
52
+ background: white;
53
+ opacity: 0.9;
54
+ }
55
+
56
+ .topbar-brand .title {
57
+ font-weight: 600;
28
58
  font-size: 13px;
59
+ color: #e2e8f0;
60
+ letter-spacing: -0.01em;
61
+ }
62
+
63
+ #stats-bar {
64
+ display: flex;
65
+ gap: 6px;
66
+ align-items: center;
29
67
  flex: 1;
30
68
  }
31
- #topbar .filters {
69
+
70
+ .stat-chip {
71
+ display: inline-flex;
72
+ align-items: center;
73
+ gap: 5px;
74
+ font-size: 11px;
75
+ font-weight: 500;
76
+ color: #64748b;
77
+ background: #0f1624;
78
+ border: 1px solid #1e2d45;
79
+ border-radius: 20px;
80
+ padding: 2px 9px;
81
+ line-height: 1.6;
82
+ transition:
83
+ color 0.15s,
84
+ border-color 0.15s;
85
+ }
86
+
87
+ .stat-chip .dot {
88
+ width: 6px;
89
+ height: 6px;
90
+ border-radius: 50%;
91
+ flex-shrink: 0;
92
+ }
93
+
94
+ .topbar-controls {
32
95
  display: flex;
33
96
  gap: 8px;
34
97
  align-items: center;
35
- font-size: 13px;
98
+ padding-left: 16px;
99
+ border-left: 1px solid #1a2035;
36
100
  }
37
- #topbar input[type="text"] {
38
- background: #111827;
39
- color: #f9fafb;
40
- border: 1px solid #374151;
41
- border-radius: 4px;
42
- padding: 4px 8px;
101
+
102
+ #search {
103
+ background: #0f1624;
104
+ color: #c9d1d9;
105
+ border: 1px solid #1e2d45;
106
+ border-radius: 6px;
107
+ padding: 4px 10px 4px 28px;
43
108
  width: 160px;
109
+ font-size: 12px;
110
+ outline: none;
111
+ transition:
112
+ border-color 0.15s,
113
+ box-shadow 0.15s;
114
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%234b5563' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='8'/%3E%3Cline x1='21' y1='21' x2='16.65' y2='16.65'/%3E%3C/svg%3E");
115
+ background-repeat: no-repeat;
116
+ background-position: 10px center;
44
117
  }
45
- #topbar button {
46
- background: #374151;
47
- color: #f9fafb;
48
- border: none;
118
+
119
+ #search:focus {
120
+ border-color: #3b82f6;
121
+ box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.15);
122
+ }
123
+
124
+ #search::placeholder {
125
+ color: #374151;
126
+ }
127
+
128
+ .filter-group {
129
+ display: flex;
130
+ gap: 2px;
131
+ background: #0f1624;
132
+ border: 1px solid #1e2d45;
133
+ border-radius: 6px;
134
+ padding: 3px;
135
+ }
136
+
137
+ .filter-group label {
138
+ cursor: pointer;
139
+ display: flex;
140
+ align-items: center;
141
+ gap: 4px;
142
+ font-size: 11px;
143
+ font-weight: 500;
144
+ color: #4b5563;
145
+ padding: 2px 7px;
49
146
  border-radius: 4px;
50
- padding: 4px 12px;
147
+ transition:
148
+ background 0.1s,
149
+ color 0.1s;
150
+ user-select: none;
151
+ }
152
+
153
+ .filter-group label:hover {
154
+ background: #1a2540;
155
+ color: #9ca3af;
156
+ }
157
+
158
+ .filter-group label input {
159
+ display: none;
160
+ }
161
+
162
+ .filter-group label.active {
163
+ color: #e2e8f0;
164
+ background: #1a2540;
165
+ }
166
+
167
+ .filter-group label .indicator {
168
+ width: 6px;
169
+ height: 6px;
170
+ border-radius: 2px;
171
+ }
172
+
173
+ #reset-btn {
174
+ background: transparent;
175
+ color: #4b5563;
176
+ border: 1px solid #1e2d45;
177
+ border-radius: 6px;
178
+ padding: 4px 10px;
51
179
  cursor: pointer;
180
+ font-size: 11px;
181
+ font-weight: 500;
182
+ transition:
183
+ background 0.15s,
184
+ color 0.15s,
185
+ border-color 0.15s;
186
+ white-space: nowrap;
52
187
  }
53
- #topbar button:hover {
54
- background: #4b5563;
188
+
189
+ #reset-btn:hover {
190
+ background: #1a2035;
191
+ color: #9ca3af;
192
+ border-color: #2d3f5f;
55
193
  }
194
+
195
+ /* ─── Canvas ──────────────────────────────────────── */
56
196
  #canvas {
57
- background: #fafbfc;
197
+ background-color: #050810;
198
+ background-image: radial-gradient(rgba(148, 163, 184, 0.06) 1px, transparent 1px);
199
+ background-size: 28px 28px;
58
200
  position: relative;
201
+ min-height: 0;
202
+ overflow: hidden;
203
+ }
204
+
205
+ #progress-wrap {
206
+ position: fixed;
207
+ top: 52px;
208
+ left: 0;
209
+ right: 0;
210
+ height: 2px;
211
+ z-index: 100;
212
+ pointer-events: none;
213
+ overflow: hidden;
59
214
  }
215
+
216
+ #progress-bar {
217
+ height: 100%;
218
+ width: 0%;
219
+ background: linear-gradient(90deg, #3b82f6, #6366f1);
220
+ transition: width 0.1s linear;
221
+ box-shadow: 0 0 6px rgba(99, 102, 241, 0.6);
222
+ }
223
+
224
+ /* ─── Side panel ──────────────────────────────────── */
60
225
  #sidepanel {
61
- background: #ffffff;
62
- border-left: 1px solid #e5e7eb;
63
- padding: 16px;
226
+ background: #080c14;
227
+ border-left: 1px solid #1a2035;
64
228
  overflow-y: auto;
229
+ color: #e2e8f0;
230
+ display: flex;
231
+ flex-direction: column;
232
+ transition: opacity 0.2s;
65
233
  }
234
+
66
235
  #sidepanel.hidden {
67
- display: none;
236
+ visibility: hidden;
237
+ opacity: 0;
238
+ pointer-events: none;
239
+ }
240
+
241
+ .sp-header {
242
+ padding: 16px 16px 12px;
243
+ border-bottom: 1px solid #1a2035;
244
+ }
245
+
246
+ .sp-group-badge {
247
+ display: inline-block;
248
+ font-size: 9px;
249
+ font-weight: 700;
250
+ letter-spacing: 0.08em;
251
+ text-transform: uppercase;
252
+ border-radius: 3px;
253
+ padding: 2px 6px;
254
+ margin-bottom: 8px;
68
255
  }
69
- #sidepanel h3 {
70
- margin: 0 0 8px;
256
+
257
+ .sp-group-badge.ticket {
258
+ background: rgba(59, 130, 246, 0.15);
259
+ color: #60a5fa;
260
+ border: 1px solid rgba(59, 130, 246, 0.25);
261
+ }
262
+ .sp-group-badge.scenario {
263
+ background: rgba(16, 185, 129, 0.12);
264
+ color: #34d399;
265
+ border: 1px solid rgba(16, 185, 129, 0.2);
266
+ }
267
+ .sp-group-badge.scenario-fail {
268
+ background: rgba(239, 68, 68, 0.12);
269
+ color: #f87171;
270
+ border: 1px solid rgba(239, 68, 68, 0.2);
271
+ }
272
+ .sp-group-badge.pom {
273
+ background: rgba(245, 158, 11, 0.12);
274
+ color: #fbbf24;
275
+ border: 1px solid rgba(245, 158, 11, 0.2);
276
+ }
277
+
278
+ .sp-title {
71
279
  font-size: 14px;
280
+ font-weight: 600;
281
+ color: #f1f5f9;
282
+ line-height: 1.4;
283
+ margin: 0;
72
284
  }
73
- #sidepanel p {
74
- margin: 0 0 12px;
75
- font-size: 13px;
76
- color: #4b5563;
285
+
286
+ .sp-desc {
287
+ padding: 12px 16px;
288
+ font-size: 12px;
289
+ color: #64748b;
290
+ line-height: 1.6;
291
+ border-bottom: 1px solid #1a2035;
292
+ flex: 1;
77
293
  }
294
+
295
+ .sp-actions {
296
+ padding: 12px 16px;
297
+ }
298
+
299
+ .sp-actions button {
300
+ background: linear-gradient(135deg, #1d4ed8, #4f46e5);
301
+ color: #eff6ff;
302
+ border: none;
303
+ border-radius: 6px;
304
+ padding: 7px 12px;
305
+ font-size: 12px;
306
+ font-weight: 500;
307
+ cursor: pointer;
308
+ width: 100%;
309
+ transition:
310
+ opacity 0.15s,
311
+ transform 0.1s;
312
+ letter-spacing: 0.01em;
313
+ }
314
+
315
+ .sp-actions button:hover {
316
+ opacity: 0.9;
317
+ }
318
+
319
+ .sp-actions button:active {
320
+ transform: scale(0.98);
321
+ }
322
+
323
+ .sp-actions button.copied {
324
+ background: linear-gradient(135deg, #065f46, #047857);
325
+ }
326
+
327
+ /* ─── Footer ──────────────────────────────────────── */
78
328
  #footer {
79
329
  grid-column: 1 / -1;
80
- background: #f3f4f6;
81
- padding: 6px 16px;
82
- font-size: 11px;
330
+ background: #050810;
331
+ padding: 4px 16px;
332
+ font-size: 10px;
333
+ color: #1e2d45;
334
+ border-top: 1px solid #0d1117;
335
+ display: flex;
336
+ align-items: center;
337
+ gap: 12px;
338
+ }
339
+
340
+ #footer span {
341
+ color: #374151;
342
+ }
343
+
344
+ /* ─── Coverage tab + subtabs ──────────────────────── */
345
+ .toplevel-tabs {
346
+ display: flex;
347
+ gap: 4px;
348
+ }
349
+ .toplevel-tabs button {
350
+ padding: 6px 12px;
351
+ border: 0;
352
+ background: transparent;
353
+ cursor: pointer;
354
+ color: #9ca3af;
355
+ }
356
+ .toplevel-tabs button.active {
357
+ border-bottom: 2px solid #3b82f6;
358
+ font-weight: bold;
359
+ color: #e2e8f0;
360
+ }
361
+ [data-tab-panel] {
362
+ display: none;
363
+ }
364
+ [data-tab-panel].active {
365
+ display: block;
366
+ }
367
+ .subtabs {
368
+ display: flex;
369
+ gap: 4px;
370
+ padding: 8px;
371
+ }
372
+ .subtabs button {
373
+ padding: 4px 10px;
374
+ border: 1px solid #d1d5db;
375
+ background: white;
376
+ cursor: pointer;
377
+ }
378
+ .subtabs button.active {
379
+ background: #3b82f6;
380
+ color: white;
381
+ }
382
+ [data-subpanel] {
383
+ padding: 12px;
384
+ }
385
+ .subpanel-hint {
83
386
  color: #6b7280;
84
- border-top: 1px solid #e5e7eb;
387
+ font-size: 13px;
388
+ margin: 4px 0 12px;
389
+ }
390
+ #coverage-list-table,
391
+ #coverage-ac-table {
392
+ border-collapse: collapse;
393
+ width: 100%;
394
+ font-size: 13px;
395
+ }
396
+ #coverage-list-table th,
397
+ #coverage-list-table td,
398
+ #coverage-ac-table th,
399
+ #coverage-ac-table td {
400
+ border: 1px solid #e5e7eb;
401
+ padding: 6px 10px;
402
+ text-align: left;
403
+ }
404
+ #coverage-list-table th {
405
+ background: #f9fafb;
406
+ cursor: pointer;
407
+ }
408
+ .status-uncovered {
409
+ background: #fee2e2;
410
+ }
411
+ .status-stale {
412
+ background: #fef3c7;
413
+ }
414
+ .status-covered {
415
+ background: #d1fae5;
416
+ }
417
+ #coverage-map-canvas {
418
+ width: 100%;
419
+ height: 600px;
85
420
  }
86
- .disputed-outline {
87
- border: 2px dashed #ef4444;
421
+ #coverage-trend-svg {
422
+ width: 100%;
423
+ min-height: 300px;
88
424
  }