@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
@@ -1,3 +1,8 @@
1
+ import { normalizeReqKinds } from './ontology.js';
2
+ /** Die Kinds, die eine Wirkkette bzw. ein Verhalten traegt. */
3
+ const BEHAVIOURAL_KINDS = ['functional', 'precondition', 'postcondition'];
4
+ /** Die Kinds, die eine Struktur (Modul/System) traegt. */
5
+ const STRUCTURAL_KINDS = ['non-functional', 'risk', 'mitigation'];
1
6
  export const TRACE_PATTERNS = [
2
7
  // ── compose (parent → child) ──
3
8
  { source: 'SYS', target: 'SYS', type: 'compose', cardinality: '0..*', description: 'System decomposes into subsystems' },
@@ -15,11 +20,25 @@ export const TRACE_PATTERNS = [
15
20
  { source: 'FLOW', target: 'ACTOR', type: 'io', description: 'Flow delivers to actor' },
16
21
  { source: 'FUNC', target: 'FLOW', type: 'io', description: 'Function outputs to flow' },
17
22
  { source: 'FLOW', target: 'FUNC', type: 'io', description: 'Flow feeds into function' },
18
- { source: 'ACTOR', target: 'UC', type: 'io', description: 'Actor triggers use case' },
19
- { source: 'FLOW', target: 'UC', type: 'io', description: 'Flow feeds use case' },
20
- { source: 'MOD', target: 'MOD', type: 'io', description: 'Module communicates with module (ACL)' },
23
+ // CR-SM-266 D1: `ACTOR -io-> UC` ENTFAELLT (BREAKING). Der tragende Pfad existiert und ist
24
+ // typisiert: `ACTOR -io-> FLOW -io-> FUNC`, FUNC in der FCHAIN des UC. Die Direktkante war
25
+ // die TYPLOSE Abkuerzung daneben ein Actor-Beitrag ohne FLOW traegt kein SCHEMA und
26
+ // entzieht sich damit derselben Typgarantie, die fuer jeden anderen Fluss gilt. Migration
27
+ // je Bestandskante: FLOW einfuehren oder Kante loeschen (70 Kanten ueber 6 Graphen).
28
+ // CR-SM-266 D2: `MOD -io-> MOD` ENTFAELLT (BREAKING). Modul-Kopplung ist vollstaendig
29
+ // ABLEITBAR: `FUNC -allocate-> MOD` x `FUNC -io-> FLOW -io-> FUNC` ergibt die
30
+ // Modul-Ebenen-Projektion. Eine BEHAUPTETE Kopplungskante kann von der tatsaechlichen
31
+ // driften, eine berechnete nicht — und die behauptete gewann, weil sie billiger war.
32
+ // Soll-Architektur wird als REQ (non-functional) modelliert, nicht als Kante.
33
+ // CR-SM-266 D4: `FLOW -io-> UC` ENTFAELLT (BREAKING). Zweitweg neben `FLOW -io-> FUNC` mit
34
+ // FUNC in der FCHAIN des UC. Die UC-Grenze ERGIBT SICH aus den FLOWs der Kettenglieder; eine
35
+ // direkte FLOW-UC-Kante umgeht die Kettenzuordnung und damit IO-01 und R-21.
21
36
  // ── satisfy (implementation) ──
22
- { source: 'FUNC', target: 'REQ', type: 'satisfy', description: 'Function implements requirement' },
37
+ {
38
+ source: 'FUNC', target: 'REQ', type: 'satisfy',
39
+ where: { on: 'target', field: 'kinds', allowed: BEHAVIOURAL_KINDS },
40
+ description: 'Function implements requirement — behavioural kinds only',
41
+ },
23
42
  // CR-GC-366: `FUNC -satisfy-> UC` ENTFAELLT (BREAKING). Es war ein zweiter, redundanter Weg
24
43
  // vom Verhalten zum Use Case neben dem einzig tragenden `UC -compose-> FCHAIN -compose-> FUNC`.
25
44
  // Zwei Wege = zwei Wahrheiten: eine FUNC konnte einen UC bedienen, ohne in dessen Wirkkette zu
@@ -29,14 +48,42 @@ export const TRACE_PATTERNS = [
29
48
  // stand ohnehin in einer FCHAIN); die restlichen 13 sind der Migrationsanlass, nicht ein
30
49
  // Argument fuer das Pattern. Ersatz: die FUNC in die FCHAIN des UC aufnehmen (R-30).
31
50
  // CR-154: NFR satisfy — non-functional REQs can be satisfied by chains, modules, or the system
51
+ //
52
+ // CR-SM-266 B: die drei strukturellen satisfy-Patterns tragen jetzt ein `where`. Ohne es
53
+ // konnte ein MOD oder SYS ein FUNKTIONALES REQ erfuellen, ohne dass eine Wirkkette es traegt
54
+ // — der Zweitweg aus CR-GC-366, eine Ebene hoeher. Die FCHAIN behaelt bewusst ALLE Kinds:
55
+ // sie IST die Wirkkette, an ihr haengen IO-01 und R-21, sie ist also nie die Abkuerzung.
32
56
  { source: 'FCHAIN', target: 'REQ', type: 'satisfy', description: 'Function chain satisfies end-to-end NFR (e.g. latency)' },
33
- { source: 'MOD', target: 'REQ', type: 'satisfy', description: 'Module satisfies budget NFR (e.g. uptime, memory)' },
34
- { source: 'SYS', target: 'REQ', type: 'satisfy', description: 'System satisfies system-level NFR (e.g. availability)' },
57
+ {
58
+ source: 'MOD', target: 'REQ', type: 'satisfy',
59
+ where: { on: 'target', field: 'kinds', allowed: STRUCTURAL_KINDS },
60
+ description: 'Module satisfies budget NFR (e.g. uptime, memory) — structural kinds only',
61
+ },
62
+ {
63
+ source: 'SYS', target: 'REQ', type: 'satisfy',
64
+ where: { on: 'target', field: 'kinds', allowed: STRUCTURAL_KINDS },
65
+ description: 'System satisfies system-level NFR (e.g. availability) — structural kinds only',
66
+ },
35
67
  // ── verify (INCOSE: test verifies requirement) ──
36
68
  { source: 'TEST', target: 'REQ', type: 'verify', description: 'Test verifies requirement' },
37
69
  // ── allocate (deployment/assignment) ──
38
- { source: 'FUNC', target: 'MOD', type: 'allocate', description: 'Function deployed in module' },
39
- { source: 'FLOW', target: 'SCHEMA', type: 'relation', description: 'Flow data format defined by schema' },
70
+ // CR-SM-266b A/C: OBERGRENZEN als Grammatik. Beide Kanten sind Besitz-Aussagen, und Besitz
71
+ // ist nicht mehrfach vergebbar eine FUNC wohnt in EINEM Modul, ein FLOW traegt EINEN
72
+ // Datenvertrag. Zwei Allokationen sind kein "staerkeres" Modell, sondern eine Frage ohne
73
+ // Antwort ("in welchem Modul liegt der Code?"), und zwei SCHEMA an einem FLOW heben genau
74
+ // die Typgarantie auf, fuer die der geteilte SCHEMA-Knoten existiert (CR-SM-266 C): zwei
75
+ // Vertraege koennen divergieren, einer kann es nicht.
76
+ //
77
+ // `FUNC -allocate-> MOD` traegt nur die OBERE Grenze (`0..1`): seine Untergrenze bleibt
78
+ // R-22 (warning) — eine frisch angelegte FUNC ist legitim noch unalloziert, und ein
79
+ // error-Bein wuerde jedes schrittweise Authoring per Delta-Semantik blockieren.
80
+ //
81
+ // CR-SM-271 Teil 2: `FLOW -relation-> SCHEMA` ist `1..1` — beide Grenzen Grammatik. Ein
82
+ // FLOW ohne SCHEMA ist kein schwaecherer Typ, sondern untypisiert; die fruehere Untergrenze
83
+ // SC-04 (warning) ist damit ersatzlos entfallen (Grammatik ersetzt Regel, das urspruengliche
84
+ // Ziel von CR-SM-266 C). Durchgesetzt als drittes Bein von R-18 (s. REQUIRED_PATTERNS).
85
+ { source: 'FUNC', target: 'MOD', type: 'allocate', cardinality: '0..1', description: 'Function deployed in exactly one module' },
86
+ { source: 'FLOW', target: 'SCHEMA', type: 'relation', cardinality: '1', description: 'Flow data format defined by exactly one schema' },
40
87
  // CR-228: REQ→MOD allocate removed — a MOD does not own a REQ. Physical NFRs use
41
88
  // MOD→satisfy→REQ, behavioral NFRs FCHAIN→satisfy→REQ; structural constraints are
42
89
  // rules, not REQs. R-18 now flags any REQ→MOD allocate edge as invalid.
@@ -44,24 +91,118 @@ export const TRACE_PATTERNS = [
44
91
  { source: 'MS', target: 'FUNC', type: 'compose', description: 'Milestone includes function' },
45
92
  { source: 'MS', target: 'REQ', type: 'compose', description: 'Milestone includes requirement' },
46
93
  { source: 'MS', target: 'UC', type: 'compose', description: 'Milestone includes use case' },
47
- { source: 'MS', target: 'MS', type: 'compose', description: 'Sub-milestone' },
48
- { source: 'MS', target: 'MS', type: 'relation', label: 'depends-on', description: 'Milestone dependency' },
94
+ // CR-SM-266 D3: `MS -compose-> MS` ENTFAELLT (BREAKING). Zwei Kantenarten zwischen denselben
95
+ // zwei Meilensteinen sagten dasselbe auf zwei Arten Schachtelung und Abfolge sind an einem
96
+ // Meilenstein nicht unterscheidbar, MS-01 zaehlte den Scope einmal ueber `CR -relation-> MS`
97
+ // (seit CR-SM-245 der einzige Weg) und die compose-Kante trug nichts mehr bei. `relation`
98
+ // bleibt als EINZIGE MS-MS-Kante, Semantik: Ordnung/Abfolge (Vorgaenger → Nachfolger).
99
+ // Migration: keine — 0 solche Kanten in allen 9 aktiven Familie-Graphen (gemessen 2026-08-25).
100
+ { source: 'MS', target: 'MS', type: 'relation', label: 'depends-on', description: 'Milestone order: predecessor → successor' },
49
101
  { source: 'CR', target: 'MS', type: 'relation', description: 'CR assigned to milestone' },
50
102
  // ── CR traceability (CR-155: CR tracks mutated elements) ──
51
103
  { source: 'CR', target: 'UC', type: 'relation', description: 'CR affects use case' },
52
104
  { source: 'CR', target: 'REQ', type: 'relation', description: 'CR affects requirement' },
53
105
  { source: 'CR', target: 'FUNC', type: 'relation', description: 'CR affects function' },
54
106
  { source: 'CR', target: 'MOD', type: 'relation', description: 'CR affects module' },
55
- // ── produces (audit lineage) ──
56
- { source: 'SESSION', target: '*', type: 'produces', category: 'audit', description: 'Session produced/modified element' },
107
+ // CR-SM-266 D5: `SESSION -produces-> *` ENTFAELLT ersatzlos, mit ihm der TraceType `produces`
108
+ // und der ElementType `SESSION`. Der urspruengliche Entwurf hielt das Pattern fuer die
109
+ // Provenance-Kante und wollte es behalten; die MESSUNG hat das widerlegt: 0 produces-Kanten
110
+ // und 0 SESSION-Knoten in ALLEN 9 aktiven Familie-Graphen, auch im graphcode-Selbstmodell
111
+ // nach 195 gegateten Versionen. Die reale Provenance lebt seit CR-GC-347 in
112
+ // `.graphcode/audit.jsonl` — audit_trail/audit_stats lesen die DATEI, nie den Graphen.
113
+ // Einziger Schreiber war ein toter Pfad (CR-195d in graph-api-core), Leser mit Wirkung: keiner.
114
+ // Nicht auf `relation` umgebogen: eine Kante ohne Schreiber UND ohne Leser wird gestrichen,
115
+ // nicht umbenannt.
57
116
  ];
117
+ /**
118
+ * Erfuellt das betroffene Kantenende das Praedikat?
119
+ *
120
+ * TEILMENGEN-Semantik, nicht Schnittmenge: JEDER deklarierte Kind muss in der erlaubten Liste
121
+ * liegen. Die schwaechere Lesart ("mindestens einer passt") waere das Loch, das der ganze
122
+ * Abschnitt schliessen will — ein funktionales REQ zusaetzlich als `non-functional` zu
123
+ * deklarieren haette gereicht, damit ein MOD es wieder erfuellen darf, und der billige
124
+ * Zweitweg waere ueber ein Attribut zurueck.
125
+ *
126
+ * Der leere Fall LEHNT AB (`kinds.length === 0`): ohne Deklaration ist nicht entscheidbar, ob
127
+ * die Kante zulaessig ist, und "unentscheidbar" darf nicht "erlaubt" heissen — sonst wird das
128
+ * WEGLASSEN des Attributs zum Umgehungsweg. Das ist die durchsetzende Haelfte von REQ-X06
129
+ * (`kinds` ist Pflicht am REQ); die meldende Haelfte haengt an BQ-07.
130
+ */
131
+ function satisfiesPredicate(where, trace) {
132
+ // `normalizeReqKinds` statt direktem Zugriff: drei Familie-Graphen tragen `kinds` als String
133
+ // statt als Liste, und ein `.every()` darauf WIRFT — mitten im Regellauf, statt einen Befund
134
+ // zu melden. Die Normalisierung heilt die Drift nicht, sie macht sie nur urteilsfaehig.
135
+ const kinds = normalizeReqKinds(where.on === 'source' ? trace.sourceKinds : trace.targetKinds);
136
+ if (kinds.length === 0)
137
+ return false;
138
+ return kinds.every(k => where.allowed.includes(k));
139
+ }
140
+ /**
141
+ * Die Obergrenze eines Kardinalitaets-Ausdrucks — `Infinity`, wo keine gilt.
142
+ *
143
+ * Getrennt von `isValidTrace`, weil es eine andere ART von Bedingung ist: `isValidTrace`
144
+ * urteilt ueber EINE Kante und braucht nur deren Enden, eine Kardinalitaet urteilt ueber die
145
+ * MENGE der Kanten eines Knotens. Der urspruengliche CR-Entwurf wollte beides in
146
+ * `isValidTrace` erledigen; das geht nicht, und die Korrektur steht im Impact-Audit.
147
+ */
148
+ export function maxOccurs(cardinality) {
149
+ if (cardinality === undefined)
150
+ return Infinity;
151
+ return cardinality === '1' || cardinality === '0..1' ? 1 : Infinity;
152
+ }
153
+ /** Die Patterns mit einer echten Obergrenze — die Arbeitsliste des Kardinalitaets-Beins. */
154
+ export const BOUNDED_PATTERNS = TRACE_PATTERNS.filter(p => maxOccurs(p.cardinality) < Infinity);
155
+ /**
156
+ * Die Untergrenze eines Kardinalitaets-Ausdrucks — `0`, wo keine gilt.
157
+ * Gegenstueck zu `maxOccurs` (CR-SM-271 Teil 2).
158
+ */
159
+ export function minOccurs(cardinality) {
160
+ if (cardinality === undefined)
161
+ return 0;
162
+ return cardinality === '1' || cardinality === '1..*' ? 1 : 0;
163
+ }
164
+ /**
165
+ * Die Patterns, deren Untergrenze DURCHGESETZT wird — nur exakt-`1` (CR-SM-271 Teil 2).
166
+ *
167
+ * Bewusst NICHT `minOccurs(p.cardinality) >= 1`: die `1..*`-compose-Patterns (SYS→UC,
168
+ * UC→FCHAIN, UC→REQ, FCHAIN→FUNC) tragen ihre Untergrenze seit jeher als
169
+ * Vollstaendigkeits-WARNUNG (UC-01, R-15, …) — ein SYS existiert legitim, BEVOR sein erster
170
+ * UC modelliert ist, und ein error-Bein wuerde jeden Kaltstart per Delta-Semantik blockieren.
171
+ * Sie hier mitzunehmen waere zudem die Doppelzaehlung aus Gate 4: eine Ursache, zwei Befunde.
172
+ * Exakt-`1` dagegen ist eine Grammatik-Aussage ohne legitimen Zwischenzustand: ein FLOW ohne
173
+ * SCHEMA ist untypisiert, nicht unfertig — sein SCHEMA gehoert in DENSELBEN Batch.
174
+ */
175
+ export const REQUIRED_PATTERNS = TRACE_PATTERNS.filter(p => p.cardinality === '1');
58
176
  /**
59
177
  * Check if a trace matches a valid pattern in the meta-model.
60
- * Uses element types (not IDs) for validation.
178
+ * Uses element types (not IDs) plus — where a pattern declares one — an attribute predicate.
61
179
  */
62
180
  export function isValidTrace(trace, patterns = TRACE_PATTERNS) {
63
181
  return patterns.some(p => (p.source === '*' || p.source === trace.source) &&
64
182
  (p.target === '*' || p.target === trace.target) &&
65
183
  p.type === trace.type &&
66
- (!p.label || p.label === trace.label));
184
+ (!p.label || p.label === trace.label) &&
185
+ (!p.where || satisfiesPredicate(p.where, trace)));
186
+ }
187
+ /** Passt das Pattern auf das Typ-Paar (Kantentyp, Label) — OHNE das `where` zu befragen? */
188
+ function matchesShape(p, trace) {
189
+ return (p.source === '*' || p.source === trace.source) &&
190
+ (p.target === '*' || p.target === trace.target) &&
191
+ p.type === trace.type &&
192
+ (!p.label || p.label === trace.label);
193
+ }
194
+ /** `undefined`, wenn die Kante gueltig ist — sonst der Grund. */
195
+ export function traceRejection(trace, patterns = TRACE_PATTERNS) {
196
+ if (isValidTrace(trace, patterns))
197
+ return undefined;
198
+ // Nur die Patterns, die am Typ-Paar ueberhaupt greifen. Gibt es keins, war es die Matrix.
199
+ // Gibt es mehrere (kaeme durch ein zweites `where` am selben Paar), entscheidet das erste —
200
+ // sie haben es alle abgelehnt, und der erste Grund ist so wahr wie jeder andere.
201
+ const candidate = patterns.find(p => matchesShape(p, trace) && p.where);
202
+ if (!candidate?.where)
203
+ return { reason: 'no-pattern' };
204
+ const declared = normalizeReqKinds(candidate.where.on === 'source' ? trace.sourceKinds : trace.targetKinds);
205
+ return declared.length === 0
206
+ ? { reason: 'kinds-undeclared', pattern: candidate }
207
+ : { reason: 'kinds-mismatch', pattern: candidate, declared };
67
208
  }
@@ -18,7 +18,7 @@ import type { MetricPolicy } from './policy.js';
18
18
  export declare function mt01Instability(graph: OntologyGraph, policy: MetricPolicy): RuleViolation[];
19
19
  /**
20
20
  * MT-02: LCOM4 (Lack of Cohesion — component count).
21
- * MOD with allocated FUNCs that share no common io/satisfy targets → cohesion problem.
21
+ * MOD with allocated FUNCs that share no common io targets → cohesion problem.
22
22
  * `policy.lcom4.info` ≤ LCOM4 < `policy.lcom4.warning` → info, darüber warning.
23
23
  *
24
24
  * CR-SM-233: die Stufen sind Eingabe, `null` heißt messen statt urteilen.
@@ -85,6 +85,25 @@ export interface ModuleMetrics {
85
85
  external: number;
86
86
  ratio: number;
87
87
  } | null;
88
+ /**
89
+ * CR-SM-301 — Martins *Stable Dependencies Principle*, als MESSUNG und ausdruecklich als
90
+ * KEINE Regel: die Zahl der Vertraege, die dieses Modul von einem WENIGER stabilen Lieferanten
91
+ * bezieht (`I_Lieferant > I_Bezieher`). Abhaengigkeiten „bergauf".
92
+ *
93
+ * Warum keine Regel: CR-SM-298 hat genau das versucht und ist an Gate 7 gescheitert. Ueber
94
+ * 19 Familiengraphen laufen 25 von 250 Abhaengigkeiten bergauf, aber der MEDIAN der
95
+ * Stabilitaetsluecke ist **0.10** — bei einer Vertragsmasse von im Mittel 5 ist das ein
96
+ * Vertrag Unterschied, also Rauschen. Filtert man auf Luecke > 0.2 und beide Enden >= 3
97
+ * Vertraege, bleiben 3 von 25, davon zwei in einem Wegwerf-Trial und einer Demo. Eine Regel
98
+ * haette zwei erfundene Schwellen gebraucht, um EINEN Befund zu produzieren.
99
+ *
100
+ * Was bleibt, ist die Zahl. `graph_metrics` traegt sie in jeder Modulzeile, ein Mensch liest
101
+ * sie neben `instability` und deutet sie — Praezedenz MT-03 (CR-SM-223) und `cohesion` eine
102
+ * Zeile weiter oben: messen, nicht urteilen, ohne Katalogeintrag und ohne Nenner-Anteil.
103
+ *
104
+ * `null`, wo `instability` es auch ist — ohne Kopplung gibt es keine Richtung.
105
+ */
106
+ uphillDependencies: number | null;
88
107
  }
89
108
  /**
90
109
  * Per-module architecture metrics as NUMBERS — one row per MOD, threshold or not
@@ -1,5 +1,8 @@
1
1
  import { decomposedFuncs } from './rules.js';
2
2
  import { indexOf } from './graph-index.js';
3
+ import { moduleCrossings } from './module-crossings.js';
4
+ /** Geteilte leere Menge — spart je Modul zwei Allokationen in `measureModulesUncached`. */
5
+ const EMPTY_SET = new Set();
3
6
  /**
4
7
  * MT-01: Module Instability (CR-165: indirect via allocate-path).
5
8
  * I = fan_out / (fan_in + fan_out) > `policy.instability` → warning.
@@ -27,6 +30,10 @@ export function mt01Instability(graph, policy) {
27
30
  severity: 'warning',
28
31
  element_id: m.moduleId,
29
32
  message: `${m.moduleName} has instability ${Math.round(m.instability * 100)}% (>${threshold * 100}%). fan_in=${m.fanIn}, fan_out=${m.fanOut}`,
33
+ // CR-SM-288: `> threshold` ist exklusiv, also liegt `threshold` selbst noch im Budget.
34
+ // Die Masse ist hier ein Bruch, kein Zaehler — sie wird nie mit der einer anderen Stufe
35
+ // verrechnet (CR-SM-287 vergleicht lexikographisch, nie summierend).
36
+ context: { value: m.instability, threshold },
30
37
  });
31
38
  }
32
39
  }
@@ -34,7 +41,7 @@ export function mt01Instability(graph, policy) {
34
41
  }
35
42
  /**
36
43
  * MT-02: LCOM4 (Lack of Cohesion — component count).
37
- * MOD with allocated FUNCs that share no common io/satisfy targets → cohesion problem.
44
+ * MOD with allocated FUNCs that share no common io targets → cohesion problem.
38
45
  * `policy.lcom4.info` ≤ LCOM4 < `policy.lcom4.warning` → info, darüber warning.
39
46
  *
40
47
  * CR-SM-233: die Stufen sind Eingabe, `null` heißt messen statt urteilen.
@@ -49,18 +56,22 @@ export function mt02Lcom4(graph, policy) {
49
56
  if (m.lcom4 === null)
50
57
  continue;
51
58
  // CR-SM-263: ein MOD aus lauter zerlegten Bloecken ist per Konstruktion zerfallen —
52
- // LCOM4 verbindet ueber geteilte io/satisfy-ZIELE, und CR-SM-256 hat festgehalten, dass
59
+ // LCOM4 verbindet ueber geteilte io-ZIELE, und CR-SM-256 hat festgehalten, dass
53
60
  // ein Rollup genau die nicht traegt. Die Regel war dort nicht streng, sondern
54
61
  // unerfuellbar: gruen wird sie erst mit der Kante, die CR-SM-256 fuer falsch erklaert.
55
62
  // Die MESSUNG bleibt (`moduleMetrics` liefert `lcom4` unveraendert) — nur das Urteil faellt.
56
63
  if (m.rollupContainer)
57
64
  continue;
58
65
  const message = `${m.moduleName} has LCOM4=${m.lcom4} (${m.allocatedFuncs} FUNCs in ${m.lcom4} disconnected groups)`;
66
+ // CR-SM-288: EIN Budget fuer beide Stufen — `steps.warning - 1` ist der groesste LCOM4,
67
+ // der noch drin liegt. Die `info`-Stufe meldet frueher, aber sie meldet keine Ueberschreitung;
68
+ // ihre Masse ist 0.
69
+ const context = { value: m.lcom4, threshold: steps.warning - 1 };
59
70
  if (m.lcom4 >= steps.info && m.lcom4 < steps.warning) {
60
- violations.push({ rule_id: 'MT-02', severity: 'info', element_id: m.moduleId, message });
71
+ violations.push({ rule_id: 'MT-02', severity: 'info', element_id: m.moduleId, message, context });
61
72
  }
62
73
  else if (m.lcom4 >= steps.warning) {
63
- violations.push({ rule_id: 'MT-02', severity: 'warning', element_id: m.moduleId, message });
74
+ violations.push({ rule_id: 'MT-02', severity: 'warning', element_id: m.moduleId, message, context });
64
75
  }
65
76
  }
66
77
  return violations;
@@ -157,58 +168,37 @@ function measureModulesUncached(graph) {
157
168
  // Eine zweite Definition hier waere ein zweiter Weg zu derselben Aussage (Gate 2).
158
169
  const decomposed = decomposedFuncs(graph);
159
170
  const rows = [];
160
- // --- CR-SM-264: die indirekte Kopplung UMGEDREHT ---
171
+ // --- CR-SM-293: fan_in/fan_out sind KOPPLUNG, und Kopplung ist der Vertrag am Rand ---
161
172
  //
162
- // Vorher lief je MOD eine Schleife ueber ALLE Traces (`MOD x T`) nach Teil c die teuerste
163
- // verbliebene Form im Katalog, und sie traegt MT-01 UND MT-02, weil beide dieselbe Rechnung
164
- // lesen. Der Index loest sie nicht auf: die Frage ist nicht "welche Kanten haengen an diesem
165
- // Knoten", sondern "welche Kanten kreuzen diese Modulgrenze".
173
+ // Bis hierher zaehlte die Rechnung JEDE Kante, deren eines Ende einem Modul gehoerte. Ueber
174
+ // 19 Familiengraphen waren das 3281 Kanten, von denen nur 1117 (34 %) Kopplung sind:
166
175
  //
167
- // Also einmal ueber die Traces statt einmal je Modul. Je Kante steht ueber die
168
- // allocate-Zuordnung fest, welche Module ihre Enden besitzen; nur die zaehlen mit. Ein
169
- // Element allokiert praktisch immer auf hoechstens ein MOD, die innere Schleife ist damit
170
- // kurz — und das Ergebnis ist Zaehler fuer Zaehler dasselbe.
171
- const modIds = idx.idsOfType('MOD');
172
- const ownersOf = new Map(); // alloziertes Element -> seine MODs
173
- const membersOf = new Map(); // MOD -> seine allozierten Elemente
174
- for (const t of idx.tracesOfType('allocate')) {
175
- if (!modIds.has(t.target))
176
- continue;
177
- const owners = ownersOf.get(t.source);
178
- if (owners)
179
- owners.push(t.target);
180
- else
181
- ownersOf.set(t.source, [t.target]);
182
- const members = membersOf.get(t.target);
183
- if (members)
184
- members.add(t.source);
185
- else
186
- membersOf.set(t.target, new Set([t.source]));
187
- }
188
- const indirectOutOf = new Map();
189
- const indirectInOf = new Map();
190
- const bump = (m, key) => { m.set(key, (m.get(key) ?? 0) + 1); };
191
- for (const t of graph.traces) {
192
- if (t.type === 'allocate')
193
- continue; // allocate itself doesn't count as coupling
194
- for (const owner of ownersOf.get(t.source) ?? []) {
195
- if (!membersOf.get(owner).has(t.target) && t.target !== owner)
196
- bump(indirectOutOf, owner);
197
- }
198
- for (const owner of ownersOf.get(t.target) ?? []) {
199
- if (!membersOf.get(owner).has(t.source) && t.source !== owner)
200
- bump(indirectInOf, owner);
201
- }
202
- }
176
+ // fan_in 2164 = 674 `allocate` (das ist die MODULGROESSE) + 610 `FLOW -io-> FUNC`
177
+ // + 348 `FCHAIN -compose-> FUNC` + 255 `CR -relation-> FUNC` + 277 Rest
178
+ // fan_out 1117 = 507 `FUNC -io-> FLOW` + 501 `FUNC -satisfy-> REQ` + 109 Rest
179
+ //
180
+ // Drei Befunde, jeder fuer sich hinreichend: ein Modul wurde STABILER, indem es Funktionen
181
+ // bekam; fast die halbe fan_out war `satisfy`, also Spezifikation und keine Laufzeitkopplung
182
+ // (dieselbe Begruendung, mit der CR-SM-297 sie aus LCOM4 genommen hat); und 70 Kanten liefen
183
+ // ueber `MOD -io-> MOD`, ein Muster, das CR-SM-266 D2 aus TRACE_PATTERNS geloescht hat.
184
+ //
185
+ // Jetzt liest MT-01 `moduleCrossings` — DIESELBE Definition von Rand und dieselbe Zaehlbasis
186
+ // (verschiedene Vertraege statt Einzelquerungen), die CR-01, R-04 und BW-02 seit CR-SM-274/276
187
+ // benutzen. Eine zweite Vorstellung davon, was eine Kreuzung ist, war genau der Defekt, den
188
+ // jener CR beseitigt hat; MT-01 hatte bis hierher noch eine dritte.
189
+ //
190
+ // RICHTUNG (CR-SM-293, Entscheidung des Auftraggebers): der KONSUMENT haengt ab. Wer nach
191
+ // draussen liefert, wird gebraucht -> `afferentContracts` -> fan_in. Wer von draussen bezieht,
192
+ // haengt ab -> `efferentContracts` -> fan_out. Das ist Martins Ca/Ce und kehrt das bisherige
193
+ // Urteil an reinen Verbrauchermodulen um: sie lasen `I = 0` (maximal stabil) und muessen `1`
194
+ // lesen. Die Konvention stand vorher NIRGENDS im Repo — sie war nie entschieden, nur codiert.
195
+ const crossings = moduleCrossings(graph);
203
196
  for (const mod of mods) {
204
197
  const allocated = idx.in(mod.id, 'allocate').map(t => t.source);
205
198
  const modFuncIds = new Set(allocated);
206
- // --- MT-01: fan-in / fan-out (CR-165, indirect via the allocate path) ---
207
- const directOut = idx.out(mod.id, 'io').filter(t => t.target !== mod.id).length +
208
- idx.out(mod.id, 'compose').filter(t => t.target !== mod.id).length;
209
- const directIn = idx.in(mod.id, 'io').length + idx.in(mod.id, 'compose').length + idx.in(mod.id, 'allocate').length;
210
- const fanOut = directOut + (indirectOutOf.get(mod.id) ?? 0);
211
- const fanIn = directIn + (indirectInOf.get(mod.id) ?? 0);
199
+ // --- MT-01: fan-in / fan-out als querende VERTRAEGE je Richtung (CR-SM-293) ---
200
+ const fanIn = (crossings.afferentContracts.get(mod.id) ?? EMPTY_SET).size;
201
+ const fanOut = (crossings.efferentContracts.get(mod.id) ?? EMPTY_SET).size;
212
202
  const instability = fanIn + fanOut === 0 ? null : fanOut / (fanIn + fanOut);
213
203
  rows.push({
214
204
  moduleId: mod.id,
@@ -220,28 +210,77 @@ function measureModulesUncached(graph) {
220
210
  lcom4: lcom4Of(graph, allocated),
221
211
  rollupContainer: allocated.length >= 2 && allocated.every(f => decomposed.has(f)),
222
212
  cohesion: cohesionOf(pairs, modFuncIds),
213
+ // CR-SM-301: braucht die Instabilitaet ALLER Module, also zweiter Durchgang unten.
214
+ uphillDependencies: null,
223
215
  });
224
216
  }
217
+ // --- CR-SM-301: Abhaengigkeiten „bergauf" (Martin, Stable Dependencies Principle) ---
218
+ //
219
+ // Ein Vertrag, den `m` von draussen bezieht, laeuft bergauf, wenn sein Lieferant WENIGER
220
+ // stabil ist als `m` selbst. Lieferant eines Vertrags ist jedes Modul, das ihn ueber seinen
221
+ // Rand LIEFERT (`afferentContracts`) — dieselbe Menge, aus der oben `fanIn` kommt, also
222
+ // dieselbe Zaehlbasis und dieselbe Besitzkette. Kein zweiter Kopplungsbegriff.
223
+ //
224
+ // Gezaehlt wird JE VERTRAG, nicht je Lieferant-Vertrag-Paar: ein Vertrag mit drei Lieferanten
225
+ // ist EINE Abhaengigkeit, von denen einer bergauf liegt. Andernfalls waere die Zahl
226
+ // hub-empfindlich — dieselbe Begruendung, mit der CR-SM-274 die Vertraege statt der
227
+ // Einzelquerungen zaehlt.
228
+ const suppliersOf = new Map();
229
+ for (const [mod, set] of crossings.afferentContracts) {
230
+ for (const c of set) {
231
+ const list = suppliersOf.get(c);
232
+ if (list)
233
+ list.push(mod);
234
+ else
235
+ suppliersOf.set(c, [mod]);
236
+ }
237
+ }
238
+ const instabilityOf = new Map(rows.map(r => [r.moduleId, r.instability]));
239
+ for (const row of rows) {
240
+ const mine = row.instability;
241
+ if (mine === null)
242
+ continue; // ohne Kopplung keine Richtung — `null` bleibt stehen
243
+ let uphill = 0;
244
+ for (const contract of crossings.efferentContracts.get(row.moduleId) ?? EMPTY_SET) {
245
+ for (const supplier of suppliersOf.get(contract) ?? []) {
246
+ if (supplier === row.moduleId)
247
+ continue;
248
+ const theirs = instabilityOf.get(supplier);
249
+ if (theirs !== null && theirs !== undefined && theirs > mine) {
250
+ uphill += 1;
251
+ break;
252
+ }
253
+ }
254
+ }
255
+ row.uphillDependencies = uphill;
256
+ }
225
257
  return rows;
226
258
  }
227
259
  /** MT-02 core: connected components over the allocated FUNCs. null below 2 FUNCs. */
228
260
  function lcom4Of(graph, funcIds) {
229
261
  if (funcIds.length < 2)
230
262
  return null;
231
- // Two FUNCs are connected if they share a common io/satisfy target.
263
+ // Zwei FUNCs sind verbunden, wenn sie ein gemeinsames `io`-Ziel haben — und NUR dann.
264
+ //
265
+ // CR-SM-297: `satisfy` ist aus der Verbindung gefallen. LCOM4 misst Datenkopplung; `satisfy`
266
+ // ist eine SPEZIFIKATIONS-Beziehung, und zwei FUNCs, die dasselbe REQ erfuellen, koennen zur
267
+ // Laufzeit vollstaendig entkoppelt sein. Die Wirkung ging dabei nur in eine Richtung:
268
+ // `satisfy` VERBINDET Gruppen, senkte also LCOM4 und liess Module kohaesiver aussehen, als
269
+ // ihr Datenfluss hergibt.
270
+ //
271
+ // Der Modellierungsgrund wiegt schwerer als der Messgrund: teilen sich zwei Funktionen ein
272
+ // Requirement, ist das REQUIREMENT zu zerlegen — nicht die Kohaesionsmessung zu beschoenigen.
273
+ // RD-02 sagt bereits das Verwandte (ein Parent-REQ mit direktem FUNC-satisfy; der satisfy
274
+ // gehoert an die Kinder).
275
+ //
232
276
  // CR-SM-264: die ausgehenden Kanten kommen aus dem Index. Vorher lief je alloziertem FUNC
233
- // ein Vollscan ueber alle Traces — MOD x FUNC x T. Die Einfuegereihenfolge in `targets`
234
- // bleibt identisch (io vor satisfy war sie nicht; der Index liefert beide Listen in
235
- // Graph-Reihenfolge, und `targets` ist ein Set, dessen Inhalt hier nur auf Mitgliedschaft
236
- // geprueft wird).
277
+ // ein Vollscan ueber alle Traces — MOD x FUNC x T.
237
278
  const idx = indexOf(graph);
238
279
  const funcTargets = new Map();
239
280
  for (const fid of funcIds) {
240
281
  const targets = new Set();
241
282
  for (const t of idx.out(fid, 'io'))
242
283
  targets.add(t.target);
243
- for (const t of idx.out(fid, 'satisfy'))
244
- targets.add(t.target);
245
284
  funcTargets.set(fid, targets);
246
285
  }
247
286
  const parent = new Map();
@@ -0,0 +1,102 @@
1
+ /**
2
+ * CR-SM-276 — EIN Begriff „kreuzender Fluss", EINE Berechnung.
3
+ *
4
+ * Zwei Katalogeintraege messen Modulrand-Kopplung: **CR-01** je Modul*paar* und **R-04** je
5
+ * Modul (Groesse GEGEN Kreuzungen). Bis CR-SM-274/276 hatte jeder seine eigene, jeweils
6
+ * falsche Vorstellung davon, was eine Kreuzung ist:
7
+ *
8
+ * - CR-01 suchte `FUNC -io-> FUNC` — ein Paar, das `TRACE_PATTERNS` nicht kennt (R-18 lehnt es
9
+ * ab). Die Regel konnte strukturell nicht feuern (CR-SM-274).
10
+ * - R-04 zaehlte „jede io-Kante einer Modul-FUNC mit einem Endpunkt ausserhalb der FUNC-Menge".
11
+ * FLOWs liegen NIE in der FUNC-Menge — also zaehlte jede io-Kante zu jedem FLOW, auch dem
12
+ * modul*internen*. Das war der io-Grad des Moduls, nicht seine Randkopplung: fuer jedes Modul
13
+ * mit irgendeinem Datenfluss trivial > 2, womit R-04 faktisch wieder „max module size" war —
14
+ * genau der Name, den CR-SM-236 verworfen hat.
15
+ *
16
+ * Hier steht der Pfad, den beide Regelkoepfe meinen: **`FUNC -io-> FLOW -io-> FUNC` ueber eine
17
+ * Modulgrenze**. MOD-Zugehoerigkeit einer FUNC = **direkte** `allocate`-Kante (`FUNC -allocate->
18
+ * MOD` ist `0..1`) — dieselbe Aufloesung wie `moduleMetrics` und die RC-05-Adjazenz.
19
+ *
20
+ * ## CR-SM-282: `byModule` rollt auf, `pairs` nicht
21
+ *
22
+ * Bis hierher las AUCH `byModule` nur die direkte Zuordnung. Ein Eltern-MOD (`MOD -compose-> MOD`)
23
+ * hat keine direkt allozierten FUNCs — es kam in `byModule` also gar nicht vor, und R-04 sah an ihm
24
+ * **null Kreuzungen**. Genau die Whitebox, in die der Leser hineinklickt, war unsichtbar. Jetzt ist
25
+ * die Zugehoerigkeit die **Besitzkette**: das direkt allozierte MOD plus seine compose-Vorfahren.
26
+ * Ein Vertrag quert den Rand von `m`, wenn ein Endpunkt in `m`s Teilbaum liegt und einer nicht.
27
+ *
28
+ * `pairs` (CR-01) bleibt bewusst auf der direkten Zuordnung: „wie stark haengen DIESE zwei Module
29
+ * aneinander" ist eine Frage der Blattebene; ein Elternpaar meldete dieselbe Kopplung ein zweites
30
+ * Mal. Ohne `MOD -compose-> MOD` im Graphen ist das Ergebnis Zeichen fuer Zeichen das bisherige.
31
+ *
32
+ * ## Zaehlbasis: verschiedene SCHEMA, nicht Einzelquerungen
33
+ *
34
+ * Begruendung in CR-SM-274 (dort familienweit gemessen): Kopplung ist die Zahl der **Vertraege**,
35
+ * auf die sich zwei Module einigen muessen — ein FLOW mit 3 Produzenten und 3 Konsumenten ist
36
+ * EIN Vertrag, nicht 9. Roh ist hub-empfindlich und durch FLOW-Splitting manipulierbar;
37
+ * `FLOW -relation-> SCHEMA` ist seit CR-SM-271 Teil 2 `1..1` und per R-18 erzwungen, die
38
+ * Zaehlbasis also garantiert vorhanden. Ein FLOW ohne SCHEMA zaehlt als eigener, untypisierter
39
+ * Vertrag (`UNBOUND:<flow>`) — er darf nicht stillschweigend mit anderen verschmelzen.
40
+ *
41
+ * ## Warum `byModule` die VEREINIGUNG ist und nicht die Summe ueber die Paare
42
+ *
43
+ * R-04 meldet an EINEM MOD. Quert derselbe Vertrag zu drei Nachbarn, besitzt das Modul trotzdem
44
+ * einen Vertrag — die Summe ueber die Paare brächte die Hub-Empfindlichkeit zurueck, die die
45
+ * distinct-Zaehlung gerade beseitigt, und zaehlte doppelt, was CR-01 je Paar ohnehin meldet.
46
+ * `byModule` ist damit die **Vertragsflaeche** des Modulrandes.
47
+ */
48
+ import type { OntologyGraph } from './ontology.js';
49
+ export interface ModulePairCrossing {
50
+ readonly modA: string;
51
+ readonly modB: string;
52
+ /** SCHEMA-ids bzw. `UNBOUND:<flow>` — die verschiedenen Vertraege an dieser Grenze. */
53
+ readonly contracts: ReadonlySet<string>;
54
+ }
55
+ export interface ModuleCrossings {
56
+ /** Je sortiertem Modulpaar (Schluessel `modA::modB`) die querenden Vertraege. */
57
+ readonly pairs: ReadonlyMap<string, ModulePairCrossing>;
58
+ /** Je MOD die Vereinigung aller Vertraege, die seine Grenze queren. */
59
+ readonly byModule: ReadonlyMap<string, ReadonlySet<string>>;
60
+ /**
61
+ * CR-SM-283: je ZERLEGTER FUNC die Vertraege, die den Rand seiner Whitebox queren —
62
+ * dieselbe Definition und derselbe Durchlauf wie `byModule`, nur ist die Innen-Menge der
63
+ * `compose`-Teilbaum statt des Modul-Teilbaums. Ein Blatt-FUNC steht NICHT drin: es ist
64
+ * keine Whitebox, seine io-Kanten sind kein Rand.
65
+ *
66
+ * Der Rollup ist hier nicht Genauigkeit, sondern Existenz: ein zerlegter FUNC traegt selbst
67
+ * keine io-Kanten (die liegen an seinen Blaettern), ohne Teilbaum-Aufloesung waere die Zahl
68
+ * strukturell immer 0. Gemessen: `FUNC-block-grounding` 0 -> 19 (Spike CR-SM-282).
69
+ */
70
+ readonly byFunc: ReadonlyMap<string, ReadonlySet<string>>;
71
+ /**
72
+ * CR-SM-282: je MOD die FUNCs seines TEILBAUMS — direkt alloziert plus die seiner
73
+ * compose-Sub-MODs. Fuer ein Blatt-MOD identisch zur direkten Allokation; fuer ein
74
+ * Eltern-MOD ist es die Groesse, die R-04 gegen die Kreuzungen abwaegt. Ohne diese
75
+ * Zahl war ein Eltern-MOD fuer R-04 leer und die Regel schwieg per `funcCount <= coupled`.
76
+ */
77
+ readonly funcsByModule: ReadonlyMap<string, ReadonlySet<string>>;
78
+ /**
79
+ * CR-SM-293: dieselbe Grenze, jetzt mit RICHTUNG — Martins Ca/Ce, gerechnet im selben
80
+ * Durchlauf und auf derselben Zaehlbasis (verschiedene Vertraege) wie `pairs`/`byModule`.
81
+ *
82
+ * `afferentContracts` = Vertraege, die dieses Modul ueber seinen Rand LIEFERT: andere
83
+ * haengen von ihm ab. `efferentContracts` = Vertraege, die es von draussen BEZIEHT: es
84
+ * haengt von anderen ab. Ein Vertrag, dessen Produzent und Konsument beide im Teilbaum
85
+ * liegen, steht in keiner der beiden Mengen — er quert den Rand nicht.
86
+ *
87
+ * Die Richtung ist die Entscheidung, die bis hierher nirgends im Repo stand: der
88
+ * KONSUMENT haengt vom Vertrag des Produzenten ab, nicht umgekehrt. `moduleMetrics`
89
+ * las vorher „Kante zeigt weg = Abhaengigkeit" und drehte damit jedes reine
90
+ * Verbrauchermodul auf `I = 0` (maximal stabil), wo es `1` sein muss.
91
+ */
92
+ readonly afferentContracts: ReadonlyMap<string, ReadonlySet<string>>;
93
+ readonly efferentContracts: ReadonlyMap<string, ReadonlySet<string>>;
94
+ }
95
+ /** Die querenden Vertraege dieses Graphen — einmal je Graph-Objekt berechnet. */
96
+ export declare function moduleCrossings(graph: OntologyGraph): ModuleCrossings;
97
+ /** Zahl der verschiedenen Vertraege, die den Rand dieses Moduls queren. */
98
+ export declare function crossingContractCount(graph: OntologyGraph, modId: string): number;
99
+ /** Zahl der verschiedenen Vertraege, die den Rand dieser FUNC-Whitebox queren — CR-SM-283. */
100
+ export declare function whiteboxContractCount(graph: OntologyGraph, funcId: string): number;
101
+ /** Die FUNCs im Teilbaum dieses Moduls (direkt alloziert + die seiner Sub-MODs) — CR-SM-282. */
102
+ export declare function subtreeFuncs(graph: OntologyGraph, modId: string): ReadonlySet<string>;