@sigloch/graph-api-core 5.5.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 -3
- package/dist/format-e-codec.js +164 -23
- package/dist/graph-service.js +7 -8
- package/dist/kuzu/schema-generator.js +0 -0
- package/dist/se-descriptor.js +10 -2
- package/package.json +1 -1
package/dist/format-e-codec.d.ts
CHANGED
|
@@ -29,6 +29,7 @@ export declare class FormatECodec {
|
|
|
29
29
|
*/
|
|
30
30
|
serialize(graph: Graph, options?: {
|
|
31
31
|
omitProvenance?: boolean;
|
|
32
|
+
roundTrip?: boolean;
|
|
32
33
|
}): string;
|
|
33
34
|
private parseNodeLine;
|
|
34
35
|
private parseEdgeLine;
|
|
@@ -65,9 +66,32 @@ export declare class FormatECodec {
|
|
|
65
66
|
* contains no comma. No edge in the SE ontology carries a structured attribute today; if one
|
|
66
67
|
* ever does, the inline block is the thing to replace, not this escape.
|
|
67
68
|
*/
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
+
};
|
|
71
95
|
/** Inline block for an EDGE — structured values stringified, see `serializeAttrs`. */
|
|
72
96
|
private serializeEdgeAttrs;
|
|
73
97
|
private edgeTypeToArrow;
|
package/dist/format-e-codec.js
CHANGED
|
@@ -22,6 +22,35 @@ const MERGE_RE = /^M\s+(.+)$/;
|
|
|
22
22
|
const FORMAT_E_FENCE = /```format-e\s*\n([\s\S]*?)```/;
|
|
23
23
|
/** Inline attribute block: [key:value,key:value] */
|
|
24
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
|
+
}
|
|
25
54
|
export class FormatECodec {
|
|
26
55
|
ontology;
|
|
27
56
|
edgeArrowToType;
|
|
@@ -157,6 +186,17 @@ export class FormatECodec {
|
|
|
157
186
|
*/
|
|
158
187
|
serialize(graph, options = {}) {
|
|
159
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
|
+
}
|
|
160
200
|
const lines = [];
|
|
161
201
|
if (graph.nodes.length > 0) {
|
|
162
202
|
lines.push('## Nodes');
|
|
@@ -173,13 +213,38 @@ export class FormatECodec {
|
|
|
173
213
|
}
|
|
174
214
|
for (const type of [...byType.keys()].sort()) {
|
|
175
215
|
lines.push(`### ${type}`);
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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);
|
|
183
248
|
}
|
|
184
249
|
}
|
|
185
250
|
}
|
|
@@ -189,6 +254,14 @@ export class FormatECodec {
|
|
|
189
254
|
const edges = omit
|
|
190
255
|
? graph.edges.map(e => ({ ...e, attributes: stripEdgeProvenance(e.attributes) }))
|
|
191
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
|
+
}
|
|
192
265
|
lines.push(...this.serializeEdges(edges));
|
|
193
266
|
}
|
|
194
267
|
return lines.join('\n');
|
|
@@ -197,9 +270,17 @@ export class FormatECodec {
|
|
|
197
270
|
// Private
|
|
198
271
|
// ---------------------------------------------------------------------------
|
|
199
272
|
parseNodeLine(line, nodeType, declared, ops, errors) {
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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();
|
|
203
284
|
// Extract inline attributes
|
|
204
285
|
let mainPart = rest;
|
|
205
286
|
let inlineAttrs;
|
|
@@ -216,6 +297,14 @@ export class FormatECodec {
|
|
|
216
297
|
errors.push(`Node line without a uid: "${line}"`);
|
|
217
298
|
return;
|
|
218
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
|
+
}
|
|
219
308
|
// CR-SM-217: with `TYPE-slug` as the family canon, the prefix restores the
|
|
220
309
|
// redundancy CR-SM-216 removed — for free, since `REQ-safety` costs fewer tokens
|
|
221
310
|
// than `REQ-safety.REQ`. A node under the wrong section is caught here. Uids with
|
|
@@ -356,7 +445,7 @@ export class FormatECodec {
|
|
|
356
445
|
entries.push({ sourceId: edge.sourceId, edgeType: edge.edgeType, targets: [edge.targetId], attrs });
|
|
357
446
|
continue;
|
|
358
447
|
}
|
|
359
|
-
const key = `${edge.sourceId}
|
|
448
|
+
const key = `${edge.sourceId}\0${edge.edgeType}`;
|
|
360
449
|
const group = groups.get(key);
|
|
361
450
|
if (group) {
|
|
362
451
|
group.targets.push(edge.targetId);
|
|
@@ -367,7 +456,6 @@ export class FormatECodec {
|
|
|
367
456
|
entries.push(entry);
|
|
368
457
|
}
|
|
369
458
|
}
|
|
370
|
-
const cmp = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
|
|
371
459
|
for (const entry of entries)
|
|
372
460
|
entry.targets.sort(cmp);
|
|
373
461
|
entries.sort((a, b) => cmp(a.sourceId, b.sourceId)
|
|
@@ -387,18 +475,71 @@ export class FormatECodec {
|
|
|
387
475
|
* contains no comma. No edge in the SE ontology carries a structured attribute today; if one
|
|
388
476
|
* ever does, the inline block is the thing to replace, not this escape.
|
|
389
477
|
*/
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
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
|
+
};
|
|
396
504
|
}
|
|
397
|
-
/**
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
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 };
|
|
402
543
|
}
|
|
403
544
|
/** Inline block for an EDGE — structured values stringified, see `serializeAttrs`. */
|
|
404
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.
|
|
@@ -489,10 +490,10 @@ export class GraphService {
|
|
|
489
490
|
// Hoist top-level fields
|
|
490
491
|
if (element.attributes) {
|
|
491
492
|
if (element.attributes.kinds != null) {
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
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)];
|
|
496
497
|
delete element.attributes.kinds;
|
|
497
498
|
}
|
|
498
499
|
if (element.attributes.asil != null) {
|
|
@@ -534,10 +535,8 @@ export class GraphService {
|
|
|
534
535
|
// Hoist top-level fields
|
|
535
536
|
if (element.attributes) {
|
|
536
537
|
if (element.attributes.kinds != null) {
|
|
537
|
-
|
|
538
|
-
element.kinds =
|
|
539
|
-
? raw.split(',').map((s) => s.trim())
|
|
540
|
-
: [raw.trim()];
|
|
538
|
+
// CR-SM-332: dieselbe eine Regel wie im Anlege-Pfad oben.
|
|
539
|
+
element.kinds = [...normalizeReqKinds(element.attributes.kinds)];
|
|
541
540
|
delete element.attributes.kinds;
|
|
542
541
|
}
|
|
543
542
|
if (element.attributes.asil != null) {
|
|
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) => ({
|