@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.
- package/LICENSE +21 -0
- package/dist/audit.d.ts +103 -0
- package/dist/audit.js +145 -0
- package/dist/browser.d.ts +21 -0
- package/dist/browser.js +17 -0
- package/dist/edge-ops.d.ts +42 -0
- package/dist/edge-ops.js +66 -0
- package/dist/factory.d.ts +26 -0
- package/dist/factory.js +23 -0
- package/dist/find-root.d.ts +34 -0
- package/dist/find-root.js +10 -0
- package/dist/format-e-codec.d.ts +23 -0
- package/dist/format-e-codec.js +296 -0
- package/dist/graph-service.d.ts +37 -0
- package/dist/graph-service.js +597 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.js +27 -0
- package/dist/memory-adapter.d.ts +28 -0
- package/dist/memory-adapter.js +72 -0
- package/dist/rule-engine.d.ts +33 -0
- package/dist/rule-engine.js +17 -0
- package/dist/schemas.d.ts +97 -0
- package/dist/schemas.js +58 -0
- package/dist/se-descriptor.d.ts +24 -0
- package/dist/se-descriptor.js +84 -0
- package/dist/storage-adapter.d.ts +32 -0
- package/dist/storage-adapter.js +1 -0
- package/dist/test-fixtures.d.ts +42 -0
- package/dist/test-fixtures.js +151 -0
- package/dist/testing/index.d.ts +11 -0
- package/dist/testing/index.js +11 -0
- package/dist/testing/storage-contract-tests.d.ts +2 -0
- package/dist/testing/storage-contract-tests.js +113 -0
- package/dist/transport-adapter.d.ts +13 -0
- package/dist/transport-adapter.js +1 -0
- package/dist/types.d.ts +147 -0
- package/dist/types.js +39 -0
- package/package.json +45 -0
- package/test-fixtures/cr-007-features.format-e.md +22 -0
- package/test-fixtures/empty.format-e.md +6 -0
- package/test-fixtures/rasentraktor.format-e.md +46 -0
|
@@ -0,0 +1,597 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GraphService — central facade for all graph operations.
|
|
3
|
+
* Framework-agnostic, ontology-agnostic.
|
|
4
|
+
*
|
|
5
|
+
* CR-195d: Enhanced mutate with hoisting, meta-model validation, audit traces.
|
|
6
|
+
*/
|
|
7
|
+
import { isValidTrace, tracePatternsOf } from './types.js';
|
|
8
|
+
import { FormatECodec } from './format-e-codec.js';
|
|
9
|
+
import { DefaultRuleEngine } from './rule-engine.js';
|
|
10
|
+
import { InMemoryAuditLog } from './audit.js';
|
|
11
|
+
import { extractFromSemanticId } from '@sigloch/contracts/se';
|
|
12
|
+
import { updateEdge, mergeNodes } from './edge-ops.js';
|
|
13
|
+
export class GraphService {
|
|
14
|
+
ontology;
|
|
15
|
+
codec;
|
|
16
|
+
storage;
|
|
17
|
+
rules;
|
|
18
|
+
audit;
|
|
19
|
+
scope;
|
|
20
|
+
/** Trace-legality patterns (CR-GC-247) — the SSOT every edge validates against. */
|
|
21
|
+
patterns;
|
|
22
|
+
version = 0;
|
|
23
|
+
constructor(config) {
|
|
24
|
+
this.ontology = config.ontology;
|
|
25
|
+
this.patterns = tracePatternsOf(config.ontology);
|
|
26
|
+
this.storage = config.storage;
|
|
27
|
+
this.rules = config.ruleEngine ?? new DefaultRuleEngine(config.ontology.version);
|
|
28
|
+
this.audit = config.auditLog ?? new InMemoryAuditLog();
|
|
29
|
+
this.scope = config.scope ?? { workspaceId: 'default', systemId: 'default' };
|
|
30
|
+
this.codec = new FormatECodec(config.ontology);
|
|
31
|
+
// Register domain rules
|
|
32
|
+
if (config.ontology.rules) {
|
|
33
|
+
this.rules.register(config.ontology.rules);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
async initialize() {
|
|
37
|
+
await this.storage.initialize();
|
|
38
|
+
}
|
|
39
|
+
async shutdown() {
|
|
40
|
+
await this.storage.shutdown();
|
|
41
|
+
}
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
// Queries
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
async query(q) {
|
|
46
|
+
if (q.type === 'subgraph' && q.root) {
|
|
47
|
+
return this.storage.getSubgraph(q.root, q.depth ?? 3);
|
|
48
|
+
}
|
|
49
|
+
return this.storage.loadGraph(this.scope);
|
|
50
|
+
}
|
|
51
|
+
async getElement(uid) {
|
|
52
|
+
return this.storage.getNode(uid);
|
|
53
|
+
}
|
|
54
|
+
async getSubgraph(root, depth = 3) {
|
|
55
|
+
return this.storage.getSubgraph(root, depth);
|
|
56
|
+
}
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
// Mutations
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
async mutate(diff, consumerId = 'system') {
|
|
61
|
+
let applied = 0;
|
|
62
|
+
let rejected = 0;
|
|
63
|
+
const violations = [];
|
|
64
|
+
const nodesToCreate = [];
|
|
65
|
+
const edgesToCreate = [];
|
|
66
|
+
// CR-195d: Hoist kinds/asil/method from attributes to top-level for SE schema
|
|
67
|
+
const isSESchema = this.ontology.name === 'SE';
|
|
68
|
+
for (const op of diff.operations) {
|
|
69
|
+
switch (op.type) {
|
|
70
|
+
case 'add_node': {
|
|
71
|
+
const existing = await this.storage.getNode(op.semanticId);
|
|
72
|
+
if (existing) {
|
|
73
|
+
// CR-169: Upsert — update existing
|
|
74
|
+
try {
|
|
75
|
+
const updated = await this.applyNodeUpdate(existing, op, isSESchema);
|
|
76
|
+
await this.storage.saveNodes([updated]);
|
|
77
|
+
applied++;
|
|
78
|
+
}
|
|
79
|
+
catch (e) {
|
|
80
|
+
violations.push({
|
|
81
|
+
ruleId: 'UPDATE_FAILED',
|
|
82
|
+
ruleName: 'update-failed',
|
|
83
|
+
severity: 'error',
|
|
84
|
+
message: e.message,
|
|
85
|
+
elementId: op.semanticId,
|
|
86
|
+
});
|
|
87
|
+
rejected++;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
// New node
|
|
92
|
+
try {
|
|
93
|
+
const node = await this.applyNodeCreate(op, isSESchema);
|
|
94
|
+
await this.storage.saveNodes([node]);
|
|
95
|
+
nodesToCreate.push(op.semanticId);
|
|
96
|
+
if (consumerId !== 'system') {
|
|
97
|
+
edgesToCreate.push({
|
|
98
|
+
sourceId: consumerId,
|
|
99
|
+
targetId: op.semanticId,
|
|
100
|
+
edgeType: 'produces',
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
applied++;
|
|
104
|
+
}
|
|
105
|
+
catch (e) {
|
|
106
|
+
violations.push({
|
|
107
|
+
ruleId: 'CREATE_FAILED',
|
|
108
|
+
ruleName: 'create-failed',
|
|
109
|
+
severity: 'error',
|
|
110
|
+
message: e.message,
|
|
111
|
+
elementId: op.semanticId,
|
|
112
|
+
});
|
|
113
|
+
rejected++;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
case 'strict_add_node': {
|
|
119
|
+
const existing = await this.storage.getNode(op.semanticId);
|
|
120
|
+
if (existing) {
|
|
121
|
+
violations.push({
|
|
122
|
+
ruleId: 'STRICT_ADD',
|
|
123
|
+
ruleName: 'strict-add',
|
|
124
|
+
severity: 'error',
|
|
125
|
+
message: `Node already exists: ${op.semanticId}`,
|
|
126
|
+
elementId: op.semanticId,
|
|
127
|
+
});
|
|
128
|
+
rejected++;
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
try {
|
|
132
|
+
const node = await this.applyNodeCreate(op, isSESchema);
|
|
133
|
+
await this.storage.saveNodes([node]);
|
|
134
|
+
nodesToCreate.push(op.semanticId);
|
|
135
|
+
if (consumerId !== 'system') {
|
|
136
|
+
edgesToCreate.push({
|
|
137
|
+
sourceId: consumerId,
|
|
138
|
+
targetId: op.semanticId,
|
|
139
|
+
edgeType: 'produces',
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
applied++;
|
|
143
|
+
}
|
|
144
|
+
catch (e) {
|
|
145
|
+
violations.push({
|
|
146
|
+
ruleId: 'CREATE_FAILED',
|
|
147
|
+
ruleName: 'create-failed',
|
|
148
|
+
severity: 'error',
|
|
149
|
+
message: e.message,
|
|
150
|
+
elementId: op.semanticId,
|
|
151
|
+
});
|
|
152
|
+
rejected++;
|
|
153
|
+
}
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
case 'update_node': {
|
|
157
|
+
const existing = await this.storage.getNode(op.semanticId);
|
|
158
|
+
if (!existing) {
|
|
159
|
+
violations.push({
|
|
160
|
+
ruleId: 'UPDATE_MISSING',
|
|
161
|
+
ruleName: 'update-missing',
|
|
162
|
+
severity: 'warning',
|
|
163
|
+
message: `Node not found for update: ${op.semanticId}`,
|
|
164
|
+
elementId: op.semanticId,
|
|
165
|
+
});
|
|
166
|
+
rejected++;
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
try {
|
|
170
|
+
const updated = await this.applyNodeUpdate(existing, op, isSESchema);
|
|
171
|
+
await this.storage.saveNodes([updated]);
|
|
172
|
+
applied++;
|
|
173
|
+
}
|
|
174
|
+
catch (e) {
|
|
175
|
+
violations.push({
|
|
176
|
+
ruleId: 'UPDATE_FAILED',
|
|
177
|
+
ruleName: 'update-failed',
|
|
178
|
+
severity: 'error',
|
|
179
|
+
message: e.message,
|
|
180
|
+
elementId: op.semanticId,
|
|
181
|
+
});
|
|
182
|
+
rejected++;
|
|
183
|
+
}
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
case 'remove_node': {
|
|
187
|
+
await this.storage.deleteNodes([op.semanticId]);
|
|
188
|
+
applied++;
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
case 'add_edge': {
|
|
192
|
+
if (!op.sourceId || !op.targetId || !op.edgeType) {
|
|
193
|
+
violations.push({
|
|
194
|
+
ruleId: 'EDGE_INVALID',
|
|
195
|
+
ruleName: 'edge-invalid',
|
|
196
|
+
severity: 'error',
|
|
197
|
+
message: 'Edge missing source/target/type',
|
|
198
|
+
});
|
|
199
|
+
rejected++;
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
try {
|
|
203
|
+
await this.validateAndApplyEdge({
|
|
204
|
+
sourceId: op.sourceId,
|
|
205
|
+
targetId: op.targetId,
|
|
206
|
+
edgeType: op.edgeType,
|
|
207
|
+
});
|
|
208
|
+
const edge = {
|
|
209
|
+
sourceId: op.sourceId,
|
|
210
|
+
targetId: op.targetId,
|
|
211
|
+
edgeType: op.edgeType,
|
|
212
|
+
attributes: op.attributes ? { ...op.attributes } : {},
|
|
213
|
+
};
|
|
214
|
+
await this.storage.saveEdges([edge]);
|
|
215
|
+
applied++;
|
|
216
|
+
}
|
|
217
|
+
catch (e) {
|
|
218
|
+
violations.push({
|
|
219
|
+
ruleId: 'EDGE_INVALID',
|
|
220
|
+
ruleName: 'edge-invalid',
|
|
221
|
+
severity: 'error',
|
|
222
|
+
message: e.message,
|
|
223
|
+
});
|
|
224
|
+
rejected++;
|
|
225
|
+
}
|
|
226
|
+
break;
|
|
227
|
+
}
|
|
228
|
+
case 'strict_add_edge': {
|
|
229
|
+
if (!op.sourceId || !op.targetId || !op.edgeType) {
|
|
230
|
+
violations.push({
|
|
231
|
+
ruleId: 'EDGE_INVALID',
|
|
232
|
+
ruleName: 'edge-invalid',
|
|
233
|
+
severity: 'error',
|
|
234
|
+
message: 'Edge missing source/target/type',
|
|
235
|
+
});
|
|
236
|
+
rejected++;
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
try {
|
|
240
|
+
// Check if edge already exists
|
|
241
|
+
const existing = await this.storage.loadGraph(this.scope);
|
|
242
|
+
const edgeExists = existing.edges.some(e => e.sourceId === op.sourceId && e.targetId === op.targetId && e.edgeType === op.edgeType);
|
|
243
|
+
if (edgeExists) {
|
|
244
|
+
violations.push({
|
|
245
|
+
ruleId: 'STRICT_EDGE_EXISTS',
|
|
246
|
+
ruleName: 'strict-edge-exists',
|
|
247
|
+
severity: 'error',
|
|
248
|
+
message: `Edge already exists: ${op.sourceId} -${op.edgeType}-> ${op.targetId}`,
|
|
249
|
+
});
|
|
250
|
+
rejected++;
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
await this.validateAndApplyEdge({
|
|
254
|
+
sourceId: op.sourceId,
|
|
255
|
+
targetId: op.targetId,
|
|
256
|
+
edgeType: op.edgeType,
|
|
257
|
+
});
|
|
258
|
+
const edge = {
|
|
259
|
+
sourceId: op.sourceId,
|
|
260
|
+
targetId: op.targetId,
|
|
261
|
+
edgeType: op.edgeType,
|
|
262
|
+
attributes: op.attributes ? { ...op.attributes } : {},
|
|
263
|
+
};
|
|
264
|
+
await this.storage.saveEdges([edge]);
|
|
265
|
+
applied++;
|
|
266
|
+
}
|
|
267
|
+
catch (e) {
|
|
268
|
+
violations.push({
|
|
269
|
+
ruleId: 'EDGE_INVALID',
|
|
270
|
+
ruleName: 'edge-invalid',
|
|
271
|
+
severity: 'error',
|
|
272
|
+
message: e.message,
|
|
273
|
+
});
|
|
274
|
+
rejected++;
|
|
275
|
+
}
|
|
276
|
+
break;
|
|
277
|
+
}
|
|
278
|
+
case 'remove_edge': {
|
|
279
|
+
if (!op.sourceId || !op.targetId || !op.edgeType) {
|
|
280
|
+
rejected++;
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
await this.storage.deleteEdges([{
|
|
284
|
+
sourceId: op.sourceId,
|
|
285
|
+
targetId: op.targetId,
|
|
286
|
+
edgeType: op.edgeType,
|
|
287
|
+
}]);
|
|
288
|
+
applied++;
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
291
|
+
case 'update_edge': {
|
|
292
|
+
if (!op.sourceId || !op.targetId || !op.edgeType || !op.set) {
|
|
293
|
+
violations.push({
|
|
294
|
+
ruleId: 'EDGE_INVALID',
|
|
295
|
+
ruleName: 'edge-invalid',
|
|
296
|
+
severity: 'error',
|
|
297
|
+
message: 'update_edge requires sourceId/targetId/edgeType identity and a set{} patch',
|
|
298
|
+
});
|
|
299
|
+
rejected++;
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
try {
|
|
303
|
+
const graph = await this.storage.loadGraph(this.scope);
|
|
304
|
+
const { removed, added } = updateEdge(graph, { sourceId: op.sourceId, targetId: op.targetId, edgeType: op.edgeType }, op.set);
|
|
305
|
+
await this.validateAndApplyEdge({
|
|
306
|
+
sourceId: added.sourceId, targetId: added.targetId, edgeType: added.edgeType,
|
|
307
|
+
});
|
|
308
|
+
await this.storage.deleteEdges([{
|
|
309
|
+
sourceId: removed.sourceId, targetId: removed.targetId, edgeType: removed.edgeType,
|
|
310
|
+
}]);
|
|
311
|
+
await this.storage.saveEdges([added]);
|
|
312
|
+
applied++;
|
|
313
|
+
}
|
|
314
|
+
catch (e) {
|
|
315
|
+
violations.push({
|
|
316
|
+
ruleId: 'EDGE_INVALID',
|
|
317
|
+
ruleName: 'edge-invalid',
|
|
318
|
+
severity: 'error',
|
|
319
|
+
message: e.message,
|
|
320
|
+
elementId: op.semanticId,
|
|
321
|
+
});
|
|
322
|
+
rejected++;
|
|
323
|
+
}
|
|
324
|
+
break;
|
|
325
|
+
}
|
|
326
|
+
case 'merge_nodes': {
|
|
327
|
+
if (!op.sourceIds || op.sourceIds.length !== 2) {
|
|
328
|
+
violations.push({
|
|
329
|
+
ruleId: 'MERGE_UNSUPPORTED',
|
|
330
|
+
ruleName: 'merge-unsupported',
|
|
331
|
+
severity: 'error',
|
|
332
|
+
message: `merge_nodes requires exactly 2 sourceIds (source absorbed into target); got ${op.sourceIds?.length ?? 0}`,
|
|
333
|
+
elementId: op.semanticId,
|
|
334
|
+
});
|
|
335
|
+
rejected++;
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
const [sourceUid, targetUid] = op.sourceIds;
|
|
339
|
+
try {
|
|
340
|
+
const graph = await this.storage.loadGraph(this.scope);
|
|
341
|
+
const { removedNode, removedEdges, addedEdges } = mergeNodes(graph, sourceUid, targetUid);
|
|
342
|
+
for (const edge of addedEdges) {
|
|
343
|
+
await this.validateAndApplyEdge({
|
|
344
|
+
sourceId: edge.sourceId, targetId: edge.targetId, edgeType: edge.edgeType,
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
if (removedEdges.length > 0) {
|
|
348
|
+
await this.storage.deleteEdges(removedEdges.map(e => ({
|
|
349
|
+
sourceId: e.sourceId, targetId: e.targetId, edgeType: e.edgeType,
|
|
350
|
+
})));
|
|
351
|
+
}
|
|
352
|
+
if (addedEdges.length > 0) {
|
|
353
|
+
await this.storage.saveEdges(addedEdges);
|
|
354
|
+
}
|
|
355
|
+
await this.storage.deleteNodes([removedNode]);
|
|
356
|
+
applied++;
|
|
357
|
+
}
|
|
358
|
+
catch (e) {
|
|
359
|
+
violations.push({
|
|
360
|
+
ruleId: 'MERGE_INVALID',
|
|
361
|
+
ruleName: 'merge-invalid',
|
|
362
|
+
severity: 'error',
|
|
363
|
+
message: e.message,
|
|
364
|
+
elementId: op.semanticId,
|
|
365
|
+
});
|
|
366
|
+
rejected++;
|
|
367
|
+
}
|
|
368
|
+
break;
|
|
369
|
+
}
|
|
370
|
+
default:
|
|
371
|
+
rejected++;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
// CR-195d: Create produces edges for newly created nodes (audit trail)
|
|
375
|
+
if (edgesToCreate.length > 0) {
|
|
376
|
+
try {
|
|
377
|
+
await this.storage.saveEdges(edgesToCreate.map(e => ({
|
|
378
|
+
...e,
|
|
379
|
+
attributes: {},
|
|
380
|
+
})));
|
|
381
|
+
}
|
|
382
|
+
catch (e) {
|
|
383
|
+
console.warn('[GraphService] Failed to create audit produces edges:', e);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
// Add parse errors as violations
|
|
387
|
+
for (const err of diff.errors) {
|
|
388
|
+
violations.push({
|
|
389
|
+
ruleId: 'PARSE_ERROR',
|
|
390
|
+
ruleName: 'parse-error',
|
|
391
|
+
severity: 'error',
|
|
392
|
+
message: err,
|
|
393
|
+
});
|
|
394
|
+
rejected++;
|
|
395
|
+
}
|
|
396
|
+
this.version++;
|
|
397
|
+
const auditId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
398
|
+
const result = rejected === 0 ? 'applied' : applied === 0 ? 'rejected' : 'partial';
|
|
399
|
+
await this.audit.record({
|
|
400
|
+
id: auditId,
|
|
401
|
+
timestamp: new Date().toISOString(),
|
|
402
|
+
consumerId,
|
|
403
|
+
consumerType: 'automation',
|
|
404
|
+
operation: 'mutate',
|
|
405
|
+
diff,
|
|
406
|
+
result,
|
|
407
|
+
violations: violations.length > 0 ? violations : undefined,
|
|
408
|
+
graphVersion: this.version,
|
|
409
|
+
});
|
|
410
|
+
return { applied, rejected, violations, version: this.version, auditId };
|
|
411
|
+
}
|
|
412
|
+
async applyBatch(diffs, consumerId = 'system') {
|
|
413
|
+
const results = [];
|
|
414
|
+
let totalApplied = 0;
|
|
415
|
+
let totalRejected = 0;
|
|
416
|
+
for (const diff of diffs) {
|
|
417
|
+
const result = await this.mutate(diff, consumerId);
|
|
418
|
+
results.push(result);
|
|
419
|
+
totalApplied += result.applied;
|
|
420
|
+
totalRejected += result.rejected;
|
|
421
|
+
}
|
|
422
|
+
return { results, totalApplied, totalRejected };
|
|
423
|
+
}
|
|
424
|
+
// ---------------------------------------------------------------------------
|
|
425
|
+
// Validation
|
|
426
|
+
// ---------------------------------------------------------------------------
|
|
427
|
+
async validate() {
|
|
428
|
+
const graph = await this.storage.loadGraph(this.scope);
|
|
429
|
+
return this.rules.evaluate(graph);
|
|
430
|
+
}
|
|
431
|
+
// ---------------------------------------------------------------------------
|
|
432
|
+
// Export
|
|
433
|
+
// ---------------------------------------------------------------------------
|
|
434
|
+
async export(format = 'json') {
|
|
435
|
+
const graph = await this.storage.loadGraph(this.scope);
|
|
436
|
+
if (format === 'format-e') {
|
|
437
|
+
return this.codec.serialize(graph);
|
|
438
|
+
}
|
|
439
|
+
return JSON.stringify(graph, null, 2);
|
|
440
|
+
}
|
|
441
|
+
// ---------------------------------------------------------------------------
|
|
442
|
+
// Health
|
|
443
|
+
// ---------------------------------------------------------------------------
|
|
444
|
+
async health() {
|
|
445
|
+
const healthy = await this.storage.isHealthy();
|
|
446
|
+
const stats = await this.storage.stats();
|
|
447
|
+
const violations = await this.validate();
|
|
448
|
+
return {
|
|
449
|
+
status: healthy ? 'ok' : 'down',
|
|
450
|
+
storage: this.storage.name,
|
|
451
|
+
ontologyName: this.ontology.name,
|
|
452
|
+
ontologyVersion: this.ontology.version,
|
|
453
|
+
elementCount: stats.nodeCount,
|
|
454
|
+
edgeCount: stats.edgeCount,
|
|
455
|
+
violationCount: violations.filter(v => v.severity === 'error').length,
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
// ---------------------------------------------------------------------------
|
|
459
|
+
// Helpers
|
|
460
|
+
// ---------------------------------------------------------------------------
|
|
461
|
+
async applyNodeCreate(op, isSESchema) {
|
|
462
|
+
let type;
|
|
463
|
+
let name;
|
|
464
|
+
if (isSESchema) {
|
|
465
|
+
try {
|
|
466
|
+
const { type: extractedType, name: extractedName } = extractFromSemanticId(op.semanticId);
|
|
467
|
+
type = extractedType;
|
|
468
|
+
name = extractedName;
|
|
469
|
+
}
|
|
470
|
+
catch {
|
|
471
|
+
throw new Error(`Invalid semantic ID: ${op.semanticId}`);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
else {
|
|
475
|
+
const parts = op.semanticId.split('.');
|
|
476
|
+
if (parts.length >= 3)
|
|
477
|
+
type = parts[parts.length - 2];
|
|
478
|
+
else if (parts.length === 2)
|
|
479
|
+
type = parts[1];
|
|
480
|
+
else
|
|
481
|
+
type = 'UNKNOWN';
|
|
482
|
+
name = parts[0];
|
|
483
|
+
}
|
|
484
|
+
const attributes = op.attributes ? { ...op.attributes } : {};
|
|
485
|
+
// CR-195d: Hoist kinds/asil/method from attributes to OntologyElement (via se_blob)
|
|
486
|
+
if (isSESchema) {
|
|
487
|
+
const element = {
|
|
488
|
+
id: op.semanticId,
|
|
489
|
+
type: type,
|
|
490
|
+
name,
|
|
491
|
+
description: op.description ?? name,
|
|
492
|
+
status: 'draft',
|
|
493
|
+
created_at: new Date().toISOString(),
|
|
494
|
+
attributes: { ...attributes },
|
|
495
|
+
};
|
|
496
|
+
// Hoist top-level fields
|
|
497
|
+
if (element.attributes) {
|
|
498
|
+
if (element.attributes.kinds != null) {
|
|
499
|
+
const raw = String(element.attributes.kinds);
|
|
500
|
+
element.kinds = raw.includes(',')
|
|
501
|
+
? raw.split(',').map((s) => s.trim())
|
|
502
|
+
: [raw.trim()];
|
|
503
|
+
delete element.attributes.kinds;
|
|
504
|
+
}
|
|
505
|
+
if (element.attributes.asil != null) {
|
|
506
|
+
element.asil = String(element.attributes.asil);
|
|
507
|
+
delete element.attributes.asil;
|
|
508
|
+
}
|
|
509
|
+
if (element.attributes.method != null) {
|
|
510
|
+
element.method = String(element.attributes.method);
|
|
511
|
+
delete element.attributes.method;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
return {
|
|
515
|
+
uid: op.semanticId,
|
|
516
|
+
type,
|
|
517
|
+
name,
|
|
518
|
+
description: op.description ?? name,
|
|
519
|
+
attributes: { se_blob: JSON.stringify(element) },
|
|
520
|
+
createdAt: new Date().toISOString(),
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
return {
|
|
524
|
+
uid: op.semanticId,
|
|
525
|
+
type,
|
|
526
|
+
name,
|
|
527
|
+
description: op.description,
|
|
528
|
+
attributes,
|
|
529
|
+
createdAt: new Date().toISOString(),
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
async applyNodeUpdate(existing, op, isSESchema) {
|
|
533
|
+
if (isSESchema && existing.attributes.se_blob) {
|
|
534
|
+
try {
|
|
535
|
+
const element = JSON.parse(String(existing.attributes.se_blob));
|
|
536
|
+
if (op.description)
|
|
537
|
+
element.description = op.description;
|
|
538
|
+
if (op.attributes && Object.keys(op.attributes).length > 0) {
|
|
539
|
+
element.attributes = { ...element.attributes, ...op.attributes };
|
|
540
|
+
}
|
|
541
|
+
// Hoist top-level fields
|
|
542
|
+
if (element.attributes) {
|
|
543
|
+
if (element.attributes.kinds != null) {
|
|
544
|
+
const raw = String(element.attributes.kinds);
|
|
545
|
+
element.kinds = raw.includes(',')
|
|
546
|
+
? raw.split(',').map((s) => s.trim())
|
|
547
|
+
: [raw.trim()];
|
|
548
|
+
delete element.attributes.kinds;
|
|
549
|
+
}
|
|
550
|
+
if (element.attributes.asil != null) {
|
|
551
|
+
element.asil = String(element.attributes.asil);
|
|
552
|
+
delete element.attributes.asil;
|
|
553
|
+
}
|
|
554
|
+
if (element.attributes.method != null) {
|
|
555
|
+
element.method = String(element.attributes.method);
|
|
556
|
+
delete element.attributes.method;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
element.updated_at = new Date().toISOString();
|
|
560
|
+
return {
|
|
561
|
+
...existing,
|
|
562
|
+
description: op.description ?? existing.description,
|
|
563
|
+
attributes: { se_blob: JSON.stringify(element) },
|
|
564
|
+
updatedAt: new Date().toISOString(),
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
catch (e) {
|
|
568
|
+
throw new Error(`Failed to parse se_blob: ${e.message}`);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
return {
|
|
572
|
+
...existing,
|
|
573
|
+
description: op.description ?? existing.description,
|
|
574
|
+
attributes: { ...existing.attributes, ...op.attributes },
|
|
575
|
+
updatedAt: new Date().toISOString(),
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
// CR-GC-247: one legality path for every ontology (SE + foreign). The edge type
|
|
579
|
+
// must be declared (menu enumeration via edgeTypes), then the (source,target,type)
|
|
580
|
+
// trace is checked against descriptor.patterns via the single isValidTrace —
|
|
581
|
+
// no SE/foreign fork, no validPairs re-implementation.
|
|
582
|
+
async validateAndApplyEdge(op) {
|
|
583
|
+
if (!this.ontology.edgeTypes[op.edgeType]) {
|
|
584
|
+
const known = Object.keys(this.ontology.edgeTypes).join(', ');
|
|
585
|
+
throw new Error(`Unknown edge type '${op.edgeType}' in ontology '${this.ontology.name}' (known: ${known || 'none'})`);
|
|
586
|
+
}
|
|
587
|
+
const src = await this.storage.getNode(op.sourceId);
|
|
588
|
+
const tgt = await this.storage.getNode(op.targetId);
|
|
589
|
+
if (src && tgt && !isValidTrace({ source: src.type, target: tgt.type, type: op.edgeType }, this.patterns)) {
|
|
590
|
+
const allowed = this.patterns
|
|
591
|
+
.filter(p => p.type === op.edgeType)
|
|
592
|
+
.map(p => `${p.source}->${p.target}`)
|
|
593
|
+
.join(', ');
|
|
594
|
+
throw new Error(`Invalid ontology trace: ${src.type} -${op.edgeType}-> ${tgt.type} (allowed: ${allowed || 'none'})`);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @sigloch/graph-api-core
|
|
3
|
+
* Framework-agnostic graph operations library with pluggable adapters.
|
|
4
|
+
* One contract (Zod), one operations layer (GraphService), N adapters.
|
|
5
|
+
*
|
|
6
|
+
* @author andreas@siglochconsulting
|
|
7
|
+
*/
|
|
8
|
+
export type { GraphNode, GraphEdge, Graph } from './types.js';
|
|
9
|
+
export type { OntologyDescriptor, NodeTypeDescriptor, EdgeTypeDescriptor, TracePattern } from './types.js';
|
|
10
|
+
export { isValidTrace, tracePatternsOf } from './types.js';
|
|
11
|
+
export type { GraphScope, GraphQuery, MutationResult, BatchResult, HealthStatus } from './types.js';
|
|
12
|
+
export type { FormatEOperation, FormatEDiff } from './types.js';
|
|
13
|
+
export { GraphNodeSchema, GraphEdgeSchema, GraphSchema, GraphScopeSchema, GraphQuerySchema, MutationResultSchema, HealthStatusSchema, } from './schemas.js';
|
|
14
|
+
export { GraphService } from './graph-service.js';
|
|
15
|
+
export { FormatECodec } from './format-e-codec.js';
|
|
16
|
+
export type { Rule, RuleViolation, RuleEngine } from './rule-engine.js';
|
|
17
|
+
export { DefaultRuleEngine } from './rule-engine.js';
|
|
18
|
+
export type { AuditEntry, AuditLog, OperationsLog } from './audit.js';
|
|
19
|
+
export { InMemoryAuditLog, FileOperationsLog, AUDIT_FILE, AUDIT_BASENAME, DEFAULT_COMPACT_BYTES } from './audit.js';
|
|
20
|
+
export type { StorageAdapter } from './storage-adapter.js';
|
|
21
|
+
export { MemoryAdapter } from './memory-adapter.js';
|
|
22
|
+
export type { TransportAdapter, TransportConfig } from './transport-adapter.js';
|
|
23
|
+
export { createGraphApi } from './factory.js';
|
|
24
|
+
export type { GraphApiConfig } from './factory.js';
|
|
25
|
+
export { SE_DESCRIPTOR, projectToOntologyGraph } from './se-descriptor.js';
|
|
26
|
+
export { findRoot } from './find-root.js';
|
|
27
|
+
export type { RootQueryGraph } from './find-root.js';
|
|
28
|
+
export { applyEdgeOps, updateEdge, mergeNodes } from './edge-ops.js';
|
|
29
|
+
export type { EdgeIdentity, UpdateEdgeSet, UpdateEdgeResult, MergeNodesResult, EdgeOp, } from './edge-ops.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @sigloch/graph-api-core
|
|
3
|
+
* Framework-agnostic graph operations library with pluggable adapters.
|
|
4
|
+
* One contract (Zod), one operations layer (GraphService), N adapters.
|
|
5
|
+
*
|
|
6
|
+
* @author andreas@siglochconsulting
|
|
7
|
+
*/
|
|
8
|
+
// Trace legality — the ONE checker (CR-GC-247). validPairs is menu-only now.
|
|
9
|
+
export { isValidTrace, tracePatternsOf } from './types.js';
|
|
10
|
+
// Schemas (Zod)
|
|
11
|
+
export { GraphNodeSchema, GraphEdgeSchema, GraphSchema, GraphScopeSchema, GraphQuerySchema, MutationResultSchema, HealthStatusSchema, } from './schemas.js';
|
|
12
|
+
// Service
|
|
13
|
+
export { GraphService } from './graph-service.js';
|
|
14
|
+
// Format E Codec
|
|
15
|
+
export { FormatECodec } from './format-e-codec.js';
|
|
16
|
+
export { DefaultRuleEngine } from './rule-engine.js';
|
|
17
|
+
export { InMemoryAuditLog, FileOperationsLog, AUDIT_FILE, AUDIT_BASENAME, DEFAULT_COMPACT_BYTES } from './audit.js';
|
|
18
|
+
export { MemoryAdapter } from './memory-adapter.js';
|
|
19
|
+
// Factory
|
|
20
|
+
export { createGraphApi } from './factory.js';
|
|
21
|
+
// SE OntologyDescriptor (derived from @sigloch/contracts/se) [CR-195a]
|
|
22
|
+
export { SE_DESCRIPTOR, projectToOntologyGraph } from './se-descriptor.js';
|
|
23
|
+
// Root-Suche — strukturelle Wurzel (SYS ohne eingehende compose), statt UID-Hardcode
|
|
24
|
+
export { findRoot } from './find-root.js';
|
|
25
|
+
// Edge ops — update-edge (flip/retype) + merge-nodes, shared by GraphService.mutate()
|
|
26
|
+
// and the graphcode Apply-Gate (CR-198)
|
|
27
|
+
export { applyEdgeOps, updateEdge, mergeNodes } from './edge-ops.js';
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory StorageAdapter — for tests, CLI, short-lived sessions.
|
|
3
|
+
*/
|
|
4
|
+
import type { StorageAdapter } from './storage-adapter.js';
|
|
5
|
+
import type { GraphNode, GraphEdge, Graph, GraphScope } from './types.js';
|
|
6
|
+
export declare class MemoryAdapter implements StorageAdapter {
|
|
7
|
+
readonly name = "memory";
|
|
8
|
+
private nodes;
|
|
9
|
+
private edges;
|
|
10
|
+
initialize(): Promise<void>;
|
|
11
|
+
shutdown(): Promise<void>;
|
|
12
|
+
loadGraph(_scope: GraphScope): Promise<Graph>;
|
|
13
|
+
saveNodes(nodes: GraphNode[]): Promise<void>;
|
|
14
|
+
deleteNodes(uids: string[]): Promise<void>;
|
|
15
|
+
saveEdges(edges: GraphEdge[]): Promise<void>;
|
|
16
|
+
deleteEdges(keys: Array<{
|
|
17
|
+
sourceId: string;
|
|
18
|
+
targetId: string;
|
|
19
|
+
edgeType: string;
|
|
20
|
+
}>): Promise<void>;
|
|
21
|
+
getNode(uid: string): Promise<GraphNode | null>;
|
|
22
|
+
getSubgraph(root: string, depth: number): Promise<Graph>;
|
|
23
|
+
stats(): Promise<{
|
|
24
|
+
nodeCount: number;
|
|
25
|
+
edgeCount: number;
|
|
26
|
+
}>;
|
|
27
|
+
isHealthy(): Promise<boolean>;
|
|
28
|
+
}
|