@sigloch/contracts 0.7.0 → 1.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.
@@ -1,5 +1,16 @@
1
+ /**
2
+ * Format E Parser — Graph mutations from compact text format.
3
+ * SE-relevant subset: nodes (+/-/~) and edges (+/-), no chat-canvas or views.
4
+ *
5
+ * CR-SM-216 (Format-E v2): the element type comes from the `### <TYPE>` section a node
6
+ * is declared under, never from the spelling of its id. The old id-derived typing made
7
+ * every consumer with a different id convention fail *silently* — aimpro CR-230 lost a
8
+ * whole graph that way (`TYPE-slug` ids rejected, result empty, no error).
9
+ *
10
+ * @sigloch/contracts/se
11
+ */
12
+ import { ElementType } from './ontology.js';
1
13
  import { isValidTrace } from './meta-model.js';
2
- import { isSemanticId, extractFromSemanticId } from './semantic-id.js';
3
14
  // ---------------------------------------------------------------------------
4
15
  // Extraction
5
16
  // ---------------------------------------------------------------------------
@@ -21,19 +32,48 @@ const OP_PREFIX = {
21
32
  '~': 'update',
22
33
  '!': 'strict_add',
23
34
  };
24
- const EDGE_RE = /^([+\-~!])?\s*(\S+)\s+-(\w+)->\s+(\S+)\s*$/;
35
+ /**
36
+ * CR-SM-215: the target group is `(.+)` — Format-E allows fan-out
37
+ * `A -x-> B, C, D`, one edge per target. `graph-api-core`'s codec has always parsed
38
+ * it; this parser rejected it as `Invalid edge syntax`, so the same text produced
39
+ * different operations depending on which parser saw it.
40
+ */
41
+ const EDGE_RE = /^([+\-~!])?\s*(\S+)\s+-(\w+)->\s+(.+?)\s*$/;
25
42
  const NODE_RE = /^([+\-~!])?\s*(\S+?)(?:\|(.*))?$/;
26
43
  /** CR-147: @key value attribute line (indented, below a node entry). */
27
44
  const ATTR_RE = /^\s*@(\w+)\s+(.+)$/;
