@sigloch/graph-api-core 5.3.0 → 5.4.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';
@@ -18,8 +18,20 @@ export declare class FormatECodec {
18
18
  parse(input: string, options?: {
19
19
  resolveType?: (uid: string) => string | undefined;
20
20
  }): FormatEDiff;
21
- /** Serialize a Graph to Format E text. */
22
- serialize(graph: Graph): string;
21
+ /**
22
+ * Serialize a Graph to Format E text.
23
+ *
24
+ * CR-GC-373: `options.omitProvenance` is the **agent view** — provenance attributes
25
+ * (`created_at`, `updated_at`, `ranAt` inside `testRefs` entries, `weight` at its
26
+ * default 1) are dropped; everything that is work instruction (`codeRef`, `realRef`,
27
+ * `testRefs` file/tool/level, `kinds`, `constraint`, `zodDefinition`, `status`) stays.
28
+ * NOT a second codec: the default stays complete — round-trip is the contract, the
29
+ * agent view the exception. Measured on the CR-GC-114 job slice: 18% of the text was
30
+ * pure timestamps.
31
+ */
32
+ serialize(graph: Graph, options?: {
33
+ omitProvenance?: boolean;
34
+ }): string;
23
35
  private parseNodeLine;
24
36
  private parseEdgeLine;
25
37
  private parseMerge;
@@ -149,8 +149,19 @@ export class FormatECodec {
149
149
  }
150
150
  return { operations, errors };
151
151
  }
152
- /** Serialize a Graph to Format E text. */
153
- serialize(graph) {
152
+ /**
153
+ * Serialize a Graph to Format E text.
154
+ *
155
+ * CR-GC-373: `options.omitProvenance` is the **agent view** — provenance attributes
156
+ * (`created_at`, `updated_at`, `ranAt` inside `testRefs` entries, `weight` at its
157
+ * default 1) are dropped; everything that is work instruction (`codeRef`, `realRef`,
158
+ * `testRefs` file/tool/level, `kinds`, `constraint`, `zodDefinition`, `status`) stays.
159
+ * NOT a second codec: the default stays complete — round-trip is the contract, the
160
+ * agent view the exception. Measured on the CR-GC-114 job slice: 18% of the text was
161
+ * pure timestamps.
162
+ */
163
+ serialize(graph, options = {}) {
164
+ const omit = options.omitProvenance === true;
154
165
  const lines = [];
155
166
  if (graph.nodes.length > 0) {
156
167
  lines.push('## Nodes');
@@ -169,18 +180,21 @@ export class FormatECodec {
169
180
  lines.push(`### ${type}`);
170
181
  for (const node of byType.get(type) ?? []) {
171
182
  const descr = node.description ? `|${node.description}` : '';
172
- const attrs = this.serializeAttrs(node.attributes);
173
- lines.push(`+ ${node.uid}${descr}${attrs}`);
183
+ const nodeAttrs = omit ? stripNodeProvenance(node.attributes) : node.attributes;
184
+ lines.push(`+ ${node.uid}${descr}${this.serializeAttrs(nodeAttrs)}`);
174
185
  // CR-GC-334: realRef/testRef & friends as @key {json} — the inline block above
175
186
  // cannot carry them, and dropping them here is what made bindings vanish.
176
- lines.push(...this.structuredAttrLines(node.attributes));
187
+ lines.push(...this.structuredAttrLines(nodeAttrs));
177
188
  }
178
189
  }
179
190
  }
180
191
  if (graph.edges.length > 0) {
181
192
  lines.push('');
182
193
  lines.push('## Edges');
183
- lines.push(...this.serializeEdges(graph.edges));
194
+ const edges = omit
195
+ ? graph.edges.map(e => ({ ...e, attributes: stripEdgeProvenance(e.attributes) }))
196
+ : graph.edges;
197
+ lines.push(...this.serializeEdges(edges));
184
198
  }
185
199
  return lines.join('\n');
186
200
  }
@@ -408,3 +422,27 @@ export class FormatECodec {
408
422
  return desc?.arrows[0] ?? edgeType.toLowerCase();
409
423
  }
410
424
  }
