@sigloch/contracts 6.3.0 → 10.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.
Files changed (41) hide show
  1. package/dist/se/ao-rules.d.ts +12 -12
  2. package/dist/se/ao-rules.js +88 -225
  3. package/dist/se/conformance-rules.d.ts +28 -0
  4. package/dist/se/conformance-rules.js +40 -17
  5. package/dist/se/cr-quality-rules.js +75 -58
  6. package/dist/se/evaluate-all.d.ts +4 -2
  7. package/dist/se/evaluate-all.js +40 -24
  8. package/dist/se/fchain-quality-rules.d.ts +0 -1
  9. package/dist/se/fchain-quality-rules.js +0 -44
  10. package/dist/se/flat-graph.d.ts +9 -1
  11. package/dist/se/flat-graph.js +15 -6
  12. package/dist/se/format-e-parser.d.ts +14 -2
  13. package/dist/se/format-e-parser.js +33 -9
  14. package/dist/se/grammar-snapshot.d.ts +10 -7
  15. package/dist/se/grammar-snapshot.js +172 -27
  16. package/dist/se/index.d.ts +4 -3
  17. package/dist/se/index.js +4 -3
  18. package/dist/se/meta-model.d.ts +99 -7
  19. package/dist/se/meta-model.js +155 -14
  20. package/dist/se/metric-rules.d.ts +20 -1
  21. package/dist/se/metric-rules.js +97 -58
  22. package/dist/se/module-crossings.d.ts +102 -0
  23. package/dist/se/module-crossings.js +196 -0
  24. package/dist/se/near-duplicate-rules.d.ts +16 -24
  25. package/dist/se/near-duplicate-rules.js +21 -92
  26. package/dist/se/ontology.d.ts +32 -47
  27. package/dist/se/ontology.js +56 -13
  28. package/dist/se/policy.d.ts +6 -0
  29. package/dist/se/policy.js +53 -2
  30. package/dist/se/quality-rules.d.ts +18 -0
  31. package/dist/se/quality-rules.js +68 -12
  32. package/dist/se/readiness.d.ts +7 -5
  33. package/dist/se/readiness.js +40 -22
  34. package/dist/se/rules.d.ts +6 -7
  35. package/dist/se/rules.js +358 -128
  36. package/dist/se/schema-quality-rules.d.ts +0 -1
  37. package/dist/se/schema-quality-rules.js +7 -28
  38. package/dist/se/similarity.d.ts +61 -0
  39. package/dist/se/similarity.js +116 -0
  40. package/dist/se/uc-quality-rules.js +31 -12
  41. package/package.json +3 -2
@@ -6,20 +6,20 @@ import type { OntologyGraph } from './ontology.js';
6
6
  import type { RuleDefinition, RuleViolation } from './rules.js';
7
7
  import type { MetricPolicy } from './policy.js';
8
8
  /**
9
- * AO-D01: Relay Node Detection.
10
- * A FUNC is a relay if it (a) satisfies no REQ, (b) has >=2 outgoing io to FUNCs,
11
- * and (c) target FUNCs share SCHEMA overlap >= 0.5 (via ND-02 matrix, skipped if unavailable).
9
+ * CR-SM-283: **AO-D03 (`DuplicatePathDetection`) ist ersatzlos entfallen.**
10
+ *
11
+ * Die Regel suchte `FUNC -io-> FUNC` ein Paar, das `TRACE_PATTERNS` nicht kennt und das R-18
12
+ * als `error` ablehnt. Sie konnte STRUKTURELL nicht feuern und hat es an keinem Familiengraphen
13
+ * je getan (Gate 7: 0 Befunde ueber fuenf Graphen). Eine Regel, die nie feuert, kostet trotzdem
14
+ * dauerhaft: einen Nenner-Anteil, eine Katalogzeile, eine readiness-Zuordnung, einen
15
+ * Golden-File-Eintrag und einen Leser, der sie von den anderen unterscheiden muss.
16
+ *
17
+ * **Nicht verwechseln:** die Aufgabe, die sie tragen sollte — zwei Wirkketten, die auf einer
18
+ * Ebene mitgliedsgleich werden — bleibt ungeloest (`se:top-level`: „Compare the member sets
19
+ * yourself — no rule does it for you"). BW-02 ersetzt sie nicht; das ist ein eigener CR.
12
20
  */
13
- export declare function aoD01RelayNode(graph: OntologyGraph): RuleViolation[];
14
- /**
15
- * AO-D03: Duplicate Path Detection.
16
- * FUNC A sends io to both B and C, where B and C connect to the same SCHEMA elements.
17
- */
18
- export declare function aoD03DuplicatePath(graph: OntologyGraph): RuleViolation[];
21
+ export declare function bw02WhiteboxWidth(graph: OntologyGraph, policy: MetricPolicy): RuleViolation[];
19
22
  export declare function cr01CrossingFlowCount(graph: OntologyGraph, policy: MetricPolicy): RuleViolation[];