45
+ /**
46
+ * BOK-CR-026: hydrate a JSON object/array attribute value. The ontology's bindings
47
+ * (`realRef {file,symbol?,lang?}`, `testRef {file,tool,…}`) are objects; kept as raw
48
+ * strings they fail RealRefSchema/TestRefSchema and the element reads as unbound.
49
+ * Only `{…}`/`[…]` are attempted — every other value stays the string it is, and a
50
+ * malformed literal falls back to the string rather than failing the whole parse.
51
+ */
52
+ function hydrateAttrValue(raw) {
53
+ if (!/^[{[]/.test(raw))
54
+ return raw;
55
+ try {
56
+ return JSON.parse(raw);
57
+ }
58
+ catch {
59
+ return raw;
60
+ }
61
+ }
28
62
  /** CR-148: Trace-type normalization aliases (source→target→from→to). */
29
63
  const TRACE_NORMALIZE = {
30
64
  FLOW: { SCHEMA: 'relation' }, // FLOW→SCHEMA io → relation
31
65
  };
66
+ /** `### <TYPE>` — the node type section (CR-SM-216). */
67
+ const TYPE_SECTION_RE = /^###\s+([A-Za-z_]+)\s*$/;
32
68
  /** Parse a Format E text block into validated operations. */
33
- export function parseFormatE(input) {
69
+ export function parseFormatE(input, options = {}) {
34
70
  const operations = [];
35
71
  const errors = [];
36
72
  let section = null;
73
+ let currentType = null;
74
+ /** uid → type, from this text's node sections. */
75
+ const declared = new Map();
76
+ const typeOf = (uid) => declared.get(uid) ?? options.resolveType?.(uid);
37
77
  for (const rawLine of input.split('\n')) {
38
78
  const line = rawLine.trim();
39
79
  if (!line || line.startsWith('//') || line.startsWith('#!'))
@@ -45,7 +85,7 @@ export function parseFormatE(input) {
45
85
  if (lastOp && (lastOp.type === 'add_node' || lastOp.type === 'update_node' || lastOp.type === 'strict_add_node')) {
46
86
  if (!lastOp.attributes)
47
87
  lastOp.attributes = {};
48
- lastOp.attributes[attrMatch[1]] = attrMatch[2].trim();
88
+ lastOp.attributes[attrMatch[1]] = hydrateAttrValue(attrMatch[2].trim());
49
89
  }
50
90
  else {
51
91
  errors.push(`@attribute line without preceding node: "${line}"`);
@@ -55,49 +95,64 @@ export function parseFormatE(input) {
55
95
  // Section headers
56
96
  if (/^##\s*nodes?\s*$/i.test(line)) {
57
97
  section = 'nodes';
98
+ currentType = null;
58
99
  continue;
59
100
  }
60
101
  if (/^##\s*edges?\s*$/i.test(line)) {
61
102
  section = 'edges';
103
+ currentType = null;
104
+ continue;
105
+ }
106
+ // CR-SM-216: `### <TYPE>` carries the element type for the nodes below it.
107
+ const typeSection = TYPE_SECTION_RE.exec(line);
108
+ if (typeSection && section === 'nodes') {
109
+ const parsed = ElementType.safeParse(typeSection[1]);
110
+ if (parsed.success) {
111
+ currentType = parsed.data;
112
+ }
113
+ else {
114
+ currentType = null;
115
+ errors.push(`Unknown element type section: "### ${typeSection[1]}"`);
116
+ }
62
117
  continue;
63
118
  }
64
119
  // Skip other markdown headers
65
120
  if (line.startsWith('#'))
66
121
  continue;
67
- // Try edge first (more specific pattern)
68
- const edgeMatch = EDGE_RE.exec(line);
69
- if (edgeMatch || section === 'edges') {
122
+ // Edge: detect by arrow in the structural part only — before the description
123
+ // pipe. Node descriptions may legitimately contain '->' ("FUNC->FUNC compose");
124
+ // edges carry no pipe, so splitting on '|' disambiguates (CR-GC-247). Since
125
+ // CR-SM-215 widened the target group to `(.+?)`, this guard is what keeps a
126
+ // multi-word node description from being read as an edge.
127
+ const isEdgeLine = line.split('|', 1)[0].includes('->');
128
+ if (isEdgeLine || section === 'edges') {
129
+ const edgeMatch = isEdgeLine ? EDGE_RE.exec(line) : null;
70
130
  if (edgeMatch) {
71
- parseEdge(edgeMatch, operations, errors);
131
+ parseEdge(edgeMatch, typeOf, operations, errors);
72
132
  }
73
133
  else {
74
134
  errors.push(`Invalid edge line: "${line}"`);
75
135
  }
76
136
  continue;
77
137
  }
78
- // Try node
138
+ // Node
79
139
  if (section === 'nodes') {
80
140
  const nodeMatch = NODE_RE.exec(line);
81
- if (nodeMatch) {
82
- parseNode(nodeMatch, operations, errors);
141
+ if (!nodeMatch) {
142
+ errors.push(`Invalid node line: "${line}"`);
143
+ }
144
+ else if (!currentType) {
145
+ // CR-SM-216: no type section, no type. Guessing one from the id is what
146
+ // CR-230 punished; an error is the point.
147
+ errors.push(`Node "${nodeMatch[2]}" is not under a "### <TYPE>" section`);
83
148
  }
84
149
  else {
85
- errors.push(`Invalid node line: "${line}"`);
150
+ declared.set(nodeMatch[2], currentType);
151
+ parseNode(nodeMatch, currentType, operations, errors);
86
152
  }
87
153
  continue;
88
154
  }
89
- // Outside a section try to auto-detect
90
- const autoEdge = EDGE_RE.exec(line);
91
- if (autoEdge) {
92
- parseEdge(autoEdge, operations, errors);
93
- continue;
94
- }
95
- const autoNode = NODE_RE.exec(line);
96
- if (autoNode && isSemanticId(autoNode[2])) {
97
- parseNode(autoNode, operations, errors);
98
- continue;
99
- }
100
- // Unknown line
155
+ // Unknown line (edges are handled above, nodes need their section)
101
156
  if (line.length > 0)
102
157
  errors.push(`Unrecognized line: "${line}"`);
103
158
  }
@@ -111,60 +166,40 @@ function normalizeTraceType(srcType, tgtType, traceType) {
111
166
  }
112
167
  return traceType;
113
168
  }
114
- function parseNode(m, ops, errors) {
169
+ function parseNode(m, elementType, ops, errors) {
115
170
  const opChar = m[1] || '+';
116
171
  const id = m[2];
117
172
  const descr = m[3]?.trim();
118
173
  const action = OP_PREFIX[opChar] ?? 'add';
119
- if (!isSemanticId(id)) {
120
- errors.push(`Invalid SemanticId: "${id}"`);
174
+ if (!id) {
175
+ errors.push(`Node line without an id: "${m[0]}"`);
121
176
  return;
122
177
  }
123
178
  if (action === 'remove') {
124
179
  ops.push({ type: 'remove_node', semanticId: id });
125
180
  }
126
181
  else if (action === 'update') {
127
- ops.push({ type: 'update_node', semanticId: id, description: descr });
182
+ ops.push({ type: 'update_node', semanticId: id, elementType, description: descr });
128
183
  }
129
184
  else if (action === 'strict_add') {
130
- ops.push({ type: 'strict_add_node', semanticId: id, description: descr });
185
+ ops.push({ type: 'strict_add_node', semanticId: id, elementType, description: descr });
131
186
  }
132
187
  else {
133
- ops.push({ type: 'add_node', semanticId: id, description: descr });
188
+ ops.push({ type: 'add_node', semanticId: id, elementType, description: descr });
134
189
  }
135
190
  }
136
- function parseEdge(m, ops, errors) {
191
+ function parseEdge(m, typeOf, ops, errors) {
137
192
  const opChar = m[1] || '+';
138
193
  const sourceId = m[2];
139
194
  const traceType = m[3];
140
- const targetId = m[4];
141
195
  const action = OP_PREFIX[opChar] ?? 'add';
142
- if (!isSemanticId(sourceId)) {
143
- errors.push(`Invalid source SemanticId: "${sourceId}"`);
144
- return;
145
- }
146
- if (!isSemanticId(targetId)) {
147
- errors.push(`Invalid target SemanticId: "${targetId}"`);
148
- return;
149
- }
150
196
  if (!VALID_TRACE_TYPES.has(traceType)) {
151
197
  errors.push(`Invalid trace type: "${traceType}"`);
152
198
  return;
153
199
  }
154
- // Meta-model validation (with CR-148 normalization)
155
- let resolvedTraceType = traceType;
156
- try {
157
- const src = extractFromSemanticId(sourceId);
158
- const tgt = extractFromSemanticId(targetId);
159
- // CR-148: Normalize before meta-model check (e.g. FLOW→SCHEMA io → relation)
160
- resolvedTraceType = normalizeTraceType(src.type, tgt.type, traceType);
161
- if (!isValidTrace({ source: src.type, target: tgt.type, type: resolvedTraceType })) {
162
- errors.push(`Meta-model violation: ${src.type} -${traceType}-> ${tgt.type} is not valid`);
163
- return;
164
- }
165
- }
166
- catch {
167
- errors.push(`Cannot extract types from edge: ${sourceId} -${traceType}-> ${targetId}`);
200
+ const srcType = typeOf(sourceId);
201
+ if (!srcType) {
202
+ errors.push(`Cannot resolve type of "${sourceId}" — not declared under a "### <TYPE>" section and no resolveType provided`);
168
203
  return;
169
204
  }
170
205
  const edgeType = action === 'remove'
@@ -172,33 +207,71 @@ function parseEdge(m, ops, errors) {
172
207
  : action === 'strict_add'
173
208
  ? 'strict_add_edge'
174
209
  : 'add_edge';
175
- ops.push({
176
- type: edgeType,
177
- semanticId: `${sourceId}->${targetId}`,
178
- sourceId,
179
- targetId,
180
- traceType: resolvedTraceType,
181
- });
210
+ // CR-SM-215: 1:n fan-out — `A -x-> B, C` is n independent edges. Validation runs
211
+ // per target (like `graph-api-core`'s codec), so one bad target does not discard
212
+ // its siblings.
213
+ const targets = m[4].split(',').map(t => t.trim()).filter(Boolean);
214
+ for (const targetId of targets) {
215
+ const tgtType = typeOf(targetId);
216
+ if (!tgtType) {
217
+ errors.push(`Cannot resolve type of "${targetId}" — not declared under a "### <TYPE>" section and no resolveType provided`);
218
+ continue;
219
+ }
220
+ // CR-148: Normalize before the meta-model check (e.g. FLOW→SCHEMA io → relation)
221
+ const resolvedTraceType = normalizeTraceType(srcType, tgtType, traceType);
222
+ if (!isValidTrace({ source: srcType, target: tgtType, type: resolvedTraceType })) {
223
+ errors.push(`Meta-model violation: ${srcType} -${traceType}-> ${tgtType} is not valid`);
224
+ continue;
225
+ }
226
+ ops.push({
227
+ type: edgeType,
228
+ semanticId: `${sourceId}->${targetId}`,
229
+ sourceId,
230
+ targetId,
231
+ traceType: resolvedTraceType,
232
+ });
233
+ }
182
234
  }
183
235
  // ---------------------------------------------------------------------------
184
236
  // Serializer
185
237
  // ---------------------------------------------------------------------------
186
- /** Serialize an OntologyGraph to compact Format E text. */
238
+ /**
239
+ * Serialize an OntologyGraph to compact Format E text.
240
+ *
241
+ * CR-SM-216: nodes are written under `### <TYPE>` sections — the type is declared once
242
+ * per section instead of once per id. Measured on the graphcode SSOT graph (369
243
+ * elements), 12 section headers cost ~48 tokens where a per-node type attribute would
244
+ * have cost ~1476.
245
+ */
187
246
  export function serializeToFormatE(graph) {
188
247
  const lines = [];
189
- // Nodes
190
- if (graph.elements.length > 0) {
248
+ // Nodes, grouped by type
249
+ const modelingElements = graph.elements.filter(el => el.type !== 'SESSION'); // skip audit sessions
250
+ if (modelingElements.length > 0) {
191
251
  lines.push('## Nodes');
192
- for (const el of graph.elements) {
193
- if (el.type === 'SESSION')
194
- continue; // skip audit sessions
195
- const descr = el.description ? `|${el.description}` : '';
196
- lines.push(`+ ${el.id}${descr}`);
197
- // CR-147: Serialize known attributes
198
- if (el.attributes) {
199
- for (const [k, v] of Object.entries(el.attributes)) {
200
- if (v != null && String(v).length > 0) {
201
- lines.push(` @${k} ${String(v)}`);
252
+ const byType = new Map();
253
+ for (const el of modelingElements) {
254
+ const group = byType.get(el.type);
255
+ if (group)
256
+ group.push(el);
257
+ else
258
+ byType.set(el.type, [el]);
259
+ }
260
+ for (const type of [...byType.keys()].sort()) {
261
+ lines.push(`### ${type}`);
262
+ for (const el of byType.get(type) ?? []) {
263
+ const descr = el.description ? `|${el.description}` : '';
264
+ lines.push(`+ ${el.id}${descr}`);
265
+ // CR-147: Serialize known attributes
266
+ if (el.attributes) {
267
+ for (const [k, v] of Object.entries(el.attributes)) {
268
+ if (v == null)
269
+ continue;
270
+ // BOK-CR-026: objects/arrays as JSON — String({}) collapses a realRef/testRef
271
+ // binding to "[object Object]" and loses it on the next parse.
272
+ const text = typeof v === 'object' ? JSON.stringify(v) : String(v);
273
+ if (text.length > 0)
274
+ lines.push(` @${k} ${text}`);
202
275
  }
203
276
  }
204
277
  }
@@ -3,9 +3,9 @@
3
3
  * Single source of truth for SE ontology schemas across all projects.
4
4
  */
5
5
  /** Ontology schema version (element types + trace types). */
6
- export declare const ONTOLOGY_VERSION = "3.8.0";
6
+ export declare const ONTOLOGY_VERSION = "4.0.0";
7
7
  /** Rules engine version (validation rules incl. RC conformance). */
8
- export declare const RULES_VERSION = "2.17.0";
8
+ export declare const RULES_VERSION = "2.20.0";
9
9
  /** Meta-model version (trace pattern constraints + format-e parser). */
10
10
  export declare const META_MODEL_VERSION = "1.4.0";
11
11
  export * from './ontology.js';
@@ -24,5 +24,5 @@ export * from './quality-rules.js';
24
24
  export * from './evaluate-all.js';
25
25
  export * from './readiness.js';
26
26
  export * from './meta-model.js';
27
- export * from './semantic-id.js';
27
+ export * from './element-uid.js';
28
28
  export * from './format-e-parser.js';
package/dist/se/index.js CHANGED
@@ -3,9 +3,9 @@
3
3
  * Single source of truth for SE ontology schemas across all projects.
4
4
  */
5
5
  /** Ontology schema version (element types + trace types). */
6
- export const ONTOLOGY_VERSION = '3.8.0'; // +RepoRelativePathSchema on testRef/codeRef/schemaRef .file — no absolute/`..` paths (CR-GC-255)
6
+ export const ONTOLOGY_VERSION = '4.0.0'; // BREAKING: `SemanticId` (`Name.TypeAbbr.Counter`) deleted, `ElementUid` (`<TYPE>-<slug>`) is the family canon — the old canon was used by no production graph of the family while 626 of 1145 elements already carried TYPE-slug (CR-SM-217); realRef unifies codeRef+schemaRef (+physical-MOD CAD ref), symbol optional; testRef stays separate (CR-228 C); +RepoRelativePathSchema on testRef/realRef .file — no absolute/`..` paths (CR-GC-255)
7
7
  /** Rules engine version (validation rules incl. RC conformance). */
8
- export const RULES_VERSION = '2.17.0'; // -R-24/R-25 REQ→MOD allocation rules deleted (CR-228 A: REQ→MOD allocate no longer a valid pattern, R-18 flags residual edges); NFR-01 budget target split physical→MOD / behavioral→FCHAIN; RULE_TO_DIMENSION completeness (R-18..R-23/R-26/MS-03/CR-R04 mapped, no advisory fall-through) (CR-228 B/D); +BQ-01/02/04/06/07 base-quality rules promoted from aimpro (K2-b, completes rule consolidation); +ND-01/02 near-duplicate + AO-D01/D03/CR-01/RT-01/PH-01/CA-01/IO-01 architecture rules promoted from aimpro (K2-b); +CR-R01..04/MS-03 change-request rules promoted from aimpro (K2-b); +FM-01..03/NFR-01 FMEA + VR-01/CL-01 view rules promoted from aimpro (K2-b); +MT-01..03 architecture metrics promoted from aimpro (K2-b); +SC-01..03/UC-01..06/FC-01..03 quality rules promoted from aimpro (K2-a); +RC-05 cross-module import drift (CR-212); +R-26/RC-03/RC-04 schemaRef (CR-211); +R-22..R-25/R-10/R-20 (CR-201/202/208/209/210)
8
+ export const RULES_VERSION = '2.20.0'; // +RD-04 decomposition breadth (>11 children per level → warning) and MT-03 recalibrated to FLOW-transitive connection pairs — counting raw io traces reported internal=0 on every real SE graph (15/15 false positives), because the meta-model routes FUNC↔FUNC through FLOW; 'RD-' added to the se profile prefixes, where RD-01..04 were silently missing (CR-SM-221); -SC-01/-SC-03 deleted (BOK-CR-026): `realRef` is the single SCHEMA binding truth (R-26 presence, RC-03/RC-04 resolution); the legacy `zodDefinition`/`sourceFile`/`sourceExport` attributes are gone from every producer, SC_RULES = [SC-02]; R-20/R-26/RC-01/RC-03/RC-04 read realRef (unified codeRef+schemaRef); +R-27 physical-MOD realRef presence (CR-228 C); -R-24/R-25 REQ→MOD allocation rules deleted (CR-228 A: REQ→MOD allocate no longer a valid pattern, R-18 flags residual edges); NFR-01 budget target split physical→MOD / behavioral→FCHAIN; RULE_TO_DIMENSION completeness (R-18..R-23/R-26/MS-03/CR-R04 mapped, no advisory fall-through) (CR-228 B/D); +BQ-01/02/04/06/07 base-quality rules promoted from aimpro (K2-b, completes rule consolidation); +ND-01/02 near-duplicate + AO-D01/D03/CR-01/RT-01/PH-01/CA-01/IO-01 architecture rules promoted from aimpro (K2-b); +CR-R01..04/MS-03 change-request rules promoted from aimpro (K2-b); +FM-01..03/NFR-01 FMEA + VR-01/CL-01 view rules promoted from aimpro (K2-b); +MT-01..03 architecture metrics promoted from aimpro (K2-b); +SC-01..03/UC-01..06/FC-01..03 quality rules promoted from aimpro (K2-a); +RC-05 cross-module import drift (CR-212); +R-26/RC-03/RC-04 schemaRef (CR-211); +R-22..R-25/R-10/R-20 (CR-201/202/208/209/210)
9
9
  /** Meta-model version (trace pattern constraints + format-e parser). */
10
10
  export const META_MODEL_VERSION = '1.4.0'; // -REQ→MOD allocate pattern removed (CR-228 A); +FUNC→FUNC compose (blackbox function decomposition)
11
11
  export * from './ontology.js';
@@ -24,5 +24,5 @@ export * from './quality-rules.js';
24
24
  export * from './evaluate-all.js';
25
25
  export * from './readiness.js';
26
26
  export * from './meta-model.js';
27
- export * from './semantic-id.js';
27
+ export * from './element-uid.js';
28
28
  export * from './format-e-parser.js';
@@ -8,7 +8,7 @@ export const TRACE_PATTERNS = [
8
8
  { source: 'UC', target: 'FCHAIN', type: 'compose', cardinality: '1..*', description: 'UC behavioral scenarios' },
9
9
  { source: 'UC', target: 'REQ', type: 'compose', cardinality: '1..*', description: 'UC functional/non-functional requirements' },
10
10
  { source: 'FCHAIN', target: 'FUNC', type: 'compose', cardinality: '1..*', description: 'Functions participating in chain' },
11
- { source: 'FUNC', target: 'FUNC', type: 'compose', cardinality: '0..*', description: 'Function (blackbox architecture block) decomposes into sub-functions one level deeper; leaf FUNCs carry codeRef, the parent is realized by its children' },
11
+ { source: 'FUNC', target: 'FUNC', type: 'compose', cardinality: '0..*', description: 'Function (blackbox architecture block) decomposes into sub-functions one level deeper; leaf FUNCs carry realRef, the parent is realized by its children' },
12
12
  { source: 'REQ', target: 'REQ', type: 'compose', cardinality: '0..*', description: 'Requirement decomposes into sub-requirements (incl. mitigation)' },
13
13
  // ── io (data exchange, always through FLOW) ──
14
14
  { source: 'ACTOR', target: 'FLOW', type: 'io', description: 'Actor triggers/receives via flow' },
@@ -19,11 +19,11 @@ export declare function mt01Instability(graph: OntologyGraph): RuleViolation[];
19
19
  */
20
20
  export declare function mt02Lcom4(graph: OntologyGraph): RuleViolation[];
21
21
  /**
22
- * MT-03: Allocation Cohesion (CR-191: reformulated).
23
- * cohesion = internal_flows / (internal_flows + external_flows).
24
- * internal_flows = io traces between FUNCs within the same module.
25
- * external_flows = io traces crossing module boundary (one end inside, one outside).
26
- * If external_flows === 0 → cohesion = 100% → OK.
22
+ * MT-03: Allocation Cohesion (CR-191 reformulated, CR-SM-221 recalibrated).
23
+ * cohesion = internal / (internal + external), over FLOW-transitive connection pairs.
24
+ * internal = both endpoints allocated to this module.
25
+ * external = exactly one endpoint allocated to this module.
26
+ * If external === 0 → cohesion = 100% → OK.
27
27
  */
28
28
  export declare function mt03AllocationCohesion(graph: OntologyGraph): RuleViolation[];
29
29
  export declare const MT_RULES: readonly [{
@@ -151,48 +151,95 @@ export function mt02Lcom4(graph) {
151
151
  return violations;
152
152
  }
153
153
  /**
154
- * MT-03: Allocation Cohesion (CR-191: reformulated).
155
- * cohesion = internal_flows / (internal_flows + external_flows).
156
- * internal_flows = io traces between FUNCs within the same module.
157
- * external_flows = io traces crossing module boundary (one end inside, one outside).
158
- * If external_flows === 0 cohesion = 100% OK.
154
+ * CR-SM-221: connection pairs between non-FLOW elements, FLOW-transitive.
155
+ *
156
+ * The SE meta-model routes FUNC↔FUNC communication through FLOW nodes
157
+ * (`FUNC —io→ FLOW —io FUNC`); a direct FUNC→FUNC io trace is the exception, not
158
+ * the rule. Counting raw io traces therefore found *zero* internal edges on every
159
+ * real graph — measured on graphcode (369 elements), graph-view-edit (257) and the
160
+ * family graph (638): 15 of 15 MODs reported internal=0, i.e. 15 false positives and
161
+ * no discriminating power at all.
162
+ *
163
+ * A pair counts once, no matter how many flows connect it: the metric asks "do these
164
+ * two talk to each other", not "how often".
165
+ */
166
+ function connectionPairs(graph) {
167
+ const typeOf = new Map(graph.elements.map(e => [e.id, e.type]));
168
+ const pairs = new Set();
169
+ const add = (a, b) => {
170
+ if (a !== b)
171
+ pairs.add(a < b ? `${a}|${b}` : `${b}|${a}`);
172
+ };
173
+ const intoFlow = new Map(); // flow → producers
174
+ const outOfFlow = new Map(); // flow → consumers
175
+ const push = (m, k, v) => {
176
+ const list = m.get(k);
177
+ if (list)
178
+ list.push(v);
179
+ else
180
+ m.set(k, [v]);
181
+ };
182
+ for (const t of graph.traces) {
183
+ if (t.type !== 'io')
184
+ continue;
185
+ const srcIsFlow = typeOf.get(t.source) === 'FLOW';
186
+ const tgtIsFlow = typeOf.get(t.target) === 'FLOW';
187
+ if (srcIsFlow && tgtIsFlow)
188
+ continue; // FLOW→FLOW carries no endpoint
189
+ if (!srcIsFlow && !tgtIsFlow) {
190
+ add(t.source, t.target);
191
+ continue;
192
+ }
193
+ if (tgtIsFlow)
194
+ push(intoFlow, t.target, t.source);
195
+ else
196
+ push(outOfFlow, t.source, t.target);
197
+ }
198
+ for (const [flow, producers] of intoFlow) {
199
+ for (const consumer of outOfFlow.get(flow) ?? []) {
200
+ for (const producer of producers)
201
+ add(producer, consumer);
202
+ }
203
+ }
204
+ return pairs;
205
+ }
206
+ /**
207
+ * MT-03: Allocation Cohesion (CR-191 reformulated, CR-SM-221 recalibrated).
208
+ * cohesion = internal / (internal + external), over FLOW-transitive connection pairs.
209
+ * internal = both endpoints allocated to this module.
210
+ * external = exactly one endpoint allocated to this module.
211
+ * If external === 0 → cohesion = 100% → OK.
159
212
  */
160
213
  export function mt03AllocationCohesion(graph) {
161
214
  const violations = [];
162
215
  const mods = graph.elements.filter(e => e.type === 'MOD');
163
216
  const COHESION_THRESHOLD = 0.8;
217
+ const pairs = [...connectionPairs(graph)].map(p => p.split('|'));
164
218
  for (const mod of mods) {
165
219
  const allocTraces = graph.traces.filter(t => t.type === 'allocate' && t.target === mod.id);
166
220
  const funcIds = new Set(allocTraces.map(t => t.source));
167
221
  if (funcIds.size < 2)
168
222
  continue;
169
- let internalFlows = 0;
170
- let externalFlows = 0;
171
- for (const t of graph.traces) {
172
- if (t.type !== 'io')
173
- continue;
174
- const srcIn = funcIds.has(t.source);
175
- const tgtIn = funcIds.has(t.target);
176
- if (srcIn && tgtIn) {
177
- internalFlows++;
178
- }
179
- else if (srcIn || tgtIn) {
180
- externalFlows++;
181
- }
223
+ let internal = 0;
224
+ let external = 0;
225
+ for (const [a, b] of pairs) {
226
+ const aIn = funcIds.has(a);
227
+ const bIn = funcIds.has(b);
228
+ if (aIn && bIn)
229
+ internal++;
230
+ else if (aIn || bIn)
231
+ external++;
182
232
  }
183
- // No external flows → cohesion = 100% → OK
184
- if (externalFlows === 0)
185
- continue;
186
- const total = internalFlows + externalFlows;
187
- if (total === 0)
233
+ // No external connections → cohesion = 100% → OK
234
+ if (external === 0)
188
235
  continue;
189
- const cohesion = internalFlows / total;
236
+ const cohesion = internal / (internal + external);
190
237
  if (cohesion < COHESION_THRESHOLD) {
191
238
  violations.push({
192
239
  rule_id: 'MT-03',
193
240
  severity: 'info',
194
241
  element_id: mod.id,
195
- message: `${mod.name} allocation cohesion ${Math.round(cohesion * 100)}% (<${COHESION_THRESHOLD * 100}%). internal=${internalFlows}, external=${externalFlows}`,
242
+ message: `${mod.name} allocation cohesion ${Math.round(cohesion * 100)}% (<${COHESION_THRESHOLD * 100}%). internal=${internal}, external=${external}`,
196
243
  });
197
244
  }
198
245
  }
@@ -110,40 +110,27 @@ export declare const TestRefSchema: z.ZodObject<{
110
110
  }, z.core.$strip>;
111
111
  export type TestRef = z.infer<typeof TestRefSchema>;
112
112
  /**
113
- * Code binding for a FUNC element (CR-GC-205 Item 5, ontology bump 3.6.0).
114
- * Resolves a FUNC node to the concrete code symbol that realizes it, enabling
115
- * graph<->code conformance: every non-concept/non-external FUNC must resolve to a
116
- * real symbol in its allocated MOD's file, and (via the consumer's LSP-backed
117
- * check) every cross-module-called symbol must itself be a FUNC node.
118
- * - `file` : implementation file path, e.g. `src/harness.ts` (the symbol's home).
119
- * - `symbol` : the exported symbol (function/method/class) name realizing the FUNC.
120
- * - `lang` : optional language id (default `ts`) — selects the LSP/engine the
121
- * conformance check drives, so the binding is language-agnostic.
122
- * Stored under `OntologyElement.attributes.codeRef` (additive, opt-in validation).
113
+ * Realization binding for an element (CR-228, ontology bump 3.9.0) — unifies the
114
+ * byte-identical former `codeRef` (FUNC code symbol) and `schemaRef` (SCHEMA
115
+ * Zod export), and extends to MOD (physical part → CAD/geometry artefact). The
116
+ * *type* of the pointing element disambiguates what kind of realization it is:
117
+ * FUNC→code, SCHEMA→Zod-def, physical MOD→CAD. TEST keeps its own `testRef`
118
+ * (a TEST is not just located but *executed* — the runner is the extra value).
119
+ * - `file` : realization file path, e.g. `src/harness.ts`, `part.step`.
120
+ * - `symbol` : the realizing symbol (function/class/Zod export). Optional a
121
+ * geometry artefact (CAD) has no symbol; file-exists is the binding.
122
+ * - `lang` : optional language/format id (default `ts`) selects the
123
+ * conformance engine, so the binding is language-agnostic.
124
+ * Stored under `OntologyElement.attributes.realRef` (additive, opt-in validation).
125
+ * The free-text SCHEMA `contract` attribute stays a human description, not the
126
+ * conformance basis: RC-01/RC-03 resolve this binding, RC-04 checks it is parsed.
123
127
  */
124
- export declare const CodeRefSchema: z.ZodObject<{
128
+ export declare const RealRefSchema: z.ZodObject<{
125
129
  file: z.ZodString;
126
- symbol: z.ZodString;
130
+ symbol: z.ZodOptional<z.ZodString>;
127
131
  lang: z.ZodOptional<z.ZodString>;
128
132
  }, z.core.$strip>;
129
- export type CodeRef = z.infer<typeof CodeRefSchema>;
130
- /**
131
- * Binding of a SCHEMA node to the Zod schema that defines it (CR-211), analogous
132
- * to CodeRefSchema for FUNC:
133
- * - `file` : the source file declaring the Zod schema, e.g. `src/se/ontology.ts`.
134
- * - `symbol` : the exported schema symbol, e.g. `CodeRefSchema`.
135
- * - `lang` : optional language id (default `ts`).
136
- * Stored under `OntologyElement.attributes.schemaRef`. The free-text `contract`
137
- * attribute stays as a human description but is no longer the conformance basis:
138
- * RC-03 resolves this binding to a declared export, RC-04 checks it is parsed at
139
- * the modelled interface.
140
- */
141
- export declare const SchemaRefSchema: z.ZodObject<{
142
- file: z.ZodString;
143
- symbol: z.ZodString;
144
- lang: z.ZodOptional<z.ZodString>;
145
- }, z.core.$strip>;
146
- export type SchemaRef = z.infer<typeof SchemaRefSchema>;
133
+ export type RealRef = z.infer<typeof RealRefSchema>;
147
134
  /**
148
135
  * An element (node) in the SE ontology graph.
149
136
  * `attributes` holds type-specific properties (e.g. FUNC.safety_relevant).