@sigloch/graph-api-core 5.1.0 → 5.3.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/browser.d.ts CHANGED
@@ -13,7 +13,7 @@
13
13
  */
14
14
  export { findRoot } from './find-root.js';
15
15
  export type { RootQueryGraph } from './find-root.js';
16
- export { SE_DESCRIPTOR, createSeDescriptor, projectToOntologyGraph } from './se-descriptor.js';
16
+ export { SE_DESCRIPTOR, createSeDescriptor, projectToOntologyGraph, fromOntologyGraph } from './se-descriptor.js';
17
17
  export { FormatECodec } from './format-e-codec.js';
18
18
  export { isValidTrace, tracePatternsOf } from './types.js';
19
19
  export type { GraphNode, GraphEdge, Graph } from './types.js';
package/dist/browser.js CHANGED
@@ -12,6 +12,6 @@
12
12
  * @author andreas@siglochconsulting
13
13
  */
14
14
  export { findRoot } from './find-root.js';
15
- export { SE_DESCRIPTOR, createSeDescriptor, projectToOntologyGraph } from './se-descriptor.js';
15
+ export { SE_DESCRIPTOR, createSeDescriptor, projectToOntologyGraph, fromOntologyGraph } from './se-descriptor.js';
16
16
  export { FormatECodec } from './format-e-codec.js';
17
17
  export { isValidTrace, tracePatternsOf } from './types.js';
