@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,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
|
+
}
|