@sigloch/contracts 9.1.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.
@@ -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>;
@@ -0,0 +1,196 @@
1
+ import { indexOf } from './graph-index.js';
2
+ const CACHE = new WeakMap();
3
+ function build(graph) {
4
+ const idx = indexOf(graph);
5
+ // FUNC → MOD (`FUNC -allocate-> MOD` ist `0..1`, also hoechstens ein Modul je FUNC).
6
+ const modOfFunc = new Map();
7
+ for (const mod of idx.elementsOfType('MOD')) {
8
+ for (const t of idx.in(mod.id, 'allocate')) {
9
+ if (idx.typeOf(t.source) === 'FUNC')
10
+ modOfFunc.set(t.source, mod.id);
11
+ }
12
+ }
13
+ // CR-SM-282: die Besitzkette eines MODs — es selbst plus seine `compose`-Vorfahren.
14
+ // `byModule` braucht sie, `pairs` nicht (s. Kopf).
15
+ const parentMod = new Map();
16
+ for (const mod of idx.elementsOfType('MOD')) {
17
+ for (const t of idx.in(mod.id, 'compose')) {
18
+ if (idx.typeOf(t.source) === 'MOD')
19
+ parentMod.set(mod.id, t.source);
20
+ }
21
+ }
22
+ const chainOf = (modId) => {
23
+ const out = [];
24
+ const seen = new Set();
25
+ let cur = modId;
26
+ while (cur !== undefined && !seen.has(cur)) {
27
+ seen.add(cur);
28
+ out.push(cur);
29
+ cur = parentMod.get(cur);
30
+ }
31
+ return out;
32
+ };
33
+ const ownersOfFunc = new Map();
34
+ for (const [func, mod] of modOfFunc)
35
+ ownersOfFunc.set(func, chainOf(mod));
36
+ // CR-SM-283: dieselbe Konstruktion auf dem FUNC-compose-Baum. `funcChainOf(f)` ist f selbst
37
+ // plus seine Vorfahren; nur ZERLEGTE FUNCs sind Whiteboxes und kommen als Kandidat in Frage.
38
+ const parentFunc = new Map();
39
+ const decomposed = new Set();
40
+ for (const t of graph.traces) {
41
+ if (t.type !== 'compose')
42
+ continue;
43
+ if (idx.typeOf(t.source) !== 'FUNC' || idx.typeOf(t.target) !== 'FUNC')
44
+ continue;
45
+ parentFunc.set(t.target, t.source);
46
+ decomposed.add(t.source);
47
+ }
48
+ const funcChain = new Map();
49
+ const funcChainOf = (funcId) => {
50
+ const cached = funcChain.get(funcId);
51
+ if (cached)
52
+ return cached;
53
+ const out = [];
54
+ const seen = new Set();
55
+ let cur = funcId;
56
+ while (cur !== undefined && !seen.has(cur)) {
57
+ seen.add(cur);
58
+ out.push(cur);
59
+ cur = parentFunc.get(cur);
60
+ }
61
+ funcChain.set(funcId, out);
62
+ return out;
63
+ };
64
+ const pairs = new Map();
65
+ const byModule = new Map();
66
+ const byFunc = new Map();
67
+ const afferentContracts = new Map();
68
+ const efferentContracts = new Map();
69
+ const addInto = (m, key, contracts) => {
70
+ let set = m.get(key);
71
+ if (!set) {
72
+ set = new Set();
73
+ m.set(key, set);
74
+ }
75
+ for (const c of contracts)
76
+ set.add(c);
77
+ };
78
+ const addTo = (mod, contracts) => addInto(byModule, mod, contracts);
79
+ for (const flow of idx.elementsOfType('FLOW')) {
80
+ const producerMods = new Set(idx.in(flow.id, 'io')
81
+ .filter(t => idx.typeOf(t.source) === 'FUNC')
82
+ .map(t => modOfFunc.get(t.source))
83
+ .filter((m) => !!m));
84
+ const consumerMods = new Set(idx.out(flow.id, 'io')
85
+ .filter(t => idx.typeOf(t.target) === 'FUNC')
86
+ .map(t => modOfFunc.get(t.target))
87
+ .filter((m) => !!m));
88
+ const schemas = idx.out(flow.id, 'relation')
89
+ .filter(t => idx.typeOf(t.target) === 'SCHEMA')
90
+ .map(t => t.target);
91
+ const contracts = schemas.length > 0 ? schemas : [`UNBOUND:${flow.id}`];
92
+ // CR-SM-283: derselbe Test auf dem compose-Baum. `allEndpoints` statt `endpoints` —
93
+ // die FUNC-Whitebox kennt keine Modul-Zugehoerigkeit, eine unallozierte FUNC liegt
94
+ // trotzdem drinnen oder draussen.
95
+ const allEndpoints = [
96
+ ...idx.in(flow.id, 'io').filter(t => idx.typeOf(t.source) === 'FUNC').map(t => t.source),
97
+ ...idx.out(flow.id, 'io').filter(t => idx.typeOf(t.target) === 'FUNC').map(t => t.target),
98
+ ];
99
+ const funcCandidates = new Set(allEndpoints.flatMap(f => funcChainOf(f)).filter(f => decomposed.has(f)));
100
+ for (const w of funcCandidates) {
101
+ const inside = allEndpoints.some(f => funcChainOf(f).includes(w));
102
+ const outside = allEndpoints.some(f => !funcChainOf(f).includes(w));
103
+ if (inside && outside) {
104
+ let set = byFunc.get(w);
105
+ if (!set) {
106
+ set = new Set();
107
+ byFunc.set(w, set);
108
+ }
109
+ for (const c of contracts)
110
+ set.add(c);
111
+ }
112
+ }
113
+ // Der MOD-Teil ab hier: ohne Zuordnung auf BEIDEN Seiten gibt es keine Modulgrenze.
114
+ // Der FUNC-Teil oben haengt bewusst NICHT daran — eine Whitebox ist eine Whitebox,
115
+ // auch wenn ihre Blaetter noch keinem Modul zugewiesen sind (R-22 meldet das separat).
116
+ if (producerMods.size === 0 || consumerMods.size === 0)
117
+ continue;
118
+ for (const a of producerMods) {
119
+ for (const b of consumerMods) {
120
+ if (a === b)
121
+ continue;
122
+ const [modA, modB] = a < b ? [a, b] : [b, a];
123
+ const key = `${modA}::${modB}`;
124
+ let entry = pairs.get(key);
125
+ if (!entry) {
126
+ entry = { modA, modB, contracts: new Set() };
127
+ pairs.set(key, entry);
128
+ }
129
+ for (const c of contracts)
130
+ entry.contracts.add(c);
131
+ }
132
+ }
133
+ // byModule MIT Rollup (CR-SM-282): ein Vertrag quert den Rand von `m`, wenn ein Endpunkt
134
+ // in `m`s Besitzkette liegt und einer nicht. Ohne Eltern-MODs faellt das exakt auf die
135
+ // frueheren `addTo(a)/addTo(b)` zurueck — das ist die Regressions-Invariante.
136
+ const endpoints = [
137
+ ...idx.in(flow.id, 'io').filter(t => idx.typeOf(t.source) === 'FUNC').map(t => t.source),
138
+ ...idx.out(flow.id, 'io').filter(t => idx.typeOf(t.target) === 'FUNC').map(t => t.target),
139
+ ].filter(f => ownersOfFunc.has(f));
140
+ const candidates = new Set(endpoints.flatMap(f => ownersOfFunc.get(f)));
141
+ for (const m of candidates) {
142
+ const inside = endpoints.some(f => ownersOfFunc.get(f).includes(m));
143
+ const outside = endpoints.some(f => !ownersOfFunc.get(f).includes(m));
144
+ if (inside && outside)
145
+ addTo(m, contracts);
146
+ }
147
+ // CR-SM-293: dieselben Endpunkte, nach Seite getrennt. Ein Modul, das auf BEIDEN
148
+ // Seiten steht (es produziert und konsumiert diesen Vertrag im eigenen Teilbaum),
149
+ // faellt aus beiden Mengen — der Vertrag quert seinen Rand nicht.
150
+ const producerChain = new Set(idx.in(flow.id, 'io')
151
+ .filter(t => idx.typeOf(t.source) === 'FUNC' && ownersOfFunc.has(t.source))
152
+ .flatMap(t => ownersOfFunc.get(t.source)));
153
+ const consumerChain = new Set(idx.out(flow.id, 'io')
154
+ .filter(t => idx.typeOf(t.target) === 'FUNC' && ownersOfFunc.has(t.target))
155
+ .flatMap(t => ownersOfFunc.get(t.target)));
156
+ for (const m of producerChain)
157
+ if (!consumerChain.has(m))
158
+ addInto(afferentContracts, m, contracts);
159
+ for (const m of consumerChain)
160
+ if (!producerChain.has(m))
161
+ addInto(efferentContracts, m, contracts);
162
+ }
163
+ const funcsByModule = new Map();
164
+ for (const [func, owners] of ownersOfFunc) {
165
+ for (const m of owners) {
166
+ let set = funcsByModule.get(m);
167
+ if (!set) {
168
+ set = new Set();
169
+ funcsByModule.set(m, set);
170
+ }
171
+ set.add(func);
172
+ }
173
+ }
174
+ return { pairs, byModule, byFunc, funcsByModule, afferentContracts, efferentContracts };
175
+ }
176
+ /** Die querenden Vertraege dieses Graphen — einmal je Graph-Objekt berechnet. */
177
+ export function moduleCrossings(graph) {
178
+ let result = CACHE.get(graph);
179
+ if (result === undefined) {
180
+ result = build(graph);
181
+ CACHE.set(graph, result);
182
+ }
183
+ return result;
184
+ }
185
+ /** Zahl der verschiedenen Vertraege, die den Rand dieses Moduls queren. */
186
+ export function crossingContractCount(graph, modId) {
187
+ return moduleCrossings(graph).byModule.get(modId)?.size ?? 0;
188
+ }
189
+ /** Zahl der verschiedenen Vertraege, die den Rand dieser FUNC-Whitebox queren — CR-SM-283. */
190
+ export function whiteboxContractCount(graph, funcId) {
191
+ return moduleCrossings(graph).byFunc.get(funcId)?.size ?? 0;
192
+ }
193
+ /** Die FUNCs im Teilbaum dieses Moduls (direkt alloziert + die seiner Sub-MODs) — CR-SM-282. */
194
+ export function subtreeFuncs(graph, modId) {
195
+ return moduleCrossings(graph).funcsByModule.get(modId) ?? new Set();
196
+ }
@@ -1,33 +1,25 @@
1
1
  /**
2
2
  * CR-121 P2: Near-Duplicate Detection for FUNC and SCHEMA elements.
3
- * Uses pre-injected similarity matrices (same pattern as BQ-04).
3
+ *
4
+ * CR-SM-286: **die Aehnlichkeit wird HIER gerechnet, nicht injiziert.**
5
+ *
6
+ * Bis hierher trugen beide Regeln `if (!_ndXXMatrix) return [];` und warteten auf
7
+ * `setND01SimilarityMatrix()` / `setND02SimilarityMatrix()`. Die Formel stand als Prosa in
8
+ * diesem Kopf, die Implementierung in `graphcode/src/kernel/measure/nd-similarity.ts`. Folgen,
9
+ * alle gemessen: bei severity `error` ununterscheidbares Fail-open (moneyflow trug 16
10
+ * ND-01-Befunde, die ausserhalb graphcodes niemand sah), prozessweiter Modulzustand (Graph A
11
+ * urteilte ueber Graph B, wogegen graphcode eine `finally`-Klammer brauchte), und AO-D01 —
12
+ * eine GATE-Regel — schwaechte sich stillschweigend ab ("no matrix → assume pass").
13
+ *
14
+ * Jetzt: `funcSimilarity(graph)` / `schemaSimilarity(graph)` aus `similarity.ts`, je Graph
15
+ * gecacht. Kein Setter, kein Modulzustand, kein Fail-open. Formeln und Schwelle 0,85 sind
16
+ * zeichengleich uebernommen — dasselbe Urteil an der richtigen Stelle.
4
17
  */