package/dist/edge-ops.js CHANGED
@@ -31,13 +31,20 @@ export function mergeNodes(graph, sourceUid, targetUid) {
31
31
  if (!graph.nodes.some(n => n.uid === sourceUid)) {
32
32
  throw new Error(`merge-nodes: source node not found: ${sourceUid}`);
33
33
  }
34
- const edges = [];
34
+ // CR-SM-253: two passes, not one. The single-pass form checked the REWIRED edge
35
+ // against what the loop had collected so far, but appended an untouched edge
36
+ // unchecked — so the result depended on the order of `graph.edges`: source's edge
37
+ // first gave `B-verify->R` twice, target's edge first gave it once. Kuzu keys on
38
+ // (source, type, target) and silently keeps one, so the in-memory graph then claimed
39
+ // an edge the store did not have. Collecting every survivor first and deduplicating
40
+ // against the complete set makes the result order-independent.
35
41
  const removedEdges = [];
36
- const addedEdges = [];
42
+ const kept = [];
43
+ const rewiredEdges = [];
37
44
  for (const e of graph.edges) {
38
45
  const touchesSource = e.sourceId === sourceUid || e.targetId === sourceUid;
39
46
  if (!touchesSource) {
40
- edges.push(e);
47
+ kept.push(e);
41
48
  continue;
42
49
  }
43
50
  removedEdges.push(e);
@@ -49,11 +56,18 @@ export function mergeNodes(graph, sourceUid, targetUid) {
49
56
  if (rewired.sourceId === rewired.targetId) {
50
57
  continue; // self-edge created by rewiring — discarded
51
58
  }
59
+ rewiredEdges.push(rewired);
60
+ }
61
+ // `addedEdges` is the delta the store persists. An edge the target already carried
62
+ // is not an addition — persisting it would write the duplicate into the store path
63
+ // that the in-memory dedup just prevented.
64
+ const edges = kept.slice();
65
+ const addedEdges = [];
66
+ for (const rewired of rewiredEdges) {
67
+ if (edges.some(x => sameEdge(x, rewired)))
68
+ continue;
69
+ edges.push(rewired);
52
70
  addedEdges.push(rewired);
53
- // Two incident edges (or one incident + one pre-existing) can rewire onto
54
- // the same (source,target,type) — keep the result graph free of duplicates.
55
- if (!edges.some(x => sameEdge(x, rewired)))
56
- edges.push(rewired);
57
71
  }
58
72
  const nodes = graph.nodes.filter(n => n.uid !== sourceUid);
59
73
  return { graph: { nodes, edges }, removedNode: sourceUid, removedEdges, addedEdges };
@@ -72,7 +72,6 @@ export class GraphService {
72
72
  let rejected = 0;
73
73
  const violations = [];
74
74
  const nodesToCreate = [];
75
- const edgesToCreate = [];
76
75
  // CR-195d: Hoist kinds/asil/method from attributes to top-level for SE schema
77
76
  const isSESchema = this.ontology.name === 'SE';
78
77
  for (const op of diff.operations) {
@@ -103,13 +102,6 @@ export class GraphService {
103
102
  const node = await this.applyNodeCreate(op, isSESchema);
104
103
  await this.storage.saveNodes([node]);
105
104
  nodesToCreate.push(op.semanticId);
106
- if (consumerId !== 'system') {
107
- edgesToCreate.push({
108
- sourceId: consumerId,
109
- targetId: op.semanticId,
110
- edgeType: 'produces',
111
- });
112
- }
113
105
  applied++;
114
106
  }
115
107
  catch (e) {
@@ -142,13 +134,6 @@ export class GraphService {
142
134
  const node = await this.applyNodeCreate(op, isSESchema);
143
135
  await this.storage.saveNodes([node]);
144
136
  nodesToCreate.push(op.semanticId);
145
- if (consumerId !== 'system') {
146
- edgesToCreate.push({
147
- sourceId: consumerId,
148
- targetId: op.semanticId,
149
- edgeType: 'produces',
150
- });
151
- }
152
137
  applied++;
153
138
  }
154
139
  catch (e) {
@@ -381,18 +366,11 @@ export class GraphService {
381
366
  rejected++;
382
367
  }
383
368
  }
384
- // CR-195d: Create produces edges for newly created nodes (audit trail)
385
- if (edgesToCreate.length > 0) {
386
- try {
387
- await this.storage.saveEdges(edgesToCreate.map(e => ({
388
- ...e,
389
- attributes: {},
390
- })));
391
- }
392
- catch (e) {
393
- console.warn('[GraphService] Failed to create audit produces edges:', e);
394
- }
395
- }
369
+ // CR-SM-266 D5: der CR-195d-Block, der je neuem Knoten eine `produces`-Kante vom Consumer
370
+ // schrieb, ist WEG — mit dem TraceType und dem SESSION-Elementtyp. Er war schon vorher tot:
371
+ // 0 produces-Kanten in allen 9 aktiven Familie-Graphen nach 195 gegateten Versionen, weil
372
+ // der schreibende Weg (Bridge) read-only ist und der lebende (MCP-Gate) ihn nie ging.
373
+ // Provenance liegt seit CR-GC-347 in `.graphcode/audit.jsonl`, ausserhalb des Graphen.
396
374
  // Add parse errors as violations
397
375
  for (const err of diff.errors) {
398
376
  violations.push({
package/dist/index.d.ts CHANGED
@@ -22,7 +22,7 @@ export { MemoryAdapter } from './memory-adapter.js';
22
22
  export type { TransportAdapter, TransportConfig } from './transport-adapter.js';
23
23
  export { createGraphApi } from './factory.js';
24
24
  export type { GraphApiConfig } from './factory.js';
25
- export { SE_DESCRIPTOR, createSeDescriptor, projectToOntologyGraph } from './se-descriptor.js';
25
+ export { SE_DESCRIPTOR, createSeDescriptor, projectToOntologyGraph, fromOntologyGraph } from './se-descriptor.js';
26
26
  export { findRoot } from './find-root.js';
27
27
  export type { RootQueryGraph } from './find-root.js';
28
28
  export { applyEdgeOps, updateEdge, mergeNodes } from './edge-ops.js';
package/dist/index.js CHANGED
@@ -19,7 +19,7 @@ export { MemoryAdapter } from './memory-adapter.js';
19
19
  // Factory
20
20
  export { createGraphApi } from './factory.js';
21
21
  // SE OntologyDescriptor (derived from @sigloch/contracts/se) [CR-195a]
22
- export { SE_DESCRIPTOR, createSeDescriptor, projectToOntologyGraph } from './se-descriptor.js';
22
+ export { SE_DESCRIPTOR, createSeDescriptor, projectToOntologyGraph, fromOntologyGraph } from './se-descriptor.js';
23
23
  // Root-Suche — strukturelle Wurzel (SYS ohne eingehende compose), statt UID-Hardcode
24
24
  export { findRoot } from './find-root.js';
25
25
  // Edge ops — update-edge (flip/retype) + merge-nodes, shared by GraphService.mutate()
@@ -17,6 +17,30 @@ import type { Graph, OntologyDescriptor } from './types.js';
17
17
  * attributes (asil/method/kinds/status) are lifted out of `attributes`.
18
18
  */
19
19
  export declare function projectToOntologyGraph(graph: Graph): OntologyGraph;
20
+ /**
21
+ * The INVERSE of `projectToOntologyGraph`: an OntologyGraph (elements/traces) back
22
+ * to the ontology-agnostic Graph (nodes/edges) — every attribute restored under
23
+ * `attributes`, where the rules read them.
24
+ *
25
+ * Why this must be published (CR-SM-254): only the forward direction was exported,
26
+ * so every consumer that READS a committed `*.graph.json` hand-rolled its own mirror
27
+ * — and the mirrors drifted. Two were found wrong the same day, both in the same
28
+ * half: they lifted `status/asil/method/kinds` and forgot `realRef`/`testRefs`, so
29
+ * `R-19`/`R-20` silently read "unbound" on a fully bound graph and a dashboard
30
+ * reported `TRR 0/214` against a store that said `214/214`. A hand-rolled inverse
31
+ * cannot be kept honest by review; it has to have one home.
32
+ *
33
+ * Accepts BOTH shapes in the wild, which is the whole point:
34
+ * - what THIS module emits — attributes nested under `attributes`, plus the
35
+ * lifted convenience fields (status/asil/method/kinds) alongside;
36
+ * - a FLATTENED snapshot — `{id, type, name, description, ...attributes}` with no
37
+ * `attributes` key at all (graphcode's committed SSOT since its CR-GC-219, which
38
+ * drops the nested duplicate so the file a human diffs carries each value once).
39
+ *
40
+ * Nested wins nothing: a key already present at top level is never clobbered, so
41
+ * re-import of an already-flat element is idempotent.
42
+ */
43
+ export declare function fromOntologyGraph(json: OntologyGraph): Graph;
20
44
  /**
21
45
  * Canonical SE OntologyDescriptor (ontology + V3 rules + MT metrics), version-pinned
22
46
  * to contracts/se ONTOLOGY_VERSION. Plug into GraphService / FormatECodec.
@@ -33,7 +33,9 @@ export function projectToOntologyGraph(graph) {
33
33
  source: e.sourceId,
34
34
  target: e.targetId,
35
35
  type: e.edgeType,
36
- category: e.attributes?.category,
36
+ // CR-SM-266 D5: `category` ist nicht mehr Teil des Trace-Vertrags. Die Projektion hob es
37
+ // aus den Kanten-Attributen aufs Top-Level, wo R-08/R-18 es als Pruef-Ausnahme lasen —
38
+ // genau der Weg, ueber den eine Kante die Pattern-Matrix haette umgehen koennen.
37
39
  label: e.attributes?.label,
38
40
  weight: e.attributes?.weight ?? 1,
39
41
  created_at: e.attributes?.created_at ?? '',
@@ -42,6 +44,67 @@ export function projectToOntologyGraph(graph) {
42
44
  }));
43
45
  return { elements, traces };
44
46
  }
47
+ /**
48
+ * The INVERSE of `projectToOntologyGraph`: an OntologyGraph (elements/traces) back
49
+ * to the ontology-agnostic Graph (nodes/edges) — every attribute restored under
50
+ * `attributes`, where the rules read them.
51
+ *
52
+ * Why this must be published (CR-SM-254): only the forward direction was exported,
53
+ * so every consumer that READS a committed `*.graph.json` hand-rolled its own mirror
54
+ * — and the mirrors drifted. Two were found wrong the same day, both in the same
55
+ * half: they lifted `status/asil/method/kinds` and forgot `realRef`/`testRefs`, so
56
+ * `R-19`/`R-20` silently read "unbound" on a fully bound graph and a dashboard
57
+ * reported `TRR 0/214` against a store that said `214/214`. A hand-rolled inverse
58
+ * cannot be kept honest by review; it has to have one home.
59
+ *
60
+ * Accepts BOTH shapes in the wild, which is the whole point:
61
+ * - what THIS module emits — attributes nested under `attributes`, plus the
62
+ * lifted convenience fields (status/asil/method/kinds) alongside;
63
+ * - a FLATTENED snapshot — `{id, type, name, description, ...attributes}` with no
64
+ * `attributes` key at all (graphcode's committed SSOT since its CR-GC-219, which
65
+ * drops the nested duplicate so the file a human diffs carries each value once).
66
+ *
67
+ * Nested wins nothing: a key already present at top level is never clobbered, so
68
+ * re-import of an already-flat element is idempotent.
69
+ */
70
+ export function fromOntologyGraph(json) {
71
+ const nodes = (json.elements ?? []).map((e) => {
72
+ const { id, type, name, description, created_at, updated_at, ...rest } = e;
73
+ return {
74
+ uid: id,
75
+ type,
76
+ name: name ?? id,
77
+ description: description ?? '',
78
+ createdAt: created_at,
79
+ updatedAt: updated_at,
80
+ attributes: liftAttributes(rest),
81
+ };
82
+ });
83
+ const edges = (json.traces ?? []).map((t) => {
84
+ const { source, target, type, ...rest } = t;
85
+ return { sourceId: source, targetId: target, edgeType: type, attributes: liftAttributes(rest) };
86
+ });
87
+ return { nodes, edges };
88
+ }
89
+ /**
90
+ * Merge a nested `attributes` object UP one level and drop the nesting key, so both
91
+ * the nested and the flattened shape yield the same `attributes`. Idempotent: an
92
+ * entry with no nested object is returned unchanged.
93
+ */
94
+ function liftAttributes(rest) {
95
+ const nested = rest.attributes;
96
+ if (nested === null || typeof nested !== 'object' || Array.isArray(nested)) {
97
+ const { attributes: _absent, ...flat } = rest;
98
+ return flat;
99
+ }
100
+ const { attributes: _drop, ...top } = rest;
101
+ const out = { ...top };
102
+ for (const [k, v] of Object.entries(nested)) {
103
+ if (!(k in out))
104
+ out[k] = v;
105
+ }
106
+ return out;
107
+ }
45
108
  /**
46
109
  * The contracts/se rule catalog, adapted to graph-api-core's Rule shape.
47
110
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sigloch/graph-api-core",
3
- "version": "5.1.0",
3
+ "version": "5.3.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -43,7 +43,7 @@
43
43
  "access": "public"
44
44
  },
45
45
  "peerDependencies": {
46
- "@sigloch/contracts": ">=5 <7",
46
+ "@sigloch/contracts": ">=5 <10",
47
47
  "kuzu-wasm": "^0.11.3"
48
48
  },
49
49
  "peerDependenciesMeta": {
@@ -52,7 +52,7 @@
52
52
  }
53
53
  },
54
54
  "devDependencies": {
55
- "@sigloch/contracts": "^6.0.0",
55
+ "@sigloch/contracts": ">=6 <10",
56
56
  "kuzu-wasm": "^0.11.3"
57
57
  }
58
58
  }