@sigloch/contracts 3.2.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/harness/index.d.ts +2 -2
  2. package/dist/se/action-priority.d.ts +101 -0
  3. package/dist/se/action-priority.js +124 -0
  4. package/dist/se/analysis-freshness-rules.d.ts +5 -0
  5. package/dist/se/analysis-freshness-rules.js +5 -5
  6. package/dist/se/ao-rules.d.ts +5 -39
  7. package/dist/se/ao-rules.js +20 -13
  8. package/dist/se/conformance-rules.d.ts +2 -2
  9. package/dist/se/conformance-rules.js +36 -30
  10. package/dist/se/cr-quality-rules.d.ts +2 -1
  11. package/dist/se/cr-quality-rules.js +9 -7
  12. package/dist/se/evaluate-all.d.ts +18 -3
  13. package/dist/se/evaluate-all.js +29 -26
  14. package/dist/se/fchain-quality-rules.d.ts +2 -1
  15. package/dist/se/fchain-quality-rules.js +8 -6
  16. package/dist/se/fmea-rules.d.ts +3 -11
  17. package/dist/se/fmea-rules.js +53 -16
  18. package/dist/se/format-e-parser.d.ts +14 -1
  19. package/dist/se/format-e-parser.js +8 -3
  20. package/dist/se/index.d.ts +4 -2
  21. package/dist/se/index.js +4 -2
  22. package/dist/se/metric-rules.d.ts +47 -5
  23. package/dist/se/metric-rules.js +199 -171
  24. package/dist/se/near-duplicate-rules.d.ts +2 -0
  25. package/dist/se/near-duplicate-rules.js +2 -2
  26. package/dist/se/ontology.d.ts +55 -4
  27. package/dist/se/ontology.js +46 -6
  28. package/dist/se/policy.d.ts +65 -0
  29. package/dist/se/policy.js +100 -0
  30. package/dist/se/quality-rules.d.ts +2 -1
  31. package/dist/se/quality-rules.js +9 -7
  32. package/dist/se/readiness.d.ts +9 -1
  33. package/dist/se/readiness.js +18 -2
  34. package/dist/se/rules.d.ts +25 -5
  35. package/dist/se/rules.js +112 -47
  36. package/dist/se/schema-quality-rules.d.ts +2 -1
  37. package/dist/se/schema-quality-rules.js +6 -4
  38. package/dist/se/uc-quality-rules.d.ts +2 -1
  39. package/dist/se/uc-quality-rules.js +10 -8
  40. package/dist/se/view-rules.d.ts +2 -6
  41. package/dist/se/view-rules.js +40 -13
  42. package/package.json +2 -1
