@sigloch/contracts 9.1.0 → 10.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.
Files changed (40) 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 +12 -0
  4. package/dist/se/conformance-rules.js +6 -6
  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 +61 -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/grammar-snapshot.d.ts +9 -5
  13. package/dist/se/grammar-snapshot.js +250 -15
  14. package/dist/se/index.d.ts +6 -3
  15. package/dist/se/index.js +14 -3
  16. package/dist/se/meta-model.d.ts +47 -0
  17. package/dist/se/meta-model.js +52 -5
  18. package/dist/se/metric-rules.d.ts +20 -1
  19. package/dist/se/metric-rules.js +97 -58
  20. package/dist/se/module-crossings.d.ts +102 -0
  21. package/dist/se/module-crossings.js +196 -0
  22. package/dist/se/near-duplicate-rules.d.ts +16 -24
  23. package/dist/se/near-duplicate-rules.js +21 -92
  24. package/dist/se/ontology.d.ts +0 -22
  25. package/dist/se/ontology.js +0 -2
  26. package/dist/se/policy.d.ts +6 -0
  27. package/dist/se/policy.js +53 -2
  28. package/dist/se/quality-rules.d.ts +18 -0
  29. package/dist/se/quality-rules.js +51 -9
  30. package/dist/se/readiness.d.ts +25 -3
  31. package/dist/se/readiness.js +39 -21
  32. package/dist/se/rule-help.d.ts +52 -0
  33. package/dist/se/rule-help.js +342 -0
  34. package/dist/se/rules.d.ts +6 -0
  35. package/dist/se/rules.js +303 -125
  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/package.json +4 -2
@@ -6,20 +6,20 @@ import type { OntologyGraph } from './ontology.js';
6
6
  import type { RuleDefinition, RuleViolation } from './rules.js';
7
7
  import type { MetricPolicy } from './policy.js';
8
8
  /**
9
- * AO-D01: Relay Node Detection.
10
- * A FUNC is a relay if it (a) satisfies no REQ, (b) has >=2 outgoing io to FUNCs,
11
- * and (c) target FUNCs share SCHEMA overlap >= 0.5 (via ND-02 matrix, skipped if unavailable).
9
+ * CR-SM-283: **AO-D03 (`DuplicatePathDetection`) ist ersatzlos entfallen.**
10
+ *
11
+ * Die Regel suchte `FUNC -io-> FUNC` ein Paar, das `TRACE_PATTERNS` nicht kennt und das R-18
12
+ * als `error` ablehnt. Sie konnte STRUKTURELL nicht feuern und hat es an keinem Familiengraphen
13
+ * je getan (Gate 7: 0 Befunde ueber fuenf Graphen). Eine Regel, die nie feuert, kostet trotzdem
14
+ * dauerhaft: einen Nenner-Anteil, eine Katalogzeile, eine readiness-Zuordnung, einen
15
+ * Golden-File-Eintrag und einen Leser, der sie von den anderen unterscheiden muss.
16
+ *
17
+ * **Nicht verwechseln:** die Aufgabe, die sie tragen sollte — zwei Wirkketten, die auf einer
18
+ * Ebene mitgliedsgleich werden — bleibt ungeloest (`se:top-level`: „Compare the member sets
19
+ * yourself — no rule does it for you"). BW-02 ersetzt sie nicht; das ist ein eigener CR.
12
20
  */
13
- export declare function aoD01RelayNode(graph: OntologyGraph): RuleViolation[];
14
- /**
15
- * AO-D03: Duplicate Path Detection.
16
- * FUNC A sends io to both B and C, where B and C connect to the same SCHEMA elements.
17
- */
18
- export declare function aoD03DuplicatePath(graph: OntologyGraph): RuleViolation[];
21
+ export declare function bw02WhiteboxWidth(graph: OntologyGraph, policy: MetricPolicy): RuleViolation[];
19
22
  export declare function cr01CrossingFlowCount(graph: OntologyGraph, policy: MetricPolicy): RuleViolation[];
20
- export declare function rt01PhysicalBoundaryIntegrity(graph: OntologyGraph): RuleViolation[];
21
- export declare function ph01PhysicalModCompleteness(graph: OntologyGraph): RuleViolation[];
22
- export declare function ca01CapabilityAllocation(graph: OntologyGraph): RuleViolation[];
23
23
  export declare function io01CrossModuleCompleteness(graph: OntologyGraph): RuleViolation[];
24
24
  export declare const AO_RULES: RuleDefinition[];
25
25
  export declare function evaluateAORules(graph: OntologyGraph, policy: MetricPolicy): RuleViolation[];
@@ -1,42 +1,6 @@
1
- import { getND02SimilarityMatrix } from './near-duplicate-rules.js';
2
1
  import { indexOf } from './graph-index.js';
2
+ import { moduleCrossings } from './module-crossings.js';
3
3
  const SCHEMA_OVERLAP_THRESHOLD = 0.5;
