@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,296 @@
1
+ /**
2
+ * FormatECodec — ontology-agnostic Format E parser/serializer.
3
+ * Domain-specific node types, edge types, and arrows come from OntologyDescriptor.
4
+ */
5
+ import { isValidTrace, tracePatternsOf } from './types.js';
6
+ // ---------------------------------------------------------------------------
7
+ // Regex patterns
8
+ // ---------------------------------------------------------------------------
9
+ const OP_PREFIX = {
10
+ '+': 'add',
11
+ '-': 'remove',
12
+ '~': 'update',
13
+ '!': 'strict_add',
14
+ };
15
+ const ATTR_RE = /^\s*@(\w+)\s+(.+)$/;
16
+ const MERGE_RE = /^M\s+(.+)$/;
17
+ const FORMAT_E_FENCE = /```format-e\s*\n([\s\S]*?)```/;
18
+ /** Inline attribute block: [key:value,key:value] */
19
+ const INLINE_ATTRS_RE = /\[([^\]]+)\]$/;
20
+ export class FormatECodec {
21
+ ontology;
22
+ edgeArrowToType;
23
+ validNodeTypes;
24
+ /** Trace-legality patterns (CR-GC-247) — the SSOT the parser validates edges against. */
25
+ patterns;
26
+ constructor(ontology) {
27
+ this.ontology = ontology;
28
+ // Build arrow → edge type lookup
29
+ this.edgeArrowToType = new Map();
30
+ for (const [edgeType, desc] of Object.entries(ontology.edgeTypes)) {
31
+ for (const arrow of desc.arrows) {
32
+ this.edgeArrowToType.set(arrow.toLowerCase(), edgeType);
33
+ }
34
+ }
35
+ // Valid node type abbreviations
36
+ this.validNodeTypes = new Set(Object.keys(ontology.nodeTypes));
37
+ // Meta-model legality: descriptor.patterns (or derived from validPairs).
38
+ this.patterns = tracePatternsOf(ontology);
39
+ }
40
+ /** Extract a ```format-e block from LLM output. Returns null if not found. */
41
+ extractFromLlm(llmOutput) {
42
+ const m = FORMAT_E_FENCE.exec(llmOutput);
43
+ return m ? m[1].trim() : null;
44
+ }
45
+ /** Parse Format E text into validated operations. */
46
+ parse(input) {
47
+ const operations = [];
48
+ const errors = [];
49
+ let section = null;
50
+ for (const rawLine of input.split('\n')) {
51
+ const line = rawLine.trim();
52
+ if (!line || line.startsWith('//') || line.startsWith('#!'))
53
+ continue;
54
+ // @attribute lines attach to last node operation
55
+ const attrMatch = ATTR_RE.exec(line);
56
+ if (attrMatch) {
57
+ const lastOp = operations.length > 0 ? operations[operations.length - 1] : null;
58
+ if (lastOp && (lastOp.type === 'add_node' || lastOp.type === 'update_node' || lastOp.type === 'strict_add_node')) {
59
+ if (!lastOp.attributes)
60
+ lastOp.attributes = {};
61
+ lastOp.attributes[attrMatch[1]] = attrMatch[2].trim();
62
+ }
63
+ else {
64
+ errors.push(`@attribute line without preceding node: "${line}"`);
65
+ }
66
+ continue;
67
+ }
68
+ // Section headers
69
+ if (/^##\s*nodes?\s*$/i.test(line)) {
70
+ section = 'nodes';
71
+ continue;
72
+ }
73
+ if (/^##\s*edges?\s*$/i.test(line)) {
74
+ section = 'edges';
75
+ continue;
76
+ }
77
+ if (/^##\s*merges?\s*$/i.test(line)) {
78
+ section = 'merges';
79
+ continue;
80
+ }
81
+ if (line.startsWith('#'))
82
+ continue;
83
+ // Merge operations
84
+ const mergeMatch = MERGE_RE.exec(line);
85
+ if (mergeMatch || section === 'merges') {
86
+ if (mergeMatch) {
87
+ this.parseMerge(mergeMatch[1], operations, errors);
88
+ }
89
+ else {
90
+ errors.push(`Invalid merge line: "${line}"`);
91
+ }
92
+ continue;
93
+ }
94
+ // Edge: detect by arrow pattern -xxx->, but only in the structural part
95
+ // (before the description pipe). Node descriptions may legitimately contain
96
+ // '->' (e.g. "FUNC->FUNC compose"); the arrow lives before any '|', edges
97
+ // carry no pipe — so splitting on '|' cleanly disambiguates (CR-GC-247).
98
+ if (line.split('|', 1)[0].includes('->')) {
99
+ this.parseEdgeLine(line, operations, errors);
100
+ continue;
101
+ }
102
+ // Node
103
+ if (section === 'nodes' || this.looksLikeNode(line)) {
104
+ this.parseNodeLine(line, operations, errors);
105
+ continue;
106
+ }
107
+ if (line.length > 0)
108
+ errors.push(`Unrecognized line: "${line}"`);
109
+ }
110
+ return { operations, errors };
111
+ }
112
+ /** Serialize a Graph to Format E text. */
113
+ serialize(graph) {
114
+ const lines = [];
115
+ if (graph.nodes.length > 0) {
116
+ lines.push('## Nodes');
117
+ for (const node of graph.nodes) {
118
+ const descr = node.description ? `|${node.description}` : '';
119
+ const attrs = this.serializeAttrs(node.attributes);
120
+ lines.push(`+ ${node.uid}${descr}${attrs}`);
121
+ }
122
+ }
123
+ if (graph.edges.length > 0) {
124
+ lines.push('');
125
+ lines.push('## Edges');
126
+ for (const edge of graph.edges) {
127
+ const arrow = this.edgeTypeToArrow(edge.edgeType);
128
+ const attrs = this.serializeAttrs(edge.attributes);
129
+ lines.push(`+ ${edge.sourceId} -${arrow}-> ${edge.targetId}${attrs}`);
130
+ }
131
+ }
132
+ return lines.join('\n');
133
+ }
134
+ // ---------------------------------------------------------------------------
135
+ // Private
136
+ // ---------------------------------------------------------------------------
137
+ looksLikeNode(line) {
138
+ const stripped = line.replace(/^[+\-~!]\s*/, '');
139
+ const dotParts = stripped.split('.');
140
+ return dotParts.length >= 2 && this.validNodeTypes.has(dotParts[dotParts.length - 2] ?? '');
141
+ }
142
+ parseNodeLine(line, ops, errors) {
143
+ const opChar = OP_PREFIX[line[0]] ? line[0] : '+';
144
+ const rest = OP_PREFIX[line[0]] ? line.slice(1).trim() : line.trim();
145
+ const action = OP_PREFIX[opChar] ?? 'add';
146
+ // Extract inline attributes
147
+ let mainPart = rest;
148
+ let inlineAttrs;
149
+ const attrMatch = INLINE_ATTRS_RE.exec(rest);
150
+ if (attrMatch) {
151
+ mainPart = rest.slice(0, attrMatch.index).trim();
152
+ inlineAttrs = this.parseInlineAttrs(attrMatch[1]);
153
+ }
154
+ // Split uid|description
155
+ const pipeIdx = mainPart.indexOf('|');
156
+ const uid = pipeIdx >= 0 ? mainPart.slice(0, pipeIdx).trim() : mainPart.trim();
157
+ const description = pipeIdx >= 0 ? mainPart.slice(pipeIdx + 1).trim() : undefined;
158
+ // Validate node type from uid
159
+ const nodeType = this.extractNodeType(uid);
160
+ if (!nodeType) {
161
+ errors.push(`Cannot extract node type from uid: "${uid}"`);
162
+ return;
163
+ }
164
+ if (!this.validNodeTypes.has(nodeType)) {
165
+ errors.push(`Unknown node type "${nodeType}" in: "${uid}"`);
166
+ return;
167
+ }
168
+ if (action === 'remove') {
169
+ ops.push({ type: 'remove_node', semanticId: uid });
170
+ }
171
+ else if (action === 'update') {
172
+ ops.push({ type: 'update_node', semanticId: uid, description, attributes: inlineAttrs });
173
+ }
174
+ else if (action === 'strict_add') {
175
+ ops.push({ type: 'strict_add_node', semanticId: uid, description, attributes: inlineAttrs });
176
+ }
177
+ else {
178
+ ops.push({ type: 'add_node', semanticId: uid, description, attributes: inlineAttrs });
179
+ }
180
+ }
181
+ parseEdgeLine(line, ops, errors) {
182
+ const opChar = OP_PREFIX[line[0]] ? line[0] : '+';
183
+ const rest = OP_PREFIX[line[0]] ? line.slice(1).trim() : line.trim();
184
+ const action = OP_PREFIX[opChar] ?? 'add';
185
+ // Extract inline attributes from the line
186
+ let mainPart = rest;
187
+ let inlineAttrs;
188
+ const attrMatch = INLINE_ATTRS_RE.exec(rest);
189
+ if (attrMatch) {
190
+ mainPart = rest.slice(0, attrMatch.index).trim();
191
+ inlineAttrs = this.parseInlineAttrs(attrMatch[1]);
192
+ }
193
+ // Parse: sourceId -arrowType-> targetId[, targetId2, ...]
194
+ const arrowMatch = /^(\S+)\s+-(\w+)->\s+(.+)$/.exec(mainPart);
195
+ if (!arrowMatch) {
196
+ errors.push(`Invalid edge syntax: "${line}"`);
197
+ return;
198
+ }
199
+ const sourceId = arrowMatch[1];
200
+ const arrowType = arrowMatch[2].toLowerCase();
201
+ const targetsPart = arrowMatch[3];
202
+ // Resolve arrow → edge type
203
+ const edgeType = this.edgeArrowToType.get(arrowType);
204
+ if (!edgeType) {
205
+ errors.push(`Unknown edge arrow type "${arrowType}" in: "${line}"`);
206
+ return;
207
+ }
208
+ // Validate source node type
209
+ const srcType = this.extractNodeType(sourceId);
210
+ if (!srcType) {
211
+ errors.push(`Cannot extract node type from source: "${sourceId}"`);
212
+ return;
213
+ }
214
+ // Handle 1:N targets
215
+ const targets = targetsPart.split(',').map(t => t.trim()).filter(Boolean);
216
+ for (const targetId of targets) {
217
+ // Validate target node type
218
+ const tgtType = this.extractNodeType(targetId);
219
+ if (!tgtType) {
220
+ errors.push(`Cannot extract node type from target: "${targetId}"`);
221
+ continue;
222
+ }
223
+ // Meta-model validation (CR-GC-247: single checker, patterns SSOT — honors
224
+ // '*' wildcards, unlike the old per-edgeType validPairs set-membership).
225
+ if (!isValidTrace({ source: srcType, target: tgtType, type: edgeType }, this.patterns)) {
226
+ errors.push(`Meta-model violation: ${srcType} -${edgeType}-> ${tgtType} is not valid`);
227
+ continue;
228
+ }
229
+ const opType = action === 'remove'
230
+ ? 'remove_edge'
231
+ : action === 'strict_add'
232
+ ? 'strict_add_edge'
233
+ : 'add_edge';
234
+ const op = {
235
+ type: opType,
236
+ semanticId: `${sourceId}->${targetId}`,
237
+ sourceId,
238
+ targetId,
239
+ edgeType,
240
+ };
241
+ if (inlineAttrs)
242
+ op.attributes = inlineAttrs;
243
+ ops.push(op);
244
+ }
245
+ }
246
+ parseMerge(content, ops, errors) {
247
+ const parts = content.split('+').map(p => p.trim()).filter(Boolean);
248
+ if (parts.length < 2) {
249
+ errors.push(`Merge requires at least 2 nodes: "${content}"`);
250
+ return;
251
+ }
252
+ ops.push({
253
+ type: 'merge_nodes',
254
+ semanticId: `merge:${parts.join('+')}`,
255
+ sourceIds: parts,
256
+ });
257
+ }
258
+ extractNodeType(uid) {
259
+ const parts = uid.split('.');
260
+ // Format: Name.TYPE or Name.TYPE.Counter
261
+ if (parts.length >= 2) {
262
+ // Type is the last segment that matches a known type, or second-to-last
263
+ for (let i = parts.length - 1; i >= 1; i--) {
264
+ if (this.validNodeTypes.has(parts[i]))
265
+ return parts[i];
266
+ }
267
+ // Fallback: second segment for 3-part IDs
268
+ if (parts.length >= 3)
269
+ return parts[parts.length - 2];
270
+ return parts[1];
271
+ }
272
+ return null;
273
+ }
274
+ parseInlineAttrs(raw) {
275
+ const attrs = {};
276
+ for (const pair of raw.split(',')) {
277
+ const colonIdx = pair.indexOf(':');
278
+ if (colonIdx > 0) {
279
+ attrs[pair.slice(0, colonIdx).trim()] = pair.slice(colonIdx + 1).trim();
280
+ }
281
+ }
282
+ return attrs;
283
+ }
284
+ serializeAttrs(attrs) {
285
+ const entries = Object.entries(attrs).filter(([, v]) => v != null && v !== '');
286
+ if (entries.length === 0)
287
+ return '';
288
+ const pairs = entries.map(([k, v]) => `${k}:${String(v)}`);
289
+ return ` [${pairs.join(',')}]`;
290
+ }
291
+ edgeTypeToArrow(edgeType) {
292
+ // Use first defined arrow alias
293
+ const desc = this.ontology.edgeTypes[edgeType];
294
+ return desc?.arrows[0] ?? edgeType.toLowerCase();
295
+ }
296
+ }
@@ -0,0 +1,37 @@
1
+ import type { OntologyDescriptor, GraphQuery, MutationResult, BatchResult, HealthStatus, FormatEDiff, Graph, GraphNode, GraphScope } from './types.js';
2
+ import type { StorageAdapter } from './storage-adapter.js';
3
+ import type { RuleEngine, RuleViolation } from './rule-engine.js';
4
+ import type { AuditLog } from './audit.js';
5
+ import { FormatECodec } from './format-e-codec.js';
6
+ export interface GraphServiceConfig {
7
+ ontology: OntologyDescriptor;
8
+ storage: StorageAdapter;
9
+ ruleEngine?: RuleEngine;
10
+ auditLog?: AuditLog;
11
+ scope?: GraphScope;
12
+ }
13
+ export declare class GraphService {
14
+ readonly ontology: OntologyDescriptor;
15
+ readonly codec: FormatECodec;
16
+ private readonly storage;
17
+ private readonly rules;
18
+ private readonly audit;
19
+ private readonly scope;
20
+ /** Trace-legality patterns (CR-GC-247) — the SSOT every edge validates against. */
21
+ private readonly patterns;
22
+ private version;
23
+ constructor(config: GraphServiceConfig);
24
+ initialize(): Promise<void>;
25
+ shutdown(): Promise<void>;
26
+ query(q: GraphQuery): Promise<Graph>;
27
+ getElement(uid: string): Promise<GraphNode | null>;
28
+ getSubgraph(root: string, depth?: number): Promise<Graph>;
29
+ mutate(diff: FormatEDiff, consumerId?: string): Promise<MutationResult>;
30
+ applyBatch(diffs: FormatEDiff[], consumerId?: string): Promise<BatchResult>;
31
+ validate(): Promise<RuleViolation[]>;
32
+ export(format?: 'format-e' | 'json'): Promise<string>;
33
+ health(): Promise<HealthStatus>;
34
+ private applyNodeCreate;
35
+ private applyNodeUpdate;
36
+ private validateAndApplyEdge;
37
+ }