@@ -0,0 +1,65 @@
1
+ /**
2
+ * CR-SM-233: Urteilsschwellen der Architektur-Metriken als Eingabe, nicht als Modulkonstante.
3
+ *
4
+ * Vorher stand die Schwelle als `const INSTABILITY_THRESHOLD = 0.7` in `metric-rules.ts`.
5
+ * Jede Konfigurierbarkeit von außen wäre damit automatisch ein zweiter Pfad gewesen
6
+ * (Config-Wert neben Code-Default) — und ein stillgelegter Wert hätte still weiterurteilt,
7
+ * weil der Default einspringt. Deshalb: Parameter ohne Fallback.
8
+ */
9
+ import { z } from 'zod';
10
+ /**
11
+ * Urteilsschwellen der Architektur-Metriken. `null` = messen, nicht urteilen.
12
+ *
13
+ * `null` ist ein erstklassiger Wert, kein „aus": die Kennzahl wird weiter gerechnet,
14
+ * exportiert und angezeigt; es entsteht nur kein Verstoß. Das ist die MT-03-Entscheidung
15
+ * (CR-SM-223) verallgemeinert — der Weg, einen unvalidierten Wert stillzulegen, ohne die
16
+ * Zahl zu verlieren.
17
+ */
18
+ export declare const MetricPolicySchema: z.ZodObject<{
19
+ instability: z.ZodNullable<z.ZodNumber>;
20
+ lcom4: z.ZodNullable<z.ZodObject<{
21
+ info: z.ZodNumber;
22
+ warning: z.ZodNumber;
23
+ }, z.core.$strip>>;
24
+ crossingFlows: z.ZodNullable<z.ZodObject<{
25
+ warning: z.ZodNumber;
26
+ }, z.core.$strip>>;
27
+ riskRpn: z.ZodNullable<z.ZodNumber>;
28
+ apTable: z.ZodNullable<z.ZodObject<{
29
+ severityBands: z.ZodArray<z.ZodObject<{
30
+ from: z.ZodNumber;
31
+ to: z.ZodNumber;
32
+ }, z.core.$strip>>;
33
+ occurrenceBands: z.ZodArray<z.ZodObject<{
34
+ from: z.ZodNumber;
35
+ to: z.ZodNumber;
36
+ }, z.core.$strip>>;
37
+ detectionBands: z.ZodArray<z.ZodObject<{
38
+ from: z.ZodNumber;
39
+ to: z.ZodNumber;
40
+ }, z.core.$strip>>;
41
+ cells: z.ZodArray<z.ZodArray<z.ZodArray<z.ZodEnum<{
42
+ High: "High";
43
+ Medium: "Medium";
44
+ Low: "Low";
45
+ }>>>>;
46
+ }, z.core.$strip>>;
47
+ moduleSize: z.ZodNullable<z.ZodObject<{
48
+ large: z.ZodNumber;
49
+ coupled: z.ZodNumber;
50
+ crossings: z.ZodNumber;
51
+ }, z.core.$strip>>;
52
+ }, z.core.$strip>;
53
+ export type MetricPolicy = z.infer<typeof MetricPolicySchema>;
54
+ /**
55
+ * Der Startwert, sichtbar als solcher — **kein** Fallback in der Regel.
56
+ *
57
+ * Ein Aufrufer nimmt ihn bewusst oder ersetzt ihn; er steht an genau einer Stelle und ist
58
+ * grep-bar. Die Werte sind die bisherigen (0.7 / 4 / 6-statt-5-Grenze siehe unten) und
59
+ * ausdrücklich **unvalidiert** (CR-SM-223: „Validation … is deferred") — sie sind gesetzt,
60
+ * nicht gemessen.
61
+ *
62
+ * `lcom4.warning = 6` ist die unveränderte Alt-Semantik: vorher `>= 4 && <= 5` → info,
63
+ * `> 5` → warning, also warning ab 6.
64
+ */
65
+ export declare const DEFAULT_METRIC_POLICY: MetricPolicy;
@@ -0,0 +1,100 @@
1
+ /**
2
+ * CR-SM-233: Urteilsschwellen der Architektur-Metriken als Eingabe, nicht als Modulkonstante.
3
+ *
4
+ * Vorher stand die Schwelle als `const INSTABILITY_THRESHOLD = 0.7` in `metric-rules.ts`.
5
+ * Jede Konfigurierbarkeit von außen wäre damit automatisch ein zweiter Pfad gewesen
6
+ * (Config-Wert neben Code-Default) — und ein stillgelegter Wert hätte still weiterurteilt,
7
+ * weil der Default einspringt. Deshalb: Parameter ohne Fallback.
8
+ */
9
+ import { z } from 'zod';
10
+ import { ApTableSchema } from './action-priority.js';
11
+ /**
12
+ * Urteilsschwellen der Architektur-Metriken. `null` = messen, nicht urteilen.
13
+ *
14
+ * `null` ist ein erstklassiger Wert, kein „aus": die Kennzahl wird weiter gerechnet,
15
+ * exportiert und angezeigt; es entsteht nur kein Verstoß. Das ist die MT-03-Entscheidung
16
+ * (CR-SM-223) verallgemeinert — der Weg, einen unvalidierten Wert stillzulegen, ohne die
17
+ * Zahl zu verlieren.
18
+ */
19
+ export const MetricPolicySchema = z.object({
20
+ /** MT-01: Instabilität, ab der gewarnt wird (exklusiv). null → MT-01 feuert nie. */
21
+ instability: z.number().min(0).max(1).nullable(),
22
+ /** MT-02: LCOM4-Stufen (info ab, warning ab). null → MT-02 feuert nie. */
23
+ lcom4: z
24
+ .object({
25
+ info: z.number().int().min(2),
26
+ warning: z.number().int().min(2),
27
+ })
28
+ .nullable(),
29
+ /**
30
+ * CR-01: kreuzende io-Flows je Modulpaar, ab denen gewarnt wird (inklusiv).
31
+ *
32
+ * `null` → CR-01 meldet **nichts**, auch keine info. Das ist der Aus-Zustand, den die Regel
33
+ * vorher nicht hatte: sie meldete je Modulpaar mit ≥ 1 Kreuzung, und diese info-Fälle zählen
34
+ * in die `arch`-Dimension ein (CR-SM-236).
35
+ */
36
+ crossingFlows: z.object({ warning: z.number().int().min(1) }).nullable(),
37
+ /**
38
+ * FM-03: RPN (severity · occurrence · detection), ab dem ein Risiko-REQ eine bestandene
39
+ * Verifikation braucht (exklusiv). null → FM-03 feuert nie.
40
+ *
41
+ * 100 ist Domänenstandard, aber skalenabhängig: bei anderer Belegung von S/O/D ist es die
42
+ * falsche Grenze.
43
+ */
44
+ riskRpn: z.number().int().positive().nullable(),
45
+ /**
46
+ * CR-SM-229: die lizenzierte AIAG-VDA-Tabelle, wenn der Host eine hat.
47
+ *
48
+ * Sie reist im Policy-Objekt mit, **nicht** in einem zweiten Kontext-Parameter: seit
49
+ * CR-SM-233 ist `policy` der eine Kanal, über den eine Regel ihre Urteilsgrundlage bekommt.
50
+ * Ein zweiter daneben wäre genau der parallele Pfad, den der Regelsatz verbietet.
51
+ *
52
+ * `null` → der markierte Übergang gilt (bestätigte AP-Invarianten, sonst RPN-Bänder). Der
53
+ * Konsument lädt die Tabelle aus einer lokalen, nie eingecheckten Datei; **im Repo und im
54
+ * exportierten Graphen steht sie nie** (Zellwerte sind normative Wertung, keine Fakten).
55
+ */
56
+ apTable: ApTableSchema.nullable(),
57
+ /**
58
+ * R-04: Modulgröße **gegen** Kreuzungen — die Regel wägt beides ab, ihr alter Name
59
+ * („Max module size") gab das nicht her. null → R-04 feuert nie.
60
+ *
61
+ * Drei Werte, weil die Regel drei Urteile fällt. Ein einzelner Schwellwert hätte zwei davon
62
+ * im Code gelassen — genau der Zustand, den dieser CR beseitigt.
63
+ */
64
+ moduleSize: z
65
+ .object({
66
+ /** > `large` FUNCs: mit Kreuzungen warning, ohne Kreuzungen info („kohäsiv, nur groß"). */
67
+ large: z.number().int().min(1),
68
+ /** > `coupled` FUNCs ist die Untergrenze, ab der die Regel überhaupt hinsieht. */
69
+ coupled: z.number().int().min(1),
70
+ /** Kreuzungen, ab denen ein nur `coupled`-großes Modul warnt (exklusiv). */
71
+ crossings: z.number().int().min(0),
72
+ })
73
+ .nullable(),
74
+ });
75
+ /**
76
+ * Der Startwert, sichtbar als solcher — **kein** Fallback in der Regel.
77
+ *
78
+ * Ein Aufrufer nimmt ihn bewusst oder ersetzt ihn; er steht an genau einer Stelle und ist
79
+ * grep-bar. Die Werte sind die bisherigen (0.7 / 4 / 6-statt-5-Grenze siehe unten) und
80
+ * ausdrücklich **unvalidiert** (CR-SM-223: „Validation … is deferred") — sie sind gesetzt,
81
+ * nicht gemessen.
82
+ *
83
+ * `lcom4.warning = 6` ist die unveränderte Alt-Semantik: vorher `>= 4 && <= 5` → info,
84
+ * `> 5` → warning, also warning ab 6.
85
+ */
86
+ export const DEFAULT_METRIC_POLICY = {
87
+ instability: 0.7,
88
+ lcom4: { info: 4, warning: 6 },
89
+ // CR-SM-236: die bisherigen Literale, jetzt an einer grep-baren Stelle.
90
+ // `crossingFlows.warning = 3` ist die unveränderte Alt-Semantik: vorher `count > 2` → warning.
91
+ crossingFlows: { warning: 3 },
92
+ riskRpn: 100,
93
+ // `large`/`coupled`/`crossings` = die drei Literale aus `maxModuleSize`: vorher
94
+ // `funcCount <= 8` → skip, `funcCount > 12` → groß, `crossings > 2` → gekoppelt.
95
+ moduleSize: { large: 12, coupled: 8, crossings: 2 },
96
+ // CR-SM-229: kein Startwert möglich und keiner gewollt — die Tabelle steht nur im Handbook.
97
+ // `null` heißt hier nicht „aus", sondern „markierter Übergang": bestätigte AP-Invarianten,
98
+ // sonst RPN-Bänder. `apMethod()` macht das für den Leser sichtbar.
99
+ apTable: null,
100
+ };
@@ -5,6 +5,7 @@
5
5
  */