4
- /**
5
- * AO-D01: Relay Node Detection.
6
- * A FUNC is a relay if it (a) satisfies no REQ, (b) has >=2 outgoing io to FUNCs,
7
- * and (c) target FUNCs share SCHEMA overlap >= 0.5 (via ND-02 matrix, skipped if unavailable).
8
- */
9
- export function aoD01RelayNode(graph) {
10
- const idx = indexOf(graph);
11
- const violations = [];
12
- const funcs = idx.elementsOfType('FUNC');
13
- const funcIdSet = new Set(funcs.map(f => f.id));
14
- for (const func of funcs) {
15
- // (a) No satisfy traces (FUNC as source) connecting to any REQ
16
- const hasSatisfy = graph.traces.some(t => t.type === 'satisfy' &&
17
- t.source === func.id && idx.typeOf(t.target) === 'REQ');
18
- if (hasSatisfy)
19
- continue;
20
- // (b) >=2 outgoing io traces to other FUNCs
21
- const ioTargets = graph.traces
22
- .filter(t => t.type === 'io' && t.source === func.id && funcIdSet.has(t.target))
23
- .map(t => t.target);
24
- if (ioTargets.length < 2)
25
- continue;
26
- // (c) Target FUNCs share SCHEMA overlap >= 0.5 (skip check if no matrix)
27
- if (!passesSchemaOverlap(graph, ioTargets))
28
- continue;
29
- const el = idx.byId.get(func.id);
30
- violations.push({
31
- rule_id: 'AO-D01',
32
- severity: 'info',
33
- element_id: func.id,
34
- message: `${func.id}${el?.name ? ' (' + el.name + ')' : ''} is a relay node: no REQ satisfied, ${ioTargets.length} io targets`,
35
- fix_hint: 'Consider eliminating relay by connecting consumers directly to producer',
36
- });
37
- }
38
- return violations;
39
- }
40
4
  /** CR-165: Resolve FUNC→SCHEMA via FUNC→FLOW(io) + FLOW→SCHEMA(relation). */
