@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/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-DSNw0FsP.js"></script>
8
- <link rel="stylesheet" crossorigin href="/assets/index-2Jd5dg79.css">
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.0",
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": "vitest run"
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 <7",
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.17.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 graph-api-core's Graph{nodes,edges} the ONE
163
- * definition of this repo's wire format (audit F5). graph-api-core/browser
164
- * exports only the forward direction (`projectToOntologyGraph`: Graph
165
- * OntologyGraph, what graph_export itself uses to WRITE this file) — no
166
- * inverse is published, so this is that function's exact mirror image,
167
- * derived by reading its field-lift contract (se-descriptor.ts) rather than
168
- * re-guessing the shape: status/asil/method/kinds live inside `attributes`
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.elements ?? []).map((e) => ({
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
- * Regel-Aggregat für das Recommendations-Panel (CR-GVE-250) — EINE Zeile je
209
- * ruleId statt 30 fast gleicher Blöcke.
210
- *
211
- * Die Zählung läuft über die UNGEKAPPTEN `violations`, nicht über
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
- * `elementIds` wird bei `elementLimit` abgeschnitten und sagt über
218
- * `elementIdsOmitted`, wie viele fehlen nie stumm; genau die stumme
219
- * Kappung ist der Fehler, den dieser CR behebt.
191
+ * Die Gruppierung selbst ist seit CR-GVE-259 `groupViolations` aus
192
+ * @sigloch/graphcode-clientdieselbe 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 recommendationGroups(violations, elementLimit = 10) {
222
- const byRule = new Map();
223
- for (const v of violations) {
224
- let g = byRule.get(v.ruleId);
225
- if (!g) {
226
- g = { ruleId: v.ruleId, severity: v.severity, count: 0, message: v.message, fixHint: v.fixHint, allElementIds: [] };
227
- byRule.set(v.ruleId, g);
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
- return [...byRule.values()]
233
- .sort((a, b) => b.count - a.count)
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 Recommendations-Teilstück des Dashboard-Payloads (CR-GVE-250):
243
- * `recommendationsPanel`s `items`/`total` UNVERÄNDERT die Items tragen
244
- * `topCandidate`, das die Ein-Klick-Fix-Geste (CR-GVE-111) braucht — plus
245
- * die Gruppen als zusätzliche Sicht, nicht als Ersatz.
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 recommendationsPayload(violations, limit = 50) {
248
- return { ...recommendationsPanel(violations, limit), groups: recommendationGroups(violations) };
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: readinessPanel(report),
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}