@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.
- package/dist/bin/internal.js +1415 -534
- package/dist/bin/templates/LICENSE-vis-network.txt +2 -0
- package/dist/bin/templates/coverage-panel.html.fragment +20 -0
- package/dist/bin/templates/graph.css +379 -43
- package/dist/bin/templates/graph.html.template +35 -15
- package/dist/bin/templates/graph.js +458 -56
- package/dist/bin/templates/vis-network.min.js +3 -24976
- package/dist/src/index.js +6 -0
- package/package.json +3 -3
- package/src/bin-internal/ac-coverage-backfill-finalize.ts +90 -0
- package/src/bin-internal/ac-coverage-backfill-prepare.ts +72 -0
- package/src/bin-internal/coverage-prepare.ts +123 -0
- package/src/bin-internal/fill-gap-finalize.ts +115 -0
- package/src/bin-internal/fill-gap-prepare.ts +150 -0
- package/src/bin-internal/graph-render.ts +32 -4
- package/src/bin-internal/index.ts +10 -0
- package/src/bin-internal/verify-prompts.ts +2 -0
- package/src/config/schema.ts +9 -0
- package/src/coverage/index.ts +29 -0
- package/src/coverage/report.ts +206 -0
- package/src/coverage/risk.ts +69 -0
- package/src/coverage/status.ts +76 -0
- package/src/coverage/types.ts +11 -0
- package/src/coverage/why.ts +122 -0
- package/src/graph/render.ts +16 -2
- package/src/graph/schema.ts +54 -1
- package/src/graph/store.ts +96 -6
- package/src/graph/templates/LICENSE-vis-network.txt +2 -0
- package/src/graph/templates/coverage-panel.html.fragment +20 -0
- package/src/graph/templates/graph.css +379 -43
- package/src/graph/templates/graph.html.template +35 -15
- package/src/graph/templates/graph.js +458 -56
- package/src/graph/templates/vis-network.min.js +3 -24976
- package/src/graph/types.ts +56 -1
package/src/graph/store.ts
CHANGED
|
@@ -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
|
-
|
|
106
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
|
@@ -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:
|
|
10
|
-
grid-template-columns: 1fr
|
|
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:
|
|
20
|
+
gap: 0;
|
|
17
21
|
padding: 0 16px;
|
|
18
|
-
background: #
|
|
19
|
-
|
|
20
|
-
border-bottom: 1px solid #374151;
|
|
22
|
+
background: #080c14;
|
|
23
|
+
border-bottom: 1px solid #1a2035;
|
|
21
24
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
27
|
-
|
|
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
|
-
|
|
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
|
-
|
|
98
|
+
padding-left: 16px;
|
|
99
|
+
border-left: 1px solid #1a2035;
|
|
36
100
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
border
|
|
42
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
color: #
|
|
48
|
-
|
|
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
|
-
|
|
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
|
-
|
|
54
|
-
|
|
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: #
|
|
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: #
|
|
62
|
-
border-left: 1px solid #
|
|
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
|
-
|
|
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
|
-
|
|
70
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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: #
|
|
81
|
-
padding:
|
|
82
|
-
font-size:
|
|
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
|
-
|
|
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
|
-
|
|
87
|
-
|
|
421
|
+
#coverage-trend-svg {
|
|
422
|
+
width: 100%;
|
|
423
|
+
min-height: 300px;
|
|
88
424
|
}
|