5
18
  import type { OntologyGraph } from './ontology.js';
6
19
  import type { RuleViolation } from './rules.js';
7
- /**
8
- * Inject pre-computed similarity matrix for FUNC elements.
9
- * Similarity = 0.35 × descr_jaccard + 0.25 × verb_match + 0.25 × io_topology + 0.15 × req_overlap
10
- */
11
- export declare function setND01SimilarityMatrix(data: {
12
- funcIds: string[];
13
- matrix: number[][];
14
- } | null): void;
15
- /** ND-01: FUNC pairs with similarity >= 0.85 → error. */
20
+ /** ND-01: FUNC pairs with similarity >= 0.85 -> error. */
16
21
  export declare function nd01FuncNearDuplicate(graph: OntologyGraph): RuleViolation[];
17
- /**
18
- * Inject pre-computed similarity matrix for SCHEMA elements.
19
- * Similarity = 0.50 × field_jaccard + 0.30 × descr_jaccard + 0.20 × usage_overlap
20
- */
21
- export declare function setND02SimilarityMatrix(data: {
22
- schemaIds: string[];
23
- matrix: number[][];
24
- } | null): void;
25
- /** Read-only access to current ND-02 matrix (used by AO-D01 for schema overlap). */
26
- export declare function getND02SimilarityMatrix(): {
27
- schemaIds: string[];
28
- matrix: number[][];
29
- } | null;
30
- /** ND-02: SCHEMA pairs with similarity >= 0.85 → error. */
22
+ /** ND-02: SCHEMA pairs with similarity >= 0.85 -> error. */
31
23
  export declare function nd02SchemaNearDuplicate(graph: OntologyGraph): RuleViolation[];
