@sigloch/graph-api-core 0.4.1 → 2.0.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/format-e-codec.d.ts +24 -4
- package/dist/format-e-codec.js +123 -52
- package/dist/graph-service.js +21 -10
- package/dist/se-descriptor.d.ts +2 -2
- package/dist/se-descriptor.js +13 -5
- package/dist/types.d.ts +15 -2
- package/package.json +2 -2
- package/test-fixtures/cr-007-features.format-e.md +12 -0
- package/test-fixtures/rasentraktor.format-e.md +15 -0
package/dist/format-e-codec.d.ts
CHANGED
|
@@ -8,16 +8,36 @@ export declare class FormatECodec {
|
|
|
8
8
|
constructor(ontology: OntologyDescriptor);
|
|
9
9
|
/** Extract a ```format-e block from LLM output. Returns null if not found. */
|
|
10
10
|
extractFromLlm(llmOutput: string): string | null;
|
|
11
|
-
/**
|
|
12
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Parse Format E text into validated operations.
|
|
13
|
+
*
|
|
14
|
+
* `options.resolveType` types uids this text does not declare — a mutation diff that
|
|
15
|
+
* only adds edges between existing nodes carries no `## Nodes` block. Callers bind it
|
|
16
|
+
* to their store. Without it, such a diff produces errors, never a silent skip.
|
|
17
|
+
*/
|
|
18
|
+
parse(input: string, options?: {
|
|
19
|
+
resolveType?: (uid: string) => string | undefined;
|
|
20
|
+
}): FormatEDiff;
|
|
13
21
|
/** Serialize a Graph to Format E text. */
|
|
14
22
|
serialize(graph: Graph): string;
|
|
15
|
-
private looksLikeNode;
|
|
16
23
|
private parseNodeLine;
|
|
17
24
|
private parseEdgeLine;
|
|
18
25
|
private parseMerge;
|
|
19
|
-
private extractNodeType;
|
|
20
26
|
private parseInlineAttrs;
|
|
27
|
+
/**
|
|
28
|
+
* CR-SM-215: fan-out serialization — edges sharing `(sourceId, edgeType)` collapse
|
|
29
|
+
* onto one line `A -x-> B, C, D`. The source UID is written once instead of once per
|
|
30
|
+
* edge; on the graphcode SSOT graph that is 318 lines instead of 751.
|
|
31
|
+
*
|
|
32
|
+
* Two invariants:
|
|
33
|
+
* - **Edges carrying attributes stay single-line.** `serializeAttrs` binds to one
|
|
34
|
+
* edge; a group would either drop `cardinality`/`constraint`/`notes` or wrongly
|
|
35
|
+
* share one edge's attributes with its siblings.
|
|
36
|
+
* - **Deterministic order** (graphcode `REQ-deterministic-serialization`): entries
|
|
37
|
+
* sorted by source, then edge type, then first target — code-unit order, not
|
|
38
|
+
* `localeCompare`, which is locale-dependent.
|
|
39
|
+
*/
|
|
40
|
+
private serializeEdges;
|
|
21
41
|
private serializeAttrs;
|
|
22
42
|
private edgeTypeToArrow;
|
|
23
43
|
}
|
package/dist/format-e-codec.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* FormatECodec — ontology-agnostic Format E parser/serializer.
|
|
3
3
|
* Domain-specific node types, edge types, and arrows come from OntologyDescriptor.
|
|
4
|
+
*
|
|
5
|
+
* CR-SM-216 (Format-E v2): a node's type comes from the `### <TYPE>` section it is
|
|
6
|
+
* declared under, never from the spelling of its uid. Id conventions differ across the
|
|
7
|
+
* family (`TYPE-slug`, `Name.TypeAbbr.Counter`, `cand_<hex>`); typing by spelling made
|
|
8
|
+
* every foreign convention fail silently instead of loudly.
|
|
4
9
|
*/
|
|
5
10
|
import { isValidTrace, tracePatternsOf } from './types.js';
|
|
6
11
|
// ---------------------------------------------------------------------------
|
|
@@ -42,11 +47,21 @@ export class FormatECodec {
|
|
|
42
47
|
const m = FORMAT_E_FENCE.exec(llmOutput);
|
|
43
48
|
return m ? m[1].trim() : null;
|
|
44
49
|
}
|
|
45
|
-
/**
|
|
46
|
-
|
|
50
|
+
/**
|
|
51
|
+
* Parse Format E text into validated operations.
|
|
52
|
+
*
|
|
53
|
+
* `options.resolveType` types uids this text does not declare — a mutation diff that
|
|
54
|
+
* only adds edges between existing nodes carries no `## Nodes` block. Callers bind it
|
|
55
|
+
* to their store. Without it, such a diff produces errors, never a silent skip.
|
|
56
|
+
*/
|
|
57
|
+
parse(input, options = {}) {
|
|
47
58
|
const operations = [];
|
|
48
59
|
const errors = [];
|
|
49
60
|
let section = null;
|
|
61
|
+
let currentType = null;
|
|
62
|
+
/** uid → type, declared by this text's `### <TYPE>` sections. */
|
|
63
|
+
const declared = new Map();
|
|
64
|
+
const typeOf = (uid) => declared.get(uid) ?? options.resolveType?.(uid);
|
|
50
65
|
for (const rawLine of input.split('\n')) {
|
|
51
66
|
const line = rawLine.trim();
|
|
52
67
|
if (!line || line.startsWith('//') || line.startsWith('#!'))
|
|
@@ -68,14 +83,29 @@ export class FormatECodec {
|
|
|
68
83
|
// Section headers
|
|
69
84
|
if (/^##\s*nodes?\s*$/i.test(line)) {
|
|
70
85
|
section = 'nodes';
|
|
86
|
+
currentType = null;
|
|
71
87
|
continue;
|
|
72
88
|
}
|
|
73
89
|
if (/^##\s*edges?\s*$/i.test(line)) {
|
|
74
90
|
section = 'edges';
|
|
91
|
+
currentType = null;
|
|
75
92
|
continue;
|
|
76
93
|
}
|
|
77
94
|
if (/^##\s*merges?\s*$/i.test(line)) {
|
|
78
95
|
section = 'merges';
|
|
96
|
+
currentType = null;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
// CR-SM-216: `### <TYPE>` carries the type for the nodes below it.
|
|
100
|
+
const typeSection = /^###\s+(\S+)\s*$/.exec(line);
|
|
101
|
+
if (typeSection && section === 'nodes') {
|
|
102
|
+
if (this.validNodeTypes.has(typeSection[1])) {
|
|
103
|
+
currentType = typeSection[1];
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
currentType = null;
|
|
107
|
+
errors.push(`Unknown node type section: "### ${typeSection[1]}"`);
|
|
108
|
+
}
|
|
79
109
|
continue;
|
|
80
110
|
}
|
|
81
111
|
if (line.startsWith('#'))
|
|
@@ -96,12 +126,17 @@ export class FormatECodec {
|
|
|
96
126
|
// '->' (e.g. "FUNC->FUNC compose"); the arrow lives before any '|', edges
|
|
97
127
|
// carry no pipe — so splitting on '|' cleanly disambiguates (CR-GC-247).
|
|
98
128
|
if (line.split('|', 1)[0].includes('->')) {
|
|
99
|
-
this.parseEdgeLine(line, operations, errors);
|
|
129
|
+
this.parseEdgeLine(line, typeOf, operations, errors);
|
|
100
130
|
continue;
|
|
101
131
|
}
|
|
102
|
-
// Node
|
|
103
|
-
if (section === 'nodes'
|
|
104
|
-
|
|
132
|
+
// Node — the `### <TYPE>` section supplies the type (CR-SM-216)
|
|
133
|
+
if (section === 'nodes') {
|
|
134
|
+
if (!currentType) {
|
|
135
|
+
errors.push(`Node line outside a "### <TYPE>" section: "${line}"`);
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
this.parseNodeLine(line, currentType, declared, operations, errors);
|
|
139
|
+
}
|
|
105
140
|
continue;
|
|
106
141
|
}
|
|
107
142
|
if (line.length > 0)
|
|
@@ -114,32 +149,37 @@ export class FormatECodec {
|
|
|
114
149
|
const lines = [];
|
|
115
150
|
if (graph.nodes.length > 0) {
|
|
116
151
|
lines.push('## Nodes');
|
|
152
|
+
// CR-SM-216: one `### <TYPE>` header per type instead of a type suffix on every
|
|
153
|
+
// uid. Measured on the graphcode SSOT graph: 12 headers ≈ 48 tokens, where a
|
|
154
|
+
// per-node type attribute would have cost ≈ 1476.
|
|
155
|
+
const byType = new Map();
|
|
117
156
|
for (const node of graph.nodes) {
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
157
|
+
const group = byType.get(node.type);
|
|
158
|
+
if (group)
|
|
159
|
+
group.push(node);
|
|
160
|
+
else
|
|
161
|
+
byType.set(node.type, [node]);
|
|
162
|
+
}
|
|
163
|
+
for (const type of [...byType.keys()].sort()) {
|
|
164
|
+
lines.push(`### ${type}`);
|
|
165
|
+
for (const node of byType.get(type) ?? []) {
|
|
166
|
+
const descr = node.description ? `|${node.description}` : '';
|
|
167
|
+
const attrs = this.serializeAttrs(node.attributes);
|
|
168
|
+
lines.push(`+ ${node.uid}${descr}${attrs}`);
|
|
169
|
+
}
|
|
121
170
|
}
|
|
122
171
|
}
|
|
123
172
|
if (graph.edges.length > 0) {
|
|
124
173
|
lines.push('');
|
|
125
174
|
lines.push('## Edges');
|
|
126
|
-
|
|
127
|
-
const arrow = this.edgeTypeToArrow(edge.edgeType);
|
|
128
|
-
const attrs = this.serializeAttrs(edge.attributes);
|
|
129
|
-
lines.push(`+ ${edge.sourceId} -${arrow}-> ${edge.targetId}${attrs}`);
|
|
130
|
-
}
|
|
175
|
+
lines.push(...this.serializeEdges(graph.edges));
|
|
131
176
|
}
|
|
132
177
|
return lines.join('\n');
|
|
133
178
|
}
|
|
134
179
|
// ---------------------------------------------------------------------------
|
|
135
180
|
// Private
|
|
136
181
|
// ---------------------------------------------------------------------------
|
|
137
|
-
|
|
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) {
|
|
182
|
+
parseNodeLine(line, nodeType, declared, ops, errors) {
|
|
143
183
|
const opChar = OP_PREFIX[line[0]] ? line[0] : '+';
|
|
144
184
|
const rest = OP_PREFIX[line[0]] ? line.slice(1).trim() : line.trim();
|
|
145
185
|
const action = OP_PREFIX[opChar] ?? 'add';
|
|
@@ -155,30 +195,35 @@ export class FormatECodec {
|
|
|
155
195
|
const pipeIdx = mainPart.indexOf('|');
|
|
156
196
|
const uid = pipeIdx >= 0 ? mainPart.slice(0, pipeIdx).trim() : mainPart.trim();
|
|
157
197
|
const description = pipeIdx >= 0 ? mainPart.slice(pipeIdx + 1).trim() : undefined;
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
if (!nodeType) {
|
|
161
|
-
errors.push(`Cannot extract node type from uid: "${uid}"`);
|
|
198
|
+
if (!uid) {
|
|
199
|
+
errors.push(`Node line without a uid: "${line}"`);
|
|
162
200
|
return;
|
|
163
201
|
}
|
|
164
|
-
|
|
165
|
-
|
|
202
|
+
// CR-SM-217: with `TYPE-slug` as the family canon, the prefix restores the
|
|
203
|
+
// redundancy CR-SM-216 removed — for free, since `REQ-safety` costs fewer tokens
|
|
204
|
+
// than `REQ-safety.REQ`. A node under the wrong section is caught here. Uids with
|
|
205
|
+
// no recognisable prefix (legacy ids, graphify candidates) are not an error, only
|
|
206
|
+
// unchecked — the prefix is a cross-check, never a type source.
|
|
207
|
+
const prefix = uid.split('-', 1)[0];
|
|
208
|
+
if (prefix !== uid && this.validNodeTypes.has(prefix) && prefix !== nodeType) {
|
|
209
|
+
errors.push(`Uid prefix contradicts its section: "${uid}" under "### ${nodeType}"`);
|
|
166
210
|
return;
|
|
167
211
|
}
|
|
212
|
+
declared.set(uid, nodeType);
|
|
168
213
|
if (action === 'remove') {
|
|
169
214
|
ops.push({ type: 'remove_node', semanticId: uid });
|
|
170
215
|
}
|
|
171
216
|
else if (action === 'update') {
|
|
172
|
-
ops.push({ type: 'update_node', semanticId: uid, description, attributes: inlineAttrs });
|
|
217
|
+
ops.push({ type: 'update_node', semanticId: uid, elementType: nodeType, description, attributes: inlineAttrs });
|
|
173
218
|
}
|
|
174
219
|
else if (action === 'strict_add') {
|
|
175
|
-
ops.push({ type: 'strict_add_node', semanticId: uid, description, attributes: inlineAttrs });
|
|
220
|
+
ops.push({ type: 'strict_add_node', semanticId: uid, elementType: nodeType, description, attributes: inlineAttrs });
|
|
176
221
|
}
|
|
177
222
|
else {
|
|
178
|
-
ops.push({ type: 'add_node', semanticId: uid, description, attributes: inlineAttrs });
|
|
223
|
+
ops.push({ type: 'add_node', semanticId: uid, elementType: nodeType, description, attributes: inlineAttrs });
|
|
179
224
|
}
|
|
180
225
|
}
|
|
181
|
-
parseEdgeLine(line, ops, errors) {
|
|
226
|
+
parseEdgeLine(line, typeOf, ops, errors) {
|
|
182
227
|
const opChar = OP_PREFIX[line[0]] ? line[0] : '+';
|
|
183
228
|
const rest = OP_PREFIX[line[0]] ? line.slice(1).trim() : line.trim();
|
|
184
229
|
const action = OP_PREFIX[opChar] ?? 'add';
|
|
@@ -205,19 +250,19 @@ export class FormatECodec {
|
|
|
205
250
|
errors.push(`Unknown edge arrow type "${arrowType}" in: "${line}"`);
|
|
206
251
|
return;
|
|
207
252
|
}
|
|
208
|
-
//
|
|
209
|
-
|
|
253
|
+
// CR-SM-216: endpoint types come from the declared sections or the caller's
|
|
254
|
+
// resolver — never from the uid's spelling.
|
|
255
|
+
const srcType = typeOf(sourceId);
|
|
210
256
|
if (!srcType) {
|
|
211
|
-
errors.push(`Cannot
|
|
257
|
+
errors.push(`Cannot resolve type of source "${sourceId}" — not declared under a "### <TYPE>" section and no resolveType provided`);
|
|
212
258
|
return;
|
|
213
259
|
}
|
|
214
260
|
// Handle 1:N targets
|
|
215
261
|
const targets = targetsPart.split(',').map(t => t.trim()).filter(Boolean);
|
|
216
262
|
for (const targetId of targets) {
|
|
217
|
-
|
|
218
|
-
const tgtType = this.extractNodeType(targetId);
|
|
263
|
+
const tgtType = typeOf(targetId);
|
|
219
264
|
if (!tgtType) {
|
|
220
|
-
errors.push(`Cannot
|
|
265
|
+
errors.push(`Cannot resolve type of target "${targetId}" — not declared under a "### <TYPE>" section and no resolveType provided`);
|
|
221
266
|
continue;
|
|
222
267
|
}
|
|
223
268
|
// Meta-model validation (CR-GC-247: single checker, patterns SSOT — honors
|
|
@@ -255,22 +300,6 @@ export class FormatECodec {
|
|
|
255
300
|
sourceIds: parts,
|
|
256
301
|
});
|
|
257
302
|
}
|
|
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
303
|
parseInlineAttrs(raw) {
|
|
275
304
|
const attrs = {};
|
|
276
305
|
for (const pair of raw.split(',')) {
|
|
@@ -281,6 +310,48 @@ export class FormatECodec {
|
|
|
281
310
|
}
|
|
282
311
|
return attrs;
|
|
283
312
|
}
|
|
313
|
+
/**
|
|
314
|
+
* CR-SM-215: fan-out serialization — edges sharing `(sourceId, edgeType)` collapse
|
|
315
|
+
* onto one line `A -x-> B, C, D`. The source UID is written once instead of once per
|
|
316
|
+
* edge; on the graphcode SSOT graph that is 318 lines instead of 751.
|
|
317
|
+
*
|
|
318
|
+
* Two invariants:
|
|
319
|
+
* - **Edges carrying attributes stay single-line.** `serializeAttrs` binds to one
|
|
320
|
+
* edge; a group would either drop `cardinality`/`constraint`/`notes` or wrongly
|
|
321
|
+
* share one edge's attributes with its siblings.
|
|
322
|
+
* - **Deterministic order** (graphcode `REQ-deterministic-serialization`): entries
|
|
323
|
+
* sorted by source, then edge type, then first target — code-unit order, not
|
|
324
|
+
* `localeCompare`, which is locale-dependent.
|
|
325
|
+
*/
|
|
326
|
+
serializeEdges(edges) {
|
|
327
|
+
const entries = [];
|
|
328
|
+
const groups = new Map();
|
|
329
|
+
for (const edge of edges) {
|
|
330
|
+
const attrs = this.serializeAttrs(edge.attributes);
|
|
331
|
+
if (attrs) {
|
|
332
|
+
entries.push({ sourceId: edge.sourceId, edgeType: edge.edgeType, targets: [edge.targetId], attrs });
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
const key = `${edge.sourceId}${edge.edgeType}`;
|
|
336
|
+
const group = groups.get(key);
|
|
337
|
+
if (group) {
|
|
338
|
+
group.targets.push(edge.targetId);
|
|
339
|
+
}
|
|
340
|
+
else {
|
|
341
|
+
const entry = { sourceId: edge.sourceId, edgeType: edge.edgeType, targets: [edge.targetId], attrs: '' };
|
|
342
|
+
groups.set(key, entry);
|
|
343
|
+
entries.push(entry);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
const cmp = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
|
|
347
|
+
for (const entry of entries)
|
|
348
|
+
entry.targets.sort(cmp);
|
|
349
|
+
entries.sort((a, b) => cmp(a.sourceId, b.sourceId)
|
|
350
|
+
|| cmp(a.edgeType, b.edgeType)
|
|
351
|
+
|| cmp(a.targets[0], b.targets[0])
|
|
352
|
+
|| cmp(a.attrs, b.attrs));
|
|
353
|
+
return entries.map(e => `+ ${e.sourceId} -${this.edgeTypeToArrow(e.edgeType)}-> ${e.targets.join(', ')}${e.attrs}`);
|
|
354
|
+
}
|
|
284
355
|
serializeAttrs(attrs) {
|
|
285
356
|
const entries = Object.entries(attrs).filter(([, v]) => v != null && v !== '');
|
|
286
357
|
if (entries.length === 0)
|
package/dist/graph-service.js
CHANGED
|
@@ -8,8 +8,18 @@ import { isValidTrace, tracePatternsOf } from './types.js';
|
|
|
8
8
|
import { FormatECodec } from './format-e-codec.js';
|
|
9
9
|
import { DefaultRuleEngine } from './rule-engine.js';
|
|
10
10
|
import { InMemoryAuditLog } from './audit.js';
|
|
11
|
-
import { extractFromSemanticId } from '@sigloch/contracts/se';
|
|
12
11
|
import { updateEdge, mergeNodes } from './edge-ops.js';
|
|
12
|
+
/**
|
|
13
|
+
* CR-SM-216: a display name for a typed node, derived from the uid's *shape* only.
|
|
14
|
+
* `MOD-harness` → "harness", `Mower.PR.01` → "Mower", anything else → the uid itself.
|
|
15
|
+
* Purely cosmetic — the type never comes from here.
|
|
16
|
+
*/
|
|
17
|
+
function nameFromUid(uid, type) {
|
|
18
|
+
if (uid.startsWith(`${type}-`))
|
|
19
|
+
return uid.slice(type.length + 1);
|
|
20
|
+
const dot = uid.indexOf('.');
|
|
21
|
+
return dot > 0 ? uid.slice(0, dot) : uid;
|
|
22
|
+
}
|
|
13
23
|
export class GraphService {
|
|
14
24
|
ontology;
|
|
15
25
|
codec;
|
|
@@ -461,15 +471,16 @@ export class GraphService {
|
|
|
461
471
|
async applyNodeCreate(op, isSESchema) {
|
|
462
472
|
let type;
|
|
463
473
|
let name;
|
|
464
|
-
if (
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
474
|
+
if (op.elementType) {
|
|
475
|
+
// CR-SM-216: the type travels with the operation (Format-E `### <TYPE>` section).
|
|
476
|
+
type = op.elementType;
|
|
477
|
+
name = nameFromUid(op.semanticId, type);
|
|
478
|
+
}
|
|
479
|
+
else if (isSESchema) {
|
|
480
|
+
// CR-SM-217: no id-derived typing for SE any more. A hand-built SE operation must
|
|
481
|
+
// name its type — guessing it from the uid is the CR-230 failure mode, and with
|
|
482
|
+
// `TYPE-slug` as the canon there is no counter to parse out of the id either.
|
|
483
|
+
throw new Error(`Node operation for "${op.semanticId}" carries no elementType — SE nodes must declare their type (Format-E: "### <TYPE>" section)`);
|
|
473
484
|
}
|
|
474
485
|
else {
|
|
475
486
|
const parts = op.semanticId.split('.');
|
package/dist/se-descriptor.d.ts
CHANGED
|
@@ -18,7 +18,7 @@ import type { Graph, OntologyDescriptor } from './types.js';
|
|
|
18
18
|
*/
|
|
19
19
|
export declare function projectToOntologyGraph(graph: Graph): OntologyGraph;
|
|
20
20
|
/**
|
|
21
|
-
* Canonical SE OntologyDescriptor (ontology +
|
|
22
|
-
* contracts/se ONTOLOGY_VERSION. Plug into GraphService / FormatECodec.
|
|
21
|
+
* Canonical SE OntologyDescriptor (ontology + V3 rules + MT metrics), version-pinned
|
|
22
|
+
* to contracts/se ONTOLOGY_VERSION. Plug into GraphService / FormatECodec.
|
|
23
23
|
*/
|
|
24
24
|
export declare const SE_DESCRIPTOR: OntologyDescriptor;
|
package/dist/se-descriptor.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* - Graph (nodes/edges) ⟷ OntologyGraph (elements/traces)
|
|
10
10
|
* - RuleViolation (ruleId/…) ⟷ contracts RuleViolation (rule_id/…)
|
|
11
11
|
*/
|
|
12
|
-
import { ElementType, TraceType, TRACE_PATTERNS, V3_RULES, ONTOLOGY_VERSION, } from '@sigloch/contracts/se';
|
|
12
|
+
import { ElementType, TraceType, TRACE_PATTERNS, V3_RULES, MT_RULES, ONTOLOGY_VERSION, } from '@sigloch/contracts/se';
|
|
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
|
|
@@ -42,8 +42,16 @@ export function projectToOntologyGraph(graph) {
|
|
|
42
42
|
}));
|
|
43
43
|
return { elements, traces };
|
|
44
44
|
}
|
|
45
|
-
/**
|
|
46
|
-
|
|
45
|
+
/**
|
|
46
|
+
* The contracts/se rule catalog, adapted to graph-api-core's Rule shape.
|
|
47
|
+
*
|
|
48
|
+
* V3_RULES + MT_RULES (CR-SM-222): the architecture metrics lived in contracts since
|
|
49
|
+
* K2 but ran only in aimpro's `evaluateAllRules`, so the governed repos (graphcode,
|
|
50
|
+
* graph-view-edit) never saw them. Same adapter, no fork — they inherit by
|
|
51
|
+
* consuming this descriptor. MT severities are warning/info, so they stay advisory
|
|
52
|
+
* in graphcode's gate.
|
|
53
|
+
*/
|
|
54
|
+
const SE_RULES = [...V3_RULES, ...MT_RULES].map((def) => ({
|
|
47
55
|
id: def.id,
|
|
48
56
|
name: def.name,
|
|
49
57
|
severity: def.severity,
|
|
@@ -69,8 +77,8 @@ const edgeTypes = Object.fromEntries(TraceType.options.map((tt) => {
|
|
|
69
77
|
return [tt, { arrows: [tt], validPairs: pairs.length ? pairs : [['*', '*']] }];
|
|
70
78
|
}));
|
|
71
79
|
/**
|
|
72
|
-
* Canonical SE OntologyDescriptor (ontology +
|
|
73
|
-
* contracts/se ONTOLOGY_VERSION. Plug into GraphService / FormatECodec.
|
|
80
|
+
* Canonical SE OntologyDescriptor (ontology + V3 rules + MT metrics), version-pinned
|
|
81
|
+
* to contracts/se ONTOLOGY_VERSION. Plug into GraphService / FormatECodec.
|
|
74
82
|
*/
|
|
75
83
|
export const SE_DESCRIPTOR = {
|
|
76
84
|
name: 'se',
|
package/dist/types.d.ts
CHANGED
|
@@ -103,8 +103,21 @@ export interface FormatEOperation {
|
|
|
103
103
|
type: 'add_node' | 'remove_node' | 'update_node' | 'add_edge' | 'remove_edge' | 'update_edge' | 'strict_add_node' | 'strict_add_edge' | 'merge_nodes';
|
|
104
104
|
/** Node uid or edge key (source->target). */
|
|
105
105
|
semanticId: string;
|
|
106
|
+
/**
|
|
107
|
+
* CR-SM-216: the node type, taken from the `### <TYPE>` section the node was declared
|
|
108
|
+
* under. Set on every node-creating operation. Consumers must read this instead of
|
|
109
|
+
* re-deriving a type from the uid — uid conventions differ per repo, and guessing
|
|
110
|
+
* from the spelling fails silently on the ones it does not recognise.
|
|
111
|
+
*/
|
|
112
|
+
elementType?: string;
|
|
106
113
|
description?: string;
|
|
107
|
-
|
|
114
|
+
/**
|
|
115
|
+
* BOK-CR-026: `unknown`, not `string` — object-valued bindings (`realRef`,
|
|
116
|
+
* `testRef`) must survive a Format-E mutation as objects, or the element reads as
|
|
117
|
+
* unbound. GraphService already handled them as unknown internally; this is the
|
|
118
|
+
* public type catching up (a plain `@key value` line still yields a string).
|
|
119
|
+
*/
|
|
120
|
+
attributes?: Record<string, unknown>;
|
|
108
121
|
sourceId?: string;
|
|
109
122
|
targetId?: string;
|
|
110
123
|
edgeType?: string;
|
|
@@ -114,7 +127,7 @@ export interface FormatEOperation {
|
|
|
114
127
|
set?: {
|
|
115
128
|
edgeType?: string;
|
|
116
129
|
flip?: boolean;
|
|
117
|
-
attributes?: Record<string,
|
|
130
|
+
attributes?: Record<string, unknown>;
|
|
118
131
|
};
|
|
119
132
|
}
|
|
120
133
|
export interface FormatEDiff {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sigloch/graph-api-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"prepublishOnly": "npm run build && npm run test"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@sigloch/contracts": "^0.
|
|
24
|
+
"@sigloch/contracts": "^2.0.0",
|
|
25
25
|
"zod": "^4.3.6"
|
|
26
26
|
},
|
|
27
27
|
"license": "MIT",
|
|
@@ -4,19 +4,31 @@ Exercises the reserved inline attributes from CR-007:
|
|
|
4
4
|
- Nodes: `notes`
|
|
5
5
|
- Edges: `cardinality`, `constraint`, `notes`
|
|
6
6
|
|
|
7
|
+
Plus both serializer branches from CR-SM-215: attributed edges stay one per line,
|
|
8
|
+
attribute-free edges of the same `(source, type)` collapse into one fan-out line.
|
|
9
|
+
|
|
7
10
|
Uses the minimal `TEST_ONTOLOGY` shipped from `@sigloch/graph-api-core/test-fixtures`.
|
|
8
11
|
|
|
12
|
+
Format-E **v2** (CR-SM-216): Typ aus dem `### <TYPE>`-Sektionsheader.
|
|
13
|
+
|
|
9
14
|
```format-e
|
|
10
15
|
## Nodes
|
|
16
|
+
### PR
|
|
11
17
|
+ Mower.PR.01|Riding mower R15 [notes:Phase-2-Vertical-Slice-Wurzel]
|
|
18
|
+
### C
|
|
12
19
|
+ Engine.C.01|Combustion engine [power:gas]
|
|
13
20
|
+ Gearbox.C.02|Mowing-deck gearbox family [kind:family,notes:drei Varianten geplant]
|
|
21
|
+
+ Blade.C.03|Mowing blade
|
|
22
|
+
+ Deck.C.04|Cutting deck shell
|
|
23
|
+
### FR
|
|
14
24
|
+ DFMEA-Engine-001.FR.01|Not-Stop-Funktion am Motor [external_id:DFMEA-Engine-001]
|
|
25
|
+
### CTRL
|
|
15
26
|
+ EmergencyBrake.CTRL.01|Mechanische Bremse Stillstandszeit max 5 sec
|
|
16
27
|
|
|
17
28
|
## Edges
|
|
18
29
|
+ Mower.PR.01 -compose-> Engine.C.01 [cardinality:1..1]
|
|
19
30
|
+ Mower.PR.01 -compose-> Gearbox.C.02 [cardinality:1..n,constraint:mindestens eine Variante je Schnittbreite]
|
|
31
|
+
+ Gearbox.C.02 -compose-> Blade.C.03, Deck.C.04
|
|
20
32
|
+ Engine.C.01 -addressed_by-> DFMEA-Engine-001.FR.01 [cardinality:1..n,notes:DFMEA-Hotspot]
|
|
21
33
|
+ DFMEA-Engine-001.FR.01 -addressed_by-> EmergencyBrake.CTRL.01 [cardinality:1..1,constraint:Stillstandszeit <= 5 sec]
|
|
22
34
|
```
|
|
@@ -7,24 +7,39 @@ sigloch-weit nutzbare Variante für CI-Smokes.
|
|
|
7
7
|
|
|
8
8
|
Validiert gegen `TEST_ONTOLOGY` aus `@sigloch/graph-api-core/test-fixtures`.
|
|
9
9
|
|
|
10
|
+
Format-E **v2** (CR-SM-216): der Typ steht im `### <TYPE>`-Sektionsheader, nicht in der UID.
|
|
11
|
+
Die UIDs bleiben unverändert — sie werden nur nicht mehr als Typquelle gelesen.
|
|
12
|
+
|
|
10
13
|
```format-e
|
|
11
14
|
## Nodes
|
|
15
|
+
### OU
|
|
12
16
|
+ Konstruktion.OU.01|Konstruktion
|
|
13
17
|
+ Tooling.OU.02|Tooling
|
|
18
|
+
### R
|
|
14
19
|
+ DesignLead.R.01|Design Lead
|
|
15
20
|
+ Tooling.R.02|Tooling
|
|
21
|
+
### PR
|
|
16
22
|
+ R15.PR.01|Rasentraktor R15 [code:R15]
|
|
23
|
+
### VAR
|
|
17
24
|
+ R15-1000.VAR.01|R15 mit 1000 mm Schnittbreite [attribute_key:Schnittbreite,attribute_value:1000mm]
|
|
18
25
|
+ R15-1500.VAR.02|R15 mit 1500 mm Schnittbreite [attribute_key:Schnittbreite,attribute_value:1500mm]
|
|
26
|
+
### C
|
|
19
27
|
+ MaehwerkGetriebe.C.01|Maehwerk-Getriebe Familie [kind:family]
|
|
20
28
|
+ Getriebe-1000.C.02|Getriebe 1000 [kind:variant,part_no:MG-1000]
|
|
21
29
|
+ Getriebe-1500.C.03|Getriebe 1500 [kind:variant,part_no:MG-1500]
|
|
30
|
+
### F
|
|
22
31
|
+ UnterschiedlicheMaehbreiten.F.01|Unterschiedliche Maehbreiten [customer_priority:hoch]
|
|
32
|
+
### FR
|
|
23
33
|
+ DFMEA-MG-001.FR.01|Not-Stop-Funktion [external_id:DFMEA-MG-001,status:verifiziert]
|
|
34
|
+
### CTRL
|
|
24
35
|
+ MechBremse.CTRL.01|Mechanische Bremse Stillstandszeit max 5 sec
|
|
36
|
+
### TC
|
|
25
37
|
+ Stillstandszeitmessung.TC.01|Stillstandszeit-Messung nach Not-Aus [criterion:t_stop <= 5 sec]
|
|
38
|
+
### TL
|
|
26
39
|
+ WerkzeugSetGehaeuse.TL.01|Werkzeug-Set Gehaeuse Druckguss [material:Aluminium-Druckguss]
|
|
40
|
+
### MT
|
|
27
41
|
+ M3.MT.01|GO Milestone [sequence_no:3]
|
|
42
|
+
### SL
|
|
28
43
|
+ PDM.SL.01|PDM-System [type:PLM]
|
|
29
44
|
|
|
30
45
|
## Edges
|