@sigloch/graph-view-edit 0.5.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-DwmmYp3d.js"></script>
8
- <link rel="stylesheet" crossorigin href="/assets/index-P7N8B6D1.css">
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.5.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": {
@@ -21,16 +21,16 @@
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.0.0",
25
- "@sigloch/graph-api-core": "^5.0.0",
26
- "@sigloch/graphcode-client": "^1.0.0",
24
+ "@sigloch/contracts": ">=5 <7",
25
+ "@sigloch/graph-api-core": ">=5 <6",
26
+ "@sigloch/graphcode-client": ">=1 <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.13.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
- return artifactsPanel(items);
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: recommendationsPanel(violations, 50),
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
- if (req.url !== '/api/dashboard') return next();
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
  };