6
6
  import type { OntologyGraph } from './ontology.js';
7
7
  import type { RuleDefinition, RuleViolation } from './rules.js';
8
+ import type { MetricPolicy } from './policy.js';
8
9
  export declare function bq01Unambiguous(graph: OntologyGraph): RuleViolation[];
9
10
  export declare function bq02Verifiable(graph: OntologyGraph): RuleViolation[];
10
11
  /**
@@ -25,4 +26,4 @@ export declare function bq06Conforming(graph: OntologyGraph): RuleViolation[];
25
26
  export declare function bq07Complete(graph: OntologyGraph): RuleViolation[];
26
27
  export declare const BQ_RULES: RuleDefinition[];
27
28
  /** Run all BQ rules against a graph, returning combined violations. */
28
- export declare function evaluateBQRules(graph: OntologyGraph): RuleViolation[];
29
+ export declare function evaluateBQRules(graph: OntologyGraph, policy: MetricPolicy): RuleViolation[];
@@ -194,13 +194,15 @@ export function bq07Complete(graph) {
194
194
  // Aggregated array & convenience runner
195
195
  // ---------------------------------------------------------------------------
196
196
  export const BQ_RULES = [
197
- { id: 'BQ-01', name: 'Unambiguous', severity: 'warning', evaluate: bq01Unambiguous },
198
- { id: 'BQ-02', name: 'Verifiable', severity: 'warning', evaluate: bq02Verifiable },
199
- { id: 'BQ-04', name: 'Necessary', severity: 'warning', evaluate: bq04Necessary },
200
- { id: 'BQ-06', name: 'Conforming', severity: 'warning', evaluate: bq06Conforming },
201
- { id: 'BQ-07', name: 'Complete', severity: 'warning', evaluate: bq07Complete },
197
+ { id: 'BQ-01', name: 'Unambiguous', severity: 'warning', evaluate: bq01Unambiguous, domain: ['REQ'] },
198
+ { id: 'BQ-02', name: 'Verifiable', severity: 'warning', evaluate: bq02Verifiable, domain: ['REQ'] },
199
+ { id: 'BQ-04', name: 'Necessary', severity: 'warning', evaluate: bq04Necessary, domain: ['REQ'] },
200
+ { id: 'BQ-06', name: 'Conforming', severity: 'warning', evaluate: bq06Conforming, domain: ['REQ'] },
201
+ { id: 'BQ-07', name: 'Complete', severity: 'warning', evaluate: bq07Complete, domain: ['REQ'] },
202
202
  ];
203
203
  /** Run all BQ rules against a graph, returning combined violations. */
204
- export function evaluateBQRules(graph) {
205
- return BQ_RULES.flatMap(rule => rule.evaluate(graph));
204
+ // CR-SM-236: `policy` wird durchgereicht, auch wo diese Familie heute keine Schwelle hat —
205
+ // ein Sonderweg je Familie waere genau der zweite Pfad, den der Regelsatz verbietet.
206
+ export function evaluateBQRules(graph, policy) {
207
+ return BQ_RULES.flatMap(rule => rule.evaluate(graph, policy));
206
208
  }
@@ -36,6 +36,15 @@ export declare const ReadinessScore: z.ZodObject<{
36
36
  ready: z.ZodBoolean;
37
37
  }, z.core.$strip>;
38
38
  export type ReadinessScoreType = z.infer<typeof ReadinessScore>;
39
+ /**
40
+ * CR-SM-237: `overallScore` ist weg. Es war das **ungewichtete** Mittel der acht
41
+ * Dimensions-Scores — `ms` (3 Regeln) zählte so viel wie `arch` (~20 Regeln über Hunderte
42
+ * Elemente), die Zahl bewegte sich also mit dem Größenverhältnis der Dimensionen und nicht mit
43
+ * dem Zustand des Modells. Gelesen hat sie ohnehin niemand: der einzige Zugriff in der Familie
44
+ * war eine Formprüfung (`'overallScore' in x`) im inzwischen gelöschten `weight-vector.ts`.
45
+ * Ein interpretierbarer Ersatz (`1 − Σviolations / Σapplicable`) kommt, wenn er einen
46
+ * Konsumenten hat — nicht auf Vorrat.
47
+ */
39
48
  export declare const ReadinessReport: z.ZodObject<{
40
49
  scores: z.ZodArray<z.ZodObject<{
41
50
  dimension: z.ZodEnum<{
@@ -53,7 +62,6 @@ export declare const ReadinessReport: z.ZodObject<{
53
62
  applicable: z.ZodNumber;
54
63
  ready: z.ZodBoolean;
55
64
  }, z.core.$strip>>;
56
- overallScore: z.ZodNumber;
57
65
  timestamp: z.ZodISODateTime;
58
66
  }, z.core.$strip>;
59
67
  export type ReadinessReportType = z.infer<typeof ReadinessReport>;
@@ -23,11 +23,21 @@ export const ReadinessScore = z.object({
23
23
  score: z.number().min(0).max(1), // 1 - (violations / applicable)
24
24
  violations: z.number().int(),
25
25
  applicable: z.number().int(),
26
- ready: z.boolean(), // score >= 0.7
26
+ // CR-SM-235: `score >= readyThreshold`. Die Schwelle steht bewusst NICHT hier — sie ist
27
+ // Eingabe von `computeReadiness`, und eine Zahl im Kommentar wäre die dritte Meinung dazu.
28
+ ready: z.boolean(),
27
29
  });
30
+ /**
31
+ * CR-SM-237: `overallScore` ist weg. Es war das **ungewichtete** Mittel der acht
32
+ * Dimensions-Scores — `ms` (3 Regeln) zählte so viel wie `arch` (~20 Regeln über Hunderte
33
+ * Elemente), die Zahl bewegte sich also mit dem Größenverhältnis der Dimensionen und nicht mit
34
+ * dem Zustand des Modells. Gelesen hat sie ohnehin niemand: der einzige Zugriff in der Familie
35
+ * war eine Formprüfung (`'overallScore' in x`) im inzwischen gelöschten `weight-vector.ts`.
36
+ * Ein interpretierbarer Ersatz (`1 − Σviolations / Σapplicable`) kommt, wenn er einen
37
+ * Konsumenten hat — nicht auf Vorrat.
38
+ */
28
39
  export const ReadinessReport = z.object({
29
40
  scores: z.array(ReadinessScore),
30
- overallScore: z.number().min(0).max(1),
31
41
  timestamp: z.iso.datetime(),
32
42
  });
33
43
  /**
@@ -44,6 +54,9 @@ export const RULE_TO_DIMENSION = {
44
54
  'RD-04': 'arch',
45
55
  // trace/realization/allocation completeness rules (CR-228 D: previously unmapped → advisory fall-through)
46
56
  'R-18': 'arch', 'R-19': 'ver', 'R-20': 'arch', 'R-21': 'ver',
57
+ // CR-SM-231: R-29 (Testdatei-Exklusivitaet) gehoert zu 'ver' wie R-19 — beide bewerten die
58
+ // Evidenz-Bindung einer Abnahme, R-19 ihre Praesenz, R-29 ihre Eindeutigkeit.
59
+ 'R-29': 'ver',
47
60
  'R-22': 'alloc', 'R-23': 'alloc', 'R-26': 'schema', 'R-27': 'arch',
48
61
  // CR-SM-226: R-28 Ebenen-Präsenz (>1 FUNC needs FLOW+SCHEMA) is an architecture rule.
49
62
  'R-28': 'arch',
@@ -148,6 +161,9 @@ export const RULE_TO_PHASE = {
148
161
  'NFR-01': 'CDR',
149
162
  // TRR — test readiness.
150
163
  'R-01': 'TRR', 'R-05': 'TRR', 'R-19': 'TRR', 'R-20': 'TRR', 'R-21': 'TRR',
164
+ // CR-SM-231: eine doppelt beanspruchte Testdatei laesst den TRR-Gate dieselbe Evidenz
165
+ // doppelt zaehlen — der Befund gehoert genau an dieses Gate.
166
+ 'R-29': 'TRR',
151
167
  'VR-01': 'TRR',
152
168
  'FM-03': 'TRR',
153
169
  'CR-R02': 'TRR',
@@ -5,10 +5,11 @@
5
5
  */
6
6
  import { z } from 'zod/v4';
7
7
  import type { OntologyGraph } from './ontology.js';
8
+ import type { MetricPolicy } from './policy.js';
8
9
  export declare const RuleSeverity: z.ZodEnum<{
9
10
  error: "error";
10
- warning: "warning";
11
11
  info: "info";
12
+ warning: "warning";
12
13
  }>;
13
14
  export type RuleSeverity = z.infer<typeof RuleSeverity>;
14
15
  /** Candidate target for resolving a violation (e.g. a REQ to satisfy, a TEST to link). */
@@ -89,8 +90,8 @@ export declare const RuleViolation: z.ZodObject<{
89
90
  rule_id: z.ZodString;
90
91
  severity: z.ZodEnum<{
91
92
  error: "error";
92
- warning: "warning";
93
93
  info: "info";
94
+ warning: "warning";
94
95
  }>;
95
96
  element_id: z.ZodString;
96
97
  message: z.ZodString;
@@ -152,8 +153,27 @@ export interface RuleDefinition {
152
153
  id: string;
153
154
  name: string;
154
155
  severity: 'error' | 'warning' | 'info';
155
- evaluate: (graph: OntologyGraph) => RuleViolation[];
156
+ /**
157
+ * CR-SM-236: `policy` steht jeder Regel zur Verfuegung, damit eine Urteilsschwelle nirgends
158
+ * als Literal im Regelcode zurueckbleibt. Regeln ohne Schwelle deklarieren den Parameter
159
+ * schlicht nicht — eine einstellige Funktion bleibt zuweisbar.
160
+ */
161
+ evaluate: (graph: OntologyGraph, policy: MetricPolicy) => RuleViolation[];
162
+ /**
163
+ * CR-SM-235: die Grundgesamtheit, ueber die die Regel feuert — der Nenner ihres
164
+ * Readiness-Anteils. **Der Elementtyp, ueber den sie feuert, nicht das Thema, dem sie
165
+ * gehoert:** MS-03 („CR ohne Milestone") feuert je CR, nicht je MS.
166
+ *
167
+ * Pflichtfeld, damit ein neuer Regeleintrag ohne Grundgesamtheit den Build bricht. Vorher
168
+ * lag die Zuordnung als Handtabelle in einem anderen Paket (`se-steering`), 18 von 71 Regeln
169
+ * fehlten dort — sie erhoehten den Zaehler, nie den Nenner, und jede neue Regel senkte den
170
+ * Score automatisch, bis jemand die zweite Tabelle nachzog.
171
+ *
172
+ * Mehrere Typen, wo die Regel ueber mehrere feuert (RD-04: FUNC, MOD und SYS als Eltern
173
+ * einer Zerlegung). `['all']` = jedes Nicht-SESSION-Element (R-08/R-18 pruefen Traces).
174
+ */
175
+ domain: readonly string[];
156
176
  }
157
177
  export declare const V3_RULES: RuleDefinition[];
158
- /** Run all rules against a graph */
159
- export declare function evaluateRules(graph: OntologyGraph): RuleViolation[];
178
+ /** Run all rules against a graph. CR-SM-236: `policy` ist Pflicht — wie bei `evaluateAllRules`. */
179
+ export declare function evaluateRules(graph: OntologyGraph, policy: MetricPolicy): RuleViolation[];
package/dist/se/rules.js CHANGED
@@ -4,7 +4,7 @@
4
4
  * @sigloch/contracts/se — single source of truth for SE validation rules.
5
5
  */
6
6
  import { z } from 'zod/v4';
7
- import { ElementType, TraceType, TestRefSchema, RealRefSchema } from './ontology.js';
7
+ import { ElementType, TraceType, TestRefsSchema, RealRefSchema } from './ontology.js';
8
8
  import { isValidTrace } from './meta-model.js';
9
9
  export const RuleSeverity = z.enum(['error', 'warning', 'info']);
10
10
  /** Candidate target for resolving a violation (e.g. a REQ to satisfy, a TEST to link). */
@@ -168,16 +168,26 @@ function asilIsolation(graph) {
168
168
  return violations;
169
169
  }
170
170
  // ---------------------------------------------------------------------------
171
- // R-04: Max module size
171
+ // R-04: Modulgroesse GEGEN Kreuzungen (nicht „max module size")
172
+ //
173
+ // CR-SM-236: die Regel waegt Groesse gegen kreuzende io-Flows ab — ein grosses Modul ohne
174
+ // Kreuzungen ist info („kohaesiv, nur gross"), dasselbe Modul mit Kreuzungen ist warning.
175
+ // Der alte Name gab das nicht her, und die drei Schwellen (8/12/2) standen inline: eine
176
+ // Urteilsschwelle, die sich weder per grep noch aus dem Regelnamen ablesen laesst, ist nicht
177
+ // ueberpruefbar. Jetzt sind es `policy.moduleSize.{coupled,large,crossings}`.
178
+ // `null` → messen, nicht urteilen: die Regel schweigt.
172
179
  // ---------------------------------------------------------------------------
173
- function maxModuleSize(graph) {
174
- const modules = graph.elements.filter(e => e.type === 'MOD');
180
+ function maxModuleSize(graph, policy) {
175
181
  const violations = [];
182
+ const steps = policy.moduleSize;
183
+ if (steps === null)
184
+ return violations;
185
+ const modules = graph.elements.filter(e => e.type === 'MOD');
176
186
  for (const mod of modules) {
177
187
  const allocatedIds = graph.traces.filter(t => t.target === mod.id && t.type === 'allocate').map(t => t.source);
178
188
  const allocated = allocatedIds.map(id => graph.elements.find(e => e.id === id)).filter((e) => !!e);
179
189
  const funcCount = allocated.length;
180
- if (funcCount <= 8)
190
+ if (funcCount <= steps.coupled)
181
191
  continue;
182
192
  // Count crossing flows: io paths from FUNCs in this module to FUNCs in other modules
183
193
  const funcIds = new Set(allocatedIds);
@@ -188,27 +198,27 @@ function maxModuleSize(graph) {
188
198
  const tgtIn = funcIds.has(t.target);
189
199
  return (srcIn && !tgtIn) || (!srcIn && tgtIn);
190
200
  }).length;
191
- if (funcCount > 12 && crossings === 0) {
201
+ if (funcCount > steps.large && crossings === 0) {
192
202
  violations.push({
193
203
  rule_id: 'R-04',
194
204
  severity: 'info',
195
205
  element_id: mod.id,
196
206
  message: `${mod.id} has ${funcCount} functions but 0 crossings (cohesive, just large)`,
197
- fix_hint: 'Consider splitting if complexity grows',
207
+ fix_hint: `Size alone is not the finding: > ${steps.large} functions without crossing flows is cohesive. Split only if crossings appear`,
198
208
  context: { element_type: mod.type, element_name: mod.name, candidate_targets: toCandidates(allocated) },
199
209
  });
200
210
  }
201
- else if (funcCount > 12 && crossings > 0) {
211
+ else if (funcCount > steps.large && crossings > 0) {
202
212
  violations.push({
203
213
  rule_id: 'R-04',
204
214
  severity: 'warning',
205
215
  element_id: mod.id,
206
216
  message: `${mod.id} has ${funcCount} functions and ${crossings} crossing flows (split recommended)`,
207
- fix_hint: 'Split module into smaller units to reduce coupling',
217
+ fix_hint: `Split module into smaller units to reduce coupling (> ${steps.large} functions AND crossing flows)`,
208
218
  context: { element_type: mod.type, element_name: mod.name, candidate_targets: toCandidates(allocated) },
209
219
  });
210
220
  }
211
- else if (funcCount > 8 && crossings > 2) {
221
+ else if (funcCount > steps.coupled && crossings > steps.crossings) {
212
222
  violations.push({
213
223
  rule_id: 'R-04',
214
224
  severity: 'warning',
@@ -439,7 +449,7 @@ function decompositionBreadth(graph) {
439
449
  const byId = new Map(graph.elements.map(e => [e.id, e]));
440
450
  const counts = new Map();
441
451
  const bump = (parentId, kind) => {
442
- const key = `${parentId}${kind}`;
452
+ const key = `${parentId}\u0000${kind}`;
443
453
  const entry = counts.get(key);
444
454
  if (entry)
445
455
  entry.n++;
@@ -621,11 +631,11 @@ function validTracePattern(graph) {
621
631
  }
622
632
  // ---------------------------------------------------------------------------
623
633
  // R-19: Runnable TEST binding (CR-GC-205 Item 4) — a TEST that is not explicitly
624
- // concept-only (attributes.concept === true) should carry a valid testRef runnable
634
+ // concept-only (attributes.concept === true) should carry a valid testRefs runnable
625
635
  // binding so the deduced selective test set is trustworthy. WARNING (a completeness
626
636
  // signal like R-05, not a hard gate): a freshly-spec'd TEST is legitimately concept-
627
637
  // level until implemented, and the no-phantom-file guarantee is enforced separately
628
- // by the consumer's export materialization (every testRef.file is scaffolded). File
638
+ // by the consumer's export materialization (every testRefs entry file is scaffolded). File
629
639
  // EXISTENCE is out of scope here (rules are pure, no I/O); this guards presence/shape
630
640
  // only and surfaces an unbound runnable TEST in rules_evaluate / readiness.
631
641
  // ---------------------------------------------------------------------------
@@ -633,17 +643,71 @@ function testMustHaveRunnableBinding(graph) {
633
643
  return graph.elements
634
644
  .filter(e => e.type === 'TEST')
635
645
  .filter(e => e.attributes?.concept !== true) // concept-only TESTs are exempt
636
- .filter(e => !TestRefSchema.safeParse(e.attributes?.testRef).success)
646
+ // CR-SM-231: `testRefs` (Array, min 1) statt `testRef` (Objekt). Semantik unveraendert —
647
+ // Praesenz und Form, keine Datei-Existenz; die pruefst RC-02.
648
+ .filter(e => !TestRefsSchema.safeParse(e.attributes?.testRefs).success)
637
649
  .map(test => ({
638
650
  rule_id: 'R-19',
639
651
  severity: 'warning',
640
652
  element_id: test.id,
641
- message: `${test.id} is a runnable TEST without a valid testRef binding`,
642
- fix_hint: 'Add attributes.testRef {file, case?, tool, level?}, or set attributes.concept:true if it has no run artifact yet',
653
+ message: `${test.id} is a runnable TEST without a valid testRefs binding`,
654
+ fix_hint: 'Add attributes.testRefs [{file, case?, tool, level?}, …] with at least one entry, or set attributes.concept:true if it has no run artifact yet',
643
655
  context: { element_type: test.type, element_name: test.name },
644
656
  }));
645
657
  }
646
658
  // ---------------------------------------------------------------------------
659
+ // R-29: Testdatei-Exklusivitaet (CR-SM-231)
660
+ //
661
+ // Jede Testdatei erscheint in hoechstens einem `testRefs`. Das ist die Haelfte, die 1:n
662
+ // **erzwingt** statt es nur zu erlauben — ohne sie driftet das Attribut zurueck nach n:m,
663
+ // und genau das war der belegte Ist-Zustand: dieselbe Spec-Datei stand im `testRef` zweier
664
+ // TEST-Knoten. Folge: ein roter Lauf ist keiner Abnahme mehr eindeutig zuordenbar, und der
665
+ // TRR-Gate zaehlt dieselbe Evidenz doppelt.
666
+ //
667
+ // `error`, nicht `warning` (Entscheid 2026-08-13): eine doppelt beanspruchte Datei macht
668
+ // Gate-Zahlen nachweislich falsch. Das ist eine Fehlmessung, kein Vollstaendigkeits-Signal —
669
+ // und damit eine andere Klasse als R-19/R-20, die beide `warning` sind.
670
+ //
671
+ // Rein graph-strukturell, kein I/O: die Datei muss nicht existieren, um doppelt beansprucht
672
+ // zu sein.
673
+ // ---------------------------------------------------------------------------
674
+ function testFileExclusivity(graph) {
675
+ const claimedBy = new Map();
676
+ for (const el of graph.elements) {
677
+ if (el.type !== 'TEST')
678
+ continue;
679
+ const parsed = TestRefsSchema.safeParse(el.attributes?.testRefs);
680
+ if (!parsed.success)
681
+ continue; // keine/ungueltige Bindung → R-19-Gebiet
682
+ // Innerhalb EINES Knotens ist dieselbe Datei zweimal kein Konflikt, sondern eine
683
+ // Redundanz — sie wuerde sonst als Selbstkollision gemeldet.
684
+ for (const file of new Set(parsed.data.map(r => r.file))) {
685
+ const owners = claimedBy.get(file) ?? [];
686
+ owners.push(el.id);
687
+ claimedBy.set(file, owners);
688
+ }
689
+ }
690
+ const violations = [];
691
+ for (const [file, owners] of claimedBy) {
692
+ if (owners.length < 2)
693
+ continue;
694
+ const sorted = [...owners].sort();
695
+ // Einmal je beanspruchendem Knoten: jeder von ihnen muss handeln, und eine Meldung an
696
+ // nur einem waere fuer die anderen unsichtbar.
697
+ for (const owner of sorted) {
698
+ violations.push({
699
+ rule_id: 'R-29',
700
+ severity: 'error',
701
+ element_id: owner,
702
+ message: `${owner} claims test file '${file}', which is also claimed by ${sorted.filter(o => o !== owner).join(', ')}`,
703
+ fix_hint: 'A test file belongs to at most one TEST. Split the file, or drop the entry from all but the one acceptance it really evidences',
704
+ context: { element_type: 'TEST', element_name: graph.elements.find(e => e.id === owner)?.name },
705
+ });
706
+ }
707
+ }
708
+ return violations;
709
+ }
710
+ // ---------------------------------------------------------------------------
647
711
  // R-20: FUNC realRef binding (CR-GC-205 Item 5, extended by CR-210, unified CR-228)
648
712
  // — a FUNC that is not explicitly concept-only (attributes.concept === true) or
649
713
  // externally realized (attributes.external === true) counts as BOUND when it EITHER
@@ -876,7 +940,7 @@ function modMustHaveAllocatedFunc(graph) {
876
940
  // so its Zod definition is machine-resolvable (RC-03 then checks it resolves, RC-04
877
941
  // that it is parsed at the interface). WARNING, not error: currently-unbound SCHEMAs
878
942
  // on the reference model must not turn readiness red before any binding exists — the
879
- // presence signal mirrors R-20 (FUNC realRef) / R-19 (testRef). Symbol RESOLUTION is
943
+ // presence signal mirrors R-20 (FUNC realRef) / R-19 (testRefs). Symbol RESOLUTION is
880
944
  // out of scope here (pure, no I/O — that is RC-03's job).
881
945
  // ---------------------------------------------------------------------------
882
946
  function schemaMustHaveSchemaRef(graph) {
@@ -953,35 +1017,36 @@ function ebenenPraesenz(graph) {
953
1017
  }];
954
1018
  }
955
1019
  export const V3_RULES = [
956
- { id: 'R-01', name: 'REQ must have verification', severity: 'error', evaluate: reqMustHaveVerification },
957
- { id: 'R-02', name: 'FUNC must satisfy REQ', severity: 'warning', evaluate: funcMustSatisfyReq },
958
- { id: 'R-03', name: 'ASIL isolation', severity: 'error', evaluate: asilIsolation },
959
- { id: 'R-04', name: 'Max module size', severity: 'warning', evaluate: maxModuleSize },
960
- { id: 'R-05', name: 'TEST must verify REQ', severity: 'warning', evaluate: testMustVerifyReq },
961
- { id: 'R-14', name: 'UC must have compose', severity: 'warning', evaluate: ucMustHaveCompose },
962
- { id: 'R-15', name: 'FCHAIN must have compose', severity: 'warning', evaluate: fchainMustHaveCompose },
963
- { id: 'R-16', name: 'ACTOR must have io', severity: 'warning', evaluate: actorMustHaveTrace },
964
- { id: 'R-17', name: 'SYS must have compose', severity: 'warning', evaluate: sysMustHaveCompose },
965
- { id: 'R-08', name: 'Trace consistency', severity: 'error', evaluate: traceConsistency },
966
- { id: 'R-10', name: 'FLOW completeness', severity: 'warning', evaluate: flowCompleteness },
967
- { id: 'R-12', name: 'No circular dependencies', severity: 'warning', evaluate: noDirectCircular },
968
- { id: 'R-18', name: 'Valid trace pattern', severity: 'error', evaluate: validTracePattern },
969
- { id: 'R-19', name: 'Runnable TEST binding', severity: 'warning', evaluate: testMustHaveRunnableBinding },
970
- { id: 'R-20', name: 'FUNC realRef binding', severity: 'warning', evaluate: funcMustHaveCodeBinding },
971
- { id: 'R-21', name: 'FUNC↔FUNC connection needs integration test', severity: 'warning', evaluate: fchainMustHaveIntegrationTest },
972
- { id: 'R-22', name: 'FUNC must be allocated to MOD', severity: 'warning', evaluate: funcMustBeAllocated },
973
- { id: 'R-23', name: 'MOD must have allocated FUNC', severity: 'warning', evaluate: modMustHaveAllocatedFunc },
974
- { id: 'R-26', name: 'SCHEMA must have realRef', severity: 'warning', evaluate: schemaMustHaveSchemaRef },
975
- { id: 'R-27', name: 'Physical MOD must have realRef', severity: 'warning', evaluate: physicalModMustHaveRealRef },
976
- { id: 'RD-01', name: 'Unresolved requirement', severity: 'warning', evaluate: unresolvedRequirement },
977
- { id: 'RD-02', name: 'Decomposition consistency', severity: 'warning', evaluate: decompositionConsistency },
978
- { id: 'RD-03', name: 'No premature decomposition', severity: 'info', evaluate: noPrematureDecomposition },
979
- { id: 'RD-04', name: 'Decomposition breadth', severity: 'warning', evaluate: decompositionBreadth },
980
- { id: 'MS-01', name: 'Milestone empty scope', severity: 'warning', evaluate: msEmptyScope },
981
- { id: 'MS-02', name: 'Milestone dangling dependency', severity: 'error', evaluate: msDanglingDependency },
982
- { id: 'R-28', name: 'Ebenen-Präsenz (FLOW+SCHEMA when funcCount>1)', severity: 'warning', evaluate: ebenenPraesenz },
1020
+ { id: 'R-01', name: 'REQ must have verification', severity: 'error', evaluate: reqMustHaveVerification, domain: ['REQ'] },
1021
+ { id: 'R-02', name: 'FUNC must satisfy REQ', severity: 'warning', evaluate: funcMustSatisfyReq, domain: ['FUNC'] },
1022
+ { id: 'R-03', name: 'ASIL isolation', severity: 'error', evaluate: asilIsolation, domain: ['FUNC'] },
1023
+ { id: 'R-04', name: 'Module size relative to crossing flows', severity: 'warning', evaluate: maxModuleSize, domain: ['MOD'] },
1024
+ { id: 'R-05', name: 'TEST must verify REQ', severity: 'warning', evaluate: testMustVerifyReq, domain: ['TEST'] },
1025
+ { id: 'R-14', name: 'UC must have compose', severity: 'warning', evaluate: ucMustHaveCompose, domain: ['UC'] },
1026
+ { id: 'R-15', name: 'FCHAIN must have compose', severity: 'warning', evaluate: fchainMustHaveCompose, domain: ['FCHAIN'] },
1027
+ { id: 'R-16', name: 'ACTOR must have io', severity: 'warning', evaluate: actorMustHaveTrace, domain: ['ACTOR'] },
1028
+ { id: 'R-17', name: 'SYS must have compose', severity: 'warning', evaluate: sysMustHaveCompose, domain: ['SYS'] },
1029
+ { id: 'R-08', name: 'Trace consistency', severity: 'error', evaluate: traceConsistency, domain: ['all'] },
1030
+ { id: 'R-10', name: 'FLOW completeness', severity: 'warning', evaluate: flowCompleteness, domain: ['FUNC'] },
1031
+ { id: 'R-12', name: 'No circular dependencies', severity: 'warning', evaluate: noDirectCircular, domain: ['FUNC'] },
1032
+ { id: 'R-18', name: 'Valid trace pattern', severity: 'error', evaluate: validTracePattern, domain: ['all'] },
1033
+ { id: 'R-19', name: 'Runnable TEST binding', severity: 'warning', evaluate: testMustHaveRunnableBinding, domain: ['TEST'] },
1034
+ { id: 'R-29', name: 'Test file exclusivity', severity: 'error', evaluate: testFileExclusivity, domain: ['TEST'] },
1035
+ { id: 'R-20', name: 'FUNC realRef binding', severity: 'warning', evaluate: funcMustHaveCodeBinding, domain: ['FUNC'] },
1036
+ { id: 'R-21', name: 'FUNC↔FUNC connection needs integration test', severity: 'warning', evaluate: fchainMustHaveIntegrationTest, domain: ['FCHAIN'] },
1037
+ { id: 'R-22', name: 'FUNC must be allocated to MOD', severity: 'warning', evaluate: funcMustBeAllocated, domain: ['FUNC'] },
1038
+ { id: 'R-23', name: 'MOD must have allocated FUNC', severity: 'warning', evaluate: modMustHaveAllocatedFunc, domain: ['MOD'] },
1039
+ { id: 'R-26', name: 'SCHEMA must have realRef', severity: 'warning', evaluate: schemaMustHaveSchemaRef, domain: ['SCHEMA'] },
1040
+ { id: 'R-27', name: 'Physical MOD must have realRef', severity: 'warning', evaluate: physicalModMustHaveRealRef, domain: ['MOD'] },
1041
+ { id: 'RD-01', name: 'Unresolved requirement', severity: 'warning', evaluate: unresolvedRequirement, domain: ['REQ'] },
1042
+ { id: 'RD-02', name: 'Decomposition consistency', severity: 'warning', evaluate: decompositionConsistency, domain: ['REQ'] },
1043
+ { id: 'RD-03', name: 'No premature decomposition', severity: 'info', evaluate: noPrematureDecomposition, domain: ['REQ'] },
1044
+ { id: 'RD-04', name: 'Decomposition breadth', severity: 'warning', evaluate: decompositionBreadth, domain: ['FUNC', 'MOD', 'SYS'] },
1045
+ { id: 'MS-01', name: 'Milestone empty scope', severity: 'warning', evaluate: msEmptyScope, domain: ['MS'] },
1046
+ { id: 'MS-02', name: 'Milestone dangling dependency', severity: 'error', evaluate: msDanglingDependency, domain: ['MS'] },
1047
+ { id: 'R-28', name: 'Ebenen-Präsenz (FLOW+SCHEMA when funcCount>1)', severity: 'warning', evaluate: ebenenPraesenz, domain: ['SYS'] },
983
1048
  ];
984
- /** Run all rules against a graph */
985
- export function evaluateRules(graph) {
986
- return V3_RULES.flatMap(rule => rule.evaluate(graph));
1049
+ /** Run all rules against a graph. CR-SM-236: `policy` ist Pflicht — wie bei `evaluateAllRules`. */
1050
+ export function evaluateRules(graph, policy) {
1051
+ return V3_RULES.flatMap(rule => rule.evaluate(graph, policy));
987
1052
  }
@@ -14,7 +14,8 @@
14
14
  */
15
15
  import type { OntologyGraph } from './ontology.js';
16
16
  import type { RuleDefinition, RuleViolation } from './rules.js';
17
+ import type { MetricPolicy } from './policy.js';
17
18
  export declare function sc02IsReferenced(graph: OntologyGraph): RuleViolation[];
18
19
  export declare function sc04FlowHasSchema(graph: OntologyGraph): RuleViolation[];
19
20
  export declare const SC_RULES: RuleDefinition[];
20
- export declare function evaluateSCRules(graph: OntologyGraph): RuleViolation[];
21
+ export declare function evaluateSCRules(graph: OntologyGraph, policy: MetricPolicy): RuleViolation[];
@@ -49,9 +49,11 @@ export function sc04FlowHasSchema(graph) {
49
49
  // Aggregated array & convenience runner
50
50
  // ---------------------------------------------------------------------------
51
51
  export const SC_RULES = [
52
- { id: 'SC-02', name: 'Schema referenced by FLOW', severity: 'warning', evaluate: sc02IsReferenced },
53
- { id: 'SC-04', name: 'FLOW has SCHEMA binding', severity: 'warning', evaluate: sc04FlowHasSchema },
52
+ { id: 'SC-02', name: 'Schema referenced by FLOW', severity: 'warning', evaluate: sc02IsReferenced, domain: ['SCHEMA'] },
53
+ { id: 'SC-04', name: 'FLOW has SCHEMA binding', severity: 'warning', evaluate: sc04FlowHasSchema, domain: ['SCHEMA'] },
54
54
  ];
55
- export function evaluateSCRules(graph) {
56
- return SC_RULES.flatMap(rule => rule.evaluate(graph));
55
+ // CR-SM-236: `policy` wird durchgereicht, auch wo diese Familie heute keine Schwelle hat —
56
+ // ein Sonderweg je Familie waere genau der zweite Pfad, den der Regelsatz verbietet.
57
+ export function evaluateSCRules(graph, policy) {
58
+ return SC_RULES.flatMap(rule => rule.evaluate(graph, policy));
57
59
  }
@@ -3,6 +3,7 @@
3
3
  */
4
4
  import type { OntologyGraph } from './ontology.js';
5
5
  import type { RuleDefinition, RuleViolation } from './rules.js';
6
+ import type { MetricPolicy } from './policy.js';
6
7
  export declare function uc01HasRequirements(graph: OntologyGraph): RuleViolation[];
7
8
  export declare function uc02HasActor(graph: OntologyGraph): RuleViolation[];
8
9
  export declare function uc03HasScenario(graph: OntologyGraph): RuleViolation[];
@@ -10,4 +11,4 @@ export declare function uc04GoalDefined(graph: OntologyGraph): RuleViolation[];
10
11
  export declare function uc05HasPostcondition(graph: OntologyGraph): RuleViolation[];
11
12
  export declare function uc06HasPrecondition(graph: OntologyGraph): RuleViolation[];
12
13
  export declare const UC_RULES: RuleDefinition[];
13
- export declare function evaluateUCRules(graph: OntologyGraph): RuleViolation[];
14
+ export declare function evaluateUCRules(graph: OntologyGraph, policy: MetricPolicy): RuleViolation[];