20
- export declare function rt01PhysicalBoundaryIntegrity(graph: OntologyGraph): RuleViolation[];
21
- export declare function ph01PhysicalModCompleteness(graph: OntologyGraph): RuleViolation[];
22
- export declare function ca01CapabilityAllocation(graph: OntologyGraph): RuleViolation[];
23
23
  export declare function io01CrossModuleCompleteness(graph: OntologyGraph): RuleViolation[];
24
24
  export declare const AO_RULES: RuleDefinition[];
25
25
  export declare function evaluateAORules(graph: OntologyGraph, policy: MetricPolicy): RuleViolation[];
@@ -1,42 +1,6 @@
1
- import { getND02SimilarityMatrix } from './near-duplicate-rules.js';
2
1
  import { indexOf } from './graph-index.js';
2
+ import { moduleCrossings } from './module-crossings.js';
3
3
  const SCHEMA_OVERLAP_THRESHOLD = 0.5;
4
- /**
5
- * AO-D01: Relay Node Detection.
6
- * A FUNC is a relay if it (a) satisfies no REQ, (b) has >=2 outgoing io to FUNCs,
7
- * and (c) target FUNCs share SCHEMA overlap >= 0.5 (via ND-02 matrix, skipped if unavailable).
8
- */
9
- export function aoD01RelayNode(graph) {
10
- const idx = indexOf(graph);
11
- const violations = [];
12
- const funcs = idx.elementsOfType('FUNC');
13
- const funcIdSet = new Set(funcs.map(f => f.id));
14
- for (const func of funcs) {
15
- // (a) No satisfy traces (FUNC as source) connecting to any REQ
16
- const hasSatisfy = graph.traces.some(t => t.type === 'satisfy' &&
17
- t.source === func.id && idx.typeOf(t.target) === 'REQ');
18
- if (hasSatisfy)
19
- continue;
20
- // (b) >=2 outgoing io traces to other FUNCs
21
- const ioTargets = graph.traces
22
- .filter(t => t.type === 'io' && t.source === func.id && funcIdSet.has(t.target))
23
- .map(t => t.target);
24
- if (ioTargets.length < 2)
25
- continue;
26
- // (c) Target FUNCs share SCHEMA overlap >= 0.5 (skip check if no matrix)
27
- if (!passesSchemaOverlap(graph, ioTargets))
28
- continue;
29
- const el = idx.byId.get(func.id);
30
- violations.push({
31
- rule_id: 'AO-D01',
32
- severity: 'info',
33
- element_id: func.id,
34
- message: `${func.id}${el?.name ? ' (' + el.name + ')' : ''} is a relay node: no REQ satisfied, ${ioTargets.length} io targets`,
35
- fix_hint: 'Consider eliminating relay by connecting consumers directly to producer',
36
- });
37
- }
38
- return violations;
39
- }
40
4
  /** CR-165: Resolve FUNC→SCHEMA via FUNC→FLOW(io) + FLOW→SCHEMA(relation). */
