@sigloch/graph-view-edit 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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-DSNw0FsP.js"></script>
|
|
8
|
+
<link rel="stylesheet" crossorigin href="/assets/index-2Jd5dg79.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.0",
|
|
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": {
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"@playwright/test": "^1.58.2",
|
|
33
|
-
"@sigloch/graphcode": "^0.
|
|
33
|
+
"@sigloch/graphcode": "^0.17.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
|
@@ -204,6 +204,50 @@ export function loadGraph(file) {
|
|
|
204
204
|
return { nodes, edges, graphVersion: json.graphVersion ?? 0 };
|
|
205
205
|
}
|
|
206
206
|
|
|
207
|
+
/**
|
|
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.
|
|
216
|
+
*
|
|
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.
|
|
220
|
+
*/
|
|
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);
|
|
228
|
+
}
|
|
229
|
+
g.count += 1;
|
|
230
|
+
if (v.elementId) g.allElementIds.push(v.elementId);
|
|
231
|
+
}
|
|
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
|
+
}));
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
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.
|
|
246
|
+
*/
|
|
247
|
+
export function recommendationsPayload(violations, limit = 50) {
|
|
248
|
+
return { ...recommendationsPanel(violations, limit), groups: recommendationGroups(violations) };
|
|
249
|
+
}
|
|
250
|
+
|
|
207
251
|
function dashboardApiPlugin() {
|
|
208
252
|
const cwd = resolveRepoRoot();
|
|
209
253
|
const GRAPH_DIR = join(cwd, 'docs', 'graph');
|
|
@@ -241,7 +285,18 @@ function dashboardApiPlugin() {
|
|
|
241
285
|
const staleVsGraph = exists ? statSync(p).mtimeMs < graphMtime : false;
|
|
242
286
|
return { id: entry.id, exists, staleVsGraph };
|
|
243
287
|
});
|
|
244
|
-
|
|
288
|
+
// CR-GVE-252: welche Zeile ein Dokument oeffnen kann, entscheidet der
|
|
289
|
+
// KATALOG, nicht `kind`. 14 der 15 Artefakte haben einen VIEW_FILENAMES-
|
|
290
|
+
// Eintrag — auch vier der fuenf `analysis`-Creations (conops/fmea/trade/
|
|
291
|
+
// implplan). Nur `assumption-review` hat keinen. Die frueher hier
|
|
292
|
+
// implizite Gleichsetzung `kind === 'render'` == "hat eine Datei" war
|
|
293
|
+
// schlicht falsch (CR-GVE-250 §8.1). Dieselbe Tabelle, gegen die
|
|
294
|
+
// /api/view-doc aufloest — kein zweites Kriterium im Client.
|
|
295
|
+
const panel = artifactsPanel(items);
|
|
296
|
+
return {
|
|
297
|
+
...panel,
|
|
298
|
+
artifacts: panel.artifacts.map((a) => ({ ...a, hasDoc: !!VIEW_FILENAMES[a.id] })),
|
|
299
|
+
};
|
|
245
300
|
}
|
|
246
301
|
|
|
247
302
|
function synthHealth(graph) {
|
|
@@ -271,15 +326,45 @@ function dashboardApiPlugin() {
|
|
|
271
326
|
repoRoot: servedRepoRoot(),
|
|
272
327
|
empty: graph.nodes.length === 0,
|
|
273
328
|
readiness: readinessPanel(report),
|
|
274
|
-
recommendations:
|
|
329
|
+
recommendations: recommendationsPayload(violations, 50),
|
|
275
330
|
artifacts: scanArtifacts(file, graph),
|
|
276
331
|
health: synthHealth(graph),
|
|
277
332
|
computedAt: new Date().toISOString(),
|
|
278
333
|
};
|
|
279
334
|
}
|
|
280
335
|
|
|
336
|
+
/**
|
|
337
|
+
* GET /api/view-doc?id=<artifactId> (CR-GVE-250) — der einzige Weg von der
|
|
338
|
+
* Artefaktzeile des Dashboards zu docs/views/<name>.md: `dist/` liefert
|
|
339
|
+
* `docs/` nicht aus, ein statischer Link geht also nicht.
|
|
340
|
+
*
|
|
341
|
+
* `id` wird AUSSCHLIESSLICH über den Katalog `VIEW_FILENAMES` aufgelöst —
|
|
342
|
+
* aus der Anfrage wird nie ein Pfad zusammengesetzt, deshalb läuft ein
|
|
343
|
+
* `../..`-Versuch nicht ins Dateisystem, sondern in den Katalog-Miss (404).
|
|
344
|
+
* `Object.hasOwn` statt `in`, sonst würden `__proto__`/`constructor` als
|
|
345
|
+
* Treffer gelten.
|
|
346
|
+
*/
|
|
347
|
+
function readViewDoc(id) {
|
|
348
|
+
if (!id || !Object.hasOwn(VIEW_FILENAMES, id)) return null;
|
|
349
|
+
const file = join(VIEWS_DIR, VIEW_FILENAMES[id]);
|
|
350
|
+
return existsSync(file) ? readFileSync(file, 'utf8') : null;
|
|
351
|
+
}
|
|
352
|
+
|
|
281
353
|
const middleware = (req, res, next) => {
|
|
282
|
-
|
|
354
|
+
const url = new URL(req.url ?? '/', 'http://localhost');
|
|
355
|
+
if (url.pathname === '/api/view-doc') {
|
|
356
|
+
const markdown = readViewDoc(url.searchParams.get('id'));
|
|
357
|
+
if (markdown === null) {
|
|
358
|
+
res.statusCode = 404;
|
|
359
|
+
res.setHeader('Content-Type', 'application/json');
|
|
360
|
+
res.end(JSON.stringify({ error: 'unknown view-doc id' }));
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
res.setHeader('Content-Type', 'text/markdown; charset=utf-8');
|
|
364
|
+
res.end(markdown);
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
if (url.pathname !== '/api/dashboard') return next();
|
|
283
368
|
res.setHeader('Content-Type', 'application/json');
|
|
284
369
|
res.end(JSON.stringify(buildDashboard()));
|
|
285
370
|
};
|