@sigloch/graph-api-core 5.5.0 → 5.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.
@@ -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
- private serializeAttrs;
69
- /** CR-GC-334: object/array attributes as `@key {json}` lines below the node entry. */
70
- private structuredAttrLines;
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;
@@ -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
- for (const node of byType.get(type) ?? []) {
177
- const descr = node.description ? `|${node.description}` : '';
178
- const nodeAttrs = omit ? stripNodeProvenance(node.attributes) : node.attributes;
179
- lines.push(`+ ${node.uid}${descr}${this.serializeAttrs(nodeAttrs)}`);
180
- // CR-GC-334: realRef/testRef & friends as @key {json} the inline block above
181
- // cannot carry them, and dropping them here is what made bindings vanish.
182
- lines.push(...this.structuredAttrLines(nodeAttrs));
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
- const opChar = OP_PREFIX[line[0]] ? line[0] : '+';
201
- const rest = OP_PREFIX[line[0]] ? line.slice(1).trim() : line.trim();
202
- const action = OP_PREFIX[opChar] ?? 'add';
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}${edge.edgeType}`;
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
- serializeAttrs(attrs) {
391
- const entries = Object.entries(attrs).filter(([, v]) => v != null && v !== '' && typeof v !== 'object');
392
- if (entries.length === 0)
393
- return '';
394
- const pairs = entries.map(([k, v]) => `${k}:${String(v)}`);
395
- return ` [${pairs.join(',')}]`;
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
- /** CR-GC-334: object/array attributes as `@key {json}` lines below the node entry. */
398
- structuredAttrLines(attrs) {
399
- return Object.entries(attrs)
400
- .filter(([, v]) => v != null && typeof v === 'object')
401
- .map(([k, v]) => `@${k} ${JSON.stringify(v)}`);
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) {
@@ -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
- const raw = String(element.attributes.kinds);
493
- element.kinds = raw.includes(',')
494
- ? raw.split(',').map((s) => s.trim())
495
- : [raw.trim()];
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
- const raw = String(element.attributes.kinds);
538
- element.kinds = raw.includes(',')
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
@@ -13,30 +13,11 @@ export interface RuleViolation {
13
13
  fixHint?: string;
14
14
  /** Carried from the contracts rule: candidate_targets / existing_traces context for fix automation. */
15
15
  context?: unknown;
16
- /**
17
- * Stamped by the engine from the rule that produced this violation (CR-GC-312).
18
- * `false` = visible but not gate-relevant. Absent means gating, so every existing
19
- * consumer keeps its behaviour without reading the field.
20
- */
21
- gating?: boolean;
22
16
  }
23
17
  export interface Rule {
24
18
  id: string;
25
19
  name: string;
26
20
  severity: 'error' | 'warning' | 'info';
27
- /**
28
- * May an `error` from this rule BLOCK a write? (CR-GC-312) Default `true`.
29
- *
30
- * Severity says how bad a finding is; this says whether the gate acts on it. The
31
- * two are separate because a rule family can be correct and worth surfacing long
32
- * before a repo has paid off its backlog — switching one on would otherwise freeze
33
- * every write on debt the writer did not create. Set `false` to surface a rule in
34
- * `evaluate`/readiness while it is still being worked down, then promote it.
35
- *
36
- * NOT a severity downgrade: the violation keeps `severity: 'error'` and reads as one
37
- * everywhere it is displayed.
38
- */
39
- gating?: boolean;
40
21
  /**
41
22
  * `policy` (CR-GC-402) overrides the threshold set this rule was BOUND with at
42
23
  * descriptor construction, for this call only. Omitted = judge with the bound one,
@@ -19,12 +19,9 @@ export class DefaultRuleEngine {
19
19
  evaluate(graph, policy) {
20
20
  const violations = [];
21
21
  for (const rule of this.rules) {
22
- // CR-GC-312: stamp `gating` from the rule so a consumer's gate can filter
23
- // without a second lookup into the catalog. Only stamped when the rule opts
24
- // OUT — an absent field means gating, the pre-existing behaviour.
25
- const stamp = rule.gating === false ? { gating: false } : undefined;
22
+ // CR-SM-353: kein `gating`-Stempel mehr die Schwere IST die Gate-Wirkung (`error` blockt).
26
23
  for (const v of rule.evaluate(graph, policy))
27
- violations.push(stamp ? { ...v, ...stamp } : v);
24
+ violations.push(v);
28
25
  }
29
26
  return violations;
30
27
  }
@@ -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, TASK_OUTCOME_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
- kinds: n.attributes?.kinds,
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) => ({
@@ -128,19 +136,14 @@ function liftAttributes(rest) {
128
136
  * - RC (conformance) needs `CodeFacts` from a repo checkout, which this package has
129
137
  * no access to; graphcode evaluates those itself in `src/conformance.ts`.
130
138
  *
131
- * `gating` (see `Rule`) is what makes the switch-on safe. V3/MT keep their gate power;
132
- * the newly wired families are advisory until a repo has worked its backlog down —
133
- * otherwise the 114 pre-existing errors in graphcode's own graph would block every
134
- * write on debt the writer did not create. Promotion is a per-family decision, not a
135
- * side effect of being registered.
136
- *
137
- * CR-SM-309: IO is promoted (decision 2026-09-11). IO-02 ("a FLOW has exactly ONE
138
- * producer", error) guards the one-FLOW-per-connection cut; without gate power a
139
- * suggested merge put two producers back into one FLOW and passed the gate. The delta
140
- * baseline keeps the switch-on safe: only NEW findings block, graphcode's own graph is
141
- * at IO-02 = 0, and IO-01 shares the prefix but is a warning, which never blocks.
139
+ * CR-SM-353: `gating` entfaellt die Schwere IST die Gate-Wirkung. Der Einschalt-Schutz, den das
140
+ * Flag einmal bot (114 Altfunde in graphcodes eigenem Graphen haetten jeden Write blockiert), ist
141
+ * seit der Delta-Semantik des Gates doppelt: nur NEU eingefuehrte error-Funde blocken, Altlast nie.
142
+ * Was blieb, war eine zweite Bedeutung von `error`, die jeder Konsument einzeln auseinanderhalten
143
+ * musste (Rewind opus5-12: `blockingErrors 0 → 8` fuer FM-03-Funde, die nie geblockt haetten).
144
+ * Jetzt: jede Regel mit `severity: 'error'` in `ALL_RULE_DEFS` steht im Gate-Katalog und blockt
145
+ * (IO-02, R-01, R-08, R-18, R-29); alles andere ist `warning` oder `info`.
142
146
  */
143
- const GATING_PREFIXES = ['R-', 'RD-', 'MT-', 'IO-'];
144
147
  const SE_RULE_DEFS = [
145
148
  ...V3_RULES,
146
149
  ...UC_RULES,
@@ -151,13 +154,13 @@ const SE_RULE_DEFS = [
151
154
  ...FM_RULES,
152
155
  ...VIEW_RULES,
153
156
  ...AF_RULES,
157
+ ...TASK_OUTCOME_RULES, // CR-SM-355: Task-Ausgaenge trade / irr
154
158
  ];
155
159
  function adapt(def, evaluate, boundPolicy) {
156
160
  return {
157
161
  id: def.id,
158
162
  name: def.name,
159
163
  severity: def.severity,
160
- gating: GATING_PREFIXES.some((p) => def.id.startsWith(p)),
161
164
  // CR-GC-402: the CALLER's policy wins for this evaluation, the descriptor's bound
162
165
  // one is the fallback. Without this the threshold was frozen at construction, so a
163
166
  // consumer of the published `SE_DESCRIPTOR` judged with `DEFAULT_METRIC_POLICY` no
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sigloch/graph-api-core",
3
- "version": "5.5.0",
3
+ "version": "5.7.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": ">=10 <11",
46
+ "@sigloch/contracts": ">=10.10 <11",
47
47
  "kuzu-wasm": "^0.11.3"
48
48
  },
49
49
  "peerDependenciesMeta": {