@sigloch/contracts 6.1.0 → 9.1.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.
@@ -3,24 +3,69 @@
3
3
  * Single source of truth for which ElementType pairs are valid for which TraceType.
4
4
  * @sigloch/contracts/se
5
5
  */
6
- import type { ElementType, TraceType } from './ontology.js';
6
+ import type { ElementType, ReqKind, TraceType } from './ontology.js';
7
+ /**
8
+ * Ein Attribut-Praedikat am Pattern (CR-SM-266 B, REQ-X03).
9
+ *
10
+ * Bis hierher entschied AUSSCHLIESSLICH das Typ-Paar ueber die Gueltigkeit einer Kante. Das
11
+ * liess vier satisfy-Patterns ununterscheidbar nebeneinander stehen: ein FUNKTIONALES REQ
12
+ * konnte von einem MOD oder SYS "erfuellt" werden, ohne dass je eine Wirkkette es trug —
13
+ * strukturell derselbe billige Zweitweg, den CR-GC-366 fuer `FUNC -satisfy-> UC` entfernt hat,
14
+ * und er umging dieselben Pruefungen (IO-01, R-21 sehen nur INNERHALB einer Kette).
15
+ *
16
+ * Bewusst eng gehalten: `kinds` ist das einzige mehrwertige Enum-Feld am Knoten, und ein
17
+ * Praedikat ueber freien Text oder abgeleitete Werte waere nicht deterministisch
18
+ * entscheidbar. Kein generischer Ausdruck, keine Funktion — eine Liste erlaubter Enum-Werte.
19
+ */
20
+ export interface AttributePredicate {
21
+ /** An welchem Kantenende das Praedikat haengt. */
22
+ on: 'source' | 'target';
23
+ /** Das geprueft Feld — heute nur `kinds` (REQ). */
24
+ field: 'kinds';
25
+ /** Die erlaubten Werte. */
26
+ allowed: readonly ReqKind[];
27
+ }
7
28
  export interface TracePattern {
8
29
  source: ElementType | '*';
9
30
  target: ElementType | '*';
10
31
  type: TraceType;
11
- category?: 'modeling' | 'audit';
12
32
  label?: string;
13
- cardinality?: '1' | '1..*' | '0..*';
33
+ cardinality?: '1' | '0..1' | '1..*' | '0..*';
34
+ /** Zusatzbedingung am Quell-/Zielknoten. Fehlt sie, entscheidet allein das Typ-Paar. */
35
+ where?: AttributePredicate;
14
36
  description: string;
15
37
  }
16
38
  export declare const TRACE_PATTERNS: TracePattern[];
17
39
  /**
18
- * Check if a trace matches a valid pattern in the meta-model.
19
- * Uses element types (not IDs) for validation.
40
+ * Die Kante, wie `isValidTrace` sie sieht: die beiden Typen, der Kantentyp — und, sobald ein
41
+ * Pattern ein `where` traegt, die deklarierten Kinds des betroffenen Endes.
42
+ *
43
+ * `sourceKinds`/`targetKinds` sind OPTIONAL, und ihr Fehlen bedeutet "nicht deklariert", nicht
44
+ * "beliebig": ein Pattern mit `where` lehnt dann ab (s. `satisfiesPredicate`). Wer nur das
45
+ * Typ-Paar kennt — die Vorschlags-Pfade in fix-templates/rule-apply, die eine Kante erst noch
46
+ * bauen — bekommt fuer die where-Patterns korrekterweise `false` und schlaegt sie nicht vor.
20
47
  */