41
5
  function funcToSchemas(graph, funcId) {
42
6
  const idx = indexOf(graph);
@@ -44,7 +8,7 @@ function funcToSchemas(graph, funcId) {
44
8
  // Find FLOWs connected to this FUNC via io (FUNC→FLOW or FLOW→FUNC)
45
9
  //
46
10
  // CR-SM-264: die io-Kanten dieser FUNC statt zweier Vollscans ueber ALLE Traces je FUNC
47
- // (`F x T`) — AO-D03 ruft diese Funktion einmal je FUNC. Die Reihenfolge von `flowIds` ist
11
+ // (`F x T`) — AO-D01 ruft diese Funktion einmal je FUNC. Die Reihenfolge von `flowIds` ist
48
12
  // dabei egal, die Menge wird nur auf Mitgliedschaft geprueft.
49
13
  const flowIds = new Set();
50
14
  for (const t of idx.out(funcId, 'io'))
@@ -56,7 +20,7 @@ function funcToSchemas(graph, funcId) {
56
20
  // Find SCHEMAs connected to those FLOWs via relation.
57
21
  //
58
22
  // Hier NICHT ueber `flowIds` iterieren, obwohl das billiger waere: die Einfuegereihenfolge
59
- // dieser Menge landet ueber `shared.join(', ')` woertlich in der AO-D03-Meldung. Der Lauf
23
+ // dieser Menge landet ueber `shared.join(', ')` woertlich in der AO-D01-Meldung. Der Lauf
60
24
  // ueber die relation-Kanten in Graph-Reihenfolge haelt sie identisch — und ist trotzdem
61
25
  // deutlich kuerzer als der Vollscan, weil er nur einen Kantentyp sieht.
62
26
  for (const t of idx.tracesOfType('relation')) {
@@ -65,124 +29,101 @@ function funcToSchemas(graph, funcId) {
65
29
  }
66
30
  return schemas;
67
31
  }
68
- /** Check if any pair of target FUNCs shares SCHEMA overlap via FLOW→SCHEMA(relation) + ND-02 matrix. */
69
- function passesSchemaOverlap(graph, targetFuncIds) {
70
- const idx = indexOf(graph);
71
- const nd02 = getND02SimilarityMatrix();
72
- if (!nd02)
73
- return true; // no matrix → skip check, assume pass
74
- // For each target FUNC, collect connected SCHEMA ids via FLOW→SCHEMA(relation)
75
- const funcSchemasMap = new Map();
76
- for (const fid of targetFuncIds) {
77
- funcSchemasMap.set(fid, funcToSchemas(graph, fid));
78
- }
79
- // Check pairwise: any pair of target FUNCs with overlapping SCHEMAs (similarity >= threshold)
80
- const { schemaIds, matrix } = nd02;
81
- const idxOf = new Map(schemaIds.map((id, i) => [id, i]));
82
- for (let i = 0; i < targetFuncIds.length; i++) {
83
- for (let j = i + 1; j < targetFuncIds.length; j++) {
84
- const si = funcSchemasMap.get(targetFuncIds[i]);
85
- const sj = funcSchemasMap.get(targetFuncIds[j]);
86
- for (const a of si) {
87
- for (const b of sj) {
88
- const ai = idxOf.get(a), bi = idxOf.get(b);
89
- if (ai !== undefined && bi !== undefined && matrix[ai][bi] >= SCHEMA_OVERLAP_THRESHOLD)
90
- return true;
91
- if (a === b)
92
- return true; // same SCHEMA = 100% overlap
93
- }
94
- }
95
- }
96
- }
97
- return false;
98
- }
99
32
  /**
100
- * AO-D03: Duplicate Path Detection.
101
- * FUNC A sends io to both B and C, where B and C connect to the same SCHEMA elements.
33
+ * CR-SM-283: **AO-D03 (`DuplicatePathDetection`) ist ersatzlos entfallen.**
34
+ *
35
+ * Die Regel suchte `FUNC -io-> FUNC` — ein Paar, das `TRACE_PATTERNS` nicht kennt und das R-18
36
+ * als `error` ablehnt. Sie konnte STRUKTURELL nicht feuern und hat es an keinem Familiengraphen
37
+ * je getan (Gate 7: 0 Befunde ueber fuenf Graphen). Eine Regel, die nie feuert, kostet trotzdem
38
+ * dauerhaft: einen Nenner-Anteil, eine Katalogzeile, eine readiness-Zuordnung, einen
39
+ * Golden-File-Eintrag und einen Leser, der sie von den anderen unterscheiden muss.
40
+ *
41
+ * **Nicht verwechseln:** die Aufgabe, die sie tragen sollte — zwei Wirkketten, die auf einer
42
+ * Ebene mitgliedsgleich werden — bleibt ungeloest (`se:top-level`: „Compare the member sets
43
+ * yourself — no rule does it for you"). BW-02 ersetzt sie nicht; das ist ein eigener CR.
102
44
  */
103
- export function aoD03DuplicatePath(graph) {
45
+ // ---------------------------------------------------------------------------
46
+ // BW-02: Randbreite der FUNC-Whitebox (CR-SM-283)
47
+ //
48
+ // Die Zahl der VERSCHIEDENEN Vertraege, die den Rand einer zerlegten FUNC queren — dieselbe
49
+ // Zaehlbasis wie CR-01/R-04 (SCHEMA, nicht rohe io-Kanten, CR-SM-274/276), nur ist die
50
+ // Innen-Menge der `compose`-Teilbaum statt des Modul-Teilbaums. Gerechnet im selben Durchlauf
51
+ // wie `byModule` (`module-crossings.ts`), damit es EINE Definition von „Rand" gibt.
52
+ //
53
+ // Warum es die Regel ueberhaupt braucht: eine zerlegte FUNC ist die Whitebox, in die der Leser
54
+ // hineinklickt. Ihre Randbreite entscheidet, ob er den Ueberblick behaelt — und bis hierher
55
+ // misst sie NIEMAND. AO-D01 misst Relay-Knoten, IO-01 die Kettenkohaerenz, R-31 die blosse
56
+ // Verdrahtung. Gemessen: 12 Befunde an graphcode, davon 0 in Ueberlappung mit irgendeiner
57
+ // FUNC-Regel (Gate 6).
58
+ //
59
+ // Schwelle aus der Verteilung, nicht gewaehlt: bok und graph-view-edit enden beide bei 3,
60
+ // graphcode geht bis 19, moneyflow bis 17. Kein `info`-Level — bei Schwelle 3 truege bok 4 von
61
+ // 9 Blackboxes, und `readiness` zaehlt `info` ungefiltert in den Nenner (MT-03-Praezedenz).
62
+ // ---------------------------------------------------------------------------
63
+ export function bw02WhiteboxWidth(graph, policy) {
64
+ const steps = policy.boundaryWidth;
65
+ if (steps === null)
66
+ return [];
104
67
  const idx = indexOf(graph);
105
68
  const violations = [];
106
- const funcs = idx.elementsOfType('FUNC');
107
- const funcIdSet = new Set(funcs.map(f => f.id));
108
- const seen = new Set();
109
- // Pre-compute: FUNC → set of SCHEMA targets via FLOW→SCHEMA(relation) (CR-165)
110
- const funcToSchemasMap = new Map();
111
- for (const f of funcs) {
112
- funcToSchemasMap.set(f.id, funcToSchemas(graph, f.id));
113
- }
114
- for (const a of funcs) {
115
- const ioTargets = graph.traces
116
- .filter(t => t.type === 'io' && t.source === a.id && funcIdSet.has(t.target))
117
- .map(t => t.target);
118
- if (ioTargets.length < 2)
69
+ for (const [funcId, contracts] of moduleCrossings(graph).byFunc) {
70
+ if (contracts.size < steps.warning)
119
71
  continue;
120
- for (let i = 0; i < ioTargets.length; i++) {
121
- for (let j = i + 1; j < ioTargets.length; j++) {
122
- const b = ioTargets[i], c = ioTargets[j];
123
- const key = [a.id, b, c].sort().join(':');
124
- if (seen.has(key))
125
- continue;
126
- const sb = funcToSchemasMap.get(b) ?? new Set();
127
- const sc = funcToSchemasMap.get(c) ?? new Set();
128
- const shared = [...sb].filter(s => sc.has(s));
129
- if (shared.length === 0)
130
- continue;
131
- seen.add(key);
132
- violations.push({
133
- rule_id: 'AO-D03',
134
- severity: 'info',
135
- element_id: a.id,
136
- message: `${a.id} sends io to ${b} and ${c} which share ${shared.length} SCHEMA target(s): ${shared.join(', ')}`,
137
- fix_hint: 'Consider introducing a mediator or unifying the data paths',
138
- });
139
- }
140
- }
72
+ const el = idx.byId.get(funcId);
73
+ violations.push({
74
+ rule_id: 'BW-02',
75
+ severity: 'warning',
76
+ element_id: funcId,
77
+ message: `${funcId} exposes ${contracts.size} distinct contract(s) at its whitebox boundary (>= ${steps.warning})`,
78
+ fix_hint: 'Consolidate the contracts crossing this boundary, or introduce a level so fewer of them are visible at once',
79
+ // CR-SM-288: `>= steps.warning` ist inklusiv, also ist `steps.warning - 1` der groesste
80
+ // Wert, der noch im Budget liegt — die Masse `value - threshold` ist damit > 0 fuer jeden
81
+ // Befund dieser Regel.
82
+ context: { element_type: el?.type, element_name: el?.name, value: contracts.size, threshold: steps.warning - 1 },
83
+ });
141
84
  }
142
- return violations;
85
+ // Die Einfuegereihenfolge von `byFunc` folgt der FLOW-Reihenfolge des Graphen — hier wird sie
86
+ // kanonisch gemacht, sonst haengt die Befund-Sequenz an der Trace-Reihenfolge (Gate 5).
87
+ return violations.sort((a, b) => (a.element_id < b.element_id ? -1 : a.element_id > b.element_id ? 1 : 0));
143
88
  }
144
89
  // ---------------------------------------------------------------------------
145
90
  // CR-01: Crossing Flow Count (CR-191)
146
- // For each module pair (A, B): count io paths FUNC_A FUNC_B
91
+ // Je Modulpaar (A, B): die VERSCHIEDENEN Vertraege, die den Rand queren — der io-Pfad
92
+ // `FUNC_A -io-> FLOW -io-> FUNC_B` mit FUNC_A in A und FUNC_B in B, gezaehlt ueber das
93
+ // SCHEMA des FLOW.
147
94
  //
148
95
  // CR-SM-236: die Schwelle ist Eingabe. `policy.crossingFlows === null` heisst messen statt
149
96
  // urteilen — die Regel schweigt dann vollstaendig, auch unterhalb der Schwelle. Vorher gab es
150
97
  // diesen Aus-Zustand nicht: jedes Modulpaar mit >= 1 Kreuzung erzeugte eine info-Meldung.
98
+ //
99
+ // CR-SM-274 (Befund aus Spike CR-GC-438 §6): die Regel suchte direkte `FUNC -io-> FUNC`-Kanten.
100
+ // `TRACE_PATTERNS` kennt fuer `io` nur `ACTOR<->FLOW` und `FUNC<->FLOW` — R-18 lehnt eine
101
+ // FUNC->FUNC-io-Kante ab, die Regel konnte STRUKTURELL nicht feuern und tat es in keinem der
102
+ // 10 gemessenen Familie-Graphen (0 Befunde), waehrend ihre Schwelle in der MetricPolicy
103
+ // gepflegt wurde. Jetzt laeuft sie den Pfad, den ihr Kopf meint.
104
+ //
105
+ // Zaehlbasis = SCHEMA, nicht Einzelquerung. Begruendung (CR-SM-274):
106
+ // 1. Modul-Kopplung ist die Zahl der Vertraege, auf die sich zwei Module einigen muessen.
107
+ // Ein FLOW mit 3 Produzenten und 3 Konsumenten ist EIN Vertrag, nicht 9 Kopplungen —
108
+ // der Fix-Hint ("Mediator einziehen") greift je Vertrag, nicht je Konsument.
109
+ // 2. Die rohe Produzent-x-Konsument-Zaehlung ist hub-empfindlich und durch FLOW-Splitting
110
+ // manipulierbar (CR-436 Befund 3: X 172 -> 1539 durch einen Merge ohne Code-Kopplung).
111
+ // SCHEMA ist die Zaehlbasis, die der Spike als gaming-fest gemessen hat (Kill 2).
112
+ // 3. `FLOW -relation-> SCHEMA` ist seit CR-SM-271 Teil 2 `1..1` und per R-18 erzwungen —
113
+ // die Zaehlbasis ist damit garantiert vorhanden, kein optionales Feld.
114
+ // Ein FLOW ohne SCHEMA zaehlt als eigener, untypisierter Vertrag (R-18 meldet ihn separat);
115
+ // er darf nicht stillschweigend mit anderen verschmelzen.
116
+ //
117
+ // CR-SM-276: die Traversierung steht in `module-crossings.ts` — dieselbe Definition von
118
+ // „Kreuzung" fuer CR-01 (je Modulpaar) und R-04 (je Modul). Zwei Kopien waeren zwei Begriffe.
151
119
  // ---------------------------------------------------------------------------
152
120
  export function cr01CrossingFlowCount(graph, policy) {
153
- const idx = indexOf(graph);
154
121
  const violations = [];
155
122
  const steps = policy.crossingFlows;
156
123
  if (steps === null)
157
124
  return violations;
158
- const mods = idx.elementsOfType('MOD');
159
- // Build MOD → Set<FUNC-id> mapping
160
- const modFuncs = new Map();
161
- for (const mod of mods) {
162
- const funcIds = new Set(idx.in(mod.id, 'allocate').map(t => t.source));
163
- modFuncs.set(mod.id, funcIds);
164
- }
165
- // Count crossings per module pair
166
- const pairCounts = new Map();
167
- for (const t of graph.traces) {
168
- if (t.type !== 'io')
169
- continue;
170
- let srcMod;
171
- let tgtMod;
172
- for (const [mid, fids] of modFuncs) {
173
- if (fids.has(t.source))
174
- srcMod = mid;
175
- if (fids.has(t.target))
176
- tgtMod = mid;
177
- }
178
- if (!srcMod || !tgtMod || srcMod === tgtMod)
179
- continue;
180
- const key = [srcMod, tgtMod].sort().join('::');
181
- if (!pairCounts.has(key))
182
- pairCounts.set(key, { modA: srcMod, modB: tgtMod, count: 0 });
183
- pairCounts.get(key).count++;
184
- }
185
- for (const { modA, modB, count } of pairCounts.values()) {
125
+ for (const { modA, modB, contracts } of moduleCrossings(graph).pairs.values()) {
126
+ const count = contracts.size;
186
127
  if (count <= 0)
187
128
  continue;
188
129
  const over = count >= steps.warning;
@@ -190,93 +131,19 @@ export function cr01CrossingFlowCount(graph, policy) {
190
131
  rule_id: 'CR-01',
191
132
  severity: over ? 'warning' : 'info',
192
133
  element_id: modA,
193
- message: `${modA} ↔ ${modB}: ${count} crossing io flow(s)`,
194
- fix_hint: over ? 'Reduce coupling between modules or introduce a mediator' : 'Consider if coupling is intentional',
134
+ message: `${modA} ↔ ${modB}: ${count} distinct contract(s) crossing the module boundary (io path FUNC→FLOW→FUNC)`,
135
+ fix_hint: over
136
+ ? `Reduce coupling between modules or introduce a mediator (>= ${steps.warning} distinct contracts at one boundary)`
137
+ : 'Consider if coupling is intentional',
138
+ // CR-SM-288: dasselbe Budget fuer beide Severities. Ein `info`-Paar liegt per Konstruktion
139
+ // INNERHALB des Budgets und traegt damit die Masse 0 — es ist kein kleiner Verstoss,
140
+ // sondern keiner.
141
+ context: { value: count, threshold: steps.warning - 1 },
195
142
  });
196
143
  }
197
144
  return violations;
198
145
  }
199
146
  // ---------------------------------------------------------------------------
200
- // RT-01: Physical boundary integrity (CR-191)
201
- // FUNCs must not be allocated directly to physical MODs — use logical sub-modules.
202
- // ---------------------------------------------------------------------------
203
- export function rt01PhysicalBoundaryIntegrity(graph) {
204
- const idx = indexOf(graph);
205
- const physicalMods = graph.elements.filter(e => e.type === 'MOD' && e.attributes?.kind === 'physical');
206
- return graph.traces
207
- .filter(t => t.type === 'allocate' && physicalMods.some(m => m.id === t.target))
208
- .filter(t => idx.typeOf(t.source) === 'FUNC')
209
- .map(t => ({
210
- rule_id: 'RT-01',
211
- severity: 'error',
212
- element_id: t.source,
213
- message: `${t.source} allocated directly to physical module ${t.target} — must use logical sub-module`,
214
- fix_hint: 'Create a logical MOD inside the physical MOD and allocate the FUNC there',
215
- }));
216
- }
217
- // ---------------------------------------------------------------------------
218
- // PH-01: Physical MOD completeness (CR-191)
219
- // Physical MODs should have at least one logical sub-module via compose trace.
220
- // ---------------------------------------------------------------------------
221
- export function ph01PhysicalModCompleteness(graph) {
222
- const idx = indexOf(graph);
223
- const physicalMods = graph.elements.filter(e => e.type === 'MOD' && e.attributes?.kind === 'physical');
224
- return physicalMods
225
- .filter(pm => !graph.traces.some(t => t.source === pm.id && t.type === 'compose' &&
226
- idx.typeOf(t.target) === 'MOD'))
227
- .map(pm => ({
228
- rule_id: 'PH-01',
229
- severity: 'info',
230
- element_id: pm.id,
231
- message: `${pm.id} (physical) has no logical sub-modules`,
232
- fix_hint: 'Add at least one logical MOD via compose trace',
233
- context: { element_type: pm.type, element_name: pm.name },
234
- }));
235
- }
236
- // ---------------------------------------------------------------------------
237
- // CA-01: Capability allocation check (CR-191)
238
- // FUNC @requires must be satisfied by the physical MOD's @capability.
239
- // ---------------------------------------------------------------------------
240
- export function ca01CapabilityAllocation(graph) {
241
- const idx = indexOf(graph);
242
- const violations = [];
243
- const funcsWithRequires = graph.elements.filter(e => e.type === 'FUNC' && e.attributes?.requires);
244
- for (const func of funcsWithRequires) {
245
- const requires = (Array.isArray(func.attributes.requires)
246
- ? func.attributes.requires
247
- : [func.attributes.requires]);
248
- // Find logical MOD via allocate
249
- const allocTrace = graph.traces.find(t => t.source === func.id && t.type === 'allocate');
250
- if (!allocTrace)
251
- continue;
252
- const logicalModCandidate = idx.byId.get(allocTrace.target);
253
- const logicalMod = logicalModCandidate?.type === 'MOD' ? logicalModCandidate : undefined;
254
- if (!logicalMod)
255
- continue;
256
- // Find physical parent MOD (compose source → logical MOD target)
257
- const physParentTrace = graph.traces.find(t => t.type === 'compose' && t.target === logicalMod.id &&
258
- (idx.byId.get(t.source)?.type === 'MOD' && idx.byId.get(t.source)?.attributes?.kind === 'physical'));
259
- if (!physParentTrace)
260
- continue;
261
- const physMod = idx.byId.get(physParentTrace.source);
262
- const capabilities = (Array.isArray(physMod.attributes?.capability)
263
- ? physMod.attributes.capability
264
- : physMod.attributes?.capability ? [physMod.attributes.capability] : []);
265
- const missing = requires.filter(r => !capabilities.includes(r));
266
- if (missing.length > 0) {
267
- violations.push({
268
- rule_id: 'CA-01',
269
- severity: 'error',
270
- element_id: func.id,
271
- message: `${func.id} requires [${missing.join(', ')}] but ${physMod.id} only provides [${capabilities.join(', ')}]`,
272
- fix_hint: `Add missing capabilities to ${physMod.id} or move ${func.id} to a capable module`,
273
- context: { element_type: func.type, element_name: func.name, parent_module: physMod.id },
274
- });
275
- }
276
- }
277
- return violations;
278
- }
279
- // ---------------------------------------------------------------------------
280
147
  // IO-01: die Wirkkette haengt zusammen (CR-192, CR-SM-226, CR-SM-250)
281
148
  //
282
149
  // Geprueft wird ERREICHBARKEIT im io-Teilgraphen der Kette, nicht Adjazenz. Bis CR-SM-250
@@ -393,12 +260,8 @@ export function io01CrossModuleCompleteness(graph) {
393
260
  return violations;
394
261
  }
395
262
  export const AO_RULES = [
396
- { id: 'AO-D01', name: 'RelayNodeDetection', severity: 'info', evaluate: aoD01RelayNode, domain: ['FUNC'] },
397
- { id: 'AO-D03', name: 'DuplicatePathDetection', severity: 'info', evaluate: aoD03DuplicatePath, domain: ['FUNC'] },
263
+ { id: 'BW-02', name: 'Whitebox boundary width', severity: 'warning', evaluate: bw02WhiteboxWidth, domain: ['FUNC'] },
398
264
  { id: 'CR-01', name: 'CrossingFlowCount', severity: 'warning', evaluate: cr01CrossingFlowCount, domain: ['MOD'] },
399
- { id: 'RT-01', name: 'PhysicalBoundaryIntegrity', severity: 'error', evaluate: rt01PhysicalBoundaryIntegrity, domain: ['FUNC'] },
400
- { id: 'PH-01', name: 'PhysicalModCompleteness', severity: 'info', evaluate: ph01PhysicalModCompleteness, domain: ['MOD'] },
401
- { id: 'CA-01', name: 'CapabilityAllocation', severity: 'error', evaluate: ca01CapabilityAllocation, domain: ['FUNC'] },
402
265
  // CR-SM-242: domain ist FUNC, nicht MOD — die Regel laeuft ueber FUNC-Paare in FCHAINs und
403
266
  // meldet am FUNC. Mit ['MOD'] uebertraf ihr Zaehler (21) ihren eigenen Nenner-Beitrag (10);
404
267
  // nur der Clamp verhinderte einen negativen Score.
@@ -59,6 +59,34 @@ export interface ConformanceRuleDefinition {
59
59
  severity: RuleSeverity;
60
60
  evaluate: (graph: OntologyGraph, facts: CodeFacts) => RuleViolation[];
61
61
  }
62
+ /**
63
+ * Wie viel des Import-Graphen konnte RC-05 überhaupt ansehen? (CR-SM-268 Teil 2)
64
+ *
65
+ * Eine MESSUNG über den Lauf, kein Befund über das Modell — deshalb eine eigene Funktion neben
66
+ * den Regeln und keine `RuleViolation`. „12 von 178 Endpunkten nicht zugeordnet" sagt nichts
67
+ * über den Graphen aus; es sagt, wie belastbar die Zahl daneben ist.
68
+ *
69
+ * Vorher hing die Liste als Textanhang an der Meldung eines ANDEREN Befundes und verschwand mit
70
+ * ihm: CR-GC-423 hat die drei RC-05-Drift-Befunde von graphcode geschlossen, und mit ihnen war
71
+ * die Liste der nicht zugeordneten Dateien weg — sie musste in CR-GC-424/425 von Hand
72
+ * rekonstruiert werden. „RC-05: 0 Befunde" hieß damit wahlweise „alle 178 Endpunkte geprüft,
73
+ * keiner driftet" oder „166 geprüft, 12 gar nicht angesehen", und niemand konnte die beiden
74
+ * unterscheiden.
75
+ *
76
+ * Geschwister von `skipped` (CR-GC-398), nicht dasselbe: `skipped` heißt „diese Quelle wurde GAR
77
+ * NICHT ausgewertet" (kein lesbarer repoRoot). Hier ist sie ausgewertet worden, und trotzdem
78
+ * fällt ein Teil durch. Zwei Zustände, zwei Felder — zusammengelegt wären „nicht gelaufen" und
79
+ * „gelaufen, aber blind" wieder ununterscheidbar.
80
+ */
81
+ export interface ImportCoverage {
82
+ /** Verschiedene Dateien, die überhaupt als Import-Endpunkt vorkommen. */
83
+ endpoints: number;
84
+ /** Davon einer MOD zugeordnet — nur diese konnte RC-05 beurteilen. */
85
+ assigned: number;
86
+ /** Der Rest, benannt statt gezählt: ohne die Namen ist die Lücke nicht schließbar. */
87
+ unassigned: string[];
88
+ }
89
+ export declare function importCoverage(graph: OntologyGraph, facts: CodeFacts): ImportCoverage;
62
90
  /** All RC conformance rules — evaluated by executors that can supply CodeFacts. */
63
91
  export declare const CODE_CONFORMANCE_RULES: ConformanceRuleDefinition[];
64
92
  /** Run all RC rules against a graph + extracted code facts. */
@@ -267,15 +267,19 @@ function schemaRefMustBeUsed(graph, facts) {
267
267
  // such adjacency ⇒ one RC-05 per (fromMod → toMod) pair, evidence = the
268
268
  // crossing file imports.
269
269
  // ---------------------------------------------------------------------------
270
- function importDriftConformance(graph, facts) {
271
- const importEdges = facts.importEdges ?? [];
272
- if (importEdges.length === 0)
273
- return [];
270
+ /**
271
+ * Datei MOD, die EINE Auflösung für RC-05 und `importCoverage` (CR-SM-268 Teil 2).
272
+ *
273
+ * Bewusst herausgezogen und nicht zweimal geschrieben: die Abdeckungszahl muss dieselbe Frage
274
+ * stellen wie die Regel, sonst misst sie etwas anderes als das, was geprüft wird — und eine
275
+ * Abdeckungszahl, die nicht zur Prüfung passt, ist schlimmer als keine.
276
+ *
277
+ * Zwei Wege, in dieser Reihenfolge: die `realRef` einer allozierten FUNC (eine gebundene Datei
278
+ * gehört zur MOD ihrer FUNC), danach das `path`-Präfix einer MOD, longest-prefix. Wer keinen
279
+ * von beiden trifft, ist `unassigned` — und wird nie still verworfen.
280
+ */
281
+ function buildModResolver(graph) {
274
282
  const typeOf = new Map(graph.elements.map(e => [e.id, e.type]));
275
- const modIds = graph.elements.filter(e => e.type === 'MOD').map(e => e.id);
276
- if (modIds.length === 0)
277
- return [];
278
- // 1. file → MOD. Direct realRef bindings first.
279
283
  const fileToMod = new Map();
280
284
  for (const el of graph.elements) {
281
285
  if (el.type !== 'FUNC')
@@ -292,7 +296,7 @@ function importDriftConformance(graph, facts) {
292
296
  .filter(e => e.type === 'MOD' && typeof e.attributes?.path === 'string')
293
297
  .map(e => ({ id: e.id, path: e.attributes.path.replace(/[*].*$/, '').replace(/\/+$/, '') }))
294
298
  .filter(m => m.path.length > 0);
295
- const resolveMod = (file) => {
299
+ return (file) => {
296
300
  if (fileToMod.has(file))
297
301
  return fileToMod.get(file);
298
302
  let best;
@@ -303,6 +307,27 @@ function importDriftConformance(graph, facts) {
303
307
  }
304
308
  return best?.id;
305
309
  };
310
+ }
311
+ export function importCoverage(graph, facts) {
312
+ const importEdges = facts.importEdges ?? [];
313
+ const resolveMod = buildModResolver(graph);
314
+ const endpoints = new Set();
315
+ for (const edge of importEdges) {
316
+ endpoints.add(edge.from);
317
+ endpoints.add(edge.to);
318
+ }
319
+ const unassigned = [...endpoints].filter(f => resolveMod(f) === undefined).sort();
320
+ return { endpoints: endpoints.size, assigned: endpoints.size - unassigned.length, unassigned };
321
+ }
322
+ function importDriftConformance(graph, facts) {
323
+ const importEdges = facts.importEdges ?? [];
324
+ if (importEdges.length === 0)
325
+ return [];
326
+ const modIds = graph.elements.filter(e => e.type === 'MOD').map(e => e.id);
327
+ if (modIds.length === 0)
328
+ return [];
329
+ const typeOf = new Map(graph.elements.map(e => [e.id, e.type]));
330
+ const resolveMod = buildModResolver(graph);
306
331
  // 2. graph MOD-adjacency (undirected) from io crossings FUNC_A ─io→ FLOW ─io→ FUNC_B.
307
332
  const funcMod = (funcId) => {
308
333
  const t = graph.traces.find(tr => tr.source === funcId && tr.type === 'allocate' && typeOf.get(tr.target) === 'MOD');
@@ -332,16 +357,15 @@ function importDriftConformance(graph, facts) {
332
357
  const [x, y] = [a, b].sort();
333
358
  return adjacent.has(`${x}|${y}`);
334
359
  };
335
- // 3. drift per (fromMod → toMod), aggregating evidence; collect unassigned files.
360
+ // 3. drift per (fromMod → toMod), aggregating evidence.
361
+ // CR-SM-268 Teil 2: die nicht zugeordneten Dateien werden hier NICHT mehr gesammelt. Sie
362
+ // hingen als Textanhang an der Meldung eines anderen Befundes und verschwanden mit ihm
363
+ // (CR-GC-423). Ihr Ort ist jetzt `importCoverage` — eine Messung, die auch dann etwas sagt,
364
+ // wenn diese Regel schweigt.
336
365
  const drift = new Map();
337
- const unassigned = new Set();
338
366
  for (const edge of importEdges) {
339
367
  const fromMod = resolveMod(edge.from);
340
368
  const toMod = resolveMod(edge.to);
341
- if (!fromMod)
342
- unassigned.add(edge.from);
343
- if (!toMod)
344
- unassigned.add(edge.to);
345
369
  if (!fromMod || !toMod || fromMod === toMod)
346
370
  continue;
347
371
  if (isAdjacent(fromMod, toMod))
@@ -352,7 +376,6 @@ function importDriftConformance(graph, facts) {
352
376
  drift.set(key, entry);
353
377
  }
354
378
  const nameOf = new Map(graph.elements.map(e => [e.id, e.name]));
355
- const unassignedNote = unassigned.size > 0 ? ` (unassigned files not mapped to a MOD: ${[...unassigned].sort().join(', ')})` : '';
356
379
  return [...drift.values()].map(d => {
357
380
  const shown = d.evidence.slice(0, 5);
358
381
  const more = d.evidence.length > shown.length ? ` +${d.evidence.length - shown.length} more` : '';
@@ -360,7 +383,7 @@ function importDriftConformance(graph, facts) {
360
383
  rule_id: 'RC-05',
361
384
  severity: 'warning',
362
385
  element_id: d.fromMod,
363
- message: `${d.fromMod} imports ${d.toMod} across a module boundary the graph does not document — evidence: ${shown.join('; ')}${more}${unassignedNote}`,
386
+ message: `${d.fromMod} imports ${d.toMod} across a module boundary the graph does not document — evidence: ${shown.join('; ')}${more}`,
364
387
  fix_hint: `Document the ${d.fromMod}→${d.toMod} dependency in the graph (an io/FLOW connection between their FUNCs), or remove the import`,
365
388
  context: {
366
389
  element_type: 'MOD',