425
+ // ---------------------------------------------------------------------------
426
+ // CR-GC-373: agent-view provenance filters (pure — never mutate the input graph)
427
+ // ---------------------------------------------------------------------------
428
+ const PROVENANCE_KEYS = new Set(['created_at', 'updated_at']);
429
+ /** Drop created_at/updated_at; inside testRefs entries additionally ranAt. */
430
+ function stripNodeProvenance(attrs) {
431
+ const out = {};
432
+ for (const [k, v] of Object.entries(attrs)) {
433
+ if (PROVENANCE_KEYS.has(k))
434
+ continue;
435
+ if (k === 'testRefs' && Array.isArray(v)) {
436
+ out[k] = v.map(entry => entry != null && typeof entry === 'object' && !Array.isArray(entry)
437
+ ? Object.fromEntries(Object.entries(entry).filter(([ek]) => ek !== 'ranAt'))
438
+ : entry);
439
+ continue;
440
+ }
441
+ out[k] = v;
442
+ }
443
+ return out;
444
+ }
445
+ /** Drop created_at/updated_at and the default weight 1 (a non-default weight stays). */
446
+ function stripEdgeProvenance(attrs) {
447
+ return Object.fromEntries(Object.entries(attrs).filter(([k, v]) => !PROVENANCE_KEYS.has(k) && !(k === 'weight' && Number(v) === 1)));
448
+ }
@@ -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';
@@ -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,
@@ -140,14 +146,17 @@ const SE_RULE_DEFS = [
140
146
  ...VIEW_RULES,
141
147
  ...AF_RULES,
142
148
  ];
143
- function adapt(def, evaluate) {
144
- // (evaluate is already policy-bound by the caller — see seRules below)
149
+ function adapt(def, evaluate, boundPolicy) {
145
150
  return {
146
151
  id: def.id,
147
152
  name: def.name,
148
153
  severity: def.severity,
149
154
  gating: GATING_PREFIXES.some((p) => def.id.startsWith(p)),
150
- evaluate: (graph) => evaluate(projectToOntologyGraph(graph)).map((v) => ({
155
+ // CR-GC-402: the CALLER's policy wins for this evaluation, the descriptor's bound
156
+ // one is the fallback. Without this the threshold was frozen at construction, so a
157
+ // consumer of the published `SE_DESCRIPTOR` judged with `DEFAULT_METRIC_POLICY` no
158
+ // matter what its own config said (the MT-01 split between host and dashboard).
159
+ evaluate: (graph, policy) => evaluate(projectToOntologyGraph(graph), policy ?? boundPolicy).map((v) => ({
151
160
  ruleId: v.rule_id,
152
161
  ruleName: def.name,
153
162
  severity: v.severity,
@@ -175,9 +184,14 @@ function adapt(def, evaluate) {
175
184
  *
176
185
  * A `null` threshold means measure, don't judge: the rule stays registered and simply
177
186
  * reports nothing.
187
+ *
188
+ * CR-GC-402: `policy` is the BOUND value — the one that judges when a caller names none.
189
+ * `DefaultRuleEngine.evaluate(graph, policy)` may override it per call, which is what lets
190
+ * a consumer of the published descriptor judge with its own `graphcode.config.jsonc`
191
+ * instead of silently with the default.
178
192
  */
179
193
  function seRules(policy) {
180
- return [...SE_RULE_DEFS, ...MT_RULES].map((def) => adapt(def, (g) => def.evaluate(g, policy)));
194
+ return [...SE_RULE_DEFS, ...MT_RULES].map((def) => adapt(def, (g, p) => def.evaluate(g, p), policy));
181
195
  }
182
196
  // `label` is the node-table identifier (kuzu table name) — must be a valid
183
197
  // identifier, so it's the ElementType key (SYS, UC, …), not the human
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.4.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
  }