32
24
  export declare const ND_RULES: readonly [{
33
25
  readonly id: "ND-01";
@@ -1,101 +1,30 @@
1
+ import { funcSimilarity, schemaSimilarity, pairsAbove } from './similarity.js';
1
2
  const ND_SIMILARITY_THRESHOLD = 0.85;
2
- // ---------------------------------------------------------------------------
3
- // ND-01: FUNC near-duplicates
4
- // ---------------------------------------------------------------------------
5
- let _nd01Matrix = null;
6
- /**
7
- * Inject pre-computed similarity matrix for FUNC elements.
8
- * Similarity = 0.35 × descr_jaccard + 0.25 × verb_match + 0.25 × io_topology + 0.15 × req_overlap
9
- */
10
- export function setND01SimilarityMatrix(data) {
11
- _nd01Matrix = data;
3
+ /** Der gemeinsame Rumpf beider Regeln — eine Schleife, zwei Meldungstexte. */
4
+ function nearDuplicates(graph, sim, ruleId, what, fixHint) {
5
+ const byId = new Map(graph.elements.map(e => [e.id, e]));
6
+ return pairsAbove(sim, ND_SIMILARITY_THRESHOLD).map(({ a, b, similarity }) => {
7
+ const elA = byId.get(a);
8
+ const elB = byId.get(b);
9
+ const pct = Math.round(similarity * 100);
10
+ return {
11
+ rule_id: ruleId,
12
+ severity: 'error',
13
+ element_id: b,
14
+ message: `${b}${elB?.name ? ' (' + elB.name + ')' : ''} is ${pct}% similar to ${a}${elA?.name ? ' (' + elA.name + ')' : ''} — potential duplicate ${what}`,
15
+ fix_hint: fixHint,
16
+ context: { element_type: elB?.type, element_name: elB?.name },
17
+ };
18
+ });
12
19
  }
13
- /** ND-01: FUNC pairs with similarity >= 0.85 error. */
20
+ /** ND-01: FUNC pairs with similarity >= 0.85 -> error. */
14
21
  export function nd01FuncNearDuplicate(graph) {
15
- if (!_nd01Matrix)
16
- return [];
17
- const { funcIds, matrix } = _nd01Matrix;
18
- const violations = [];
19
- const seen = new Set();
20
- for (let i = 0; i < funcIds.length; i++) {
21
- for (let j = i + 1; j < funcIds.length; j++) {
22
- const sim = matrix[i][j];
23
- if (sim < ND_SIMILARITY_THRESHOLD)
24
- continue;
25
- const pairKey = `${funcIds[i]}:${funcIds[j]}`;
26
- if (seen.has(pairKey))
27
- continue;
28
- seen.add(pairKey);
29
- const pct = Math.round(sim * 100);
30
- const elI = graph.elements.find(e => e.id === funcIds[i]);
31
- const elJ = graph.elements.find(e => e.id === funcIds[j]);
32
- violations.push({
33
- rule_id: 'ND-01',
34
- severity: 'error',
35
- element_id: funcIds[j],
36
- message: `${funcIds[j]}${elJ?.name ? ' (' + elJ.name + ')' : ''} is ${pct}% similar to ${funcIds[i]}${elI?.name ? ' (' + elI.name + ')' : ''} — potential duplicate function`,
37
- fix_hint: 'Merge into single FUNC or differentiate responsibilities',
38
- context: {
39
- element_type: elJ?.type,
40
- element_name: elJ?.name,
41
- },
42
- });
43
- }
44
- }
45
- return violations;
22
+ return nearDuplicates(graph, funcSimilarity(graph), 'ND-01', 'function', 'Merge into single FUNC or differentiate responsibilities');
46
23
  }
47
- // ---------------------------------------------------------------------------
48
- // ND-02: SCHEMA near-duplicates
49
- // ---------------------------------------------------------------------------
50
- let _nd02Matrix = null;
51
- /**
52
- * Inject pre-computed similarity matrix for SCHEMA elements.
53
- * Similarity = 0.50 × field_jaccard + 0.30 × descr_jaccard + 0.20 × usage_overlap
54
- */
55
- export function setND02SimilarityMatrix(data) {
56
- _nd02Matrix = data;
57
- }
58
- /** Read-only access to current ND-02 matrix (used by AO-D01 for schema overlap). */
59
- export function getND02SimilarityMatrix() {
60
- return _nd02Matrix;
61
- }
62
- /** ND-02: SCHEMA pairs with similarity >= 0.85 → error. */
24
+ /** ND-02: SCHEMA pairs with similarity >= 0.85 -> error. */
63
25
  export function nd02SchemaNearDuplicate(graph) {
64
- if (!_nd02Matrix)
65
- return [];
66
- const { schemaIds, matrix } = _nd02Matrix;
67
- const violations = [];
68
- const seen = new Set();
69
- for (let i = 0; i < schemaIds.length; i++) {
70
- for (let j = i + 1; j < schemaIds.length; j++) {
71
- const sim = matrix[i][j];
72
- if (sim < ND_SIMILARITY_THRESHOLD)
73
- continue;
74
- const pairKey = `${schemaIds[i]}:${schemaIds[j]}`;
75
- if (seen.has(pairKey))
76
- continue;
77
- seen.add(pairKey);
78
- const pct = Math.round(sim * 100);
79
- const elI = graph.elements.find(e => e.id === schemaIds[i]);
80
- const elJ = graph.elements.find(e => e.id === schemaIds[j]);
81
- violations.push({
82
- rule_id: 'ND-02',
83
- severity: 'error',
84
- element_id: schemaIds[j],
85
- message: `${schemaIds[j]}${elJ?.name ? ' (' + elJ.name + ')' : ''} is ${pct}% similar to ${schemaIds[i]}${elI?.name ? ' (' + elI.name + ')' : ''} — potential duplicate schema`,
86
- fix_hint: 'Merge schemas or differentiate field sets',
87
- context: {
88
- element_type: elJ?.type,
89
- element_name: elJ?.name,
90
- },
91
- });
92
- }
93
- }
94
- return violations;
26
+ return nearDuplicates(graph, schemaSimilarity(graph), 'ND-02', 'schema', 'Merge schemas or differentiate field sets');
95
27
  }
96
- // ---------------------------------------------------------------------------
97
- // Aggregate
98
- // ---------------------------------------------------------------------------
99
28
  export const ND_RULES = [
100
29
  { id: 'ND-01', name: 'FuncNearDuplicate', severity: 'error', evaluate: nd01FuncNearDuplicate, domain: ['FUNC'] },
101
30
  { id: 'ND-02', name: 'SchemaNearDuplicate', severity: 'error', evaluate: nd02SchemaNearDuplicate, domain: ['SCHEMA'] },
@@ -79,14 +79,6 @@ export type ReqKind = z.infer<typeof ReqKind>;
79
79
  * und faellt danach am Enum-Vergleich durch. Die Drift wird sichtbar, nicht geheilt.
80
80
  */
81
81
  export declare function normalizeReqKinds(raw: unknown): readonly string[];
82
- export declare const AsilLevel: z.ZodEnum<{
83
- QM: "QM";
84
- A: "A";
85
- B: "B";
86
- C: "C";
87
- D: "D";
88
- }>;
89
- export type AsilLevel = z.infer<typeof AsilLevel>;
90
82
  /** INCOSE TIAD verification method (only relevant for type=TEST). */
91
83
  export declare const VerificationMethod: z.ZodEnum<{
92
84
  test: "test";
@@ -254,13 +246,6 @@ export declare const OntologyElement: z.ZodObject<{
254
246
  }>;
255
247
  name: z.ZodString;
256
248
  description: z.ZodString;
257
- asil: z.ZodOptional<z.ZodEnum<{
258
- QM: "QM";
259
- A: "A";
260
- B: "B";
261
- C: "C";
262
- D: "D";
263
- }>>;
264
249
  method: z.ZodOptional<z.ZodEnum<{
265
250
  test: "test";
266
251
  inspection: "inspection";
@@ -327,13 +312,6 @@ export declare const OntologyGraph: z.ZodObject<{
327
312
  }>;
328
313
  name: z.ZodString;
329
314
  description: z.ZodString;
330
- asil: z.ZodOptional<z.ZodEnum<{
331
- QM: "QM";
332
- A: "A";
333
- B: "B";
334
- C: "C";
335
- D: "D";
336
- }>>;
337
315
  method: z.ZodOptional<z.ZodEnum<{
338
316
  test: "test";
339
317
  inspection: "inspection";
@@ -65,7 +65,6 @@ export function normalizeReqKinds(raw) {
65
65
  return raw.map(k => String(k).trim()).filter(Boolean);
66
66
  return String(raw).split(',').map(k => k.trim()).filter(Boolean);
67
67
  }
68
- export const AsilLevel = z.enum(['QM', 'A', 'B', 'C', 'D']);
69
68
  /** INCOSE TIAD verification method (only relevant for type=TEST). */
70
69
  export const VerificationMethod = z.enum(['test', 'inspection', 'analysis', 'demonstration']);
71
70
  /** Test execution result (only relevant for type=TEST). */
@@ -197,7 +196,6 @@ export const OntologyElement = z.object({
197
196
  type: ElementType,
198
197
  name: z.string(),
199
198
  description: z.string(),
200
- asil: AsilLevel.optional(),
201
199
  /** INCOSE TIAD verification method — only for TEST elements (CR-057). */
202
200
  method: VerificationMethod.optional(),
203
201
  /** REQ kinds: multi-valued classification (CR-180). Only for REQ elements. */
@@ -24,6 +24,12 @@ export declare const MetricPolicySchema: z.ZodObject<{
24
24
  crossingFlows: z.ZodNullable<z.ZodObject<{
25
25
  warning: z.ZodNumber;
26
26
  }, z.core.$strip>>;
27
+ decompositionBreadth: z.ZodNullable<z.ZodObject<{
28
+ warning: z.ZodNumber;
29
+ }, z.core.$strip>>;
30
+ boundaryWidth: z.ZodNullable<z.ZodObject<{
31
+ warning: z.ZodNumber;
32
+ }, z.core.$strip>>;
27
33
  riskRpn: z.ZodNullable<z.ZodNumber>;
28
34
  apTable: z.ZodNullable<z.ZodObject<{
29
35
  severityBands: z.ZodArray<z.ZodObject<{
package/dist/se/policy.js CHANGED
@@ -34,6 +34,27 @@ export const MetricPolicySchema = z.object({
34
34
  * in die `arch`-Dimension ein (CR-SM-236).
35
35
  */
36
36
  crossingFlows: z.object({ warning: z.number().int().min(1) }).nullable(),
37
+ /**
38
+ * CR-SM-282: Kinderzahl je Blackbox (RD-04). Stand bis hierher als
39
+ * `DECOMPOSITION_BREADTH_MAX = 11` inline in `rules.ts` — eine Urteilsschwelle, die sich
40
+ * weder per grep noch aus dem Regelnamen ablesen liess (dieselbe Klasse, die CR-SM-236 fuer
41
+ * R-04 aufgeloest hat). `null` -> messen, nicht urteilen: die Regel schweigt.
42
+ *
43
+ * Der Default bleibt 11 und ist damit verhaltensgleich zum Bisherigen. Die Doktrin
44
+ * (`se:top-level`: „max 5 per level") ist bewusst NICHT der Default: `info` bei 5 haette am
45
+ * graphcode-Selbstmodell 20 zusaetzliche Befunde erzeugt, und `readiness` zaehlt `info`
46
+ * ungefiltert in den Nenner (`score = 1 - Verstoesse / applicable`) — genau der Grund, aus dem
47
+ * MT-03 zur Messung statt zur Regel wurde. Wer doktrin-streng fahren will, setzt 5.
48
+ */
49
+ decompositionBreadth: z.object({ warning: z.number().int().min(2) }).nullable(),
50
+ /**
51
+ * CR-SM-283: Randbreite je Blackbox (BW-02) — verschiedene Vertraege, die den Rand queren.
52
+ * Bewusst OHNE `info`-Stufe: die Verteilung gibt sie nicht her. bok und graph-view-edit enden
53
+ * beide bei 3; eine info-Schwelle dort truege bok 4 Befunde auf 9 Blackboxes, und `readiness`
54
+ * zaehlt `info` ungefiltert in den Nenner (`score = 1 - Verstoesse / applicable`) — der Grund,
55
+ * aus dem MT-03 zur Messung statt zur Regel wurde. `null` -> messen, nicht urteilen.
56
+ */
57
+ boundaryWidth: z.object({ warning: z.number().int().min(1) }).nullable(),
37
58
  /**
38
59
  * FM-03: RPN (severity · occurrence · detection), ab dem ein Risiko-REQ eine bestandene
39
60
  * Verifikation braucht (exklusiv). null → FM-03 feuert nie.
@@ -84,15 +105,45 @@ export const MetricPolicySchema = z.object({
84
105
  * `> 5` → warning, also warning ab 6.
85
106
  */
86
107
  export const DEFAULT_METRIC_POLICY = {
87
- instability: 0.7,
108
+ // CR-SM-293: `null` — MESSEN, NICHT URTEILEN. Die 0.7 stand hier als Startwert und war laut
109
+ // CR-SM-223 ausdruecklich "gesetzt, nicht gemessen". Nachdem MT-01 nur noch Kopplung zaehlt,
110
+ // ist die Verteilung ueber 149 messbare Module ZWEIGIPFLIG: 44 liegen exakt auf 0.00 (reine
111
+ // Produzenten), 45 exakt auf 1.00 (reine Konsumenten), und 45 der 51 Befunde bei 0.7 sind
112
+ // genau die 1.00. Aus einer solchen Verteilung laesst sich keine Schwelle ABLESEN (Praezedenz
113
+ // CR-SM-283/BW-02), und der Grund ist Struktur, nicht Zaehlweise: die meisten Module tragen
114
+ // zwei bis drei kreuzende Vertraege, da ist das Verhaeltnis 0 oder 1.
115
+ //
116
+ // Tiefer liegt der Einwand von Martin selbst: I ist eine KOORDINATE, kein Defekt. Sein Satz
117
+ // lautet "in Richtung Stabilitaet abhaengen", nicht "I klein halten" — ein Blattmodul mit
118
+ // I = 1.0 ist richtig, nicht kaputt. Damit fehlt MT-01 die Eigenschaft, die CR-SM-287 §2 von
119
+ // einer Steuerdimension verlangt: GERICHTET, "weniger ist besser, ohne Diskussion".
120
+ //
121
+ // Die Zahl bleibt vollstaendig erhalten — in jeder Modulzeile von `graph_metrics`, im Export
122
+ // und im gve-Dashboard, das den Zustand bereits rendert ("Instabilitaet wird hier nur
123
+ // gemessen"). Es entsteht nur kein Verstoss. graphcode faehrt das seit CR-GC-329 in seiner
124
+ // eigenen Config; dieser Default macht aus der Konfigurationszufaelligkeit eine Entscheidung.
125
+ instability: null,
88
126
  lcom4: { info: 4, warning: 6 },
89
127
  // CR-SM-236: die bisherigen Literale, jetzt an einer grep-baren Stelle.
90
128
  // `crossingFlows.warning = 3` ist die unveränderte Alt-Semantik: vorher `count > 2` → warning.
91
129
  crossingFlows: { warning: 3 },
130
+ // CR-SM-282: unveraenderte Alt-Semantik (`DECOMPOSITION_BREADTH_MAX = 11`, `> max` meldet).
131
+ // CR-SM-296: 7+-2 ist die Doktrin (`se:top-level`), 9 ist ihre Obergrenze. Bis hierher stand
132
+ // hier 11 -- gesetzt, nicht abgelesen. CR-SM-282 hatte gegen die Doktrin-Zahl 5 entschieden,
133
+ // weil `info` bei 5 den readiness-Nenner verwaessert haette; das Argument traegt fuer die
134
+ // OBERgrenze nicht, denn RD-04 meldet als `warning`. Die Zahl steht jetzt an EINER Stelle und
135
+ // gilt fuer beide Zerlegungsfragen -- Sub-FUNC und Sub-MOD.
136
+ decompositionBreadth: { warning: 9 },
137
+ // CR-SM-283: aus der Verteilung abgelesen — bok/gve max 3, graphcode 19, moneyflow 17.
138
+ boundaryWidth: { warning: 5 },
92
139
  riskRpn: 100,
93
140
  // `large`/`coupled`/`crossings` = die drei Literale aus `maxModuleSize`: vorher
94
141
  // `funcCount <= 8` → skip, `funcCount > 12` → groß, `crossings > 2` → gekoppelt.
95
- moduleSize: { large: 12, coupled: 8, crossings: 2 },
142
+ // CR-SM-296: an dieselbe Doktrin gebunden statt danebengesetzt. R-04 ist seit diesem CR ALLEIN
143
+ // fuer die Modulgroesse zustaendig (RD-04 hat ihr Allokations-Bein abgegeben), also gilt hier
144
+ // dieselbe 7+-2: `large` = 9 (Obergrenze), `coupled` = 7 (Untergrenze, ab der die Regel
145
+ // hinsieht). Vorher 12/8 -- zwei Zahlen fuer dieselbe Frage, unverbunden mit den 11 darueber.
146
+ moduleSize: { large: 9, coupled: 7, crossings: 2 },
96
147
  // CR-SM-229: kein Startwert möglich und keiner gewollt — die Tabelle steht nur im Handbook.
97
148
  // `null` heißt hier nicht „aus", sondern „markierter Übergang": bestätigte AP-Invarianten,
98
149
  // sonst RPN-Bänder. `apMethod()` macht das für den Leser sichtbar.
@@ -20,6 +20,24 @@ export declare function setBQ04SimilarityMatrix(data: {
20
20
  /**
21
21
  * BQ-04 checks for duplicate / near-duplicate requirements using
22
22
  * pre-computed embedding similarity. Returns [] when no matrix is set.
23
+ *
24
+ * CR-SM-286: **bewusst NICHT angeschlossen — anders als ND-01/ND-02.**
25
+ *
26
+ * Die Naht ist dieselbe und sie ist genauso tot: `setBQ04SimilarityMatrix()` ruft im gesamten
27
+ * Familienbaum niemand (CR-SM-278, erneut geprueft). Der Unterschied liegt in der Eingabe. ND-01
28
+ * und ND-02 nennen deterministische Formeln ueber Graphinhalt (Jaccard ueber Beschreibung,
29
+ * Verb, io-Topologie, Felder) — die konnten nach `similarity.ts` wandern und tun dort dasselbe.
30
+ * BQ-04 verlangt laut eigener Zeile "pre-computed EMBEDDING similarity", und Embeddings kann ein
31
+ * reines Vertragspaket nicht berechnen: kein Modell, kein Netz, kein Zufall.
32
+ *
33
+ * Ein Ersatzmass haette ich erfinden muessen. Ein Versuch mit 0.7*Beschreibung + 0.3*Name lief:
34
+ * **0 Befunde an allen neun Familiengraphen** und 4950 an einer templatierten Fixture — also
35
+ * genau die zwei Gate-7-Ausreisser zugleich ("0 Befunde am Selbstmodell" und "quadratisch mit
36
+ * der Graphgroesse"). Erfundene Gewichte ohne Messung sind keine Reparatur.
37
+ *
38
+ * Damit ist BQ-04 der naechste AO-D03-Fall: entweder eine Eingabe, die der Host liefern MUSS
39
+ * (dann gehoert die Regel nicht in ein reines Paket), oder streichen. Das ist eine
40
+ * Grammatik-Entscheidung — `se-grammar-review`, eigener CR, nicht hier nebenbei.
23
41
  */
24
42
  export declare function bq04Necessary(graph: OntologyGraph): RuleViolation[];
25
43
  export declare function bq06Conforming(graph: OntologyGraph): RuleViolation[];