41
5
  function funcToSchemas(graph, funcId) {
42
6
  const idx = indexOf(graph);
@@ -44,7 +8,7 @@ function funcToSchemas(graph, funcId) {
44
8
  // Find FLOWs connected to this FUNC via io (FUNC→FLOW or FLOW→FUNC)
45
9
  //
46
10
  // CR-SM-264: die io-Kanten dieser FUNC statt zweier Vollscans ueber ALLE Traces je FUNC
47
- // (`F x T`) — AO-D03 ruft diese Funktion einmal je FUNC. Die Reihenfolge von `flowIds` ist
11
+ // (`F x T`) — AO-D01 ruft diese Funktion einmal je FUNC. Die Reihenfolge von `flowIds` ist
48
12
  // dabei egal, die Menge wird nur auf Mitgliedschaft geprueft.
49
13
  const flowIds = new Set();
50
14
  for (const t of idx.out(funcId, 'io'))
@@ -56,7 +20,7 @@ function funcToSchemas(graph, funcId) {
56
20
  // Find SCHEMAs connected to those FLOWs via relation.
57
21
  //
58
22
  // Hier NICHT ueber `flowIds` iterieren, obwohl das billiger waere: die Einfuegereihenfolge
59
- // dieser Menge landet ueber `shared.join(', ')` woertlich in der AO-D03-Meldung. Der Lauf
23
+ // dieser Menge landet ueber `shared.join(', ')` woertlich in der AO-D01-Meldung. Der Lauf
60
24
  // ueber die relation-Kanten in Graph-Reihenfolge haelt sie identisch — und ist trotzdem
61
25
  // deutlich kuerzer als der Vollscan, weil er nur einen Kantentyp sieht.
62
26
  for (const t of idx.tracesOfType('relation')) {
@@ -65,124 +29,101 @@ function funcToSchemas(graph, funcId) {
65
29
  }
66
30
  return schemas;
67
31
  }
68
- /** Check if any pair of target FUNCs shares SCHEMA overlap via FLOW→SCHEMA(relation) + ND-02 matrix. */
69
- function passesSchemaOverlap(graph, targetFuncIds) {
70
- const idx = indexOf(graph);
71
- const nd02 = getND02SimilarityMatrix();
72
- if (!nd02)
73
- return true; // no matrix → skip check, assume pass
74
- // For each target FUNC, collect connected SCHEMA ids via FLOW→SCHEMA(relation)
75
- const funcSchemasMap = new Map();
76
- for (const fid of targetFuncIds) {
77
- funcSchemasMap.set(fid, funcToSchemas(graph, fid));
78
- }
79
- // Check pairwise: any pair of target FUNCs with overlapping SCHEMAs (similarity >= threshold)
80
- const { schemaIds, matrix } = nd02;
81
- const idxOf = new Map(schemaIds.map((id, i) => [id, i]));
82
- for (let i = 0; i < targetFuncIds.length; i++) {
83
- for (let j = i + 1; j < targetFuncIds.length; j++) {
84
- const si = funcSchemasMap.get(targetFuncIds[i]);
85
- const sj = funcSchemasMap.get(targetFuncIds[j]);
86
- for (const a of si) {
87
- for (const b of sj) {
88
- const ai = idxOf.get(a), bi = idxOf.get(b);
89
- if (ai !== undefined && bi !== undefined && matrix[ai][bi] >= SCHEMA_OVERLAP_THRESHOLD)
90
- return true;
91
- if (a === b)
92
- return true; // same SCHEMA = 100% overlap
93
- }
94
- }
95
- }
96
- }
97
- return false;
98
- }
99
32
  /**
100
- * AO-D03: Duplicate Path Detection.
101
- * FUNC A sends io to both B and C, where B and C connect to the same SCHEMA elements.
33
+ * CR-SM-283: **AO-D03 (`DuplicatePathDetection`) ist ersatzlos entfallen.**
34
+ *
35
+ * Die Regel suchte `FUNC -io-> FUNC` — ein Paar, das `TRACE_PATTERNS` nicht kennt und das R-18
36
+ * als `error` ablehnt. Sie konnte STRUKTURELL nicht feuern und hat es an keinem Familiengraphen
37
+ * je getan (Gate 7: 0 Befunde ueber fuenf Graphen). Eine Regel, die nie feuert, kostet trotzdem
38
+ * dauerhaft: einen Nenner-Anteil, eine Katalogzeile, eine readiness-Zuordnung, einen
39
+ * Golden-File-Eintrag und einen Leser, der sie von den anderen unterscheiden muss.
40
+ *
41
+ * **Nicht verwechseln:** die Aufgabe, die sie tragen sollte — zwei Wirkketten, die auf einer
42
+ * Ebene mitgliedsgleich werden — bleibt ungeloest (`se:top-level`: „Compare the member sets
43
+ * yourself — no rule does it for you"). BW-02 ersetzt sie nicht; das ist ein eigener CR.
102
44
  */
103
- export function aoD03DuplicatePath(graph) {
45
+ // ---------------------------------------------------------------------------
46
+ // BW-02: Randbreite der FUNC-Whitebox (CR-SM-283)
47
+ //
48
+ // Die Zahl der VERSCHIEDENEN Vertraege, die den Rand einer zerlegten FUNC queren — dieselbe
49
+ // Zaehlbasis wie CR-01/R-04 (SCHEMA, nicht rohe io-Kanten, CR-SM-274/276), nur ist die
50
+ // Innen-Menge der `compose`-Teilbaum statt des Modul-Teilbaums. Gerechnet im selben Durchlauf
51
+ // wie `byModule` (`module-crossings.ts`), damit es EINE Definition von „Rand" gibt.
52
+ //
53
+ // Warum es die Regel ueberhaupt braucht: eine zerlegte FUNC ist die Whitebox, in die der Leser
54
+ // hineinklickt. Ihre Randbreite entscheidet, ob er den Ueberblick behaelt — und bis hierher
55
+ // misst sie NIEMAND. AO-D01 misst Relay-Knoten, IO-01 die Kettenkohaerenz, R-31 die blosse
56
+ // Verdrahtung. Gemessen: 12 Befunde an graphcode, davon 0 in Ueberlappung mit irgendeiner
57
+ // FUNC-Regel (Gate 6).
58
+ //
59
+ // Schwelle aus der Verteilung, nicht gewaehlt: bok und graph-view-edit enden beide bei 3,
60
+ // graphcode geht bis 19, moneyflow bis 17. Kein `info`-Level — bei Schwelle 3 truege bok 4 von
61
+ // 9 Blackboxes, und `readiness` zaehlt `info` ungefiltert in den Nenner (MT-03-Praezedenz).
62
+ // ---------------------------------------------------------------------------
63
+ export function bw02WhiteboxWidth(graph, policy) {
64
+ const steps = policy.boundaryWidth;
65
+ if (steps === null)
66
+ return [];
104
67
  const idx = indexOf(graph);
105
68
  const violations = [];
106
- const funcs = idx.elementsOfType('FUNC');
107
- const funcIdSet = new Set(funcs.map(f => f.id));
108
- const seen = new Set();
109
- // Pre-compute: FUNC → set of SCHEMA targets via FLOW→SCHEMA(relation) (CR-165)
110
- const funcToSchemasMap = new Map();
111
- for (const f of funcs) {
112
- funcToSchemasMap.set(f.id, funcToSchemas(graph, f.id));
113
- }
114
- for (const a of funcs) {
115
- const ioTargets = graph.traces
116
- .filter(t => t.type === 'io' && t.source === a.id && funcIdSet.has(t.target))
117
- .map(t => t.target);
118
- if (ioTargets.length < 2)
69
+ for (const [funcId, contracts] of moduleCrossings(graph).byFunc) {
70
+ if (contracts.size < steps.warning)
119
71
  continue;
120
- for (let i = 0; i < ioTargets.length; i++) {
121
- for (let j = i + 1; j < ioTargets.length; j++) {
122
- const b = ioTargets[i], c = ioTargets[j];
123
- const key = [a.id, b, c].sort().join(':');
124
- if (seen.has(key))
125
- continue;
126
- const sb = funcToSchemasMap.get(b) ?? new Set();
127
- const sc = funcToSchemasMap.get(c) ?? new Set();
128
- const shared = [...sb].filter(s => sc.has(s));
129
- if (shared.length === 0)
130
- continue;
131
- seen.add(key);
132
- violations.push({
133
- rule_id: 'AO-D03',
134
- severity: 'info',
135
- element_id: a.id,
136
- message: `${a.id} sends io to ${b} and ${c} which share ${shared.length} SCHEMA target(s): ${shared.join(', ')}`,
137
- fix_hint: 'Consider introducing a mediator or unifying the data paths',
138
- });
139
- }
140
- }
72
+ const el = idx.byId.get(funcId);
73
+ violations.push({
74
+ rule_id: 'BW-02',
75
+ severity: 'warning',
76
+ element_id: funcId,
77
+ message: `${funcId} exposes ${contracts.size} distinct contract(s) at its whitebox boundary (>= ${steps.warning})`,
78
+ fix_hint: 'Consolidate the contracts crossing this boundary, or introduce a level so fewer of them are visible at once',
79
+ // CR-SM-288: `>= steps.warning` ist inklusiv, also ist `steps.warning - 1` der groesste
80
+ // Wert, der noch im Budget liegt — die Masse `value - threshold` ist damit > 0 fuer jeden
81
+ // Befund dieser Regel.
82
+ context: { element_type: el?.type, element_name: el?.name, value: contracts.size, threshold: steps.warning - 1 },
83
+ });
141
84
  }
142
- return violations;
85
+ // Die Einfuegereihenfolge von `byFunc` folgt der FLOW-Reihenfolge des Graphen — hier wird sie
86
+ // kanonisch gemacht, sonst haengt die Befund-Sequenz an der Trace-Reihenfolge (Gate 5).
87
+ return violations.sort((a, b) => (a.element_id < b.element_id ? -1 : a.element_id > b.element_id ? 1 : 0));
143
88
  }
144
89
  // ---------------------------------------------------------------------------
145
90
  // CR-01: Crossing Flow Count (CR-191)
146
- // For each module pair (A, B): count io paths FUNC_A FUNC_B
91
+ // Je Modulpaar (A, B): die VERSCHIEDENEN Vertraege, die den Rand queren — der io-Pfad
92
+ // `FUNC_A -io-> FLOW -io-> FUNC_B` mit FUNC_A in A und FUNC_B in B, gezaehlt ueber das
93
+ // SCHEMA des FLOW.
147
94
  //
148
95
  // CR-SM-236: die Schwelle ist Eingabe. `policy.crossingFlows === null` heisst messen statt
149
96
  // urteilen — die Regel schweigt dann vollstaendig, auch unterhalb der Schwelle. Vorher gab es
150
97
  // diesen Aus-Zustand nicht: jedes Modulpaar mit >= 1 Kreuzung erzeugte eine info-Meldung.
98
+ //
99
+ // CR-SM-274 (Befund aus Spike CR-GC-438 §6): die Regel suchte direkte `FUNC -io-> FUNC`-Kanten.
100
+ // `TRACE_PATTERNS` kennt fuer `io` nur `ACTOR<->FLOW` und `FUNC<->FLOW` — R-18 lehnt eine
101
+ // FUNC->FUNC-io-Kante ab, die Regel konnte STRUKTURELL nicht feuern und tat es in keinem der
102
+ // 10 gemessenen Familie-Graphen (0 Befunde), waehrend ihre Schwelle in der MetricPolicy
103
+ // gepflegt wurde. Jetzt laeuft sie den Pfad, den ihr Kopf meint.
104
+ //
105
+ // Zaehlbasis = SCHEMA, nicht Einzelquerung. Begruendung (CR-SM-274):
106
+ // 1. Modul-Kopplung ist die Zahl der Vertraege, auf die sich zwei Module einigen muessen.
107
+ // Ein FLOW mit 3 Produzenten und 3 Konsumenten ist EIN Vertrag, nicht 9 Kopplungen —
108
+ // der Fix-Hint ("Mediator einziehen") greift je Vertrag, nicht je Konsument.
109
+ // 2. Die rohe Produzent-x-Konsument-Zaehlung ist hub-empfindlich und durch FLOW-Splitting
110
+ // manipulierbar (CR-436 Befund 3: X 172 -> 1539 durch einen Merge ohne Code-Kopplung).
111
+ // SCHEMA ist die Zaehlbasis, die der Spike als gaming-fest gemessen hat (Kill 2).
112
+ // 3. `FLOW -relation-> SCHEMA` ist seit CR-SM-271 Teil 2 `1..1` und per R-18 erzwungen —
113
+ // die Zaehlbasis ist damit garantiert vorhanden, kein optionales Feld.
114
+ // Ein FLOW ohne SCHEMA zaehlt als eigener, untypisierter Vertrag (R-18 meldet ihn separat);
115
+ // er darf nicht stillschweigend mit anderen verschmelzen.
116
+ //
117
+ // CR-SM-276: die Traversierung steht in `module-crossings.ts` — dieselbe Definition von
118
+ // „Kreuzung" fuer CR-01 (je Modulpaar) und R-04 (je Modul). Zwei Kopien waeren zwei Begriffe.
151
119
  // ---------------------------------------------------------------------------
152
120
  export function cr01CrossingFlowCount(graph, policy) {
153
- const idx = indexOf(graph);
154
121
  const violations = [];
155
122
  const steps = policy.crossingFlows;
156
123
  if (steps === null)
157
124
  return violations;
158
- const mods = idx.elementsOfType('MOD');
159
- // Build MOD → Set<FUNC-id> mapping
160
- const modFuncs = new Map();
161
- for (const mod of mods) {
162
- const funcIds = new Set(idx.in(mod.id, 'allocate').map(t => t.source));
163
- modFuncs.set(mod.id, funcIds);
164
- }
165
- // Count crossings per module pair
166
- const pairCounts = new Map();
167
- for (const t of graph.traces) {
168
- if (t.type !== 'io')
169
- continue;
170
- let srcMod;
171
- let tgtMod;
172
- for (const [mid, fids] of modFuncs) {
173
- if (fids.has(t.source))
174
- srcMod = mid;
175
- if (fids.has(t.target))
176
- tgtMod = mid;
177
- }
178
- if (!srcMod || !tgtMod || srcMod === tgtMod)
179
- continue;
180
- const key = [srcMod, tgtMod].sort().join('::');
181
- if (!pairCounts.has(key))
182
- pairCounts.set(key, { modA: srcMod, modB: tgtMod, count: 0 });
183
- pairCounts.get(key).count++;
184
- }
185
- for (const { modA, modB, count } of pairCounts.values()) {
125
+ for (const { modA, modB, contracts } of moduleCrossings(graph).pairs.values()) {
126
+ const count = contracts.size;
186
127
  if (count <= 0)
187
128
  continue;
188
129
  const over = count >= steps.warning;
@@ -190,93 +131,19 @@ export function cr01CrossingFlowCount(graph, policy) {
190
131
  rule_id: 'CR-01',
191
132
  severity: over ? 'warning' : 'info',
192
133
  element_id: modA,
193
- message: `${modA} ↔ ${modB}: ${count} crossing io flow(s)`,
194
- fix_hint: over ? 'Reduce coupling between modules or introduce a mediator' : 'Consider if coupling is intentional',
134
+ message: `${modA} ↔ ${modB}: ${count} distinct contract(s) crossing the module boundary (io path FUNC→FLOW→FUNC)`,
135
+ fix_hint: over
136
+ ? `Reduce coupling between modules or introduce a mediator (>= ${steps.warning} distinct contracts at one boundary)`
137
+ : 'Consider if coupling is intentional',
138
+ // CR-SM-288: dasselbe Budget fuer beide Severities. Ein `info`-Paar liegt per Konstruktion
139
+ // INNERHALB des Budgets und traegt damit die Masse 0 — es ist kein kleiner Verstoss,
140
+ // sondern keiner.
141
+ context: { value: count, threshold: steps.warning - 1 },
195
142
  });
196
143
  }
197
144
  return violations;
198
145
  }
199
146
  // ---------------------------------------------------------------------------
200
- // RT-01: Physical boundary integrity (CR-191)
201
- // FUNCs must not be allocated directly to physical MODs — use logical sub-modules.
202
- // ---------------------------------------------------------------------------
203
- export function rt01PhysicalBoundaryIntegrity(graph) {
204
- const idx = indexOf(graph);
205
- const physicalMods = graph.elements.filter(e => e.type === 'MOD' && e.attributes?.kind === 'physical');
206
- return graph.traces
207
- .filter(t => t.type === 'allocate' && physicalMods.some(m => m.id === t.target))
208
- .filter(t => idx.typeOf(t.source) === 'FUNC')
209
- .map(t => ({
210
- rule_id: 'RT-01',
211
- severity: 'error',
212
- element_id: t.source,
213
- message: `${t.source} allocated directly to physical module ${t.target} — must use logical sub-module`,
214
- fix_hint: 'Create a logical MOD inside the physical MOD and allocate the FUNC there',
215
- }));
216
- }
217
- // ---------------------------------------------------------------------------
218
- // PH-01: Physical MOD completeness (CR-191)
219
- // Physical MODs should have at least one logical sub-module via compose trace.
220
- // ---------------------------------------------------------------------------
221
- export function ph01PhysicalModCompleteness(graph) {
222
- const idx = indexOf(graph);
223
- const physicalMods = graph.elements.filter(e => e.type === 'MOD' && e.attributes?.kind === 'physical');
224
- return physicalMods
225
- .filter(pm => !graph.traces.some(t => t.source === pm.id && t.type === 'compose' &&
226
- idx.typeOf(t.target) === 'MOD'))
227
- .map(pm => ({
228
- rule_id: 'PH-01',
229
- severity: 'info',
230
- element_id: pm.id,
231
- message: `${pm.id} (physical) has no logical sub-modules`,
232
- fix_hint: 'Add at least one logical MOD via compose trace',
233
- context: { element_type: pm.type, element_name: pm.name },
234
- }));
235
- }
236
- // ---------------------------------------------------------------------------
237
- // CA-01: Capability allocation check (CR-191)
238
- // FUNC @requires must be satisfied by the physical MOD's @capability.
239
- // ---------------------------------------------------------------------------
240
- export function ca01CapabilityAllocation(graph) {
241
- const idx = indexOf(graph);
242
- const violations = [];
243
- const funcsWithRequires = graph.elements.filter(e => e.type === 'FUNC' && e.attributes?.requires);
244
- for (const func of funcsWithRequires) {
245
- const requires = (Array.isArray(func.attributes.requires)
246
- ? func.attributes.requires
247
- : [func.attributes.requires]);
248
- // Find logical MOD via allocate
249
- const allocTrace = graph.traces.find(t => t.source === func.id && t.type === 'allocate');
250
- if (!allocTrace)
251
- continue;
252
- const logicalModCandidate = idx.byId.get(allocTrace.target);
253
- const logicalMod = logicalModCandidate?.type === 'MOD' ? logicalModCandidate : undefined;
254
- if (!logicalMod)
255
- continue;
256
- // Find physical parent MOD (compose source → logical MOD target)
257
- const physParentTrace = graph.traces.find(t => t.type === 'compose' && t.target === logicalMod.id &&
258
- (idx.byId.get(t.source)?.type === 'MOD' && idx.byId.get(t.source)?.attributes?.kind === 'physical'));
259
- if (!physParentTrace)
260
- continue;
261
- const physMod = idx.byId.get(physParentTrace.source);
262
- const capabilities = (Array.isArray(physMod.attributes?.capability)
263
- ? physMod.attributes.capability
264
- : physMod.attributes?.capability ? [physMod.attributes.capability] : []);
265
- const missing = requires.filter(r => !capabilities.includes(r));
266
- if (missing.length > 0) {
267
- violations.push({
268
- rule_id: 'CA-01',
269
- severity: 'error',
270
- element_id: func.id,
271
- message: `${func.id} requires [${missing.join(', ')}] but ${physMod.id} only provides [${capabilities.join(', ')}]`,
272
- fix_hint: `Add missing capabilities to ${physMod.id} or move ${func.id} to a capable module`,
273
- context: { element_type: func.type, element_name: func.name, parent_module: physMod.id },
274
- });
275
- }
276
- }
277
- return violations;
278
- }
279
- // ---------------------------------------------------------------------------
280
147
  // IO-01: die Wirkkette haengt zusammen (CR-192, CR-SM-226, CR-SM-250)
281
148
  //
282
149
  // Geprueft wird ERREICHBARKEIT im io-Teilgraphen der Kette, nicht Adjazenz. Bis CR-SM-250
@@ -393,12 +260,8 @@ export function io01CrossModuleCompleteness(graph) {
393
260
  return violations;
394
261
  }
395
262
  export const AO_RULES = [
396
- { id: 'AO-D01', name: 'RelayNodeDetection', severity: 'info', evaluate: aoD01RelayNode, domain: ['FUNC'] },
397
- { id: 'AO-D03', name: 'DuplicatePathDetection', severity: 'info', evaluate: aoD03DuplicatePath, domain: ['FUNC'] },
263
+ { id: 'BW-02', name: 'Whitebox boundary width', severity: 'warning', evaluate: bw02WhiteboxWidth, domain: ['FUNC'] },
398
264
  { id: 'CR-01', name: 'CrossingFlowCount', severity: 'warning', evaluate: cr01CrossingFlowCount, domain: ['MOD'] },
399
- { id: 'RT-01', name: 'PhysicalBoundaryIntegrity', severity: 'error', evaluate: rt01PhysicalBoundaryIntegrity, domain: ['FUNC'] },
400
- { id: 'PH-01', name: 'PhysicalModCompleteness', severity: 'info', evaluate: ph01PhysicalModCompleteness, domain: ['MOD'] },
401
- { id: 'CA-01', name: 'CapabilityAllocation', severity: 'error', evaluate: ca01CapabilityAllocation, domain: ['FUNC'] },
402
265
  // CR-SM-242: domain ist FUNC, nicht MOD — die Regel laeuft ueber FUNC-Paare in FCHAINs und
403
266
  // meldet am FUNC. Mit ['MOD'] uebertraf ihr Zaehler (21) ihren eigenen Nenner-Beitrag (10);
404
267
  // nur der Clamp verhinderte einen negativen Score.
@@ -57,6 +57,18 @@ export interface ConformanceRuleDefinition {
57
57
  id: string;
58
58
  name: string;
59
59
  severity: RuleSeverity;
60
+ /**
61
+ * CR-SM-305: die Grundgesamtheit, ueber die die Regel meldet — dieselbe Bedeutung wie an
62
+ * jeder Katalogregel (CR-SM-235), damit RC in `ALL_RULE_DEFS` stehen kann, ohne dass ein
63
+ * Feld erfunden werden muss.
64
+ *
65
+ * GEMESSEN, nicht gesetzt: RC-01 meldet 10x an FUNC, RC-04 4x an SCHEMA, RC-05 5x an MOD
66
+ * ueber fuenf Familien-Repos; RC-02/RC-03 sind dort still und ihr Typ ist aus der Schleife
67
+ * gelesen (`el.type !== 'TEST'` / `'SCHEMA'`). RC-06 laeuft ueber jedes Element mit
68
+ * `external` + `realRef`, und genau drei Typen duerfen diese Attribute tragen
69
+ * (`ELEMENT_ATTRIBUTES`): FUNC, MOD, SCHEMA.
70
+ */
71
+ domain: readonly string[];
60
72
  evaluate: (graph: OntologyGraph, facts: CodeFacts) => RuleViolation[];
61
73
  }
62
74
  /**
@@ -450,12 +450,12 @@ function externalRefMustNameDependency(graph, facts) {
450
450
  }
451
451
  /** All RC conformance rules — evaluated by executors that can supply CodeFacts. */
452
452
  export const CODE_CONFORMANCE_RULES = [
453
- { id: 'RC-01', name: 'FUNC realRef resolves to a declared symbol', severity: 'error', evaluate: codeRefMustResolve },
454
- { id: 'RC-02', name: 'testRefs entries resolve to runnable tests', severity: 'error', evaluate: testRefMustResolve },
455
- { id: 'RC-03', name: 'SCHEMA realRef resolves to a declared export', severity: 'error', evaluate: schemaRefMustResolve },
456
- { id: 'RC-04', name: 'SCHEMA realRef is parsed at its interface', severity: 'warning', evaluate: schemaRefMustBeUsed },
457
- { id: 'RC-05', name: 'cross-module import drift', severity: 'warning', evaluate: importDriftConformance },
458
- { id: 'RC-06', name: 'external realRef names a declared dependency', severity: 'warning', evaluate: externalRefMustNameDependency },
453
+ { id: 'RC-01', name: 'FUNC realRef resolves to a declared symbol', severity: 'error', domain: ['FUNC'], evaluate: codeRefMustResolve },
454
+ { id: 'RC-02', name: 'testRefs entries resolve to runnable tests', severity: 'error', domain: ['TEST'], evaluate: testRefMustResolve },
455
+ { id: 'RC-03', name: 'SCHEMA realRef resolves to a declared export', severity: 'error', domain: ['SCHEMA'], evaluate: schemaRefMustResolve },
456
+ { id: 'RC-04', name: 'SCHEMA realRef is parsed at its interface', severity: 'warning', domain: ['SCHEMA'], evaluate: schemaRefMustBeUsed },
457
+ { id: 'RC-05', name: 'cross-module import drift', severity: 'warning', domain: ['MOD'], evaluate: importDriftConformance },
458
+ { id: 'RC-06', name: 'external realRef names a declared dependency', severity: 'warning', domain: ['FUNC', 'MOD', 'SCHEMA'], evaluate: externalRefMustNameDependency },
459
459
  ];
460
460
  /** Run all RC rules against a graph + extracted code facts. */
461
461
  export function evaluateConformanceRules(graph, facts) {
@@ -1,16 +1,55 @@
1
1
  // ---------------------------------------------------------------------------
2
- // CR-R01: CR must have at least one outgoing relation trace
2
+ // CR-R01: ein offener CR nennt, WAS er aendert (CR-SM-295)
3
+ //
4
+ // Bis hierher waren es zwei Regeln, und beide zielten daneben. CR-R01 verlangte
5
+ // IRGENDEINE relation-Kante — und akzeptierte damit einen CR, der nur an einem
6
+ // Meilenstein haengt: das ist ein Termin, kein Umfang. Ueber 514 CR-Knoten in 19
7
+ // Familiengraphen feuerte sie 5-mal, waehrend 49 CRs ausschliesslich auf ein MS zeigten
8
+ // und unbeanstandet blieben. CR-R04 verlangte umgekehrt einen FUNC — zu eng: 21 CRs
9
+ // meldeten, und ein grosser Teil davon aendert Daten, Doku oder Konfiguration und
10
+ // beruehrt zu Recht keine Funktion.
11
+ //
12
+ // Die tragende Frage ist keine von beiden, sondern: nennt dieser CR ein Element, das er
13
+ // AENDERT? Umfang sind FUNC, MOD, SCHEMA, REQ und UC. MS ist Terminplanung und zaehlt
14
+ // nicht — MS-03 stellt diese Frage getrennt und bleibt unberuehrt.
15
+ //
16
+ // Grundgesamtheit wie bei CR-R04 (CR-SM-255) — nur was noch steuerbar ist. An einem
17
+ // geschlossenen CR ist die Frage Archaeologie: was er beruehrt hat, steht im Commit.
18
+ //
19
+ // Die Menge ist aber UMGEDREHT formuliert: nicht "open oder in-progress", sondern "nicht
20
+ // abgeschlossen". Der Grund ist gemessen — im Bestand tragen CRs die Status `done` (463),
21
+ // `open` (34), `planned` (12), `rejected` (2), `dropped` (1) und keinen (2). `planned`
22
+ // steht in keinem Enum der Ontologie, wird aber real geschrieben; eine Positivliste haette
23
+ // diese zwoelf CRs stillschweigend uebersprungen. Eine Regel, die durch einen ihr
24
+ // unbekannten Statuswert stumm wird, ist von einer bestandenen Pruefung nicht zu
25
+ // unterscheiden — dieselbe Fail-open-Klasse wie ND-01/ND-02 vor CR-SM-286.
26
+ //
27
+ // Ein CR ganz OHNE status ist erst recht nicht abgeschlossen.
28
+ //
29
+ // Gemessen vor der Umstellung: von 514 CRs tragen 54 keinen Umfangsbezug — davon sind
30
+ // **0 offen**, 52 `done` und 2 ohne Status. Die neue Fassung feuert am Bestand also auf
31
+ // hoechstens 2 und laesst die Historie unangetastet; ihre Wirkung beginnt beim naechsten
32
+ // CR, der geschrieben wird.
3
33
  // ---------------------------------------------------------------------------
34
+ /** Elementtypen, die einen Aenderungsumfang darstellen — MS gehoert bewusst nicht dazu. */
35
+ const SCOPE_TYPES = new Set(['FUNC', 'MOD', 'SCHEMA', 'REQ', 'UC']);
36
+ /** Abgeschlossen heisst: nicht mehr steuerbar. Alles andere ist Grundgesamtheit. */
37
+ const CLOSED_STATUS = new Set(['done', 'dropped', 'rejected']);
4
38
  function crMustTrack(graph) {
5
- const crs = graph.elements.filter(e => e.type === 'CR');
39
+ const typeOf = new Map(graph.elements.map(e => [e.id, e.type]));
40
+ const crs = graph.elements.filter(e => {
41
+ if (e.type !== 'CR')
42
+ return false;
43
+ return !CLOSED_STATUS.has(String(e.attributes?.status ?? ''));
44
+ });
6
45
  return crs
7
- .filter(cr => !graph.traces.some(t => t.source === cr.id && t.type === 'relation'))
46
+ .filter(cr => !graph.traces.some(t => t.source === cr.id && t.type === 'relation' && SCOPE_TYPES.has(typeOf.get(t.target) ?? '')))
8
47
  .map(cr => ({
9
48
  rule_id: 'CR-R01',
10
49
  severity: 'error',
11
50
  element_id: cr.id,
12
- message: `${cr.id} has no relation traces (tracks nothing)`,
13
- fix_hint: 'Add relation traces to affected UC, REQ, FUNC, or MOD elements',
51
+ message: `${cr.id} names no scope (no relation to FUNC/MOD/SCHEMA/REQ/UC)`,
52
+ fix_hint: 'Add a relation trace to the FUNC, MOD, SCHEMA, REQ or UC this CR changes — a milestone alone is a date, not a scope',
14
53
  context: {
15
54
  element_type: cr.type,
16
55
  element_name: cr.name,
@@ -63,71 +102,50 @@ function noConcurrentMutation(graph) {
63
102
  targetToCrs.set(tgt, list);
64
103
  }
65
104
  }
66
- const violations = [];
67
- const seen = new Set();
68
- for (const [target, crIds] of targetToCrs) {
105
+ // CR-SM-304 EIN Befund je Konflikt-MENGE, aber an einem BERECHENBAREN Element.
106
+ //
107
+ // Die Ein-Befund-Regel bleibt: dieselben zwei offenen CRs, die sich an drei Knoten begegnen,
108
+ // sind EIN Koordinationsproblem und nicht drei (Gate 4, Doppelzaehlung). Falsch war, woher der
109
+ // Anker kam: `targetToCrs` folgt der TRACE-Reihenfolge, also entschied die Array-Ordnung im
110
+ // Graphen, welches Ziel meldet — "kein erster Treffer" ist Gate 5 woertlich, und dieselbe
111
+ // Klasse wie der offene R-28-Fall (CR-SM-245). Gemessen am graphcode-Selbstmodell wechselte
112
+ // der Befund bei umgedrehter Reihenfolge von FUNC-graph-suggest auf MS-9-generation; ein
113
+ // Reseed aus dem Kuzu-Store reichte, damit eine Vorher/Nachher-Bilanz bei NULL Zuegen
114
+ // "einen geschlossen, einen neu" meldete.
115
+ //
116
+ // Zweiter Teil desselben Defekts: das unterdrueckte Ziel wurde nicht nur nicht angekert, es
117
+ // wurde gar nicht genannt. Wer koordinieren soll, braucht die vollstaendige Liste — sie steht
118
+ // jetzt in der Meldung und als `also_affects` im Kontext.
119
+ const bySet = new Map();
120
+ for (const target of [...targetToCrs.keys()].sort()) {
121
+ const crIds = [...targetToCrs.get(target)].sort();
69
122
  if (crIds.length < 2)
70
123
  continue;
71
- const key = crIds.sort().join(',');
72
- if (seen.has(key))
73
- continue;
74
- seen.add(key);
124
+ const key = crIds.join(',');
125
+ bySet.set(key, [...(bySet.get(key) ?? []), target]);
126
+ }
127
+ const violations = [];
128
+ for (const [key, targets] of bySet) {
129
+ const crIds = key.split(',');
130
+ const [anchor, ...also] = targets;
131
+ const el = graph.elements.find(e => e.id === anchor);
75
132
  violations.push({
76
133
  rule_id: 'CR-R03',
77
134
  severity: 'warning',
78
- element_id: target,
79
- message: `${target} is tracked by ${crIds.length} open CRs: ${crIds.join(', ')}`,
135
+ element_id: anchor,
136
+ message: `${anchor} is tracked by ${crIds.length} open CRs: ${crIds.join(', ')}` +
137
+ (also.length > 0 ? ` — the same CRs also track ${also.join(', ')}` : ''),
80
138
  fix_hint: 'Coordinate changes to avoid merge conflicts',
81
139
  context: {
82
- element_type: graph.elements.find(e => e.id === target)?.type,
83
- element_name: graph.elements.find(e => e.id === target)?.name,
140
+ element_type: el?.type,
141
+ element_name: el?.name,
142
+ also_affects: also,
84
143
  },
85
144
  });
86
145
  }
87
146
  return violations;
88
147
  }
89
148
  // ---------------------------------------------------------------------------
90
- // CR-R04: an OPEN CR must have at least one relation→FUNC trace (CR-207,
91
- // Grundgesamtheit verengt in CR-SM-255)
92
- //
93
- // "Welche Funktionen fasst dieser CR an" ist eine PLANUNGSfrage — sie steuert, bevor
94
- // gebaut wird. An einem geschlossenen CR ist sie Archaeologie: was er beruehrt hat,
95
- // steht im Commit und im Diff, nicht im Graphen. Wer die Kanten nachtraeglich zieht,
96
- // raet, und erfundene Praezision ist teurer als eine fehlende Kante.
97
- //
98
- // Gemessen am graphcode-Selbstmodell (graphVersion 171): 41 meldende CRs, davon 40
99
- // `done` und 1 `dropped` — kein einziger offener. Die Regel feuerte ausschliesslich
100
- // dort, wo sie nichts mehr steuern kann, und trug damit 41 unbearbeitbare Befunde.
101
- //
102
- // Die Statusmenge ist woertlich die von CR-R03 (s. o.): dieselbe Frage nach "noch in
103
- // Arbeit", dieselbe Antwort. CR-R01 ("ein CR trackt ueberhaupt etwas") bleibt fuer
104
- // ALLE CRs gueltig — nur die Verschaerfung "und zwar einen FUNC" ist Planung.
105
- // ---------------------------------------------------------------------------
106
- function crMustHaveFunc(graph) {
107
- // Ein CR ganz OHNE status ist kein geschlossener CR — die Regel darf nicht durch ein
108
- // fehlendes Attribut stumm werden.
109
- const crs = graph.elements.filter(e => {
110
- if (e.type !== 'CR')
111
- return false;
112
- const status = e.attributes?.status;
113
- return status === undefined || status === 'open' || status === 'in-progress';
114
- });
115
- return crs
116
- .filter(cr => !graph.traces.some(t => t.source === cr.id && t.type === 'relation' &&
117
- graph.elements.some(e => e.id === t.target && e.type === 'FUNC')))
118
- .map(cr => ({
119
- rule_id: 'CR-R04',
120
- severity: 'warning',
121
- element_id: cr.id,
122
- message: `${cr.id} has no relation→FUNC trace (no implementation scope)`,
123
- fix_hint: 'Add CR→FUNC [relation] traces to define which functions this CR changes',
124
- context: {
125
- element_type: cr.type,
126
- element_name: cr.name,
127
- },
128
- }));
129
- }
130
- // ---------------------------------------------------------------------------
131
149
  // MS-03: CR should have a CR→MS [relation] trace (CR-207)
132
150
  // ---------------------------------------------------------------------------
133
151
  function crShouldHaveMilestone(graph) {
@@ -159,7 +177,6 @@ export const CR_RULES = [
159
177
  // (= alle Elemente) ist die einzige Menge, aus der der Zielknoten garantiert stammt.
160
178
  // Damit ist sie nach R-08/R-18 die dritte 'all'-Regel — bewusst, nicht vergessen.
161
179
  { id: 'CR-R03', name: 'No concurrent mutation', severity: 'warning', evaluate: noConcurrentMutation, domain: ['all'] },
162
- { id: 'CR-R04', name: 'Open CR must have FUNC', severity: 'warning', evaluate: crMustHaveFunc, domain: ['CR'] },
163
180
  { id: 'MS-03', name: 'CR without milestone', severity: 'info', evaluate: crShouldHaveMilestone, domain: ['CR'] },
164
181
  ];
165
182
  // CR-SM-236: `policy` wird durchgereicht, auch wo diese Familie heute keine Schwelle hat —
@@ -13,14 +13,16 @@ import type { MetricPolicy } from './policy.js';
13
13
  * hier statt eine eigene Tabelle zu fuehren. Eine zweite Tabelle kann nicht hinterherhinken,
14
14
  * wenn es keine zweite gibt.
15
15
  */
16
+ /** Profile groupings — see CATALOGS. */
17
+ export type ProfileId = 'default' | 'se' | 'coding' | 'conformance';
16
18
  export declare const ALL_RULE_DEFS: ReadonlyArray<{
17
19
  id: string;
18
20
  name: string;
19
21
  severity: 'error' | 'warning' | 'info';
20
22
  domain: readonly string[];
23
+ /** CR-SM-285: das Profil reist mit der Regel, statt aus ihrer ID geraten zu werden. */
24
+ profile: Exclude<ProfileId, 'default'>;
21
25
  }>;
22
- /** Profile groupings derived from rule prefixes. */
23
- export type ProfileId = 'default' | 'se' | 'coding';
24
26
  export declare function getRuleDefsForProfile(profile: ProfileId): typeof ALL_RULE_DEFS;
25
27
  /**
26
28
  * Evaluate all rules against a graph. Single call replaces the individual evaluator calls.