@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 +2 -0
- package/dist/browser.js +1 -0
- package/dist/format-e-codec.d.ts +14 -4
- package/dist/format-e-codec.js +47 -17
- package/dist/graph-service.js +17 -4
- package/dist/impact-slice.d.ts +57 -0
- package/dist/impact-slice.js +49 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3 -0
- package/dist/kuzu/graph-cypher-engine.js +21 -1
- package/dist/rule-engine.d.ts +18 -3
- package/dist/rule-engine.js +11 -2
- package/dist/se-descriptor.d.ts +8 -1
- package/dist/se-descriptor.js +27 -7
- package/dist/types.d.ts +7 -14
- package/dist/types.js +7 -14
- package/package.json +3 -3
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';
|
package/dist/format-e-codec.d.ts
CHANGED
|
@@ -3,8 +3,6 @@ export declare class FormatECodec {
|
|
|
3
3
|
private readonly ontology;
|
|
4
4
|
private readonly edgeArrowToType;
|
|
5
5
|
private readonly validNodeTypes;
|
|
6
|
-
/** Trace-legality patterns (CR-GC-247) — the SSOT the parser validates edges against. */
|
|
7
|
-
private readonly patterns;
|
|
8
6
|
constructor(ontology: OntologyDescriptor);
|
|
9
7
|
/** Extract a ```format-e block from LLM output. Returns null if not found. */
|
|
10
8
|
extractFromLlm(llmOutput: string): string | null;
|
|
@@ -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
|
-
/**
|
|
22
|
-
|
|
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;
|
package/dist/format-e-codec.js
CHANGED
|
@@ -8,7 +8,6 @@
|
|
|
8
8
|
* every foreign convention fail silently instead of loudly.
|
|
9
9
|
*/
|
|
10
10
|
import { hydrateAttrValue, attributeTypeOf } from '@sigloch/contracts/se';
|
|
11
|
-
import { isValidTrace, tracePatternsOf } from './types.js';
|
|
12
11
|
// ---------------------------------------------------------------------------
|
|
13
12
|
// Regex patterns
|
|
14
13
|
// ---------------------------------------------------------------------------
|
|
@@ -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
|
-
/**
|
|
153
|
-
|
|
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
|
|
173
|
-
lines.push(`+ ${node.uid}${descr}${
|
|
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(
|
|
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
|
-
|
|
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
|
-
//
|
|
280
|
-
//
|
|
281
|
-
|
|
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
|
+
}
|
package/dist/graph-service.js
CHANGED
|
@@ -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
|
|
569
|
-
//
|
|
570
|
-
//
|
|
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({
|
|
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
|
-
|
|
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) {
|
package/dist/rule-engine.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/dist/rule-engine.js
CHANGED
|
@@ -7,14 +7,23 @@ export class DefaultRuleEngine {
|
|
|
7
7
|
register(rules) {
|
|
8
8
|
this.rules.push(...rules);
|
|
9
9
|
}
|
|
10
|
-
|
|
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;
|
package/dist/se-descriptor.d.ts
CHANGED
|
@@ -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 (
|
|
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
|
/**
|
package/dist/se-descriptor.js
CHANGED
|
@@ -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 (
|
|
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
|
-
|
|
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
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
* A pattern's `label` is descriptive metadata (e.g. MS→MS relation 'depends-on'),
|
|
144
|
-
* NOT a gate: label is trace data that isn't uniformly carried through the
|
|
145
|
-
* Graph/codec pipeline, and the legality this replaces (validPairs) never keyed on
|
|
146
|
-
* it. Enforcing label is a deliberate tightening (needs codec label round-tripping),
|
|
147
|
-
* out of scope for the centralization.
|
|
134
|
+
* Trace legality for any ontology, decided by contracts' `isValidTrace` — the routine R-18
|
|
135
|
+
* runs. No logic of its own: `label` and endpoint `kinds` are part of the rule exactly as
|
|
136
|
+
* R-18 reads them. This signature only opens the SE-closed contracts types to the
|
|
137
|
+
* ontology-agnostic strings this package speaks; the runtime pattern shape is the same.
|
|
148
138
|
*/
|
|
149
139
|
export declare function isValidTrace(trace: {
|
|
150
140
|
source: string;
|
|
151
141
|
target: string;
|
|
152
142
|
type: string;
|
|
143
|
+
label?: string;
|
|
144
|
+
sourceKinds?: readonly string[];
|
|
145
|
+
targetKinds?: readonly string[];
|
|
153
146
|
}, patterns: TracePattern[]): boolean;
|
|
154
147
|
/**
|
|
155
148
|
* The patterns a descriptor validates against: explicit `patterns` (SSOT) if set,
|
package/dist/types.js
CHANGED
|
@@ -2,25 +2,18 @@
|
|
|
2
2
|
* Core types for graph-api-core — ontology-agnostic.
|
|
3
3
|
* Domains register their node/edge types via OntologyDescriptor.
|
|
4
4
|
*/
|
|
5
|
+
import { isValidTrace as contractsIsValidTrace } from '@sigloch/contracts/se';
|
|
5
6
|
// ---------------------------------------------------------------------------
|
|
6
|
-
// Trace legality —
|
|
7
|
+
// Trace legality — ONE rule (CR-GC-247, CR-SM-323)
|
|
7
8
|
// ---------------------------------------------------------------------------
|
|
8
9
|
/**
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* A pattern's `label` is descriptive metadata (e.g. MS→MS relation 'depends-on'),
|
|
15
|
-
* NOT a gate: label is trace data that isn't uniformly carried through the
|
|
16
|
-
* Graph/codec pipeline, and the legality this replaces (validPairs) never keyed on
|
|
17
|
-
* it. Enforcing label is a deliberate tightening (needs codec label round-tripping),
|
|
18
|
-
* out of scope for the centralization.
|
|
10
|
+
* Trace legality for any ontology, decided by contracts' `isValidTrace` — the routine R-18
|
|
11
|
+
* runs. No logic of its own: `label` and endpoint `kinds` are part of the rule exactly as
|
|
12
|
+
* R-18 reads them. This signature only opens the SE-closed contracts types to the
|
|
13
|
+
* ontology-agnostic strings this package speaks; the runtime pattern shape is the same.
|
|
19
14
|
*/
|
|
20
15
|
export function isValidTrace(trace, patterns) {
|
|
21
|
-
return
|
|
22
|
-
(p.target === '*' || p.target === trace.target) &&
|
|
23
|
-
p.type === trace.type);
|
|
16
|
+
return contractsIsValidTrace(trace, patterns);
|
|
24
17
|
}
|
|
25
18
|
/**
|
|
26
19
|
* The patterns a descriptor validates against: explicit `patterns` (SSOT) if set,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sigloch/graph-api-core",
|
|
3
|
-
"version": "5.
|
|
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": ">=
|
|
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": ">=
|
|
55
|
+
"@sigloch/contracts": ">=10 <11",
|
|
56
56
|
"kuzu-wasm": "^0.11.3"
|
|
57
57
|
}
|
|
58
58
|
}
|