21
- export declare function isValidTrace(trace: {
48
+ export interface TraceShape {
22
49
  source: ElementType;
23
50
  target: ElementType;
24
51
  type: TraceType;
25
52
  label?: string;
26
- }, patterns?: TracePattern[]): boolean;
53
+ sourceKinds?: readonly ReqKind[];
54
+ targetKinds?: readonly ReqKind[];
55
+ }
56
+ /**
57
+ * Die Obergrenze eines Kardinalitaets-Ausdrucks — `Infinity`, wo keine gilt.
58
+ *
59
+ * Getrennt von `isValidTrace`, weil es eine andere ART von Bedingung ist: `isValidTrace`
60
+ * urteilt ueber EINE Kante und braucht nur deren Enden, eine Kardinalitaet urteilt ueber die
61
+ * MENGE der Kanten eines Knotens. Der urspruengliche CR-Entwurf wollte beides in
62
+ * `isValidTrace` erledigen; das geht nicht, und die Korrektur steht im Impact-Audit.
63
+ */
64
+ export declare function maxOccurs(cardinality: TracePattern['cardinality']): number;
65
+ /** Die Patterns mit einer echten Obergrenze — die Arbeitsliste des Kardinalitaets-Beins. */
66
+ export declare const BOUNDED_PATTERNS: readonly TracePattern[];
67
+ /**
68
+ * Check if a trace matches a valid pattern in the meta-model.
69
+ * Uses element types (not IDs) plus — where a pattern declares one — an attribute predicate.
70
+ */
71
+ export declare function isValidTrace(trace: TraceShape, patterns?: TracePattern[]): boolean;
@@ -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,38 @@ 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
+ // Nur die OBERE Grenze steht hier, bewusst als `0..1` und nicht `1..1`. Die untere haben
78
+ // R-22 (FUNC muss alloziert sein) und SC-04 (FLOW hat SCHEMA) laengst — sie hier zu
79
+ // wiederholen waere die Doppelzaehlung aus Gate 4: eine Ursache, zwei Befunde, ein Nenner
80
+ // doppelt belastet. Die beiden Haelften ergeben zusammen das `[1..1]` des CR.
81
+ { source: 'FUNC', target: 'MOD', type: 'allocate', cardinality: '0..1', description: 'Function deployed in exactly one module' },
82
+ { source: 'FLOW', target: 'SCHEMA', type: 'relation', cardinality: '0..1', description: 'Flow data format defined by exactly one schema' },
40
83
  // CR-228: REQ→MOD allocate removed — a MOD does not own a REQ. Physical NFRs use
41
84
  // MOD→satisfy→REQ, behavioral NFRs FCHAIN→satisfy→REQ; structural constraints are
42
85
  // rules, not REQs. R-18 now flags any REQ→MOD allocate edge as invalid.
