@sigloch/graph-api-core 0.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.
Files changed (41) hide show
  1. package/LICENSE +21 -0
  2. package/dist/audit.d.ts +103 -0
  3. package/dist/audit.js +145 -0
  4. package/dist/browser.d.ts +21 -0
  5. package/dist/browser.js +17 -0
  6. package/dist/edge-ops.d.ts +42 -0
  7. package/dist/edge-ops.js +66 -0
  8. package/dist/factory.d.ts +26 -0
  9. package/dist/factory.js +23 -0
  10. package/dist/find-root.d.ts +34 -0
  11. package/dist/find-root.js +10 -0
  12. package/dist/format-e-codec.d.ts +23 -0
  13. package/dist/format-e-codec.js +296 -0
  14. package/dist/graph-service.d.ts +37 -0
  15. package/dist/graph-service.js +597 -0
  16. package/dist/index.d.ts +29 -0
  17. package/dist/index.js +27 -0
  18. package/dist/memory-adapter.d.ts +28 -0
  19. package/dist/memory-adapter.js +72 -0
  20. package/dist/rule-engine.d.ts +33 -0
  21. package/dist/rule-engine.js +17 -0
  22. package/dist/schemas.d.ts +97 -0
  23. package/dist/schemas.js +58 -0
  24. package/dist/se-descriptor.d.ts +24 -0
  25. package/dist/se-descriptor.js +84 -0
  26. package/dist/storage-adapter.d.ts +32 -0
  27. package/dist/storage-adapter.js +1 -0
  28. package/dist/test-fixtures.d.ts +42 -0
  29. package/dist/test-fixtures.js +151 -0
  30. package/dist/testing/index.d.ts +11 -0
  31. package/dist/testing/index.js +11 -0
  32. package/dist/testing/storage-contract-tests.d.ts +2 -0
  33. package/dist/testing/storage-contract-tests.js +113 -0
  34. package/dist/transport-adapter.d.ts +13 -0
  35. package/dist/transport-adapter.js +1 -0
  36. package/dist/types.d.ts +147 -0
  37. package/dist/types.js +39 -0
  38. package/package.json +45 -0
  39. package/test-fixtures/cr-007-features.format-e.md +22 -0
  40. package/test-fixtures/empty.format-e.md +6 -0
  41. package/test-fixtures/rasentraktor.format-e.md +46 -0
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Contract test suite for StorageAdapter implementations.
3
+ * Every adapter (Neo4j, SQLite, Memory, File) must pass these tests.
4
+ */
5
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
6
+ const SCOPE = { workspaceId: 'test', systemId: 'test' };
7
+ function makeNode(uid, type = 'ACT') {
8
+ return {
9
+ uid,
10
+ type,
11
+ name: uid.split('.')[0],
12
+ description: `Test node ${uid}`,
13
+ attributes: {},
14
+ createdAt: new Date().toISOString(),
15
+ };
16
+ }
17
+ function makeEdge(sourceId, targetId, edgeType = 'FLOWS_TO') {
18
+ return { sourceId, targetId, edgeType, attributes: {} };
19
+ }
20
+ export function runStorageContractTests(factory) {
21
+ describe('StorageAdapter Contract', () => {
22
+ let adapter;
23
+ beforeEach(async () => {
24
+ adapter = await factory();
25
+ await adapter.initialize();
26
+ });
27
+ afterEach(async () => {
28
+ await adapter.shutdown();
29
+ });
30
+ it('starts with empty graph', async () => {
31
+ const graph = await adapter.loadGraph(SCOPE);
32
+ expect(graph.nodes).toHaveLength(0);
33
+ expect(graph.edges).toHaveLength(0);
34
+ });
35
+ it('saves and loads nodes', async () => {
36
+ const node = makeNode('TestActor.ACT.01');
37
+ await adapter.saveNodes([node]);
38
+ const graph = await adapter.loadGraph(SCOPE);
39
+ expect(graph.nodes).toHaveLength(1);
40
+ expect(graph.nodes[0].uid).toBe('TestActor.ACT.01');
41
+ });
42
+ it('saves and loads edges', async () => {
43
+ const n1 = makeNode('Source.ACT.01');
44
+ const n2 = makeNode('Target.ACT.02');
45
+ await adapter.saveNodes([n1, n2]);
46
+ await adapter.saveEdges([makeEdge('Source.ACT.01', 'Target.ACT.02')]);
47
+ const graph = await adapter.loadGraph(SCOPE);
48
+ expect(graph.edges).toHaveLength(1);
49
+ expect(graph.edges[0].sourceId).toBe('Source.ACT.01');
50
+ });
51
+ it('deletes nodes and cascades edges', async () => {
52
+ const n1 = makeNode('A.ACT.01');
53
+ const n2 = makeNode('B.ACT.02');
54
+ await adapter.saveNodes([n1, n2]);
55
+ await adapter.saveEdges([makeEdge('A.ACT.01', 'B.ACT.02')]);
56
+ await adapter.deleteNodes(['A.ACT.01']);
57
+ const graph = await adapter.loadGraph(SCOPE);
58
+ expect(graph.nodes).toHaveLength(1);
59
+ expect(graph.edges).toHaveLength(0);
60
+ });
61
+ it('deletes edges', async () => {
62
+ const n1 = makeNode('A.ACT.01');
63
+ const n2 = makeNode('B.ACT.02');
64
+ await adapter.saveNodes([n1, n2]);
65
+ await adapter.saveEdges([makeEdge('A.ACT.01', 'B.ACT.02')]);
66
+ await adapter.deleteEdges([{ sourceId: 'A.ACT.01', targetId: 'B.ACT.02', edgeType: 'FLOWS_TO' }]);
67
+ const graph = await adapter.loadGraph(SCOPE);
68
+ expect(graph.nodes).toHaveLength(2);
69
+ expect(graph.edges).toHaveLength(0);
70
+ });
71
+ it('getNode returns null for missing', async () => {
72
+ const result = await adapter.getNode('DoesNotExist.ACT.99');
73
+ expect(result).toBeNull();
74
+ });
75
+ it('getNode returns existing', async () => {
76
+ await adapter.saveNodes([makeNode('Found.ACT.01')]);
77
+ const result = await adapter.getNode('Found.ACT.01');
78
+ expect(result).not.toBeNull();
79
+ expect(result.uid).toBe('Found.ACT.01');
80
+ });
81
+ it('getSubgraph returns correct depth', async () => {
82
+ const nodes = [makeNode('Root.ACT.01'), makeNode('Child.ACT.02'), makeNode('Grand.ACT.03')];
83
+ await adapter.saveNodes(nodes);
84
+ await adapter.saveEdges([
85
+ makeEdge('Root.ACT.01', 'Child.ACT.02'),
86
+ makeEdge('Child.ACT.02', 'Grand.ACT.03'),
87
+ ]);
88
+ const depth1 = await adapter.getSubgraph('Root.ACT.01', 1);
89
+ expect(depth1.nodes).toHaveLength(2);
90
+ const depth2 = await adapter.getSubgraph('Root.ACT.01', 2);
91
+ expect(depth2.nodes).toHaveLength(3);
92
+ });
93
+ it('stats returns correct counts', async () => {
94
+ await adapter.saveNodes([makeNode('A.ACT.01'), makeNode('B.ACT.02')]);
95
+ await adapter.saveEdges([makeEdge('A.ACT.01', 'B.ACT.02')]);
96
+ const stats = await adapter.stats();
97
+ expect(stats.nodeCount).toBe(2);
98
+ expect(stats.edgeCount).toBe(1);
99
+ });
100
+ it('upserts nodes (save twice)', async () => {
101
+ const node = makeNode('Upsert.ACT.01');
102
+ await adapter.saveNodes([node]);
103
+ const updated = { ...node, description: 'Updated' };
104
+ await adapter.saveNodes([updated]);
105
+ const graph = await adapter.loadGraph(SCOPE);
106
+ expect(graph.nodes).toHaveLength(1);
107
+ expect(graph.nodes[0].description).toBe('Updated');
108
+ });
109
+ it('isHealthy returns true', async () => {
110
+ expect(await adapter.isHealthy()).toBe(true);
111
+ });
112
+ });
113
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * TransportAdapter interface — binds GraphService to HTTP/WS/MCP/CLI frameworks.
3
+ */
4
+ import type { GraphService } from './graph-service.js';
5
+ export interface TransportConfig {
6
+ port?: number;
7
+ host?: string;
8
+ }
9
+ export interface TransportAdapter {
10
+ mount(service: GraphService, config: TransportConfig): void;
11
+ start(): Promise<void>;
12
+ stop(): Promise<void>;
13
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Core types for graph-api-core — ontology-agnostic.
3
+ * Domains register their node/edge types via OntologyDescriptor.
4
+ */
5
+ export interface GraphNode {
6
+ uid: string;
7
+ type: string;
8
+ name: string;
9
+ description?: string;
10
+ attributes: Record<string, unknown>;
11
+ createdAt?: string;
12
+ updatedAt?: string;
13
+ }
14
+ export interface GraphEdge {
15
+ sourceId: string;
16
+ targetId: string;
17
+ edgeType: string;
18
+ attributes: Record<string, unknown>;
19
+ }
20
+ export interface Graph {
21
+ nodes: GraphNode[];
22
+ edges: GraphEdge[];
23
+ }
24
+ export interface NodeTypeDescriptor {
25
+ label: string;
26
+ requiredAttrs: string[];
27
+ }
28
+ export interface EdgeTypeDescriptor {
29
+ /** Arrow aliases for Format E parsing (e.g. ['compose', 'cp']). */
30
+ arrows: string[];
31
+ /** Valid source→target node type pairs. */
32
+ validPairs: [string, string][];
33
+ }
34
+ export interface TracePattern {
35
+ source: string;
36
+ target: string;
37
+ type: string;
38
+ /** Optional label refinement (e.g. MS→MS relation 'depends-on'). */
39
+ label?: string;
40
+ }
41
+ export interface OntologyDescriptor {
42
+ name: string;
43
+ version: string;
44
+ nodeTypes: Record<string, NodeTypeDescriptor>;
45
+ edgeTypes: Record<string, EdgeTypeDescriptor>;
46
+ /**
47
+ * Authoritative trace-legality patterns (CR-GC-247). The ONE source the engine
48
+ * validates edges against — supersedes per-edgeType `validPairs`, which is now
49
+ * only a menu/arrow enumeration. Omit to derive patterns from validPairs
50
+ * (foreign ontologies that declare only pairs); see {@link tracePatternsOf}.
51
+ */
52
+ patterns?: TracePattern[];
53
+ /** Domain-specific rules. */
54
+ rules?: Rule[];
55
+ }
56
+ interface Rule {
57
+ id: string;
58
+ name: string;
59
+ severity: 'error' | 'warning' | 'info';
60
+ evaluate: (graph: Graph) => RuleViolation[];
61
+ }
62
+ interface RuleViolation {
63
+ ruleId: string;
64
+ ruleName: string;
65
+ severity: 'error' | 'warning' | 'info';
66
+ message: string;
67
+ elementId?: string;
68
+ }
69
+ export interface GraphScope {
70
+ workspaceId: string;
71
+ systemId: string;
72
+ }
73
+ export interface GraphQuery {
74
+ type: 'elements' | 'edges' | 'subgraph' | 'view';
75
+ filter?: Record<string, unknown>;
76
+ view?: string;
77
+ root?: string;
78
+ depth?: number;
79
+ format?: 'json' | 'format-e';
80
+ }
81
+ export interface MutationResult {
82
+ applied: number;
83
+ rejected: number;
84
+ violations: RuleViolation[];
85
+ version: number;
86
+ auditId: string;
87
+ }
88
+ export interface BatchResult {
89
+ results: MutationResult[];
90
+ totalApplied: number;
91
+ totalRejected: number;
92
+ }
93
+ export interface HealthStatus {
94
+ status: 'ok' | 'degraded' | 'down';
95
+ storage: string;
96
+ ontologyName: string;
97
+ ontologyVersion: string;
98
+ elementCount: number;
99
+ edgeCount: number;
100
+ violationCount: number;
101
+ }
102
+ export interface FormatEOperation {
103
+ type: 'add_node' | 'remove_node' | 'update_node' | 'add_edge' | 'remove_edge' | 'update_edge' | 'strict_add_node' | 'strict_add_edge' | 'merge_nodes';
104
+ /** Node uid or edge key (source->target). */
105
+ semanticId: string;
106
+ description?: string;
107
+ attributes?: Record<string, string>;
108
+ sourceId?: string;
109
+ targetId?: string;
110
+ edgeType?: string;
111
+ /** Merge operation: source node IDs to merge. */
112
+ sourceIds?: string[];
113
+ /** update_edge: identity is sourceId/targetId/edgeType above; this is the change (CR-198). */
114
+ set?: {
115
+ edgeType?: string;
116
+ flip?: boolean;
117
+ attributes?: Record<string, string>;
118
+ };
119
+ }
120
+ export interface FormatEDiff {
121
+ operations: FormatEOperation[];
122
+ errors: string[];
123
+ }
124
+ /**
125
+ * Structural trace-legality check. Ontology-agnostic: matches a trace against
126
+ * TracePattern[] with '*' wildcards on source/target. Both graph-service and
127
+ * format-e-codec route every check through this — validPairs is no longer a
128
+ * parallel re-implementation.
129
+ *
130
+ * A pattern's `label` is descriptive metadata (e.g. MS→MS relation 'depends-on'),
131
+ * NOT a gate: label is trace data that isn't uniformly carried through the
132
+ * Graph/codec pipeline, and the legality this replaces (validPairs) never keyed on
133
+ * it. Enforcing label is a deliberate tightening (needs codec label round-tripping),
134
+ * out of scope for the centralization.
135
+ */
136
+ export declare function isValidTrace(trace: {
137
+ source: string;
138
+ target: string;
139
+ type: string;
140
+ }, patterns: TracePattern[]): boolean;
141
+ /**
142
+ * The patterns a descriptor validates against: explicit `patterns` (SSOT) if set,
143
+ * else derived from each edgeType's `validPairs` (menu → legality) so foreign
144
+ * ontologies that declare only pairs keep working.
145
+ */
146
+ export declare function tracePatternsOf(ontology: OntologyDescriptor): TracePattern[];
147
+ export {};
package/dist/types.js ADDED
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Core types for graph-api-core — ontology-agnostic.
3
+ * Domains register their node/edge types via OntologyDescriptor.
4
+ */
5
+ // ---------------------------------------------------------------------------
6
+ // Trace legality — the ONE checker (CR-GC-247)
7
+ // ---------------------------------------------------------------------------
8
+ /**
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.
19
+ */
20
+ 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);
24
+ }
25
+ /**
26
+ * The patterns a descriptor validates against: explicit `patterns` (SSOT) if set,
27
+ * else derived from each edgeType's `validPairs` (menu → legality) so foreign
28
+ * ontologies that declare only pairs keep working.
29
+ */
30
+ export function tracePatternsOf(ontology) {
31
+ if (ontology.patterns)
32
+ return ontology.patterns;
33
+ const derived = [];
34
+ for (const [type, desc] of Object.entries(ontology.edgeTypes)) {
35
+ for (const [source, target] of desc.validPairs)
36
+ derived.push({ source, target, type });
37
+ }
38
+ return derived;
39
+ }
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@sigloch/graph-api-core",
3
+ "version": "0.4.0",
4
+ "type": "module",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "files": [
8
+ "dist",
9
+ "LICENSE",
10
+ "test-fixtures"
11
+ ],
12
+ "exports": {
13
+ ".": "./dist/index.js",
14
+ "./browser": "./dist/browser.js",
15
+ "./testing": "./dist/testing/index.js",
16
+ "./test-fixtures": "./dist/test-fixtures.js"
17
+ },
18
+ "scripts": {
19
+ "build": "rm -rf dist && tsc",
20
+ "test": "vitest run",
21
+ "prepublishOnly": "npm run build && npm run test"
22
+ },
23
+ "dependencies": {
24
+ "@sigloch/contracts": "^0.7.0",
25
+ "zod": "^4.3.6"
26
+ },
27
+ "license": "MIT",
28
+ "description": "Framework-agnostic graph engine core — ontology-typed nodes/traces, rule evaluation, views",
29
+ "author": "sigloch-consulting",
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "git+https://github.com/andreassigloch/sigloch-modules.git",
33
+ "directory": "packages/graph-api-core"
34
+ },
35
+ "homepage": "https://github.com/andreassigloch/sigloch-modules#readme",
36
+ "bugs": {
37
+ "url": "https://github.com/andreassigloch/sigloch-modules/issues"
38
+ },
39
+ "engines": {
40
+ "node": ">=22"
41
+ },
42
+ "publishConfig": {
43
+ "access": "public"
44
+ }
45
+ }
@@ -0,0 +1,22 @@
1
+ # Format-E Test Fixture · CR-007 features
2
+
3
+ Exercises the reserved inline attributes from CR-007:
4
+ - Nodes: `notes`
5
+ - Edges: `cardinality`, `constraint`, `notes`
6
+
7
+ Uses the minimal `TEST_ONTOLOGY` shipped from `@sigloch/graph-api-core/test-fixtures`.
8
+
9
+ ```format-e
10
+ ## Nodes
11
+ + Mower.PR.01|Riding mower R15 [notes:Phase-2-Vertical-Slice-Wurzel]
12
+ + Engine.C.01|Combustion engine [power:gas]
13
+ + Gearbox.C.02|Mowing-deck gearbox family [kind:family,notes:drei Varianten geplant]
14
+ + DFMEA-Engine-001.FR.01|Not-Stop-Funktion am Motor [external_id:DFMEA-Engine-001]
15
+ + EmergencyBrake.CTRL.01|Mechanische Bremse Stillstandszeit max 5 sec
16
+
17
+ ## Edges
18
+ + Mower.PR.01 -compose-> Engine.C.01 [cardinality:1..1]
19
+ + Mower.PR.01 -compose-> Gearbox.C.02 [cardinality:1..n,constraint:mindestens eine Variante je Schnittbreite]
20
+ + Engine.C.01 -addressed_by-> DFMEA-Engine-001.FR.01 [cardinality:1..n,notes:DFMEA-Hotspot]
21
+ + DFMEA-Engine-001.FR.01 -addressed_by-> EmergencyBrake.CTRL.01 [cardinality:1..1,constraint:Stillstandszeit <= 5 sec]
22
+ ```
@@ -0,0 +1,6 @@
1
+ # Format-E Test Fixture · empty
2
+
3
+ Empty diff. Codec must accept and yield zero operations.
4
+
5
+ ```format-e
6
+ ```
@@ -0,0 +1,46 @@
1
+ # Format-E Test Fixture · Rasentraktor R15 (Subset)
2
+
3
+ Vertical-Slice-Auszug aus dem ontoagent-Trockenlauf v0.3 ("Rasentraktor R15 /
4
+ Mähwerk-Getriebe"). Vollständige Domäne siehe ontoagent
5
+ `docs/examples/format-e-rasentraktor.md`; diese Fixture ist die kompakte,
6
+ sigloch-weit nutzbare Variante für CI-Smokes.
7
+
8
+ Validiert gegen `TEST_ONTOLOGY` aus `@sigloch/graph-api-core/test-fixtures`.
9
+
10
+ ```format-e
11
+ ## Nodes
12
+ + Konstruktion.OU.01|Konstruktion
13
+ + Tooling.OU.02|Tooling
14
+ + DesignLead.R.01|Design Lead
15
+ + Tooling.R.02|Tooling
16
+ + R15.PR.01|Rasentraktor R15 [code:R15]
17
+ + R15-1000.VAR.01|R15 mit 1000 mm Schnittbreite [attribute_key:Schnittbreite,attribute_value:1000mm]
18
+ + R15-1500.VAR.02|R15 mit 1500 mm Schnittbreite [attribute_key:Schnittbreite,attribute_value:1500mm]
19
+ + MaehwerkGetriebe.C.01|Maehwerk-Getriebe Familie [kind:family]
20
+ + Getriebe-1000.C.02|Getriebe 1000 [kind:variant,part_no:MG-1000]
21
+ + Getriebe-1500.C.03|Getriebe 1500 [kind:variant,part_no:MG-1500]
22
+ + UnterschiedlicheMaehbreiten.F.01|Unterschiedliche Maehbreiten [customer_priority:hoch]
23
+ + DFMEA-MG-001.FR.01|Not-Stop-Funktion [external_id:DFMEA-MG-001,status:verifiziert]
24
+ + MechBremse.CTRL.01|Mechanische Bremse Stillstandszeit max 5 sec
25
+ + Stillstandszeitmessung.TC.01|Stillstandszeit-Messung nach Not-Aus [criterion:t_stop <= 5 sec]
26
+ + WerkzeugSetGehaeuse.TL.01|Werkzeug-Set Gehaeuse Druckguss [material:Aluminium-Druckguss]
27
+ + M3.MT.01|GO Milestone [sequence_no:3]
28
+ + PDM.SL.01|PDM-System [type:PLM]
29
+
30
+ ## Edges
31
+ + R15.PR.01 -compose-> MaehwerkGetriebe.C.01 [cardinality:1..1]
32
+ + MaehwerkGetriebe.C.01 -compose-> Getriebe-1000.C.02 [cardinality:1..n,constraint:eine Variante je Schnittbreite]
33
+ + MaehwerkGetriebe.C.01 -compose-> Getriebe-1500.C.03 [cardinality:1..n]
34
+ + R15.PR.01 -has_variant-> R15-1000.VAR.01 [cardinality:1..n]
35
+ + R15.PR.01 -has_variant-> R15-1500.VAR.02 [cardinality:1..n]
36
+ + UnterschiedlicheMaehbreiten.F.01 -realized_by-> MaehwerkGetriebe.C.01 [cardinality:1..n]
37
+ + MaehwerkGetriebe.C.01 -addressed_by-> DFMEA-MG-001.FR.01 [cardinality:1..n]
38
+ + DFMEA-MG-001.FR.01 -addressed_by-> MechBremse.CTRL.01 [cardinality:1..1,constraint:Stillstandszeit <= 5 sec]
39
+ + MechBremse.CTRL.01 -verified_by-> Stillstandszeitmessung.TC.01 [cardinality:1..1]
40
+ + Tooling.OU.02 -responsible_for-> WerkzeugSetGehaeuse.TL.01 [cardinality:1..1]
41
+ + Konstruktion.OU.01 -responsible_for-> MaehwerkGetriebe.C.01 [cardinality:1..1]
42
+ + DesignLead.R.01 -within-> Konstruktion.OU.01 [cardinality:1..1]
43
+ + Tooling.R.02 -within-> Tooling.OU.02 [cardinality:1..1]
44
+ + M3.MT.01 -requires_document_of_type-> DFMEA-MG-001.FR.01 [cardinality:1..n,notes:GO-Gate verlangt finalisierte D-FMEA]
45
+ + MaehwerkGetriebe.C.01 -stored_in-> PDM.SL.01 [cardinality:1..1]
46
+ ```