@sigloch/graph-api-core 5.4.0 → 5.6.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/format-e-codec.d.ts +27 -5
- package/dist/format-e-codec.js +167 -34
- package/dist/graph-service.js +24 -12
- package/dist/kuzu/graph-cypher-engine.js +21 -1
- package/dist/kuzu/schema-generator.js +0 -0
- package/dist/se-descriptor.js +17 -3
- package/dist/types.d.ts +7 -14
- package/dist/types.js +7 -14
- package/package.json +1 -1
package/dist/format-e-codec.d.ts
CHANGED
|
@@ -3,8 +3,6 @@ export declare class FormatECodec {
|
|
|
3
3
|
private readonly ontology;
|
|
4
4
|
private readonly edgeArrowToType;
|
|
5
5
|
private readonly validNodeTypes;
|
|
6
|
-
/** Trace-legality patterns (CR-GC-247) — the SSOT the parser validates edges against. */
|
|
7
|
-
private readonly patterns;
|
|
8
6
|
constructor(ontology: OntologyDescriptor);
|
|
9
7
|
/** Extract a ```format-e block from LLM output. Returns null if not found. */
|
|
10
8
|
extractFromLlm(llmOutput: string): string | null;
|
|
@@ -31,6 +29,7 @@ export declare class FormatECodec {
|
|
|
31
29
|
*/
|
|
32
30
|
serialize(graph: Graph, options?: {
|
|
33
31
|
omitProvenance?: boolean;
|
|
32
|
+
roundTrip?: boolean;
|
|
34
33
|
}): string;
|
|
35
34
|
private parseNodeLine;
|
|
36
35
|
private parseEdgeLine;
|
|
@@ -67,9 +66,32 @@ export declare class FormatECodec {
|
|
|
67
66
|
* contains no comma. No edge in the SE ontology carries a structured attribute today; if one
|
|
68
67
|
* ever does, the inline block is the thing to replace, not this escape.
|
|
69
68
|
*/
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
69
|
+
/**
|
|
70
|
+
* CR-SM-332: EIN Kriterium entscheidet, ob ein Attribut inline oder auf eine `@key`-Zeile
|
|
71
|
+
* geht — naemlich, ob sein Wert den Inline-Block brechen wuerde.
|
|
72
|
+
*
|
|
73
|
+
* Vorher entschied der TYP: `typeof v === 'object'` ging auf die Folgezeile, alles andere
|
|
74
|
+
* inline. Damit blieb eine Luecke derselben Klasse wie der Zeilenumbruch offen: ein STRING
|
|
75
|
+
* mit Komma oder Klammer ("a, b" oder "f(x)") wurde inline geschrieben, und
|
|
76
|
+
* `parseInlineAttrs` splittet auf Kommas — der Wert kam zerteilt oder gar nicht zurueck.
|
|
77
|
+
* Still, wie immer bei dieser Klasse. graphcodes Fork hatte genau dafuer bereits
|
|
78
|
+
* `UNSAFE_ATTR_RE`; die Regel wandert hierher, wo der einzige Codec steht.
|
|
79
|
+
*
|
|
80
|
+
* Schluessel aufsteigend nach Code-Einheiten, nicht `localeCompare` — dieselbe Begruendung
|
|
81
|
+
* wie bei `serializeEdges`: eine Sortierung, die von der Locale abhaengt, ist nicht
|
|
82
|
+
* deterministisch.
|
|
83
|
+
*/
|
|
84
|
+
private nodeAttrParts;
|
|
85
|
+
/**
|
|
86
|
+
* CR-SM-332 (aus graphcodes Fork uebernommen, CR-GC-200/CR-GC-531): Knotentypen gegen die
|
|
87
|
+
* Ontologie, Kantentypen gegen die Ontologie, doppelte uids, aufloesbare Endpunkte.
|
|
88
|
+
* Paar-Legalitaet gehoert NICHT hierher — das ist R-18, dort wo Daten in den Speicher
|
|
89
|
+
* gehen.
|
|
90
|
+
*/
|
|
91
|
+
validate(graph: Graph, resolveType?: (uid: string) => string | undefined): {
|
|
92
|
+
valid: boolean;
|
|
93
|
+
errors: string[];
|
|
94
|
+
};
|
|
73
95
|
/** Inline block for an EDGE — structured values stringified, see `serializeAttrs`. */
|
|
74
96
|
private serializeEdgeAttrs;
|
|
75
97
|
private edgeTypeToArrow;
|
package/dist/format-e-codec.js
CHANGED
|
@@ -8,7 +8,6 @@
|
|
|
8
8
|
* every foreign convention fail silently instead of loudly.
|
|
9
9
|
*/
|
|
10
10
|
import { hydrateAttrValue, attributeTypeOf } from '@sigloch/contracts/se';
|
|
11
|
-
import { isValidTrace, tracePatternsOf } from './types.js';
|
|
12
11
|
// ---------------------------------------------------------------------------
|
|
13
12
|
// Regex patterns
|
|
14
13
|
// ---------------------------------------------------------------------------
|
|
@@ -23,12 +22,39 @@ const MERGE_RE = /^M\s+(.+)$/;
|
|
|
23
22
|
const FORMAT_E_FENCE = /```format-e\s*\n([\s\S]*?)```/;
|
|
24
23
|
/** Inline attribute block: [key:value,key:value] */
|
|
25
24
|
const INLINE_ATTRS_RE = /\[([^\]]+)\]$/;
|
|
25
|
+
/**
|
|
26
|
+
* CR-SM-332 (ITEM-2026-183) — Format E ist ZEILENBASIERT, also darf kein Feld eine Zeile
|
|
27
|
+
* beenden koennen. `serialize` schrieb `|${description}` roh hinaus; eine Beschreibung mit
|
|
28
|
+
* `\n` erzeugte damit eine zweite Zeile, die beim naechsten Lesen als eigener KNOTEN ankam
|
|
29
|
+
* (uid = der Resttext, Typ = die offene `### <TYPE>`-Sektion). Am Gate reproduziert: success
|
|
30
|
+
* true, mutations 2 — der Phantom-Knoten waere angelegt worden.
|
|
31
|
+
*
|
|
32
|
+
* Beide Enden sind noetig. Nur lesend zu pruefen hiesse, der kaputte Text entsteht weiter und
|
|
33
|
+
* faellt erst beim naechsten Lesen auf — dann ohne den Knoten, der ihn verursacht hat.
|
|
34
|
+
*/
|
|
35
|
+
const LINE_BREAK_RE = /[\r\n]/;
|
|
36
|
+
/**
|
|
37
|
+
* CR-SM-332: Zeichen, an denen der Inline-Block `[k:v,k:v]` zerbricht. `parseInlineAttrs`
|
|
38
|
+
* splittet auf Kommas, also kann kein Wert mit Komma oder Klammer dort stehen — gleich ob er
|
|
39
|
+
* ein Objekt ist oder ein String. Uebernommen aus graphcodes Fork (UNSAFE_ATTR_RE).
|
|
40
|
+
*/
|
|
41
|
+
const UNSAFE_INLINE_RE = /[,[\]{}]/;
|
|
42
|
+
/**
|
|
43
|
+
* Vergleich nach CODE-EINHEITEN, nicht `localeCompare`: eine Sortierung, die von der Locale
|
|
44
|
+
* des laufenden Prozesses abhaengt, ist nicht deterministisch (REQ-deterministic-serialization).
|
|
45
|
+
*/
|
|
46
|
+
const cmp = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
|
|
47
|
+
function assertSingleLine(where, field, value) {
|
|
48
|
+
if (typeof value === 'string' && LINE_BREAK_RE.test(value)) {
|
|
49
|
+
throw new Error(`FormatECodec.serialize: ${where} — ${field} enthaelt einen Zeilenumbruch. Format E ist `
|
|
50
|
+
+ `zeilenbasiert; der Umbruch wuerde beim Lesen als eigene Knotenzeile ankommen `
|
|
51
|
+
+ `(ITEM-2026-183). Ein langer Text gehoert zerlegt, nicht umgebrochen.`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
26
54
|
export class FormatECodec {
|
|
27
55
|
ontology;
|
|
28
56
|
edgeArrowToType;
|
|
29
57
|
validNodeTypes;
|
|
30
|
-
/** Trace-legality patterns (CR-GC-247) — the SSOT the parser validates edges against. */
|
|
31
|
-
patterns;
|
|
32
58
|
constructor(ontology) {
|
|
33
59
|
this.ontology = ontology;
|
|
34
60
|
// Build arrow → edge type lookup
|
|
@@ -40,8 +66,6 @@ export class FormatECodec {
|
|
|
40
66
|
}
|
|
41
67
|
// Valid node type abbreviations
|
|
42
68
|
this.validNodeTypes = new Set(Object.keys(ontology.nodeTypes));
|
|
43
|
-
// Meta-model legality: descriptor.patterns (or derived from validPairs).
|
|
44
|
-
this.patterns = tracePatternsOf(ontology);
|
|
45
69
|
}
|
|
46
70
|
/** Extract a ```format-e block from LLM output. Returns null if not found. */
|
|
47
71
|
extractFromLlm(llmOutput) {
|
|
@@ -162,6 +186,17 @@ export class FormatECodec {
|
|
|
162
186
|
*/
|
|
163
187
|
serialize(graph, options = {}) {
|
|
164
188
|
const omit = options.omitProvenance === true;
|
|
189
|
+
const roundTrip = options.roundTrip === true;
|
|
190
|
+
// CR-SM-332: `roundTrip` ist die Fassung, die graphcode bis hierher als EIGENEN encode
|
|
191
|
+
// nachgebaut hat (projections/codec.ts) — sie traegt zusaetzlich die Felder, die neben
|
|
192
|
+
// `attributes` leben (name/createdAt/updatedAt), und prueft den Graphen vorher. Kein
|
|
193
|
+
// zweiter Codec, eine Option: der Standard bleibt zeichengleich die Agenten-Sicht.
|
|
194
|
+
if (roundTrip) {
|
|
195
|
+
const { valid, errors } = this.validate(graph);
|
|
196
|
+
if (!valid) {
|
|
197
|
+
throw new Error(`FormatECodec.serialize: graph validation failed:\n - ${errors.join('\n - ')}`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
165
200
|
const lines = [];
|
|
166
201
|
if (graph.nodes.length > 0) {
|
|
167
202
|
lines.push('## Nodes');
|
|
@@ -178,13 +213,38 @@ export class FormatECodec {
|
|
|
178
213
|
}
|
|
179
214
|
for (const type of [...byType.keys()].sort()) {
|
|
180
215
|
lines.push(`### ${type}`);
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
216
|
+
// CR-SM-332: innerhalb der Sektion nach uid sortiert. Vorher stand hier die
|
|
217
|
+
// EINGABEREIHENFOLGE — zwei encode-Laeufe auf demselben Graphen konnten sich also
|
|
218
|
+
// unterscheiden, sobald der Speicher anders lieferte. Das war der Grund, aus dem
|
|
219
|
+
// graphcode einen eigenen encode hielt (REQ-deterministic-serialization).
|
|
220
|
+
const group = [...(byType.get(type) ?? [])].sort((a, b) => cmp(a.uid, b.uid));
|
|
221
|
+
for (const node of group) {
|
|
222
|
+
// CR-SM-332: kein Umbruch verlaesst diesen Codec.
|
|
223
|
+
assertSingleLine(`node "${node.uid}"`, 'description', node.description);
|
|
224
|
+
const base = omit ? stripNodeProvenance(node.attributes) : node.attributes;
|
|
225
|
+
// Die Felder, die NEBEN `attributes` am Knoten haengen — ohne sie ist
|
|
226
|
+
// decode(encode(g)) nicht deep-equal g. Nur in der Rundlauf-Fassung: in der
|
|
227
|
+
// Agenten-Sicht waere `__name` an jedem Knoten reines Rauschen.
|
|
228
|
+
const nodeAttrs = roundTrip
|
|
229
|
+
? {
|
|
230
|
+
...base,
|
|
231
|
+
__name: node.name,
|
|
232
|
+
...(node.createdAt !== undefined ? { __createdAt: node.createdAt } : {}),
|
|
233
|
+
...(node.updatedAt !== undefined ? { __updatedAt: node.updatedAt } : {}),
|
|
234
|
+
}
|
|
235
|
+
: base;
|
|
236
|
+
for (const [k, v] of Object.entries(nodeAttrs)) {
|
|
237
|
+
// Strukturierte Werte gehen als JSON auf @key-Zeilen; JSON.stringify maskiert
|
|
238
|
+
// den Umbruch, dort kann er die Zeile nicht brechen.
|
|
239
|
+
if (typeof v !== 'object')
|
|
240
|
+
assertSingleLine(`node "${node.uid}"`, `attribute "${k}"`, v);
|
|
241
|
+
}
|
|
242
|
+
// In der Rundlauf-Fassung steht der Pipe IMMER: ohne ihn kaeme eine leere
|
|
243
|
+
// Beschreibung als `undefined` zurueck statt als '' (graphcodes encode tat dasselbe).
|
|
244
|
+
const descr = node.description ? `|${node.description}` : (roundTrip ? '|' : '');
|
|
245
|
+
const { inline, follow } = this.nodeAttrParts(nodeAttrs);
|
|
246
|
+
lines.push(`+ ${node.uid}${descr}${inline}`);
|
|
247
|
+
lines.push(...follow);
|
|
188
248
|
}
|
|
189
249
|
}
|
|
190
250
|
}
|
|
@@ -194,6 +254,14 @@ export class FormatECodec {
|
|
|
194
254
|
const edges = omit
|
|
195
255
|
? graph.edges.map(e => ({ ...e, attributes: stripEdgeProvenance(e.attributes) }))
|
|
196
256
|
: graph.edges;
|
|
257
|
+
// CR-SM-332: dieselbe Zusicherung an der Kante — ihr Inline-Block steht auf derselben Zeile.
|
|
258
|
+
for (const e of edges) {
|
|
259
|
+
for (const [k, v] of Object.entries(e.attributes)) {
|
|
260
|
+
if (typeof v !== 'object') {
|
|
261
|
+
assertSingleLine(`edge "${e.sourceId} -${e.edgeType}-> ${e.targetId}"`, `attribute "${k}"`, v);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
197
265
|
lines.push(...this.serializeEdges(edges));
|
|
198
266
|
}
|
|
199
267
|
return lines.join('\n');
|
|
@@ -202,9 +270,17 @@ export class FormatECodec {
|
|
|
202
270
|
// Private
|
|
203
271
|
// ---------------------------------------------------------------------------
|
|
204
272
|
parseNodeLine(line, nodeType, declared, ops, errors) {
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
273
|
+
// CR-SM-332: das Operator-Praefix ist PFLICHT. Vorher war es optional ("kein Praefix =
|
|
274
|
+
// add"), womit JEDE Textzeile eine gueltige Knotenzeile war — genau der Weg, auf dem eine
|
|
275
|
+
// uebergelaufene Beschreibung zum Phantom-Knoten wurde. Ein Praefix zu verlangen kostet
|
|
276
|
+
// nichts (jeder Erzeuger schreibt es) und schliesst die Klasse.
|
|
277
|
+
const action = OP_PREFIX[line[0]];
|
|
278
|
+
if (!action) {
|
|
279
|
+
errors.push(`Node line without an operator prefix (+ - ~ !): "${line}" — eine Zeile ohne Operator `
|
|
280
|
+
+ `ist keine Knotenzeile. Haeufigste Ursache: eine Beschreibung mit Zeilenumbruch.`);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
const rest = line.slice(1).trim();
|
|
208
284
|
// Extract inline attributes
|
|
209
285
|
let mainPart = rest;
|
|
210
286
|
let inlineAttrs;
|
|
@@ -221,6 +297,14 @@ export class FormatECodec {
|
|
|
221
297
|
errors.push(`Node line without a uid: "${line}"`);
|
|
222
298
|
return;
|
|
223
299
|
}
|
|
300
|
+
// CR-SM-332: die uid war "alles vor dem Pipe", Leerzeichen eingeschlossen — der zweite Teil
|
|
301
|
+
// desselben Einfallstors. Der (entfernte) contracts-Parser verlangte hier `\S+?` und haette
|
|
302
|
+
// den Fall gefangen; die Strenge wandert mit ihm hierher.
|
|
303
|
+
if (/\s/.test(uid)) {
|
|
304
|
+
errors.push(`Uid contains whitespace: "${uid}" — uids sind zusammenhaengend. Eine uebergelaufene `
|
|
305
|
+
+ `Beschreibungszeile sieht genau so aus.`);
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
224
308
|
// CR-SM-217: with `TYPE-slug` as the family canon, the prefix restores the
|
|
225
309
|
// redundancy CR-SM-216 removed — for free, since `REQ-safety` costs fewer tokens
|
|
226
310
|
// than `REQ-safety.REQ`. A node under the wrong section is caught here. Uids with
|
|
@@ -290,12 +374,9 @@ export class FormatECodec {
|
|
|
290
374
|
errors.push(`Cannot resolve type of target "${targetId}" — not declared under a "### <TYPE>" section and no resolveType provided`);
|
|
291
375
|
continue;
|
|
292
376
|
}
|
|
293
|
-
//
|
|
294
|
-
//
|
|
295
|
-
|
|
296
|
-
errors.push(`Meta-model violation: ${srcType} -${edgeType}-> ${tgtType} is not valid`);
|
|
297
|
-
continue;
|
|
298
|
-
}
|
|
377
|
+
// CR-SM-324: no legality verdict while parsing — the parser knows neither label nor
|
|
378
|
+
// the kinds of existing nodes. The write path judges with the one rule (gate R-18,
|
|
379
|
+
// GraphService.validateAndApplyEdge).
|
|
299
380
|
const opType = action === 'remove'
|
|
300
381
|
? 'remove_edge'
|
|
301
382
|
: action === 'strict_add'
|
|
@@ -364,7 +445,7 @@ export class FormatECodec {
|
|
|
364
445
|
entries.push({ sourceId: edge.sourceId, edgeType: edge.edgeType, targets: [edge.targetId], attrs });
|
|
365
446
|
continue;
|
|
366
447
|
}
|
|
367
|
-
const key = `${edge.sourceId}
|
|
448
|
+
const key = `${edge.sourceId}\0${edge.edgeType}`;
|
|
368
449
|
const group = groups.get(key);
|
|
369
450
|
if (group) {
|
|
370
451
|
group.targets.push(edge.targetId);
|
|
@@ -375,7 +456,6 @@ export class FormatECodec {
|
|
|
375
456
|
entries.push(entry);
|
|
376
457
|
}
|
|
377
458
|
}
|
|
378
|
-
const cmp = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
|
|
379
459
|
for (const entry of entries)
|
|
380
460
|
entry.targets.sort(cmp);
|
|
381
461
|
entries.sort((a, b) => cmp(a.sourceId, b.sourceId)
|
|
@@ -395,18 +475,71 @@ export class FormatECodec {
|
|
|
395
475
|
* contains no comma. No edge in the SE ontology carries a structured attribute today; if one
|
|
396
476
|
* ever does, the inline block is the thing to replace, not this escape.
|
|
397
477
|
*/
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
478
|
+
/**
|
|
479
|
+
* CR-SM-332: EIN Kriterium entscheidet, ob ein Attribut inline oder auf eine `@key`-Zeile
|
|
480
|
+
* geht — naemlich, ob sein Wert den Inline-Block brechen wuerde.
|
|
481
|
+
*
|
|
482
|
+
* Vorher entschied der TYP: `typeof v === 'object'` ging auf die Folgezeile, alles andere
|
|
483
|
+
* inline. Damit blieb eine Luecke derselben Klasse wie der Zeilenumbruch offen: ein STRING
|
|
484
|
+
* mit Komma oder Klammer ("a, b" oder "f(x)") wurde inline geschrieben, und
|
|
485
|
+
* `parseInlineAttrs` splittet auf Kommas — der Wert kam zerteilt oder gar nicht zurueck.
|
|
486
|
+
* Still, wie immer bei dieser Klasse. graphcodes Fork hatte genau dafuer bereits
|
|
487
|
+
* `UNSAFE_ATTR_RE`; die Regel wandert hierher, wo der einzige Codec steht.
|
|
488
|
+
*
|
|
489
|
+
* Schluessel aufsteigend nach Code-Einheiten, nicht `localeCompare` — dieselbe Begruendung
|
|
490
|
+
* wie bei `serializeEdges`: eine Sortierung, die von der Locale abhaengt, ist nicht
|
|
491
|
+
* deterministisch.
|
|
492
|
+
*/
|
|
493
|
+
nodeAttrParts(attrs) {
|
|
494
|
+
const entries = Object.entries(attrs)
|
|
495
|
+
.filter(([, v]) => v != null && v !== '')
|
|
496
|
+
.sort(([a], [b]) => cmp(a, b))
|
|
497
|
+
.map(([k, v]) => [k, typeof v === 'object' ? JSON.stringify(v) : String(v)]);
|
|
498
|
+
const safe = entries.filter(([, v]) => !UNSAFE_INLINE_RE.test(v));
|
|
499
|
+
const unsafe = entries.filter(([, v]) => UNSAFE_INLINE_RE.test(v));
|
|
500
|
+
return {
|
|
501
|
+
inline: safe.length > 0 ? ` [${safe.map(([k, v]) => `${k}:${v}`).join(',')}]` : '',
|
|
502
|
+
follow: unsafe.map(([k, v]) => `@${k} ${v}`),
|
|
503
|
+
};
|
|
404
504
|
}
|
|
405
|
-
/**
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
505
|
+
/**
|
|
506
|
+
* CR-SM-332 (aus graphcodes Fork uebernommen, CR-GC-200/CR-GC-531): Knotentypen gegen die
|
|
507
|
+
* Ontologie, Kantentypen gegen die Ontologie, doppelte uids, aufloesbare Endpunkte.
|
|
508
|
+
* Paar-Legalitaet gehoert NICHT hierher — das ist R-18, dort wo Daten in den Speicher
|
|
509
|
+
* gehen.
|
|
510
|
+
*/
|
|
511
|
+
validate(graph, resolveType) {
|
|
512
|
+
const errors = [];
|
|
513
|
+
const declaredTypes = new Map(graph.nodes.map(n => [n.uid, n.type]));
|
|
514
|
+
const typeOf = (uid) => declaredTypes.get(uid) ?? resolveType?.(uid);
|
|
515
|
+
// Doppelte uids: die Map oben dedupliziert still, zwei Knoten mit derselben uid faellen
|
|
516
|
+
// sonst zu einem zusammen und die Kollision bleibt ungesehen (CR-GC-200).
|
|
517
|
+
const counts = new Map();
|
|
518
|
+
for (const node of graph.nodes)
|
|
519
|
+
counts.set(node.uid, (counts.get(node.uid) ?? 0) + 1);
|
|
520
|
+
for (const [uid, count] of counts) {
|
|
521
|
+
if (count > 1)
|
|
522
|
+
errors.push(`Duplicate node uid "${uid}" (${count} nodes share it)`);
|
|
523
|
+
}
|
|
524
|
+
for (const node of graph.nodes) {
|
|
525
|
+
if (!this.validNodeTypes.has(node.type)) {
|
|
526
|
+
errors.push(`Unknown node type "${node.type}" for node "${node.uid}"`);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
for (const edge of graph.edges) {
|
|
530
|
+
if (!this.ontology.edgeTypes[edge.edgeType]) {
|
|
531
|
+
errors.push(`Unknown edge type "${edge.edgeType}" for edge "${edge.sourceId}" → "${edge.targetId}"`);
|
|
532
|
+
continue;
|
|
533
|
+
}
|
|
534
|
+
if (!typeOf(edge.sourceId)) {
|
|
535
|
+
errors.push(`Edge references unknown source node "${edge.sourceId}"`);
|
|
536
|
+
continue;
|
|
537
|
+
}
|
|
538
|
+
if (!typeOf(edge.targetId)) {
|
|
539
|
+
errors.push(`Edge references unknown target node "${edge.targetId}"`);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
return { valid: errors.length === 0, errors };
|
|
410
543
|
}
|
|
411
544
|
/** Inline block for an EDGE — structured values stringified, see `serializeAttrs`. */
|
|
412
545
|
serializeEdgeAttrs(attrs) {
|
package/dist/graph-service.js
CHANGED
|
@@ -8,6 +8,7 @@ import { isValidTrace, tracePatternsOf } from './types.js';
|
|
|
8
8
|
import { FormatECodec } from './format-e-codec.js';
|
|
9
9
|
import { DefaultRuleEngine } from './rule-engine.js';
|
|
10
10
|
import { InMemoryAuditLog } from './audit.js';
|
|
11
|
+
import { normalizeReqKinds } from '@sigloch/contracts/se';
|
|
11
12
|
import { updateEdge, mergeNodes } from './edge-ops.js';
|
|
12
13
|
/**
|
|
13
14
|
* CR-SM-216: a display name for a typed node, derived from the uid's *shape* only.
|
|
@@ -199,6 +200,7 @@ export class GraphService {
|
|
|
199
200
|
sourceId: op.sourceId,
|
|
200
201
|
targetId: op.targetId,
|
|
201
202
|
edgeType: op.edgeType,
|
|
203
|
+
label: op.attributes?.label,
|
|
202
204
|
});
|
|
203
205
|
const edge = {
|
|
204
206
|
sourceId: op.sourceId,
|
|
@@ -249,6 +251,7 @@ export class GraphService {
|
|
|
249
251
|
sourceId: op.sourceId,
|
|
250
252
|
targetId: op.targetId,
|
|
251
253
|
edgeType: op.edgeType,
|
|
254
|
+
label: op.attributes?.label,
|
|
252
255
|
});
|
|
253
256
|
const edge = {
|
|
254
257
|
sourceId: op.sourceId,
|
|
@@ -299,6 +302,7 @@ export class GraphService {
|
|
|
299
302
|
const { removed, added } = updateEdge(graph, { sourceId: op.sourceId, targetId: op.targetId, edgeType: op.edgeType }, op.set);
|
|
300
303
|
await this.validateAndApplyEdge({
|
|
301
304
|
sourceId: added.sourceId, targetId: added.targetId, edgeType: added.edgeType,
|
|
305
|
+
label: added.attributes?.label,
|
|
302
306
|
});
|
|
303
307
|
await this.storage.deleteEdges([{
|
|
304
308
|
sourceId: removed.sourceId, targetId: removed.targetId, edgeType: removed.edgeType,
|
|
@@ -337,6 +341,7 @@ export class GraphService {
|
|
|
337
341
|
for (const edge of addedEdges) {
|
|
338
342
|
await this.validateAndApplyEdge({
|
|
339
343
|
sourceId: edge.sourceId, targetId: edge.targetId, edgeType: edge.edgeType,
|
|
344
|
+
label: edge.attributes?.label,
|
|
340
345
|
});
|
|
341
346
|
}
|
|
342
347
|
if (removedEdges.length > 0) {
|
|
@@ -485,10 +490,10 @@ export class GraphService {
|
|
|
485
490
|
// Hoist top-level fields
|
|
486
491
|
if (element.attributes) {
|
|
487
492
|
if (element.attributes.kinds != null) {
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
493
|
+
// CR-SM-332: `normalizeReqKinds` statt einer zweiten, handgeschriebenen Fassung
|
|
494
|
+
// (`String(raw).includes(',') ? split : [raw]`). Dieselbe Regel, ein Ort — die
|
|
495
|
+
// Funktion liegt bei den Schemata, die sie fuettert.
|
|
496
|
+
element.kinds = [...normalizeReqKinds(element.attributes.kinds)];
|
|
492
497
|
delete element.attributes.kinds;
|
|
493
498
|
}
|
|
494
499
|
if (element.attributes.asil != null) {
|
|
@@ -530,10 +535,8 @@ export class GraphService {
|
|
|
530
535
|
// Hoist top-level fields
|
|
531
536
|
if (element.attributes) {
|
|
532
537
|
if (element.attributes.kinds != null) {
|
|
533
|
-
|
|
534
|
-
element.kinds =
|
|
535
|
-
? raw.split(',').map((s) => s.trim())
|
|
536
|
-
: [raw.trim()];
|
|
538
|
+
// CR-SM-332: dieselbe eine Regel wie im Anlege-Pfad oben.
|
|
539
|
+
element.kinds = [...normalizeReqKinds(element.attributes.kinds)];
|
|
537
540
|
delete element.attributes.kinds;
|
|
538
541
|
}
|
|
539
542
|
if (element.attributes.asil != null) {
|
|
@@ -565,9 +568,11 @@ export class GraphService {
|
|
|
565
568
|
};
|
|
566
569
|
}
|
|
567
570
|
// CR-GC-247: one legality path for every ontology (SE + foreign). The edge type
|
|
568
|
-
// must be declared (menu enumeration via edgeTypes), then the
|
|
569
|
-
//
|
|
570
|
-
//
|
|
571
|
+
// must be declared (menu enumeration via edgeTypes), then the trace is checked against
|
|
572
|
+
// descriptor.patterns. CR-SM-323: that checker delegates to contracts' isValidTrace — the
|
|
573
|
+
// routine R-18 runs — fed label and endpoint kinds exactly as R-18 reads them. A foreign
|
|
574
|
+
// ontology's patterns (derived from validPairs) carry neither label nor where, so there
|
|
575
|
+
// the type pair alone decides.
|
|
571
576
|
async validateAndApplyEdge(op) {
|
|
572
577
|
if (!this.ontology.edgeTypes[op.edgeType]) {
|
|
573
578
|
const known = Object.keys(this.ontology.edgeTypes).join(', ');
|
|
@@ -575,7 +580,14 @@ export class GraphService {
|
|
|
575
580
|
}
|
|
576
581
|
const src = await this.storage.getNode(op.sourceId);
|
|
577
582
|
const tgt = await this.storage.getNode(op.targetId);
|
|
578
|
-
if (src && tgt && !isValidTrace({
|
|
583
|
+
if (src && tgt && !isValidTrace({
|
|
584
|
+
source: src.type,
|
|
585
|
+
target: tgt.type,
|
|
586
|
+
type: op.edgeType,
|
|
587
|
+
label: op.label,
|
|
588
|
+
sourceKinds: src.attributes?.kinds,
|
|
589
|
+
targetKinds: tgt.attributes?.kinds,
|
|
590
|
+
}, this.patterns)) {
|
|
579
591
|
const allowed = this.patterns
|
|
580
592
|
.filter(p => p.type === op.edgeType)
|
|
581
593
|
.map(p => `${p.source}->${p.target}`)
|
|
@@ -12,7 +12,27 @@
|
|
|
12
12
|
* Cypher) does not break consumers.
|
|
13
13
|
*/
|
|
14
14
|
import { generateSchema } from './schema-generator.js';
|
|
15
|
-
|
|
15
|
+
/**
|
|
16
|
+
* Prozessweit genau EINE kuzu-Instanz (CR-SM-316).
|
|
17
|
+
*
|
|
18
|
+
* `kuzu.init()` ist nicht idempotent: jeder Aufruf legt eine NEUE Emscripten-Instanz
|
|
19
|
+
* mit eigenem Heap an (~186 MB) und haengt die alte ab. Zurueck gibt die niemand —
|
|
20
|
+
* WASM-Speicher schrumpft nicht, und weder `db.close()` noch der GC raeumen ihn ab.
|
|
21
|
+
* Vorher rief jedes `GraphCypherEngine.init()` erneut auf; im graphcode-Volllauf waren
|
|
22
|
+
* das 544 Aufrufe und 9641 MB Spitzenverbrauch, bis der Heap mit "memory access out of
|
|
23
|
+
* bounds" faultete und dabei traf, was gerade lief — in drei Laeufen drei verschiedene
|
|
24
|
+
* Testdateien.
|
|
25
|
+
*
|
|
26
|
+
* Der Cache haelt das PROMISE, nicht das Ergebnis: zwei Engines, die gleichzeitig
|
|
27
|
+
* initialisieren, warten damit auf denselben Ladevorgang statt zwei zu starten.
|
|
28
|
+
*/
|
|
29
|
+
let kuzuModule = null;
|
|
30
|
+
function loadKuzu() {
|
|
31
|
+
if (!kuzuModule)
|
|
32
|
+
kuzuModule = initKuzu();
|
|
33
|
+
return kuzuModule;
|
|
34
|
+
}
|
|
35
|
+
async function initKuzu() {
|
|
16
36
|
// Node path: sync nodejs variant. Detect Node by checking for `process.versions.node`.
|
|
17
37
|
const isNode = typeof process !== 'undefined' && !!process.versions?.node;
|
|
18
38
|
if (isNode) {
|
|
Binary file
|
package/dist/se-descriptor.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* - Graph (nodes/edges) ⟷ OntologyGraph (elements/traces)
|
|
10
10
|
* - RuleViolation (ruleId/…) ⟷ contracts RuleViolation (rule_id/…)
|
|
11
11
|
*/
|
|
12
|
-
import { ElementType, TraceType, TRACE_PATTERNS, V3_RULES, MT_RULES, UC_RULES, FC_RULES, SC_RULES, CR_RULES, AO_RULES, FM_RULES, VIEW_RULES, AF_RULES, ONTOLOGY_VERSION, DEFAULT_METRIC_POLICY, } from '@sigloch/contracts/se';
|
|
12
|
+
import { ElementType, TraceType, TRACE_PATTERNS, V3_RULES, MT_RULES, UC_RULES, FC_RULES, SC_RULES, CR_RULES, AO_RULES, FM_RULES, VIEW_RULES, AF_RULES, ONTOLOGY_VERSION, DEFAULT_METRIC_POLICY, normalizeReqKinds, } from '@sigloch/contracts/se';
|
|
13
13
|
/**
|
|
14
14
|
* Project an ontology-agnostic Graph (nodes/edges) onto the SE OntologyGraph
|
|
15
15
|
* (elements/traces) that contracts/se rules evaluate against. Type-specific
|
|
@@ -32,7 +32,15 @@ export function projectToOntologyGraph(graph) {
|
|
|
32
32
|
created_at: n.createdAt ?? '',
|
|
33
33
|
updated_at: n.updatedAt,
|
|
34
34
|
method: n.attributes?.method,
|
|
35
|
-
|
|
35
|
+
// CR-SM-332 (ITEM-2026-183): `kinds` wird NORMALISIERT, nicht gecastet. Der Cast war die
|
|
36
|
+
// dritte Fassung derselben Wahrheit: der (entfernte) contracts-Parser normalisierte beim
|
|
37
|
+
// Lesen, GraphService.hoist tut es von Hand, und HIER kam ein ueber Format-E eingetragenes
|
|
38
|
+
// `@kinds non-functional` als roher STRING bei den Regeln an, wo eine Liste erwartet wird —
|
|
39
|
+
// CR-SM-320 auf diesem Pfad wieder offen. `undefined` bleibt `undefined`: eine leere Liste
|
|
40
|
+
// ist eine andere Aussage als "nicht gesetzt".
|
|
41
|
+
kinds: n.attributes?.kinds === undefined
|
|
42
|
+
? undefined
|
|
43
|
+
: normalizeReqKinds(n.attributes.kinds),
|
|
36
44
|
attributes: n.attributes,
|
|
37
45
|
}));
|
|
38
46
|
const traces = graph.edges.map((e) => ({
|
|
@@ -133,8 +141,14 @@ function liftAttributes(rest) {
|
|
|
133
141
|
* otherwise the 114 pre-existing errors in graphcode's own graph would block every
|
|
134
142
|
* write on debt the writer did not create. Promotion is a per-family decision, not a
|
|
135
143
|
* side effect of being registered.
|
|
144
|
+
*
|
|
145
|
+
* CR-SM-309: IO is promoted (decision 2026-09-11). IO-02 ("a FLOW has exactly ONE
|
|
146
|
+
* producer", error) guards the one-FLOW-per-connection cut; without gate power a
|
|
147
|
+
* suggested merge put two producers back into one FLOW and passed the gate. The delta
|
|
148
|
+
* baseline keeps the switch-on safe: only NEW findings block, graphcode's own graph is
|
|
149
|
+
* at IO-02 = 0, and IO-01 shares the prefix but is a warning, which never blocks.
|
|
136
150
|
*/
|
|
137
|
-
const GATING_PREFIXES = ['R-', 'RD-', 'MT-'];
|
|
151
|
+
const GATING_PREFIXES = ['R-', 'RD-', 'MT-', 'IO-'];
|
|
138
152
|
const SE_RULE_DEFS = [
|
|
139
153
|
...V3_RULES,
|
|
140
154
|
...UC_RULES,
|
package/dist/types.d.ts
CHANGED
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Core types for graph-api-core — ontology-agnostic.
|
|
3
|
-
* Domains register their node/edge types via OntologyDescriptor.
|
|
4
|
-
*/
|
|
5
1
|
export interface GraphNode {
|
|
6
2
|
uid: string;
|
|
7
3
|
type: string;
|
|
@@ -135,21 +131,18 @@ export interface FormatEDiff {
|
|
|
135
131
|
errors: string[];
|
|
136
132
|
}
|
|
137
133
|
/**
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
* A pattern's `label` is descriptive metadata (e.g. MS→MS relation 'depends-on'),
|
|
144
|
-
* NOT a gate: label is trace data that isn't uniformly carried through the
|
|
145
|
-
* Graph/codec pipeline, and the legality this replaces (validPairs) never keyed on
|
|
146
|
-
* it. Enforcing label is a deliberate tightening (needs codec label round-tripping),
|
|
147
|
-
* out of scope for the centralization.
|
|
134
|
+
* Trace legality for any ontology, decided by contracts' `isValidTrace` — the routine R-18
|
|
135
|
+
* runs. No logic of its own: `label` and endpoint `kinds` are part of the rule exactly as
|
|
136
|
+
* R-18 reads them. This signature only opens the SE-closed contracts types to the
|
|
137
|
+
* ontology-agnostic strings this package speaks; the runtime pattern shape is the same.
|
|
148
138
|
*/
|
|
149
139
|
export declare function isValidTrace(trace: {
|
|
150
140
|
source: string;
|
|
151
141
|
target: string;
|
|
152
142
|
type: string;
|
|
143
|
+
label?: string;
|
|
144
|
+
sourceKinds?: readonly string[];
|
|
145
|
+
targetKinds?: readonly string[];
|
|
153
146
|
}, patterns: TracePattern[]): boolean;
|
|
154
147
|
/**
|
|
155
148
|
* The patterns a descriptor validates against: explicit `patterns` (SSOT) if set,
|
package/dist/types.js
CHANGED
|
@@ -2,25 +2,18 @@
|
|
|
2
2
|
* Core types for graph-api-core — ontology-agnostic.
|
|
3
3
|
* Domains register their node/edge types via OntologyDescriptor.
|
|
4
4
|
*/
|
|
5
|
+
import { isValidTrace as contractsIsValidTrace } from '@sigloch/contracts/se';
|
|
5
6
|
// ---------------------------------------------------------------------------
|
|
6
|
-
// Trace legality —
|
|
7
|
+
// Trace legality — ONE rule (CR-GC-247, CR-SM-323)
|
|
7
8
|
// ---------------------------------------------------------------------------
|
|
8
9
|
/**
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* A pattern's `label` is descriptive metadata (e.g. MS→MS relation 'depends-on'),
|
|
15
|
-
* NOT a gate: label is trace data that isn't uniformly carried through the
|
|
16
|
-
* Graph/codec pipeline, and the legality this replaces (validPairs) never keyed on
|
|
17
|
-
* it. Enforcing label is a deliberate tightening (needs codec label round-tripping),
|
|
18
|
-
* out of scope for the centralization.
|
|
10
|
+
* Trace legality for any ontology, decided by contracts' `isValidTrace` — the routine R-18
|
|
11
|
+
* runs. No logic of its own: `label` and endpoint `kinds` are part of the rule exactly as
|
|
12
|
+
* R-18 reads them. This signature only opens the SE-closed contracts types to the
|
|
13
|
+
* ontology-agnostic strings this package speaks; the runtime pattern shape is the same.
|
|
19
14
|
*/
|
|
20
15
|
export function isValidTrace(trace, patterns) {
|
|
21
|
-
return
|
|
22
|
-
(p.target === '*' || p.target === trace.target) &&
|
|
23
|
-
p.type === trace.type);
|
|
16
|
+
return contractsIsValidTrace(trace, patterns);
|
|
24
17
|
}
|
|
25
18
|
/**
|
|
26
19
|
* The patterns a descriptor validates against: explicit `patterns` (SSOT) if set,
|