@sigloch/graph-view-edit 0.6.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 +194 -37
- package/dist/assets/index-Bn2YV92M.js +0 -181
- package/dist/assets/index-P7N8B6D1.css +0 -1
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.
|
|
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
|
|
@@ -204,10 +182,146 @@ export function loadGraph(file) {
|
|
|
204
182
|
return { nodes, edges, graphVersion: json.graphVersion ?? 0 };
|
|
205
183
|
}
|
|
206
184
|
|
|
185
|
+
/**
|
|
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.
|
|
190
|
+
*
|
|
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.
|
|
206
|
+
*/
|
|
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);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
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) }));
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
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).
|
|
242
|
+
*/
|
|
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
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
207
318
|
function dashboardApiPlugin() {
|
|
208
319
|
const cwd = resolveRepoRoot();
|
|
209
320
|
const GRAPH_DIR = join(cwd, 'docs', 'graph');
|
|
210
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');
|
|
211
325
|
const engine = new DefaultRuleEngine(SE_DESCRIPTOR.version);
|
|
212
326
|
engine.register(SE_DESCRIPTOR.rules ?? []);
|
|
213
327
|
|
|
@@ -241,7 +355,18 @@ function dashboardApiPlugin() {
|
|
|
241
355
|
const staleVsGraph = exists ? statSync(p).mtimeMs < graphMtime : false;
|
|
242
356
|
return { id: entry.id, exists, staleVsGraph };
|
|
243
357
|
});
|
|
244
|
-
|
|
358
|
+
// CR-GVE-252: welche Zeile ein Dokument oeffnen kann, entscheidet der
|
|
359
|
+
// KATALOG, nicht `kind`. 14 der 15 Artefakte haben einen VIEW_FILENAMES-
|
|
360
|
+
// Eintrag — auch vier der fuenf `analysis`-Creations (conops/fmea/trade/
|
|
361
|
+
// implplan). Nur `assumption-review` hat keinen. Die frueher hier
|
|
362
|
+
// implizite Gleichsetzung `kind === 'render'` == "hat eine Datei" war
|
|
363
|
+
// schlicht falsch (CR-GVE-250 §8.1). Dieselbe Tabelle, gegen die
|
|
364
|
+
// /api/view-doc aufloest — kein zweites Kriterium im Client.
|
|
365
|
+
const panel = artifactsPanel(items);
|
|
366
|
+
return {
|
|
367
|
+
...panel,
|
|
368
|
+
artifacts: panel.artifacts.map((a) => ({ ...a, hasDoc: !!VIEW_FILENAMES[a.id] })),
|
|
369
|
+
};
|
|
245
370
|
}
|
|
246
371
|
|
|
247
372
|
function synthHealth(graph) {
|
|
@@ -270,16 +395,48 @@ function dashboardApiPlugin() {
|
|
|
270
395
|
member: basename(file).replace(/\.graph\.json$/, ''),
|
|
271
396
|
repoRoot: servedRepoRoot(),
|
|
272
397
|
empty: graph.nodes.length === 0,
|
|
273
|
-
readiness:
|
|
274
|
-
recommendations:
|
|
398
|
+
readiness: readinessPayload(report),
|
|
399
|
+
recommendations: recommendationsPayload(violations, 50),
|
|
275
400
|
artifacts: scanArtifacts(file, graph),
|
|
276
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,
|
|
277
404
|
computedAt: new Date().toISOString(),
|
|
278
405
|
};
|
|
279
406
|
}
|
|
280
407
|
|
|
408
|
+
/**
|
|
409
|
+
* GET /api/view-doc?id=<artifactId> (CR-GVE-250) — der einzige Weg von der
|
|
410
|
+
* Artefaktzeile des Dashboards zu docs/views/<name>.md: `dist/` liefert
|
|
411
|
+
* `docs/` nicht aus, ein statischer Link geht also nicht.
|
|
412
|
+
*
|
|
413
|
+
* `id` wird AUSSCHLIESSLICH über den Katalog `VIEW_FILENAMES` aufgelöst —
|
|
414
|
+
* aus der Anfrage wird nie ein Pfad zusammengesetzt, deshalb läuft ein
|
|
415
|
+
* `../..`-Versuch nicht ins Dateisystem, sondern in den Katalog-Miss (404).
|
|
416
|
+
* `Object.hasOwn` statt `in`, sonst würden `__proto__`/`constructor` als
|
|
417
|
+
* Treffer gelten.
|
|
418
|
+
*/
|
|
419
|
+
function readViewDoc(id) {
|
|
420
|
+
if (!id || !Object.hasOwn(VIEW_FILENAMES, id)) return null;
|
|
421
|
+
const file = join(VIEWS_DIR, VIEW_FILENAMES[id]);
|
|
422
|
+
return existsSync(file) ? readFileSync(file, 'utf8') : null;
|
|
423
|
+
}
|
|
424
|
+
|
|
281
425
|
const middleware = (req, res, next) => {
|
|
282
|
-
|
|
426
|
+
const url = new URL(req.url ?? '/', 'http://localhost');
|
|
427
|
+
if (url.pathname === '/api/view-doc') {
|
|
428
|
+
const markdown = readViewDoc(url.searchParams.get('id'));
|
|
429
|
+
if (markdown === null) {
|
|
430
|
+
res.statusCode = 404;
|
|
431
|
+
res.setHeader('Content-Type', 'application/json');
|
|
432
|
+
res.end(JSON.stringify({ error: 'unknown view-doc id' }));
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
res.setHeader('Content-Type', 'text/markdown; charset=utf-8');
|
|
436
|
+
res.end(markdown);
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
if (url.pathname !== '/api/dashboard') return next();
|
|
283
440
|
res.setHeader('Content-Type', 'application/json');
|
|
284
441
|
res.end(JSON.stringify(buildDashboard()));
|
|
285
442
|
};
|