@@ -44,24 +87,75 @@ export const TRACE_PATTERNS = [
44
87
  { source: 'MS', target: 'FUNC', type: 'compose', description: 'Milestone includes function' },
45
88
  { source: 'MS', target: 'REQ', type: 'compose', description: 'Milestone includes requirement' },
46
89
  { 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' },
90
+ // CR-SM-266 D3: `MS -compose-> MS` ENTFAELLT (BREAKING). Zwei Kantenarten zwischen denselben
91
+ // zwei Meilensteinen sagten dasselbe auf zwei Arten Schachtelung und Abfolge sind an einem
92
+ // Meilenstein nicht unterscheidbar, MS-01 zaehlte den Scope einmal ueber `CR -relation-> MS`
93
+ // (seit CR-SM-245 der einzige Weg) und die compose-Kante trug nichts mehr bei. `relation`
94
+ // bleibt als EINZIGE MS-MS-Kante, Semantik: Ordnung/Abfolge (Vorgaenger → Nachfolger).
95
+ // Migration: keine — 0 solche Kanten in allen 9 aktiven Familie-Graphen (gemessen 2026-08-25).
96
+ { source: 'MS', target: 'MS', type: 'relation', label: 'depends-on', description: 'Milestone order: predecessor → successor' },
49
97
  { source: 'CR', target: 'MS', type: 'relation', description: 'CR assigned to milestone' },
50
98
  // ── CR traceability (CR-155: CR tracks mutated elements) ──
51
99
  { source: 'CR', target: 'UC', type: 'relation', description: 'CR affects use case' },
52
100
  { source: 'CR', target: 'REQ', type: 'relation', description: 'CR affects requirement' },
53
101
  { source: 'CR', target: 'FUNC', type: 'relation', description: 'CR affects function' },
54
102
  { 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' },
103
+ // CR-SM-266 D5: `SESSION -produces-> *` ENTFAELLT ersatzlos, mit ihm der TraceType `produces`
104
+ // und der ElementType `SESSION`. Der urspruengliche Entwurf hielt das Pattern fuer die
105
+ // Provenance-Kante und wollte es behalten; die MESSUNG hat das widerlegt: 0 produces-Kanten
106
+ // und 0 SESSION-Knoten in ALLEN 9 aktiven Familie-Graphen, auch im graphcode-Selbstmodell
107
+ // nach 195 gegateten Versionen. Die reale Provenance lebt seit CR-GC-347 in
108
+ // `.graphcode/audit.jsonl` — audit_trail/audit_stats lesen die DATEI, nie den Graphen.
109
+ // Einziger Schreiber war ein toter Pfad (CR-195d in graph-api-core), Leser mit Wirkung: keiner.
110
+ // Nicht auf `relation` umgebogen: eine Kante ohne Schreiber UND ohne Leser wird gestrichen,
111
+ // nicht umbenannt.
57
112
  ];
113
+ /**
114
+ * Erfuellt das betroffene Kantenende das Praedikat?
115
+ *
116
+ * TEILMENGEN-Semantik, nicht Schnittmenge: JEDER deklarierte Kind muss in der erlaubten Liste
117
+ * liegen. Die schwaechere Lesart ("mindestens einer passt") waere das Loch, das der ganze
118
+ * Abschnitt schliessen will — ein funktionales REQ zusaetzlich als `non-functional` zu
119
+ * deklarieren haette gereicht, damit ein MOD es wieder erfuellen darf, und der billige
120
+ * Zweitweg waere ueber ein Attribut zurueck.
121
+ *
122
+ * Der leere Fall LEHNT AB (`kinds.length === 0`): ohne Deklaration ist nicht entscheidbar, ob
123
+ * die Kante zulaessig ist, und "unentscheidbar" darf nicht "erlaubt" heissen — sonst wird das
124
+ * WEGLASSEN des Attributs zum Umgehungsweg. Das ist die durchsetzende Haelfte von REQ-X06
125
+ * (`kinds` ist Pflicht am REQ); die meldende Haelfte haengt an BQ-07.
126
+ */
127
+ function satisfiesPredicate(where, trace) {
128
+ // `normalizeReqKinds` statt direktem Zugriff: drei Familie-Graphen tragen `kinds` als String
129
+ // statt als Liste, und ein `.every()` darauf WIRFT — mitten im Regellauf, statt einen Befund
130
+ // zu melden. Die Normalisierung heilt die Drift nicht, sie macht sie nur urteilsfaehig.
131
+ const kinds = normalizeReqKinds(where.on === 'source' ? trace.sourceKinds : trace.targetKinds);
132
+ if (kinds.length === 0)
133
+ return false;
134
+ return kinds.every(k => where.allowed.includes(k));
135
+ }
136
+ /**
137
+ * Die Obergrenze eines Kardinalitaets-Ausdrucks — `Infinity`, wo keine gilt.
138
+ *
139
+ * Getrennt von `isValidTrace`, weil es eine andere ART von Bedingung ist: `isValidTrace`
140
+ * urteilt ueber EINE Kante und braucht nur deren Enden, eine Kardinalitaet urteilt ueber die
141
+ * MENGE der Kanten eines Knotens. Der urspruengliche CR-Entwurf wollte beides in
142
+ * `isValidTrace` erledigen; das geht nicht, und die Korrektur steht im Impact-Audit.
143
+ */
144
+ export function maxOccurs(cardinality) {
145
+ if (cardinality === undefined)
146
+ return Infinity;
147
+ return cardinality === '1' || cardinality === '0..1' ? 1 : Infinity;
148
+ }
149
+ /** Die Patterns mit einer echten Obergrenze — die Arbeitsliste des Kardinalitaets-Beins. */
150
+ export const BOUNDED_PATTERNS = TRACE_PATTERNS.filter(p => maxOccurs(p.cardinality) < Infinity);
58
151
  /**
59
152
  * Check if a trace matches a valid pattern in the meta-model.
60
- * Uses element types (not IDs) for validation.
153
+ * Uses element types (not IDs) plus — where a pattern declares one — an attribute predicate.
61
154
  */
62
155
  export function isValidTrace(trace, patterns = TRACE_PATTERNS) {
63
156
  return patterns.some(p => (p.source === '*' || p.source === trace.source) &&
64
157
  (p.target === '*' || p.target === trace.target) &&
65
158
  p.type === trace.type &&
66
- (!p.label || p.label === trace.label));
159
+ (!p.label || p.label === trace.label) &&
160
+ (!p.where || satisfiesPredicate(p.where, trace)));
67
161
  }
@@ -71,6 +71,14 @@ export interface ModuleMetrics {
71
71
  instability: number | null;
72
72
  /** MT-02 core: connected-component count. null below 2 allocated FUNCs (not measurable). */
73
73
  lcom4: number | null;
74
+ /**
75
+ * CR-SM-263: JEDE allozierte FUNC ist ein zerlegter Block (Rollup), das MOD also ein
76
+ * Container statt eines Implementierungsmoduls. Dann ist `lcom4` = Zahl der Bloecke,
77
+ * garantiert durch CR-SM-256 — eine wahre Zahl ohne Aussage ueber Kohaesion. MT-02
78
+ * urteilt hier nicht; der Wert steht trotzdem da, damit der Leser ihn deuten kann.
79
+ * `false` unterhalb von 2 allozierten FUNCs (kein Container, nur zu klein zum Messen).
80
+ */
81
+ rollupContainer: boolean;
74
82
  /** The CR-SM-223 measurement, deliberately threshold-free. null where contracts omits it. */
75
83
  cohesion: {
76
84
  internal: number;
@@ -1,3 +1,5 @@
1
+ import { decomposedFuncs } from './rules.js';
2
+ import { indexOf } from './graph-index.js';
1
3
  /**
2
4
  * MT-01: Module Instability (CR-165: indirect via allocate-path).
3
5
  * I = fan_out / (fan_in + fan_out) > `policy.instability` → warning.
@@ -46,6 +48,13 @@ export function mt02Lcom4(graph, policy) {
46
48
  for (const m of measureModules(graph)) {
47
49
  if (m.lcom4 === null)
48
50
  continue;
51
+ // 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
53
+ // ein Rollup genau die nicht traegt. Die Regel war dort nicht streng, sondern
54
+ // unerfuellbar: gruen wird sie erst mit der Kante, die CR-SM-256 fuer falsch erklaert.
55
+ // Die MESSUNG bleibt (`moduleMetrics` liefert `lcom4` unveraendert) — nur das Urteil faellt.
56
+ if (m.rollupContainer)
57
+ continue;
49
58
  const message = `${m.moduleName} has LCOM4=${m.lcom4} (${m.allocatedFuncs} FUNCs in ${m.lcom4} disconnected groups)`;
50
59
  if (m.lcom4 >= steps.info && m.lcom4 < steps.warning) {
51
60
  violations.push({ rule_id: 'MT-02', severity: 'info', element_id: m.moduleId, message });
@@ -120,30 +129,86 @@ function connectionPairs(graph) {
120
129
  * Every value that is not measurable is `null`, never 0: a module with one allocated
121
130
  * FUNC has no LCOM4, and a 1 there would be an invented statement about cohesion.
122
131
  */
132
+ const MEASURE_CACHE = new WeakMap();
133
+ /**
134
+ * CR-SM-264: die Rechnung EINMAL je Graph.
135
+ *
136
+ * `mt01Instability` und `mt02Lcom4` riefen `measureModules` beide auf, also lief die
137
+ * vollstaendige Modulmessung je Auswertungslauf zweimal — im Profil aus CR-SM-260 stehen sie
138
+ * mit 7,1 % und 7,0 % nebeneinander, und die zweite Zahl ist reine Wiederholung der ersten.
139
+ * Schluessel ist das Graph-OBJEKT (dieselbe Begruendung wie in `graph-index.ts`).
140
+ *
141
+ * Der Cache gibt seine EIGENE Liste heraus. Wer sie sortieren will, kopiert vorher —
142
+ * `moduleMetrics` tut das; ein `.sort()` auf dem Cache-Eintrag wuerde ihn dauerhaft umordnen.
143
+ */
123
144
  function measureModules(graph) {
124
- const mods = graph.elements.filter(e => e.type === 'MOD');
145
+ const cached = MEASURE_CACHE.get(graph);
146
+ if (cached !== undefined)
147
+ return cached;
148
+ const rowsForCache = measureModulesUncached(graph);
149
+ MEASURE_CACHE.set(graph, rowsForCache);
150
+ return rowsForCache;
151
+ }
152
+ function measureModulesUncached(graph) {
153
+ const idx = indexOf(graph);
154
+ const mods = idx.elementsOfType('MOD');
125
155
  const pairs = [...connectionPairs(graph)].map(p => p.split('|'));
156
+ // CR-SM-263: EIN Helper fuer "Blatt" — derselbe, den R-02/R-30/R-31 seit CR-SM-256 lesen.
157
+ // Eine zweite Definition hier waere ein zweiter Weg zu derselben Aussage (Gate 2).
158
+ const decomposed = decomposedFuncs(graph);
126
159
  const rows = [];
160
+ // --- CR-SM-264: die indirekte Kopplung UMGEDREHT ---
161
+ //
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".
166
+ //
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
+ }
127
203
  for (const mod of mods) {
128
- const allocated = graph.traces
129
- .filter(t => t.type === 'allocate' && t.target === mod.id)
130
- .map(t => t.source);
204
+ const allocated = idx.in(mod.id, 'allocate').map(t => t.source);
131
205
  const modFuncIds = new Set(allocated);
132
206
  // --- MT-01: fan-in / fan-out (CR-165, indirect via the allocate path) ---
133
- const directOut = graph.traces.filter(t => t.source === mod.id && (t.type === 'io' || t.type === 'compose') && t.target !== mod.id).length;
134
- const directIn = graph.traces.filter(t => t.target === mod.id && (t.type === 'io' || t.type === 'compose' || t.type === 'allocate')).length;
135
- let indirectOut = 0;
136
- let indirectIn = 0;
137
- for (const t of graph.traces) {
138
- if (t.type === 'allocate')
139
- continue; // allocate itself doesn't count as coupling
140
- if (modFuncIds.has(t.source) && !modFuncIds.has(t.target) && t.target !== mod.id)
141
- indirectOut++;
142
- if (modFuncIds.has(t.target) && !modFuncIds.has(t.source) && t.source !== mod.id)
143
- indirectIn++;
144
- }
145
- const fanOut = directOut + indirectOut;
146
- const fanIn = directIn + indirectIn;
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);
147
212
  const instability = fanIn + fanOut === 0 ? null : fanOut / (fanIn + fanOut);
148
213
  rows.push({
149
214
  moduleId: mod.id,
@@ -153,6 +218,7 @@ function measureModules(graph) {
153
218
  fanOut,
154
219
  instability,
155
220
  lcom4: lcom4Of(graph, allocated),
221
+ rollupContainer: allocated.length >= 2 && allocated.every(f => decomposed.has(f)),
156
222
  cohesion: cohesionOf(pairs, modFuncIds),
157
223
  });
158
224
  }
@@ -163,13 +229,19 @@ function lcom4Of(graph, funcIds) {
163
229
  if (funcIds.length < 2)
164
230
  return null;
165
231
  // Two FUNCs are connected if they share a common io/satisfy target.
232
+ // 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).
237
+ const idx = indexOf(graph);
166
238
  const funcTargets = new Map();
167
239
  for (const fid of funcIds) {
168
240
  const targets = new Set();
169
- for (const t of graph.traces) {
170
- if (t.source === fid && (t.type === 'io' || t.type === 'satisfy'))
171
- targets.add(t.target);
172
- }
241
+ for (const t of idx.out(fid, 'io'))
242
+ targets.add(t.target);
243
+ for (const t of idx.out(fid, 'satisfy'))
244
+ targets.add(t.target);
173
245
  funcTargets.set(fid, targets);
174
246
  }
175
247
  const parent = new Map();
@@ -262,7 +334,7 @@ function cohesionOf(pairs, funcIds) {
262
334
  */
263
335
  export function moduleMetrics(graph) {
264
336
  const byId = (a, b) => a.moduleId < b.moduleId ? -1 : a.moduleId > b.moduleId ? 1 : 0;
265
- return measureModules(graph).sort((a, b) => {
337
+ return [...measureModules(graph)].sort((a, b) => {
266
338
  if (a.cohesion && b.cohesion)
267
339
  return a.cohesion.ratio - b.cohesion.ratio || byId(a, b);
268
340
  if (a.cohesion)
@@ -21,7 +21,6 @@ export declare const ElementType: z.ZodEnum<{
21
21
  TEST: "TEST";
22
22
  MOD: "MOD";
23
23
  SCHEMA: "SCHEMA";
24
- SESSION: "SESSION";
25
24
  CR: "CR";
26
25
  MS: "MS";
27
26
  }>;
@@ -38,20 +37,48 @@ export declare const TraceType: z.ZodEnum<{
38
37
  verify: "verify";
39
38
  allocate: "allocate";
40
39
  relation: "relation";
41
- produces: "produces";
42
40
  }>;
43
41
  export type TraceType = z.infer<typeof TraceType>;
44
- /** REQ kind — 7 values aligned with SysML 2.0 + FMEA (CR-180). */
42
+ /**
43
+ * REQ kind — 6 values aligned with SysML 2.0 + FMEA (CR-180).
44
+ *
45
+ * CR-SM-266 B: `negative` ENTFAELLT. Ein Verbots-REQ ("das System tut X nie") ist auch eine
46
+ * Anforderung — die Implementierung muss Regeln abfragen oder Sicherungen einbauen, und das
47
+ * ist `functional`. Der Wert hatte NULL Leser (kein Regel-, Gate- oder View-Konsument; anders
48
+ * als risk/mitigation → FM-01..03, pre/postcondition → UC-05/06, non-functional → NFR-01) und
49
+ * haette durch die where-Praedikate am satisfy-Pattern per AUSLASSUNG erstmals Wirkung
50
+ * bekommen: er stand in keiner der vier Listen und waere damit nur noch per FCHAIN erfuellbar
51
+ * gewesen. Die sechs verbleibenden Werte partitionieren die where-Listen VOLLSTAENDIG — kein
52
+ * Wert ohne Zuordnung, keine Kante, die durch ein Loch in der Aufzaehlung faellt.
53
+ */
45
54
  export declare const ReqKind: z.ZodEnum<{
46
55
  functional: "functional";
47
56
  "non-functional": "non-functional";
48
57
  risk: "risk";
49
- negative: "negative";
50
58
  mitigation: "mitigation";
51
59
  precondition: "precondition";
52
60
  postcondition: "postcondition";
53
61
  }>;
54
62
  export type ReqKind = z.infer<typeof ReqKind>;
63
+ /**
64
+ * `kinds` in der Form, die WIRKLICH auf Platte liegt — als Liste.
65
+ *
66
+ * Gefunden beim Migrations-Audit zu CR-SM-266 (2026-08-25), nicht von Hand: drei Graphen der
67
+ * Familie tragen `kinds` als blossen STRING statt als Liste (`graph-view-edit`
68
+ * `REQ-edit-genesis: 'functional'`, dazu gc_test-graphview und graphify), einer davon mit dem
69
+ * Wert `'security'`, den das Enum gar nicht kennt. Ursache ist der Format-E-Weg: dort reist
70
+ * das Feld als `@kinds a,b` und wird erst vom Konsumenten gehoben (graph-api-core, CR-195d).
71
+ *
72
+ * Seit die where-Praedikate `kinds` LESEN (CR-SM-266 B), ist diese Drift tragend: ein
73
+ * `kinds.every(...)` auf einem String wirft, und eine geworfene Exception in `isValidTrace`
74
+ * reisst den ganzen Regellauf statt einen Befund zu melden. Die SSOT muss auch bei kaputten
75
+ * Daten ein URTEIL liefern, keinen Absturz.
76
+ *
77
+ * Normalisieren heisst hier ausdruecklich NICHT raten: `'a,b'` wird zu `['a','b']`, weil genau
78
+ * das die Schreibform ist — ein unbekannter Wert wie `'security'` ueberlebt die Normalisierung
79
+ * und faellt danach am Enum-Vergleich durch. Die Drift wird sichtbar, nicht geheilt.
80
+ */
81
+ export declare function normalizeReqKinds(raw: unknown): readonly string[];
55
82
  export declare const AsilLevel: z.ZodEnum<{
56
83
  QM: "QM";
57
84
  A: "A";
@@ -222,7 +249,6 @@ export declare const OntologyElement: z.ZodObject<{
222
249
  TEST: "TEST";
223
250
  MOD: "MOD";
224
251
  SCHEMA: "SCHEMA";
225
- SESSION: "SESSION";
226
252
  CR: "CR";
227
253
  MS: "MS";
228
254
  }>;
@@ -245,7 +271,6 @@ export declare const OntologyElement: z.ZodObject<{
245
271
  functional: "functional";
246
272
  "non-functional": "non-functional";
247
273
  risk: "risk";
248
- negative: "negative";
249
274
  mitigation: "mitigation";
250
275
  precondition: "precondition";
251
276
  postcondition: "postcondition";
@@ -265,12 +290,6 @@ export type OntologyElement = z.infer<typeof OntologyElement>;
265
290
  * A trace (edge) in the SE ontology graph.
266
291
  * `label` provides semantic context for 'relation' edges (e.g. 'derives', 'depends-on').
267
292
  */
268
- /** Trace category: modeling traces are user-visible, audit traces are internal. */
269
- export declare const TraceCategory: z.ZodEnum<{
270
- modeling: "modeling";
271
- audit: "audit";
272
- }>;
273
- export type TraceCategory = z.infer<typeof TraceCategory>;
274
293
  export declare const Trace: z.ZodObject<{
275
294
  source: z.ZodString;
276
295
  target: z.ZodString;
@@ -281,12 +300,7 @@ export declare const Trace: z.ZodObject<{
281
300
  verify: "verify";
282
301
  allocate: "allocate";
283
302
  relation: "relation";
284
- produces: "produces";
285
303
  }>;
286
- category: z.ZodOptional<z.ZodEnum<{
287
- modeling: "modeling";
288
- audit: "audit";
289
- }>>;
290
304
  label: z.ZodOptional<z.ZodString>;
291
305
  weight: z.ZodDefault<z.ZodNumber>;
292
306
  attributes: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
@@ -308,7 +322,6 @@ export declare const OntologyGraph: z.ZodObject<{
308
322
  TEST: "TEST";
309
323
  MOD: "MOD";
310
324
  SCHEMA: "SCHEMA";
311
- SESSION: "SESSION";
312
325
  CR: "CR";
313
326
  MS: "MS";
314
327
  }>;
@@ -331,7 +344,6 @@ export declare const OntologyGraph: z.ZodObject<{
331
344
  functional: "functional";
332
345
  "non-functional": "non-functional";
333
346
  risk: "risk";
334
- negative: "negative";
335
347
  mitigation: "mitigation";
336
348
  precondition: "precondition";
337
349
  postcondition: "postcondition";
@@ -356,12 +368,7 @@ export declare const OntologyGraph: z.ZodObject<{
356
368
  verify: "verify";
357
369
  allocate: "allocate";
358
370
  relation: "relation";
359
- produces: "produces";
360
371
  }>;
361
- category: z.ZodOptional<z.ZodEnum<{
362
- modeling: "modeling";
363
- audit: "audit";
364
- }>>;
365
372
  label: z.ZodOptional<z.ZodString>;
366
373
  weight: z.ZodDefault<z.ZodNumber>;
367
374
  attributes: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
@@ -372,7 +379,7 @@ export declare const OntologyGraph: z.ZodObject<{
372
379
  export type OntologyGraph = z.infer<typeof OntologyGraph>;
373
380
  /** Human-readable descriptions for each ElementType. */
374
381
  export declare const ELEMENT_DESCRIPTIONS: Record<ElementType, string>;
375
- /** Element types used in modeling (user-visible). Excludes audit types (SESSION, CR). */
382
+ /** Element types used in modeling (user-visible). Excludes the planning type CR. */
376
383
  export declare const MODELING_ELEMENT_TYPES: ElementType[];
377
384
  export interface AttributeSpec {
378
385
  key: string;