@sigloch/graph-api-core 5.3.0 → 5.5.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,6 +13,8 @@
13
13
  */
14
14
  export { findRoot } from './find-root.js';
15
15
  export type { RootQueryGraph } from './find-root.js';
16
+ export { impactSlice } from './impact-slice.js';
17
+ export type { ImpactSlice, ImpactSliceNode, ImpactRole } from './impact-slice.js';
16
18
  export { SE_DESCRIPTOR, createSeDescriptor, projectToOntologyGraph, fromOntologyGraph } from './se-descriptor.js';
17
19
  export { FormatECodec } from './format-e-codec.js';
18
20
  export { isValidTrace, tracePatternsOf } from './types.js';
package/dist/browser.js CHANGED
@@ -12,6 +12,7 @@
12
12
  * @author andreas@siglochconsulting
13
13
  */
14
14
  export { findRoot } from './find-root.js';
15
+ export { impactSlice } from './impact-slice.js';
15
16
  export { SE_DESCRIPTOR, createSeDescriptor, projectToOntologyGraph, fromOntologyGraph } from './se-descriptor.js';
16
17
  export { FormatECodec } from './format-e-codec.js';
17
18
  export { isValidTrace, tracePatternsOf } from './types.js';
@@ -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;
@@ -18,8 +16,20 @@ export declare class FormatECodec {
18
16
  parse(input: string, options?: {
19
17
  resolveType?: (uid: string) => string | undefined;
20
18
  }): FormatEDiff;
21
- /** Serialize a Graph to Format E text. */
22
- serialize(graph: Graph): string;
19
+ /**
20
+ * Serialize a Graph to Format E text.
21
+ *
22
+ * CR-GC-373: `options.omitProvenance` is the **agent view** — provenance attributes
23
+ * (`created_at`, `updated_at`, `ranAt` inside `testRefs` entries, `weight` at its
24
+ * default 1) are dropped; everything that is work instruction (`codeRef`, `realRef`,
25
+ * `testRefs` file/tool/level, `kinds`, `constraint`, `zodDefinition`, `status`) stays.
26
+ * NOT a second codec: the default stays complete — round-trip is the contract, the
27
+ * agent view the exception. Measured on the CR-GC-114 job slice: 18% of the text was
28
+ * pure timestamps.
29
+ */
30
+ serialize(graph: Graph, options?: {
31
+ omitProvenance?: boolean;
32
+ }): string;
23
33
  private parseNodeLine;
24
34
  private parseEdgeLine;
25
35
  private parseMerge;
@@ -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
  // ---------------------------------------------------------------------------
@@ -27,8 +26,6 @@ export class FormatECodec {
27
26
  ontology;
28
27
  edgeArrowToType;
29
28
  validNodeTypes;
30
- /** Trace-legality patterns (CR-GC-247) — the SSOT the parser validates edges against. */
31
- patterns;
32
29
  constructor(ontology) {
33
30
  this.ontology = ontology;
34
31
  // Build arrow → edge type lookup
@@ -40,8 +37,6 @@ export class FormatECodec {
40
37
  }
41
38
  // Valid node type abbreviations
42
39
  this.validNodeTypes = new Set(Object.keys(ontology.nodeTypes));
43
- // Meta-model legality: descriptor.patterns (or derived from validPairs).
44
- this.patterns = tracePatternsOf(ontology);
45
40
  }
46
41
  /** Extract a ```format-e block from LLM output. Returns null if not found. */
47
42
  extractFromLlm(llmOutput) {
@@ -149,8 +144,19 @@ export class FormatECodec {
149
144
  }
150
145
  return { operations, errors };
151
146
  }
152
- /** Serialize a Graph to Format E text. */
153
- serialize(graph) {
147
+ /**
148
+ * Serialize a Graph to Format E text.
149
+ *
150
+ * CR-GC-373: `options.omitProvenance` is the **agent view** — provenance attributes
151
+ * (`created_at`, `updated_at`, `ranAt` inside `testRefs` entries, `weight` at its
152
+ * default 1) are dropped; everything that is work instruction (`codeRef`, `realRef`,
153
+ * `testRefs` file/tool/level, `kinds`, `constraint`, `zodDefinition`, `status`) stays.
154
+ * NOT a second codec: the default stays complete — round-trip is the contract, the
155
+ * agent view the exception. Measured on the CR-GC-114 job slice: 18% of the text was
156
+ * pure timestamps.
157
+ */
158
+ serialize(graph, options = {}) {
159
+ const omit = options.omitProvenance === true;
154
160
  const lines = [];
155
161
  if (graph.nodes.length > 0) {
156
162
  lines.push('## Nodes');
@@ -169,18 +175,21 @@ export class FormatECodec {
169
175
  lines.push(`### ${type}`);
170
176
  for (const node of byType.get(type) ?? []) {
171
177
  const descr = node.description ? `|${node.description}` : '';
172
- const attrs = this.serializeAttrs(node.attributes);
173
- lines.push(`+ ${node.uid}${descr}${attrs}`);
178
+ const nodeAttrs = omit ? stripNodeProvenance(node.attributes) : node.attributes;
179
+ lines.push(`+ ${node.uid}${descr}${this.serializeAttrs(nodeAttrs)}`);
174
180
  // CR-GC-334: realRef/testRef & friends as @key {json} — the inline block above
175
181
  // cannot carry them, and dropping them here is what made bindings vanish.
176
- lines.push(...this.structuredAttrLines(node.attributes));
182
+ lines.push(...this.structuredAttrLines(nodeAttrs));
177
183
  }
178
184
  }
179
185
  }
180
186
  if (graph.edges.length > 0) {
181
187
  lines.push('');
182
188
  lines.push('## Edges');
183
- lines.push(...this.serializeEdges(graph.edges));
189
+ const edges = omit
190
+ ? graph.edges.map(e => ({ ...e, attributes: stripEdgeProvenance(e.attributes) }))
191
+ : graph.edges;
192
+ lines.push(...this.serializeEdges(edges));
184
193
  }
185
194
  return lines.join('\n');
186
195
  }
@@ -276,12 +285,9 @@ export class FormatECodec {
276
285
  errors.push(`Cannot resolve type of target "${targetId}" — not declared under a "### <TYPE>" section and no resolveType provided`);
277
286
  continue;
278
287
  }
279
- // Meta-model validation (CR-GC-247: single checker, patterns SSOThonors
280
- // '*' wildcards, unlike the old per-edgeType validPairs set-membership).
281
- if (!isValidTrace({ source: srcType, target: tgtType, type: edgeType }, this.patterns)) {
282
- errors.push(`Meta-model violation: ${srcType} -${edgeType}-> ${tgtType} is not valid`);
283
- continue;
284
- }
288
+ // CR-SM-324: no legality verdict while parsing the parser knows neither label nor
289
+ // the kinds of existing nodes. The write path judges with the one rule (gate R-18,
290
+ // GraphService.validateAndApplyEdge).
285
291
  const opType = action === 'remove'
286
292
  ? 'remove_edge'
287
293
  : action === 'strict_add'
@@ -408,3 +414,27 @@ export class FormatECodec {
408
414
  return desc?.arrows[0] ?? edgeType.toLowerCase();
409
415
  }
410
416
  }
417
+ // ---------------------------------------------------------------------------
418
+ // CR-GC-373: agent-view provenance filters (pure — never mutate the input graph)
419
+ // ---------------------------------------------------------------------------
420
+ const PROVENANCE_KEYS = new Set(['created_at', 'updated_at']);
421
+ /** Drop created_at/updated_at; inside testRefs entries additionally ranAt. */
422
+ function stripNodeProvenance(attrs) {
423
+ const out = {};
424
+ for (const [k, v] of Object.entries(attrs)) {
425
+ if (PROVENANCE_KEYS.has(k))
426
+ continue;
427
+ if (k === 'testRefs' && Array.isArray(v)) {
428
+ out[k] = v.map(entry => entry != null && typeof entry === 'object' && !Array.isArray(entry)
429
+ ? Object.fromEntries(Object.entries(entry).filter(([ek]) => ek !== 'ranAt'))
430
+ : entry);
431
+ continue;
432
+ }
433
+ out[k] = v;
434
+ }
435
+ return out;
436
+ }
437
+ /** Drop created_at/updated_at and the default weight 1 (a non-default weight stays). */
438
+ function stripEdgeProvenance(attrs) {
439
+ return Object.fromEntries(Object.entries(attrs).filter(([k, v]) => !PROVENANCE_KEYS.has(k) && !(k === 'weight' && Number(v) === 1)));
440
+ }
@@ -199,6 +199,7 @@ export class GraphService {
199
199
  sourceId: op.sourceId,
200
200
  targetId: op.targetId,
201
201
  edgeType: op.edgeType,
202
+ label: op.attributes?.label,
202
203
  });
203
204
  const edge = {
204
205
  sourceId: op.sourceId,
@@ -249,6 +250,7 @@ export class GraphService {
249
250
  sourceId: op.sourceId,
250
251
  targetId: op.targetId,
251
252
  edgeType: op.edgeType,
253
+ label: op.attributes?.label,
252
254
  });
253
255
  const edge = {
254
256
  sourceId: op.sourceId,
@@ -299,6 +301,7 @@ export class GraphService {
299
301
  const { removed, added } = updateEdge(graph, { sourceId: op.sourceId, targetId: op.targetId, edgeType: op.edgeType }, op.set);
300
302
  await this.validateAndApplyEdge({
301
303
  sourceId: added.sourceId, targetId: added.targetId, edgeType: added.edgeType,
304
+ label: added.attributes?.label,
302
305
  });
303
306
  await this.storage.deleteEdges([{
304
307
  sourceId: removed.sourceId, targetId: removed.targetId, edgeType: removed.edgeType,
@@ -337,6 +340,7 @@ export class GraphService {
337
340
  for (const edge of addedEdges) {
338
341
  await this.validateAndApplyEdge({
339
342
  sourceId: edge.sourceId, targetId: edge.targetId, edgeType: edge.edgeType,
343
+ label: edge.attributes?.label,
340
344
  });
341
345
  }
342
346
  if (removedEdges.length > 0) {
@@ -565,9 +569,11 @@ export class GraphService {
565
569
  };
566
570
  }
567
571
  // CR-GC-247: one legality path for every ontology (SE + foreign). The edge type
568
- // must be declared (menu enumeration via edgeTypes), then the (source,target,type)
569
- // trace is checked against descriptor.patterns via the single isValidTrace —
570
- // no SE/foreign fork, no validPairs re-implementation.
572
+ // must be declared (menu enumeration via edgeTypes), then the trace is checked against
573
+ // descriptor.patterns. CR-SM-323: that checker delegates to contracts' isValidTrace — the
574
+ // routine R-18 runs fed label and endpoint kinds exactly as R-18 reads them. A foreign
575
+ // ontology's patterns (derived from validPairs) carry neither label nor where, so there
576
+ // the type pair alone decides.
571
577
  async validateAndApplyEdge(op) {
572
578
  if (!this.ontology.edgeTypes[op.edgeType]) {
573
579
  const known = Object.keys(this.ontology.edgeTypes).join(', ');
@@ -575,7 +581,14 @@ export class GraphService {
575
581
  }
576
582
  const src = await this.storage.getNode(op.sourceId);
577
583
  const tgt = await this.storage.getNode(op.targetId);
578
- if (src && tgt && !isValidTrace({ source: src.type, target: tgt.type, type: op.edgeType }, this.patterns)) {
584
+ if (src && tgt && !isValidTrace({
585
+ source: src.type,
586
+ target: tgt.type,
587
+ type: op.edgeType,
588
+ label: op.label,
589
+ sourceKinds: src.attributes?.kinds,
590
+ targetKinds: tgt.attributes?.kinds,
591
+ }, this.patterns)) {
579
592
  const allowed = this.patterns
580
593
  .filter(p => p.type === op.edgeType)
581
594
  .map(p => `${p.source}->${p.target}`)
@@ -0,0 +1,57 @@
1
+ /**
2
+ * impactSlice — THE directed impact traversal (CR-GC-365, Weg b).
3
+ *
4
+ * One definition of "blast radius", two readers: graphcode's `graph_impact`
5
+ * (MCP surface, agent) and graph-view-edit's ImpactMap (viewer). Before this,
6
+ * the MCP side ran a directed incoming Kuzu query while the viewer ran its own
7
+ * undirected frontier-BFS in the browser — what the human saw was a superset
8
+ * in direction and a subset in depth of what the agent got, computed from a
9
+ * different source.
10
+ *
11
+ * Semantics (node-equal to the former Kuzu query `(m)-[*1..depth]->(root)`,
12
+ * direction 'in' — conformance-tested against KuzuAdapter.getSubgraph):
13
+ *
14
+ * - Traversal is DIRECTED INCOMING: dependents, i.e. nodes with a directed
15
+ * path INTO a seed of length <= depth (who breaks if the seed changes).
16
+ * `distance` is the shortest such path length (0 = seed).
17
+ *
18
+ * - Roles per SPIKE-GC-minimal-whitebox §8 (`seed | whitebox | blackbox`):
19
+ * seed distance 0 — the changeset,
20
+ * whitebox distance 1..depth — full node (description, attributes),
21
+ * blackbox distance depth+1 — the frontier ONE step beyond the request:
22
+ * identity only (uid/type/name), description and attributes
23
+ * stripped. The cut is materialized in the artifact, not merely
24
+ * meant in a renderer; the ring is also the honest
25
+ * "+"-affordance: a bigger slice (same function, depth+1) opens
26
+ * exactly these nodes.
27
+ *
28
+ * - Edges only when BOTH ends lie in the slice (§8 — no edges into nothing).
29
+ *
30
+ * Pure function over a loaded Graph — no store handle, browser-safe (exported
31
+ * from `./browser.js` too). Seeds not present in the graph are dropped; no
32
+ * present seed yields an empty slice, never a full dump.
33
+ *
34
+ * @author andreas@siglochconsulting
35
+ */
36
+ import type { Graph, GraphEdge, GraphNode } from './types.js';
37
+ export type ImpactRole = 'seed' | 'whitebox' | 'blackbox';
38
+ /** A slice node: the graph node plus its role and BFS distance from the changeset. */
39
+ export type ImpactSliceNode = GraphNode & {
40
+ role: ImpactRole;
41
+ distance: number;
42
+ };
43
+ export interface ImpactSlice {
44
+ /** The seeds actually present in the graph (order of first mention, deduped). */
45
+ seeds: string[];
46
+ /** The requested traversal depth (whitebox horizon; blackbox ring sits at depth+1). */
47
+ depth: number;
48
+ /** Slice nodes in the graph's node order — blackbox nodes carry identity only. */
49
+ nodes: ImpactSliceNode[];
50
+ /** Induced edges: kept iff both endpoints are in the slice. */
51
+ edges: GraphEdge[];
52
+ }
53
+ /**
54
+ * Compute the impact slice of `seedIds` over a loaded graph: directed incoming
55
+ * BFS to `depth` (whitebox) plus the materialized blackbox ring at depth+1.
56
+ */
57
+ export declare function impactSlice(graph: Graph, seedIds: readonly string[], depth: number): ImpactSlice;
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Compute the impact slice of `seedIds` over a loaded graph: directed incoming
3
+ * BFS to `depth` (whitebox) plus the materialized blackbox ring at depth+1.
4
+ */
5
+ export function impactSlice(graph, seedIds, depth) {
6
+ const present = new Set(graph.nodes.map((n) => n.uid));
7
+ const seeds = [...new Set(seedIds)].filter((id) => present.has(id));
8
+ // Reverse adjacency: an edge source depends on its target, so traversal
9
+ // walks target -> source (incoming). Dangling edge ends are ignored.
10
+ const dependents = new Map();
11
+ for (const e of graph.edges) {
12
+ if (!present.has(e.sourceId) || !present.has(e.targetId))
13
+ continue;
14
+ const list = dependents.get(e.targetId);
15
+ if (list)
16
+ list.push(e.sourceId);
17
+ else
18
+ dependents.set(e.targetId, [e.sourceId]);
19
+ }
20
+ // BFS out to depth+1: shortest incoming-path distance per reached node.
21
+ const distance = new Map(seeds.map((s) => [s, 0]));
22
+ let frontier = seeds;
23
+ for (let d = 1; d <= depth + 1 && frontier.length > 0; d++) {
24
+ const next = [];
25
+ for (const uid of frontier) {
26
+ for (const dep of dependents.get(uid) ?? []) {
27
+ if (!distance.has(dep)) {
28
+ distance.set(dep, d);
29
+ next.push(dep);
30
+ }
31
+ }
32
+ }
33
+ frontier = next;
34
+ }
35
+ const roleOf = (d) => (d === 0 ? 'seed' : d <= depth ? 'whitebox' : 'blackbox');
36
+ const nodes = [];
37
+ for (const n of graph.nodes) {
38
+ const d = distance.get(n.uid);
39
+ if (d === undefined)
40
+ continue;
41
+ const role = roleOf(d);
42
+ nodes.push(role === 'blackbox'
43
+ ? { ...n, description: undefined, attributes: {}, role, distance: d }
44
+ : { ...n, role, distance: d });
45
+ }
46
+ const inSlice = new Set(nodes.map((n) => n.uid));
47
+ const edges = graph.edges.filter((e) => inSlice.has(e.sourceId) && inSlice.has(e.targetId));
48
+ return { seeds, depth, nodes, edges };
49
+ }
package/dist/index.d.ts CHANGED
@@ -25,5 +25,7 @@ export type { GraphApiConfig } from './factory.js';
25
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
+ export { impactSlice } from './impact-slice.js';
29
+ export type { ImpactSlice, ImpactSliceNode, ImpactRole } from './impact-slice.js';
28
30
  export { applyEdgeOps, updateEdge, mergeNodes } from './edge-ops.js';
29
31
  export type { EdgeIdentity, UpdateEdgeSet, UpdateEdgeResult, MergeNodesResult, EdgeOp, } from './edge-ops.js';
package/dist/index.js CHANGED
@@ -22,6 +22,9 @@ export { createGraphApi } from './factory.js';
22
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
+ // The shared directed impact traversal (CR-GC-365 b) — one definition for
26
+ // graphcode's graph_impact and graph-view-edit's ImpactMap.
27
+ export { impactSlice } from './impact-slice.js';
25
28
  // Edge ops — update-edge (flip/retype) + merge-nodes, shared by GraphService.mutate()
26
29
  // and the graphcode Apply-Gate (CR-198)
27
30
  export { applyEdgeOps, updateEdge, mergeNodes } from './edge-ops.js';
@@ -12,7 +12,27 @@
12
12
  * Cypher) does not break consumers.
13
13
  */
14
14
  import { generateSchema } from './schema-generator.js';
15
- async function loadKuzu() {
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) {
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * Pluggable rule engine for graph validation.
3
3
  */
4
+ import type { MetricPolicy } from '@sigloch/contracts/se';
4
5
  import type { Graph } from './types.js';
5
6
  export interface RuleViolation {
6
7
  ruleId: string;
@@ -36,17 +37,31 @@ export interface Rule {
36
37
  * everywhere it is displayed.
37
38
  */
38
39
  gating?: boolean;
39
- evaluate: (graph: Graph) => RuleViolation[];
40
+ /**
41
+ * `policy` (CR-GC-402) overrides the threshold set this rule was BOUND with at
42
+ * descriptor construction, for this call only. Omitted = judge with the bound one,
43
+ * the pre-existing behaviour; a rule with no threshold ignores the argument.
44
+ */
45
+ evaluate: (graph: Graph, policy?: MetricPolicy) => RuleViolation[];
40
46
  }
41
47
  export interface RuleEngine {
42
48
  readonly version: string;
43
49
  register(rules: Rule[]): void;
44
- evaluate(graph: Graph): RuleViolation[];
50
+ evaluate(graph: Graph, policy?: MetricPolicy): RuleViolation[];
45
51
  }
46
52
  export declare class DefaultRuleEngine implements RuleEngine {
47
53
  readonly version: string;
48
54
  private rules;
49
55
  constructor(version?: string);
50
56
  register(rules: Rule[]): void;
51
- evaluate(graph: Graph): RuleViolation[];
57
+ /**
58
+ * CR-GC-402: `policy` is passed THROUGH to every rule instead of each rule judging
59
+ * with whatever policy the descriptor was built with. A consumer that registers the
60
+ * published `SE_DESCRIPTOR.rules` (which are bound to `DEFAULT_METRIC_POLICY`) but
61
+ * holds its own `graphcode.config.jsonc` could otherwise not judge with its own
62
+ * thresholds — the gve dashboard reported MT-01 on a repo whose config had switched
63
+ * it off, and disagreed with the host over the same graph. Omitting the argument
64
+ * keeps the bound policy, so no existing caller changes behaviour.
65
+ */
66
+ evaluate(graph: Graph, policy?: MetricPolicy): RuleViolation[];
52
67
  }
@@ -7,14 +7,23 @@ export class DefaultRuleEngine {
7
7
  register(rules) {
8
8
  this.rules.push(...rules);
9
9
  }
10
- evaluate(graph) {
10
+ /**
11
+ * CR-GC-402: `policy` is passed THROUGH to every rule instead of each rule judging
12
+ * with whatever policy the descriptor was built with. A consumer that registers the
13
+ * published `SE_DESCRIPTOR.rules` (which are bound to `DEFAULT_METRIC_POLICY`) but
14
+ * holds its own `graphcode.config.jsonc` could otherwise not judge with its own
15
+ * thresholds — the gve dashboard reported MT-01 on a repo whose config had switched
16
+ * it off, and disagreed with the host over the same graph. Omitting the argument
17
+ * keeps the bound policy, so no existing caller changes behaviour.
18
+ */
19
+ evaluate(graph, policy) {
11
20
  const violations = [];
12
21
  for (const rule of this.rules) {
13
22
  // CR-GC-312: stamp `gating` from the rule so a consumer's gate can filter
14
23
  // without a second lookup into the catalog. Only stamped when the rule opts
15
24
  // OUT — an absent field means gating, the pre-existing behaviour.
16
25
  const stamp = rule.gating === false ? { gating: false } : undefined;
17
- for (const v of rule.evaluate(graph))
26
+ for (const v of rule.evaluate(graph, policy))
18
27
  violations.push(stamp ? { ...v, ...stamp } : v);
19
28
  }
20
29
  return violations;
@@ -14,7 +14,14 @@ import type { Graph, OntologyDescriptor } from './types.js';
14
14
  /**
15
15
  * Project an ontology-agnostic Graph (nodes/edges) onto the SE OntologyGraph
16
16
  * (elements/traces) that contracts/se rules evaluate against. Type-specific
17
- * attributes (asil/method/kinds/status) are lifted out of `attributes`.
17
+ * attributes (method/kinds/status) are lifted out of `attributes`.
18
+ *
19
+ * CR-SM-294 hat `asil` aus der Ontologie entfernt (0 von 305 MOD trugen es, R-03 war sein
20
+ * einziger Leser). Die Spalte stand hier weiter und hob einen Schluessel heraus, den keine
21
+ * Regel mehr liest — gefunden erst vom Release-Zug, weil `npm test` gegen ein `dist/` von
22
+ * VOR der Streichung lief und nur ein frisches `tsc` darueber bricht. Ein Graph, der das
23
+ * Feld noch traegt, behaelt es im freien `attributes`-Sack; es ist nur keine typisierte
24
+ * Spalte mehr.
18
25
  */
19
26
  export declare function projectToOntologyGraph(graph: Graph): OntologyGraph;
20
27
  /**
@@ -13,7 +13,14 @@ import { ElementType, TraceType, TRACE_PATTERNS, V3_RULES, MT_RULES, UC_RULES, F
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
16
- * attributes (asil/method/kinds/status) are lifted out of `attributes`.
16
+ * attributes (method/kinds/status) are lifted out of `attributes`.
17
+ *
18
+ * CR-SM-294 hat `asil` aus der Ontologie entfernt (0 von 305 MOD trugen es, R-03 war sein
19
+ * einziger Leser). Die Spalte stand hier weiter und hob einen Schluessel heraus, den keine
20
+ * Regel mehr liest — gefunden erst vom Release-Zug, weil `npm test` gegen ein `dist/` von
21
+ * VOR der Streichung lief und nur ein frisches `tsc` darueber bricht. Ein Graph, der das
22
+ * Feld noch traegt, behaelt es im freien `attributes`-Sack; es ist nur keine typisierte
23
+ * Spalte mehr.
17
24
  */
18
25
  export function projectToOntologyGraph(graph) {
19
26
  const elements = graph.nodes.map((n) => ({
@@ -24,7 +31,6 @@ export function projectToOntologyGraph(graph) {
24
31
  status: n.attributes?.status ?? 'draft',
25
32
  created_at: n.createdAt ?? '',
26
33
  updated_at: n.updatedAt,
27
- asil: n.attributes?.asil,
28
34
  method: n.attributes?.method,
29
35
  kinds: n.attributes?.kinds,
30
36
  attributes: n.attributes,
@@ -127,8 +133,14 @@ function liftAttributes(rest) {
127
133
  * otherwise the 114 pre-existing errors in graphcode's own graph would block every
128
134
  * write on debt the writer did not create. Promotion is a per-family decision, not a
129
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.
130
142
  */
131
- const GATING_PREFIXES = ['R-', 'RD-', 'MT-'];
143
+ const GATING_PREFIXES = ['R-', 'RD-', 'MT-', 'IO-'];
132
144
  const SE_RULE_DEFS = [
133
145
  ...V3_RULES,
134
146
  ...UC_RULES,
@@ -140,14 +152,17 @@ const SE_RULE_DEFS = [
140
152
  ...VIEW_RULES,
141
153
  ...AF_RULES,
142
154
  ];
143
- function adapt(def, evaluate) {
144
- // (evaluate is already policy-bound by the caller — see seRules below)
155
+ function adapt(def, evaluate, boundPolicy) {
145
156
  return {
146
157
  id: def.id,
147
158
  name: def.name,
148
159
  severity: def.severity,
149
160
  gating: GATING_PREFIXES.some((p) => def.id.startsWith(p)),
150
- evaluate: (graph) => evaluate(projectToOntologyGraph(graph)).map((v) => ({
161
+ // CR-GC-402: the CALLER's policy wins for this evaluation, the descriptor's bound
162
+ // one is the fallback. Without this the threshold was frozen at construction, so a
163
+ // consumer of the published `SE_DESCRIPTOR` judged with `DEFAULT_METRIC_POLICY` no
164
+ // matter what its own config said (the MT-01 split between host and dashboard).
165
+ evaluate: (graph, policy) => evaluate(projectToOntologyGraph(graph), policy ?? boundPolicy).map((v) => ({
151
166
  ruleId: v.rule_id,
152
167
  ruleName: def.name,
153
168
  severity: v.severity,
@@ -175,9 +190,14 @@ function adapt(def, evaluate) {
175
190
  *
176
191
  * A `null` threshold means measure, don't judge: the rule stays registered and simply
177
192
  * reports nothing.
193
+ *
194
+ * CR-GC-402: `policy` is the BOUND value — the one that judges when a caller names none.
195
+ * `DefaultRuleEngine.evaluate(graph, policy)` may override it per call, which is what lets
196
+ * a consumer of the published descriptor judge with its own `graphcode.config.jsonc`
197
+ * instead of silently with the default.
178
198
  */
179
199
  function seRules(policy) {
180
- return [...SE_RULE_DEFS, ...MT_RULES].map((def) => adapt(def, (g) => def.evaluate(g, policy)));
200
+ return [...SE_RULE_DEFS, ...MT_RULES].map((def) => adapt(def, (g, p) => def.evaluate(g, p), policy));
181
201
  }
182
202
  // `label` is the node-table identifier (kuzu table name) — must be a valid
183
203
  // identifier, so it's the ElementType key (SYS, UC, …), not the human
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
- * Structural trace-legality check. Ontology-agnostic: matches a trace against
139
- * TracePattern[] with '*' wildcards on source/target. Both graph-service and
140
- * format-e-codec route every check through this validPairs is no longer a
141
- * parallel re-implementation.
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 — the ONE checker (CR-GC-247)
7
+ // Trace legality — ONE rule (CR-GC-247, CR-SM-323)
7
8
  // ---------------------------------------------------------------------------
8
9
  /**
9
- * Structural trace-legality check. Ontology-agnostic: matches a trace against
10
- * TracePattern[] with '*' wildcards on source/target. Both graph-service and
11
- * format-e-codec route every check through this validPairs is no longer a
12
- * parallel re-implementation.
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 patterns.some((p) => (p.source === '*' || p.source === trace.source) &&
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sigloch/graph-api-core",
3
- "version": "5.3.0",
3
+ "version": "5.5.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 <10",
46
+ "@sigloch/contracts": ">=10 <11",
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 <10",
55
+ "@sigloch/contracts": ">=10 <11",
56
56
  "kuzu-wasm": "^0.11.3"
57
57
  }
58
58
  }