@sigloch/graph-view-edit 0.7.0 → 0.7.2
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/assets/index-Cu_Z807i.css +1 -0
- package/dist/assets/index-Dbtyb0kh.js +244 -0
- package/dist/index.html +2 -2
- package/package.json +5 -5
- package/vite.config.js +139 -67
- package/dist/assets/index-2Jd5dg79.css +0 -1
- package/dist/assets/index-DSNw0FsP.js +0 -181
package/dist/index.html
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>graph-view-edit</title>
|
|
7
|
-
<script type="module" crossorigin src="/assets/index-
|
|
8
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
7
|
+
<script type="module" crossorigin src="/assets/index-Dbtyb0kh.js"></script>
|
|
8
|
+
<link rel="stylesheet" crossorigin href="/assets/index-Cu_Z807i.css">
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
|
11
11
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sigloch/graph-view-edit",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.2",
|
|
4
4
|
"description": "React/Vite viewer+editor over @sigloch/graphcode — 12 graph-views + 16 doc-views via a declarative View-Registry, plus an SE-Dashboard sibling route. Reads docs/graph/<member>.graph.json client-side (no 2nd Kuzu handle).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -17,20 +17,20 @@
|
|
|
17
17
|
"build": "vite build",
|
|
18
18
|
"preview": "vite preview",
|
|
19
19
|
"prepack": "npm run build",
|
|
20
|
-
"test": "
|
|
20
|
+
"test": "node scripts/run-tests.mjs"
|
|
21
21
|
},
|
|
22
22
|
"comment:deps": "Runtime = what vite.config.js + bin/gve.mjs load at Node level when serving the prebuilt dist/. The UI libs (react, react-dom, elkjs, @tanstack/react-table, zustand) are bundled INTO dist/ by vite and must stay devDependencies — shipping them would make consumers download the whole UI stack twice.",
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@sigloch/contracts": ">=5 <
|
|
24
|
+
"@sigloch/contracts": ">=5 <10",
|
|
25
25
|
"@sigloch/graph-api-core": ">=5 <6",
|
|
26
|
-
"@sigloch/graphcode-client": ">=1 <2",
|
|
26
|
+
"@sigloch/graphcode-client": ">=1.3 <2",
|
|
27
27
|
"@vitejs/plugin-react": "^4.3.4",
|
|
28
28
|
"vite": "^5.4.11",
|
|
29
29
|
"zod": "^4.3.6"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"@playwright/test": "^1.58.2",
|
|
33
|
-
"@sigloch/graphcode": "^0.
|
|
33
|
+
"@sigloch/graphcode": "^0.18.0",
|
|
34
34
|
"@tanstack/react-table": "^8.20.5",
|
|
35
35
|
"@testing-library/react": "^16.0.1",
|
|
36
36
|
"elkjs": "^0.11.1",
|
package/vite.config.js
CHANGED
|
@@ -3,7 +3,7 @@ import { join, basename } from 'node:path';
|
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
import { defineConfig } from 'vite';
|
|
5
5
|
import react from '@vitejs/plugin-react';
|
|
6
|
-
import { DefaultRuleEngine, SE_DESCRIPTOR } from '@sigloch/graph-api-core';
|
|
6
|
+
import { DefaultRuleEngine, SE_DESCRIPTOR, fromOntologyGraph } from '@sigloch/graph-api-core';
|
|
7
7
|
import { ONTOLOGY_VERSION, RULES_VERSION } from '@sigloch/contracts/se';
|
|
8
8
|
// CR-GC-265: these eight come from the read-side client, not the substrate.
|
|
9
9
|
// They are pure projection plus a node:net socket call — depending on
|
|
@@ -18,6 +18,8 @@ import {
|
|
|
18
18
|
VIEW_FILENAMES,
|
|
19
19
|
ARTIFACT_CATALOG,
|
|
20
20
|
analysisCreationCurrencyProvider,
|
|
21
|
+
PHASE_GATE_RULES,
|
|
22
|
+
groupViolations,
|
|
21
23
|
callHost,
|
|
22
24
|
HOST_SOCK_BASENAME,
|
|
23
25
|
} from '@sigloch/graphcode-client';
|
|
@@ -159,43 +161,19 @@ function configApiPlugin() {
|
|
|
159
161
|
* just fetches the JSON like any other static resource.
|
|
160
162
|
*/
|
|
161
163
|
/**
|
|
162
|
-
* docs/graph/*.graph.json
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
* (that's where projectToOntologyGraph reads them back out of), created_at/
|
|
170
|
-
* updated_at are GraphNode's own top-level camelCase fields (never inside
|
|
171
|
-
* attributes, or `projectToOntologyGraph` would read them back as '').
|
|
164
|
+
* docs/graph/*.graph.json → graph-api-core's Graph{nodes,edges} via the
|
|
165
|
+
* PUBLISHED inverse `fromOntologyGraph` (CR-SM-254) — no hand-rolled mirror.
|
|
166
|
+
* The previous hand-derived copy lifted exactly four flat keys
|
|
167
|
+
* (status/asil/method/kinds) into `attributes` and dropped everything else
|
|
168
|
+
* the exporter writes flat (concept, severity/occurrence/detection,
|
|
169
|
+
* analysisFreshness, testRefs, …) — the CR-GC-402 "second truth": the
|
|
170
|
+
* dashboard evaluated a castrated graph and disagreed with graph_readiness.
|
|
172
171
|
* `tests/vite-config-load-graph.test.mjs` pins the round trip against the
|
|
173
172
|
* REAL projectToOntologyGraph, so a drift in either direction fails loud.
|
|
174
173
|
*/
|
|
175
174
|
export function loadGraph(file) {
|
|
176
175
|
const json = JSON.parse(readFileSync(file, 'utf8'));
|
|
177
|
-
const nodes = (json
|
|
178
|
-
uid: e.id,
|
|
179
|
-
type: e.type,
|
|
180
|
-
name: e.name,
|
|
181
|
-
description: e.description ?? '',
|
|
182
|
-
createdAt: e.created_at,
|
|
183
|
-
updatedAt: e.updated_at,
|
|
184
|
-
attributes: { ...(e.attributes ?? {}), status: e.status, asil: e.asil, method: e.method, kinds: e.kinds },
|
|
185
|
-
}));
|
|
186
|
-
const edges = (json.traces ?? []).map((t) => ({
|
|
187
|
-
sourceId: t.source,
|
|
188
|
-
targetId: t.target,
|
|
189
|
-
edgeType: t.type,
|
|
190
|
-
attributes: {
|
|
191
|
-
...(t.attributes ?? {}),
|
|
192
|
-
category: t.category,
|
|
193
|
-
label: t.label,
|
|
194
|
-
weight: t.weight,
|
|
195
|
-
created_at: t.created_at,
|
|
196
|
-
verified_at: t.verified_at,
|
|
197
|
-
},
|
|
198
|
-
}));
|
|
176
|
+
const { nodes, edges } = fromOntologyGraph(json);
|
|
199
177
|
// CR-GC-300: graph_export now stamps graphVersion at write time — the live
|
|
200
178
|
// comparison value computeAnalysisCurrency() needs against each analysis
|
|
201
179
|
// artifact's SYS.attributes.analysisFreshness.<id>.graphVersion stamp
|
|
@@ -205,53 +183,145 @@ export function loadGraph(file) {
|
|
|
205
183
|
}
|
|
206
184
|
|
|
207
185
|
/**
|
|
208
|
-
*
|
|
209
|
-
*
|
|
210
|
-
*
|
|
211
|
-
*
|
|
212
|
-
* `recommendationsPanel(...).items`: das Panel kappt ZUERST bei 50, also
|
|
213
|
-
* käme aus den Items für eine 60× feuernde Regel "50×" heraus — eine falsche
|
|
214
|
-
* Zahl, und die ist schlimmer als eine lange Liste. Erst gruppieren, dann
|
|
215
|
-
* kappen.
|
|
186
|
+
* Das Recommendations-Teilstück des Dashboard-Payloads (CR-GVE-250):
|
|
187
|
+
* `recommendationsPanel`s `items`/`total` UNVERÄNDERT — die Items tragen
|
|
188
|
+
* `topCandidate`, das die Ein-Klick-Fix-Geste (CR-GVE-111) braucht — plus
|
|
189
|
+
* die Gruppen als zusätzliche Sicht, nicht als Ersatz.
|
|
216
190
|
*
|
|
217
|
-
*
|
|
218
|
-
*
|
|
219
|
-
*
|
|
191
|
+
* Die Gruppierung selbst ist seit CR-GVE-259 `groupViolations` aus
|
|
192
|
+
* @sigloch/graphcode-client — dieselbe Funktion, die die MCP-Fläche unter
|
|
193
|
+
* `detail:'grouped'` liefert (CR-GC-411). Die frühere lokale Kopie ist
|
|
194
|
+
* GELÖSCHT, nicht deprecated: eine Aggregation, zwei Konsumenten.
|
|
195
|
+
*/
|
|
196
|
+
export function recommendationsPayload(violations, limit = 50) {
|
|
197
|
+
return { ...recommendationsPanel(violations, limit), groups: groupViolations(violations) };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Gate-Blocker als strukturierte Gruppen (CR-GVE-256) — dieselbe Mechanik wie
|
|
202
|
+
* `recommendationsPayload` (erst gruppieren, dann kappen; Counts aus den
|
|
203
|
+
* UNGEKAPPTEN Listen), eingeschränkt auf die Regeln, die das Gate besitzt.
|
|
204
|
+
* Nur error-severity blockiert (warnings/info sind advisory `open` —
|
|
205
|
+
* readiness.js scorePhaseGate) — deshalb wird hier identisch gefiltert.
|
|
220
206
|
*/
|
|
221
|
-
export function
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
207
|
+
export function gateBlockerGroups(violations, ruleIds, elementLimit = 10) {
|
|
208
|
+
return groupViolations(
|
|
209
|
+
violations.filter((v) => v.severity === 'error' && ruleIds.includes(v.ruleId)),
|
|
210
|
+
elementLimit,
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Gate-übergreifende Aggregat-Sicht (CR-GVE-256): EINE Gruppe je ruleId über
|
|
216
|
+
* alle Gates, mit Gate-Chips — dieselbe Regel wird nicht n-fach gelistet.
|
|
217
|
+
* Die Gate-Zuordnung ist die VORHANDENE server-seitige Ableitung
|
|
218
|
+
* (`PHASE_GATE_RULES` aus @sigloch/graphcode-client — dieselbe Tabelle, aus
|
|
219
|
+
* der readiness.js die Gates scored), kein lokaler zweiter Regel-Katalog.
|
|
220
|
+
* Impl-Gates besitzen keine Element-Regeln (ihre Blocker sind CRs/Scope/
|
|
221
|
+
* Creations — strukturell, ohne ruleId) und tauchen deshalb hier nicht auf.
|
|
222
|
+
*/
|
|
223
|
+
export function gateBlockerRollup(violations, gateRules = PHASE_GATE_RULES, elementLimit = 10) {
|
|
224
|
+
const ruleToGates = new Map();
|
|
225
|
+
for (const [gateId, ruleIds] of Object.entries(gateRules)) {
|
|
226
|
+
for (const rid of ruleIds) {
|
|
227
|
+
if (!ruleToGates.has(rid)) ruleToGates.set(rid, []);
|
|
228
|
+
ruleToGates.get(rid).push(gateId);
|
|
228
229
|
}
|
|
229
|
-
g.count += 1;
|
|
230
|
-
if (v.elementId) g.allElementIds.push(v.elementId);
|
|
231
230
|
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
.map(({ allElementIds, ...g }) => ({
|
|
235
|
-
...g,
|
|
236
|
-
elementIds: allElementIds.slice(0, elementLimit),
|
|
237
|
-
elementIdsOmitted: Math.max(0, allElementIds.length - elementLimit),
|
|
238
|
-
}));
|
|
231
|
+
const owned = violations.filter((v) => v.severity === 'error' && ruleToGates.has(v.ruleId));
|
|
232
|
+
return groupViolations(owned, elementLimit).map((g) => ({ ...g, gates: ruleToGates.get(g.ruleId) }));
|
|
239
233
|
}
|
|
240
234
|
|
|
241
235
|
/**
|
|
242
|
-
* Das
|
|
243
|
-
* `
|
|
244
|
-
* `
|
|
245
|
-
*
|
|
236
|
+
* Das Readiness-Teilstück des Dashboard-Payloads (CR-GVE-256):
|
|
237
|
+
* `readinessPanel` UNVERÄNDERT als Basis, aber je Gate ersetzt das flache
|
|
238
|
+
* `blocking`-String-Array durch `blockerGroups` (regelbasierte Blocker,
|
|
239
|
+
* gruppiert) + `blockingOther` (Milestones/CRs/Creations/Completeness — die
|
|
240
|
+
* Zeilen ohne ruleId). Das flache Feld wird ENTFERNT, nicht daneben
|
|
241
|
+
* weitergereicht — sonst rendert es irgendwann wieder jemand (Parallelpfad).
|
|
246
242
|
*/
|
|
247
|
-
export function
|
|
248
|
-
|
|
243
|
+
export function readinessPayload(report) {
|
|
244
|
+
const panel = readinessPanel(report);
|
|
245
|
+
const enrich = (g) => {
|
|
246
|
+
const ruleIds = PHASE_GATE_RULES[g.id] ?? [];
|
|
247
|
+
const { blocking, ...rest } = g;
|
|
248
|
+
return {
|
|
249
|
+
...rest,
|
|
250
|
+
blockerGroups: gateBlockerGroups(report.violations, ruleIds),
|
|
251
|
+
// Regel-Zeilen sind als Gruppen abgebildet; übrig bleiben die
|
|
252
|
+
// strukturellen Blocker ("<CR> not done", "milestone … missing",
|
|
253
|
+
// "<Creation> not performed", "… completeness x/y").
|
|
254
|
+
blockingOther: blocking.filter((line) => !ruleIds.some((rid) => line.startsWith(`${rid}: `))),
|
|
255
|
+
};
|
|
256
|
+
};
|
|
257
|
+
return {
|
|
258
|
+
...panel,
|
|
259
|
+
phaseGates: panel.phaseGates.map(enrich),
|
|
260
|
+
implGates: panel.implGates.map(enrich),
|
|
261
|
+
blockerRollup: gateBlockerRollup(report.violations),
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Autopilot-Scoreboard (CR-GVE-257) — Präsentations-Aggregation der
|
|
267
|
+
* `.graphcode/trajectory.jsonl` des Ziel-Repos (Projektion des OperationsLog,
|
|
268
|
+
* graphcode CR-GC-252). NUR Zeilen zählen aus einer Datei, die graphcode
|
|
269
|
+
* schreibt — keine eigene Metrik-/Regelberechnung (Grenze, Entscheidung
|
|
270
|
+
* 2026-08-25; Spiderweb/Konvergenz sind graphcode CR-DRAFT-GC-410).
|
|
271
|
+
*
|
|
272
|
+
* Zähl-Semantik entlang der CR-Referenzzahlen (193 / 2 418 / 45 im
|
|
273
|
+
* graphcode-Repo): applied/rejected über `operation === 'mutate'`;
|
|
274
|
+
* `opsTotal` = Summe `opCounts` der APPLIED Mutationen (was wirklich durchs
|
|
275
|
+
* Gate in den Graphen ging); Sessions = distinct `consumerId`; der
|
|
276
|
+
* Autorschafts-Split zählt die applied Mutationen (wer den Graphen WIRKLICH
|
|
277
|
+
* geschrieben hat). Violations-Delta: erste → letzte Zeile der Session des
|
|
278
|
+
* letzten Eintrags. Kaputte Zeilen werden übersprungen, leere Datei → null
|
|
279
|
+
* (die Karte zeigt „keine Trajektorie vorhanden", kein Crash).
|
|
280
|
+
*/
|
|
281
|
+
export function autopilotStats(jsonlText) {
|
|
282
|
+
const entries = (jsonlText ?? '')
|
|
283
|
+
.split('\n')
|
|
284
|
+
.map((line) => {
|
|
285
|
+
try {
|
|
286
|
+
return JSON.parse(line);
|
|
287
|
+
} catch {
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
})
|
|
291
|
+
.filter((e) => e && typeof e === 'object');
|
|
292
|
+
if (entries.length === 0) return null;
|
|
293
|
+
const mutates = entries.filter((e) => e.operation === 'mutate');
|
|
294
|
+
const applied = mutates.filter((e) => e.applied === true);
|
|
295
|
+
const versions = applied.map((e) => e.graphVersion).filter((v) => typeof v === 'number');
|
|
296
|
+
const authors = { agent: 0, human: 0 };
|
|
297
|
+
for (const e of applied) authors[e.consumerType === 'agent' ? 'agent' : 'human'] += 1;
|
|
298
|
+
const lastSessionId = entries[entries.length - 1].consumerId;
|
|
299
|
+
const lastSession = entries.filter((e) => e.consumerId === lastSessionId);
|
|
300
|
+
const zero = { error: 0, warning: 0, info: 0 };
|
|
301
|
+
return {
|
|
302
|
+
applied: applied.length,
|
|
303
|
+
rejected: mutates.length - applied.length,
|
|
304
|
+
graphVersionFrom: versions.length ? Math.min(...versions) : null,
|
|
305
|
+
graphVersionTo: versions.length ? Math.max(...versions) : null,
|
|
306
|
+
opsTotal: applied.reduce((n, e) => n + (typeof e.opCounts === 'number' ? e.opCounts : 0), 0),
|
|
307
|
+
sessions: new Set(entries.map((e) => e.consumerId)).size,
|
|
308
|
+
activeDays: new Set(entries.map((e) => String(e.ts ?? '').slice(0, 10)).filter(Boolean)).size,
|
|
309
|
+
authors,
|
|
310
|
+
lastSession: {
|
|
311
|
+
consumerId: lastSessionId,
|
|
312
|
+
violationsFrom: { ...zero, ...(lastSession[0]?.violations ?? {}) },
|
|
313
|
+
violationsTo: { ...zero, ...(lastSession[lastSession.length - 1]?.violations ?? {}) },
|
|
314
|
+
},
|
|
315
|
+
};
|
|
249
316
|
}
|
|
250
317
|
|
|
251
318
|
function dashboardApiPlugin() {
|
|
252
319
|
const cwd = resolveRepoRoot();
|
|
253
320
|
const GRAPH_DIR = join(cwd, 'docs', 'graph');
|
|
254
321
|
const VIEWS_DIR = join(cwd, 'docs', 'views');
|
|
322
|
+
// CR-GVE-257: die Trajektorie liegt im Ziel-Repo, nicht im Viewer-Repo —
|
|
323
|
+
// derselbe resolveRepoRoot()-Anker wie GRAPH_DIR/host.sock.
|
|
324
|
+
const TRAJECTORY_FILE = join(cwd, '.graphcode', 'trajectory.jsonl');
|
|
255
325
|
const engine = new DefaultRuleEngine(SE_DESCRIPTOR.version);
|
|
256
326
|
engine.register(SE_DESCRIPTOR.rules ?? []);
|
|
257
327
|
|
|
@@ -325,10 +395,12 @@ function dashboardApiPlugin() {
|
|
|
325
395
|
member: basename(file).replace(/\.graph\.json$/, ''),
|
|
326
396
|
repoRoot: servedRepoRoot(),
|
|
327
397
|
empty: graph.nodes.length === 0,
|
|
328
|
-
readiness:
|
|
398
|
+
readiness: readinessPayload(report),
|
|
329
399
|
recommendations: recommendationsPayload(violations, 50),
|
|
330
400
|
artifacts: scanArtifacts(file, graph),
|
|
331
401
|
health: synthHealth(graph),
|
|
402
|
+
// CR-GVE-257: Repo ohne Trajektorie → null (Karte zeigt den Hinweis).
|
|
403
|
+
autopilot: existsSync(TRAJECTORY_FILE) ? autopilotStats(readFileSync(TRAJECTORY_FILE, 'utf8')) : null,
|
|
332
404
|
computedAt: new Date().toISOString(),
|
|
333
405
|
};
|
|
334
406
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
.gve-ctx{position:absolute;z-index:30;min-width:220px;max-width:320px;max-height:60vh;overflow-y:auto;display:flex;flex-direction:column;background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-md);box-shadow:var(--shadow-1);padding:var(--space-1);font-family:var(--font-ui);font-size:var(--text-sm)}.gve-ctx-head{padding:var(--space-1) var(--space-2);font-family:var(--font-mono);font-size:var(--text-xs);color:var(--color-text-dim);border-bottom:1px solid var(--color-border);margin-bottom:var(--space-1)}.gve-ctx-item{text-align:left;padding:var(--space-1) var(--space-2);background:none;border:none;border-radius:var(--radius-sm);color:var(--color-text);cursor:pointer}.gve-ctx-item:hover:not(:disabled){background:var(--color-accent-soft)}.gve-ctx-item:disabled{color:var(--color-text-dim);cursor:not-allowed}.gve-ctx-form{display:flex;flex-direction:column;gap:var(--space-1);padding:var(--space-2)}.gve-ctx-form label{font-size:var(--text-xs);color:var(--color-text-dim)}.gve-ctx-form input,.gve-ctx-form textarea{font-family:var(--font-ui);font-size:var(--text-sm);color:var(--color-text);background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-sm);padding:var(--space-1) var(--space-2)}.gve-ctx-empty{padding:var(--space-1) var(--space-2);color:var(--color-text-dim);font-size:var(--text-xs)}.gve-genesis{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);z-index:20;display:flex;flex-direction:column;gap:var(--space-2);min-width:240px;padding:var(--space-3);background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-md);box-shadow:var(--shadow-1);font-family:var(--font-ui);font-size:var(--text-sm);color:var(--color-text);text-align:center}.gve-genesis-title{font-family:var(--font-mono);font-size:var(--text-xs);text-transform:uppercase;letter-spacing:.04em;color:var(--color-text-dim)}.gve-genesis-hint{margin:0;color:var(--color-text-dim)}.gve-genesis-input{font-family:var(--font-ui);font-size:var(--text-sm);color:var(--color-text);background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-sm);padding:var(--space-1) var(--space-2)}.gve-genesis-create{padding:var(--space-1) var(--space-2);background:var(--color-accent-soft);border:1px solid var(--color-border);border-radius:var(--radius-sm);color:var(--color-text);cursor:pointer}.gve-genesis-create:disabled{color:var(--color-text-dim);cursor:not-allowed}.gve-doc{font-family:var(--font-ui);font-size:var(--text-base);line-height:1.55;color:var(--color-text);background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-md);padding:var(--space-8) var(--space-6);margin:var(--space-3) auto;max-width:820px}.gve-doc h1{font-size:var(--text-xl);margin:0 0 var(--space-3)}.gve-doc h2{font-size:var(--text-lg);margin:var(--space-6) 0 var(--space-3);padding-bottom:var(--space-1);border-bottom:1px solid var(--color-border)}.gve-doc h3{font-size:var(--text-base);margin:var(--space-4) 0 var(--space-2)}.gve-doc h4,.gve-doc h5,.gve-doc h6{font-size:var(--text-sm);margin:var(--space-3) 0 var(--space-1)}.gve-doc h5 code,.gve-doc h6 code{color:var(--color-text-dim)}.gve-doc p{margin:0 0 var(--space-2)}.gve-doc code{font-family:var(--font-mono);font-size:.92em;background:var(--color-surface-2);border-radius:var(--radius-sm);padding:0 var(--space-1)}.gve-doc blockquote{margin:0 0 var(--space-2);padding-left:var(--space-3);border-left:2px solid var(--color-border);color:var(--color-text-dim);font-size:var(--text-sm)}.gve-doc hr{border:0;border-top:1px solid var(--color-border);margin:var(--space-4) 0}.gve-doc pre.md-code{font-family:var(--font-mono);font-size:var(--text-sm);background:var(--color-surface-2);border-radius:var(--radius-sm);padding:var(--space-2) var(--space-3);overflow-x:auto}.edit-pen{margin-left:var(--space-2);font-size:var(--text-xs);background:var(--color-surface-2);border:1px solid var(--color-border);border-radius:var(--radius-sm);cursor:pointer}.edit-box{margin:var(--space-2) 0;padding:var(--space-3);background:var(--color-surface-2);border:1px solid var(--color-accent);border-radius:var(--radius-md)}.edit-box textarea{width:100%;box-sizing:border-box;font-family:var(--font-mono);font-size:var(--text-sm);color:var(--color-text);background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-sm);padding:var(--space-2)}.cmd-preview{font-family:var(--font-mono);font-size:var(--text-xs);color:var(--color-text-dim);background:var(--color-surface);border:1px dashed var(--color-border);border-radius:var(--radius-sm);padding:var(--space-2);overflow-x:auto;white-space:pre-wrap}.edit-actions{display:flex;gap:var(--space-2);align-items:center;margin-top:var(--space-2)}.edit-actions .btn-primary{background:var(--color-accent);color:var(--color-text-inverse);border:1px solid var(--color-accent);border-radius:var(--radius-sm);padding:var(--space-1) var(--space-3);cursor:pointer}.edit-actions .btn-primary:disabled{opacity:.5;cursor:not-allowed}[data-testid=edit-errors],[data-testid=formate-errors]{color:var(--color-error);font-size:var(--text-xs)}[data-testid=edit-verdict][data-tier=block]{color:var(--color-error)}[data-testid=edit-verdict]{font-size:var(--text-xs);color:var(--color-text-dim)}.gve-cards{flex:1;overflow:auto;display:grid;grid-template-columns:repeat(auto-fit,minmax(320px,1fr));gap:var(--space-4);padding:var(--space-4);align-content:start}.gve-card{background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-md);box-shadow:var(--shadow-1);padding:var(--space-4)}.gve-card-wide{grid-column:1 / -1}.gve-card h3{font-size:var(--text-xs);font-weight:600;text-transform:uppercase;letter-spacing:.06em;color:var(--color-text-dim);margin-bottom:var(--space-3);display:flex;align-items:center;gap:var(--space-2)}.gve-card h4{font-size:var(--text-xs);font-weight:600;color:var(--color-text-dim);margin-bottom:var(--space-2);display:flex;align-items:center;gap:var(--space-2)}.gve-count{font-size:var(--text-xs);font-weight:600;color:var(--color-text-inverse);background:var(--color-accent);border-radius:999px;padding:0 var(--space-2)}.gve-kpi-row{display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:var(--space-3);margin-bottom:var(--space-4)}.gve-kpi-tile{display:flex;flex-direction:column;gap:var(--space-1);background:var(--color-surface-2);border:1px solid var(--color-border);border-radius:var(--radius-sm);padding:var(--space-3)}.gve-kpi-value{font-size:var(--text-xl);font-weight:600;line-height:1.1}.gve-kpi-label{font-size:var(--text-xs);color:var(--color-text-dim)}.gve-kpi-tile[data-tone=ok] .gve-kpi-value{color:var(--color-ok)}.gve-kpi-tile[data-tone=warn] .gve-kpi-value{color:var(--color-warn)}.gve-kpi-tile[data-tone=error] .gve-kpi-value{color:var(--color-error)}.gve-gate-cols{display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:var(--space-4)}.gve-gates{list-style:none;padding:0;display:flex;flex-direction:column;gap:var(--space-2)}.gve-gate{display:grid;grid-template-columns:auto 44px 1fr auto 44px;align-items:center;gap:var(--space-2);font-size:var(--text-sm)}.gve-light{width:10px;height:10px;border-radius:50%;flex-shrink:0}.gve-light[data-state=passed]{background:var(--color-ok)}.gve-light[data-state=open]{background:var(--color-warn)}.gve-gate-id{font-family:var(--font-mono);font-weight:600}.gve-gate-label{color:var(--color-text-dim);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.gve-gate-track{grid-column:4;width:90px;height:12px;background:var(--color-surface-2);border-radius:999px;overflow:hidden}.gve-gate-fill{display:block;height:100%;border-radius:999px}.gve-gate-fill[data-state=passed]{background:var(--color-ok)}.gve-gate-fill[data-state=open]{background:var(--color-warn)}.gve-gate-pct{font-family:var(--font-mono);text-align:right}.gve-gate-blk{grid-column:1 / -1;font-size:var(--text-xs);color:var(--color-warn)}.gve-legend{margin-top:var(--space-3);font-size:var(--text-xs);color:var(--color-text-dim)}.gve-rec-group{margin-bottom:var(--space-4)}.gve-rec-group h4[data-severity=error] .gve-count{background:var(--color-error)}.gve-rec-group h4[data-severity=warning] .gve-count{background:var(--color-warn)}.gve-rec-group h4[data-severity=info] .gve-count{background:var(--color-accent)}.gve-rec{border:1px solid var(--color-border);border-left-width:3px;border-radius:var(--radius-sm);padding:var(--space-2) var(--space-3);margin-bottom:var(--space-2);background:var(--color-surface-2)}.gve-rec[data-severity=error]{border-left-color:var(--color-error)}.gve-rec[data-severity=warning]{border-left-color:var(--color-warn)}.gve-rec[data-severity=info]{border-left-color:var(--color-accent)}.gve-rec-head{display:flex;align-items:center;gap:var(--space-2);cursor:pointer;-webkit-user-select:none;user-select:none;list-style-position:outside}.gve-rec-head::-webkit-details-marker{display:none}.gve-rec-head:before{content:"▸";font-size:var(--text-xs);color:var(--color-text-dim)}.gve-rec[open]>.gve-rec-head:before{content:"▾"}.gve-rec[open]>.gve-rec-head{margin-bottom:var(--space-2)}.gve-rec-count{font-family:var(--font-mono);font-size:var(--text-sm);font-weight:600;min-width:var(--space-6);text-align:right}.gve-rec-count[data-severity=error]{color:var(--color-error)}.gve-rec-count[data-severity=warning]{color:var(--color-warn)}.gve-rec-count[data-severity=info]{color:var(--color-accent)}.gve-rec-els{list-style:none;padding:0;display:flex;flex-wrap:wrap;gap:var(--space-1) var(--space-3)}.gve-rec-el-item{font-size:var(--text-xs)}.gve-rec-more{font-size:var(--text-xs);font-style:italic;color:var(--color-text-dim)}.gve-rid{font-family:var(--font-mono);font-size:var(--text-xs);color:var(--color-text-dim)}.gve-rec-el{color:var(--color-accent)}.gve-sev{font-size:var(--text-xs);font-weight:600;text-transform:uppercase;border-radius:var(--radius-sm);padding:0 var(--space-2)}.gve-sev[data-severity=error]{color:var(--color-error)}.gve-sev[data-severity=warning]{color:var(--color-warn)}.gve-sev[data-severity=info]{color:var(--color-accent)}.gve-rec-msg{font-size:var(--text-sm)}.gve-hint{margin-top:var(--space-1);font-size:var(--text-xs);color:var(--color-text-dim)}.gve-empty{font-size:var(--text-sm);color:var(--color-text-dim)}.gve-rollup summary{cursor:pointer;font-size:var(--text-sm);color:var(--color-text-dim);padding:var(--space-1) 0;-webkit-user-select:none;user-select:none}.gve-rollup summary:hover{color:var(--color-text)}.gve-rollup[open] summary{margin-bottom:var(--space-2)}.gve-fresh-list{list-style:none;padding:0;display:flex;flex-direction:column;gap:var(--space-2)}.gve-fresh-2col{display:grid;grid-template-columns:1fr 1fr;column-gap:var(--space-4)}.gve-fresh-list li{display:flex;align-items:center;gap:var(--space-2);font-size:var(--text-sm)}.gve-fresh-row{display:flex;align-items:center;gap:var(--space-2);flex:1;min-width:0;text-align:left;font:inherit;color:inherit;background:none;border:0;padding:0}.gve-fresh-open{cursor:pointer}.gve-fresh-open:hover .gve-fresh-label{color:var(--color-accent);text-decoration:underline}.gve-fresh-open:focus-visible{outline:2px solid var(--color-accent);outline-offset:var(--space-1)}.gve-fresh-label{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.gve-fresh-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}.gve-fresh-dot[data-freshness=live]{background:var(--color-ok)}.gve-fresh-dot[data-freshness=stale]{background:var(--color-warn)}.gve-fresh-dot[data-freshness=absent]{background:var(--color-error)}.gve-pill{font-size:var(--text-xs);border:1px solid var(--color-border);border-radius:999px;padding:0 var(--space-2)}.gve-pill[data-freshness=live]{border-color:var(--color-ok);color:var(--color-ok)}.gve-pill[data-freshness=stale]{border-color:var(--color-warn);color:var(--color-warn)}.gve-pill[data-freshness=absent]{border-color:var(--color-error);color:var(--color-error)}.gve-health-status{margin-bottom:var(--space-3);text-transform:uppercase}.gve-health-status[data-state=ok]{color:var(--color-ok)}.gve-health-status[data-state=error]{color:var(--color-error)}.gve-vtable{width:100%;border-collapse:collapse;font-size:var(--text-sm)}.gve-vtable td{padding:var(--space-1) var(--space-2);border-bottom:1px solid var(--color-border)}.gve-vtable td:first-child{font-family:var(--font-mono);color:var(--color-text-dim);white-space:nowrap}.gve-doc-overlay{position:fixed;top:0;right:0;bottom:0;left:0;z-index:40;display:flex;align-items:center;justify-content:center;padding:var(--space-4);background:color-mix(in srgb,var(--color-text) 40%,transparent)}.gve-doc-panel{display:flex;flex-direction:column;width:min(880px,100%);max-height:100%;background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-md);box-shadow:var(--shadow-1)}.gve-doc-head{display:flex;align-items:center;gap:var(--space-2);padding:var(--space-3) var(--space-4);border-bottom:1px solid var(--color-border)}.gve-doc-head h3{flex:1;margin:0;font-size:var(--text-sm);font-weight:600;color:var(--color-text)}.gve-doc-close{font:inherit;line-height:1;color:var(--color-text-dim);background:none;border:0;padding:var(--space-1);cursor:pointer}.gve-doc-close:hover{color:var(--color-text)}.gve-doc-body{overflow:auto;padding:var(--space-4);font-size:var(--text-sm);line-height:1.55}.gve-doc-body h1,.gve-doc-body h2,.gve-doc-body h3,.gve-doc-body h4{margin:var(--space-4) 0 var(--space-2);font-weight:600}.gve-doc-body h1{font-size:var(--text-lg)}.gve-doc-body h2{font-size:var(--text-base)}.gve-doc-body h3,.gve-doc-body h4{font-size:var(--text-sm);color:var(--color-text-dim)}.gve-doc-body p,.gve-doc-body ul,.gve-doc-body ol{margin:0 0 var(--space-3)}.gve-doc-body ul,.gve-doc-body ol{padding-left:var(--space-4)}.gve-doc-body table.md-table{width:100%;border-collapse:collapse;margin:0 0 var(--space-3);font-size:var(--text-xs)}.gve-doc-body table.md-table th,.gve-doc-body table.md-table td{padding:var(--space-1) var(--space-2);border:1px solid var(--color-border);text-align:left;vertical-align:top}.gve-doc-body table.md-table th{background:var(--color-surface-2);color:var(--color-text-dim);font-weight:600}.gve-doc-body pre.md-code{overflow:auto;padding:var(--space-2) var(--space-3);margin:0 0 var(--space-3);background:var(--color-surface-2);border:1px solid var(--color-border);border-radius:var(--radius-sm);font-family:var(--font-mono);font-size:var(--text-xs)}.gve-doc-body code{font-family:var(--font-mono);font-size:var(--text-xs)}.gve-doc-body a{color:var(--color-accent)}.gve-doc-body hr{border:0;border-top:1px solid var(--color-border);margin:var(--space-4) 0}a.gve-btn{text-decoration:none;display:inline-block}.gve-appbar .title{font-size:var(--text-base);display:flex;align-items:baseline;gap:var(--space-2)}.gve-appbar .title .project{font-size:var(--text-lg);font-weight:600;color:var(--color-text)}.gve-appbar .title .tool{font-size:var(--text-sm);font-weight:400;color:var(--color-text-dim)}.gve .gve{height:auto;flex:1;min-height:0}.gve-chip .dash{width:12px;height:0;border-top:2px solid var(--color-text-dim)}.gve .gve-body{flex:1;min-height:0}.gve-canvas,.gve-explorer .exp-tree,.gve-details{overflow:auto}.gve-tree .tw{cursor:pointer;-webkit-user-select:none;user-select:none}.gve-tree .row.sel{box-shadow:inset 2px 0 0 var(--color-accent)}.gve-tree .row.dim{opacity:.45;cursor:default}.gve-grid-scroll{overflow:auto;flex:1;min-height:0}.gve-grid{border-collapse:collapse;font-size:var(--text-sm)}.gve-grid th,.gve-grid td{border:1px solid var(--color-border);padding:var(--space-1) var(--space-2);text-align:left;white-space:nowrap}.gve-grid thead th{position:sticky;top:0;z-index:1;cursor:pointer;-webkit-user-select:none;user-select:none;background:var(--color-surface-alt, var(--color-surface));color:var(--color-text)}.gve-grid thead th.corner{left:0;z-index:2}.gve-grid td.rowhead,.gve-grid th.corner{font-weight:600}.gve-grid td.rowhead{position:sticky;left:0;background:var(--color-surface)}.gve-grid td.hit{background:var(--color-accent-soft);color:var(--color-accent);text-align:center;font-weight:600}.tabular-grid th,.tabular-grid td{max-width:20rem}.tabular-grid:not(.wrap) th,.tabular-grid:not(.wrap) td{overflow:hidden;text-overflow:ellipsis}.tabular-grid.wrap th,.tabular-grid.wrap td{white-space:normal;word-break:break-word;vertical-align:top}.gve-group.tabs .gve-chip.active{background:var(--color-accent-soft);color:var(--color-accent);border-color:var(--color-accent)}:root{--color-bg: #f6f7f9;--color-surface: #ffffff;--color-surface-2: #eef0f3;--color-border: #d7dbe0;--color-text: #1a1d21;--color-text-dim: #5c6470;--color-text-inverse: #ffffff;--color-accent: #2563eb;--color-accent-soft: #dbeafe;--color-ok: #16a34a;--color-warn: #d97706;--color-error: #dc2626;--color-hotspot: #e11d48;--color-type-1: #2563eb;--color-type-2: #7c3aed;--color-type-3: #0891b2;--color-type-4: #16a34a;--color-type-5: #d97706;--color-type-6: #64748b;--color-type-7: #db2777;--color-type-8: #0d9488;--color-type-func: #0d9488;--color-type-flow: #2dd4bf;--color-type-schema: #4f46e5;--fchain-1: #8b5cf6;--fchain-2: #f59e0b;--fchain-3: #ec4899;--fchain-4: #0284c7;--fchain-5: #84cc16;--space-1: 4px;--space-2: 8px;--space-3: 12px;--space-4: 16px;--space-6: 24px;--space-8: 32px;--text-xs: 11px;--text-sm: 12.5px;--text-base: 14px;--text-lg: 16px;--text-xl: 20px;--font-ui: -apple-system, "Segoe UI", Roboto, sans-serif;--font-mono: ui-monospace, "SF Mono", Menlo, monospace;--radius-sm: 4px;--radius-md: 8px;--shadow-1: 0 1px 3px rgba(0,0,0,.12)}@media (prefers-color-scheme: dark){:root{--color-bg: #14161a;--color-surface: #1e2126;--color-surface-2: #262a31;--color-border: #363c45;--color-text: #e8eaed;--color-text-dim: #9aa3af;--color-text-inverse: #14161a;--color-accent: #60a5fa;--color-accent-soft: #1e3a5f;--color-type-2: #8b5cf6;--color-type-schema: #6366f1;--shadow-1: 0 1px 3px rgba(0,0,0,.5)}}*{box-sizing:border-box;margin:0}.gve{font-family:var(--font-ui);font-size:var(--text-base);background:var(--color-bg);color:var(--color-text);display:flex;flex-direction:column;height:100vh;overflow:hidden}.gve-bar{display:flex;align-items:center;gap:var(--space-3);padding:var(--space-2) var(--space-4);background:var(--color-surface);border-bottom:1px solid var(--color-border);flex-wrap:wrap}.gve-bar .title{font-size:var(--text-lg);font-weight:600;margin-right:var(--space-4)}.gve-bar .title small{color:var(--color-text-dim);font-weight:400;margin-left:var(--space-2)}.gve-group{display:flex;align-items:center;gap:var(--space-1);background:var(--color-surface-2);border:1px solid var(--color-border);border-radius:var(--radius-md);padding:2px var(--space-2)}.gve-group label{font-size:var(--text-xs);color:var(--color-text-dim);margin-right:var(--space-1)}.gve-btn{font:inherit;font-size:var(--text-sm);color:var(--color-text);background:transparent;border:none;border-radius:var(--radius-sm);padding:var(--space-1) var(--space-2);cursor:pointer}.gve-btn:hover{background:var(--color-accent-soft)}.gve-btn.active{background:var(--color-accent);color:var(--color-text-inverse)}.gve-input{font:inherit;font-size:var(--text-sm);color:var(--color-text);background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-sm);padding:var(--space-1) var(--space-2);width:160px}.gve-select{font:inherit;font-size:var(--text-sm);background:var(--color-surface);color:var(--color-text);border:1px solid var(--color-border);border-radius:var(--radius-sm);padding:var(--space-1)}.gve-chip{display:inline-flex;align-items:center;gap:4px;font-size:var(--text-xs);border:1px solid var(--color-border);border-radius:999px;padding:2px var(--space-2);cursor:pointer;background:var(--color-surface);color:var(--color-text)}.gve-chip .dot{width:8px;height:8px;border-radius:50%}.gve-chip.off{opacity:.35}.gve-crumbs{font-size:var(--text-sm);color:var(--color-text-dim)}.gve-crumbs a{color:var(--color-accent);text-decoration:none}.gve-body{flex:1;display:flex;min-height:0}.gve-canvas{flex:1;overflow:auto;position:relative;min-width:0}.gve-explorer{width:260px;flex:0 0 260px;display:flex;flex-direction:column;background:var(--color-surface);border-right:1px solid var(--color-border);overflow:hidden;resize:horizontal}.gve-explorer .exp-tabs{display:flex;border-bottom:1px solid var(--color-border)}.gve-explorer .exp-tab{flex:1;font:inherit;font-size:var(--text-sm);cursor:pointer;background:transparent;border:none;padding:var(--space-2);color:var(--color-text-dim);border-bottom:2px solid transparent}.gve-explorer .exp-tab.active{color:var(--color-text);border-bottom-color:var(--color-accent);font-weight:600}.gve-explorer .exp-tree{flex:1;overflow:auto;padding:var(--space-2) 0;font-size:var(--text-sm)}.gve-tree,.gve-tree ul{list-style:none;margin:0;padding:0}.gve-tree ul{margin-left:14px;border-left:1px solid var(--color-border)}.gve-tree li{position:relative}.gve-tree .row{display:flex;align-items:center;gap:6px;padding:2px var(--space-2);white-space:nowrap;cursor:pointer;border-radius:var(--radius-sm)}.gve-tree .row:hover,.gve-tree .row.sel{background:var(--color-accent-soft)}.gve-tree .tw{width:10px;color:var(--color-text-dim);font-size:10px;flex:none}.gve-tree .ic{width:8px;height:8px;border-radius:2px;flex:none}.gve-tree .ic.ell{border-radius:50%}.gve-tree .lbl{overflow:hidden;text-overflow:ellipsis}.gve-tree .kind{font-size:var(--text-xs);color:var(--color-text-dim)}.gve-details{width:240px;flex:0 0 240px;overflow:auto;background:var(--color-surface);border-left:1px solid var(--color-border);padding:var(--space-3);font-size:var(--text-sm)}.gve-details h4{font-size:var(--text-sm);margin-bottom:var(--space-2)}.gve-details dt{font-size:var(--text-xs);color:var(--color-text-dim);margin-top:var(--space-2)}.gve-details dd{margin:0}.gve-status{display:flex;gap:var(--space-6);align-items:center;padding:var(--space-1) var(--space-4);font-size:var(--text-xs);color:var(--color-text-dim);background:var(--color-surface);border-top:1px solid var(--color-border)}.gve-node rect,.gve-node ellipse{fill:var(--color-surface);stroke:var(--t, var(--color-border));stroke-width:1.5}.gve-node text{font-family:var(--font-ui);font-size:var(--text-sm);fill:var(--color-text)}.gve-node .pill{fill:var(--t)}.gve-node .pill-text{fill:var(--color-text-inverse);font-size:var(--text-xs);font-weight:600}.gve-node.hotspot rect,.gve-node.hotspot ellipse{stroke:var(--color-hotspot);stroke-width:3}.gve-edge{stroke:var(--color-text-dim);fill:none;opacity:.55}.gve-edge.emph{stroke:var(--color-accent);opacity:1}.gve-edge-label{font-size:var(--text-xs);fill:var(--color-text-dim)}.gve-grid{border-collapse:collapse;margin:var(--space-4);font-size:var(--text-sm)}.gve-grid th,.gve-grid td{border:1px solid var(--color-border);padding:var(--space-1) var(--space-2);text-align:left}.gve-grid th{background:var(--color-surface-2);position:sticky;top:0}.gve-grid td.hit{background:var(--color-accent-soft);text-align:center}.gve-grid td.miss{color:var(--color-error);text-align:center}.gve-toast{position:absolute;right:var(--space-4);bottom:var(--space-4);background:var(--color-surface);border:1px solid var(--color-border);border-left:4px solid var(--color-warn);border-radius:var(--radius-md);box-shadow:var(--shadow-1);padding:var(--space-3) var(--space-4);font-size:var(--text-sm);max-width:360px}
|