@sigloch/contracts 10.1.0 → 10.2.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,75 @@
1
+ /**
2
+ * CR-SM-314 — wie kritisch ist eine Funktion? Ketten und Use Cases, als ZAHL.
3
+ *
4
+ * Die Frage stammt aus dem Betrieb und hat drei Abnehmer, die bis hierher jeder fuer sich
5
+ * geraten haben: **Blast Radius** einer Aenderung, **Kritikalitaet** einer Funktion,
6
+ * **Rollout-Reihenfolge**. Alle drei fragen dasselbe: an wie vielen Wirkketten haengt dieses
7
+ * Stueck, und wie viele Use Cases stehen dahinter.
8
+ *
9
+ * ## Die Definition ist GELIEHEN, nicht neu
10
+ *
11
+ * "Eine FUNC ist in einer Kette" heisst hier woertlich, was R-30 darunter versteht: eine
12
+ * direkte `FCHAIN -compose-> FUNC`-Kante. Keine Vererbung ueber compose-Vorfahren.
13
+ *
14
+ * Der Rollup wurde GEMESSEN und VERWORFEN: ueber 17 Familiengraphen haetten Vorfahren-Ketten
15
+ * die Grundgesamtheit von 224 auf 227 FUNCs gehoben — drei Stueck. Er kauft nichts und waere
16
+ * eine ZWEITE Vorstellung davon, wozu eine Funktion gehoert, neben R-30s. Genau die Drift, gegen
17
+ * die `module-crossings.ts` gebaut wurde (CR-SM-276). Ein zerlegter Block misst deshalb 0, und
18
+ * das ist richtig: FC-03 verbietet ein Kettenglied mit verschachtelter Zerlegung, ein Block KANN
19
+ * die Kante gar nicht tragen.
20
+ *
21
+ * ## Diese Datei URTEILT NICHT
22
+ *
23
+ * Kein `infrastructure`-Flag, keine Schwelle, keine Severity. Ein Flag waere ein Urteil in der
24
+ * MESSUNG, und die Schwelle gehoert in `MetricPolicy` (`criticality.infrastructure`) — dieselbe
25
+ * Trennung, die `moduleMetrics` gegen MT-01/MT-02 haelt: die Zahl steht in jeder Zeile, ob eine
26
+ * Regel sie liest oder nicht.
27
+ *
28
+ * ## Warum sie nur `ontology` und `graph-index` importiert
29
+ *
30
+ * Damit `rules.ts` sie konsumieren darf. R-21 braucht dieselbe Kettenzahl fuer seine
31
+ * Infrastruktur-Ausnahme (CR-SM-313); rechnete die Regel sie selbst, haetten wir zwei
32
+ * Kettenbegriffe im selben Paket. EINE Rechnung, ZWEI Abnehmer — Vorbild `module-crossings.ts`,
33
+ * das aus demselben Grund keinen Regelcode importiert.
34
+ *
35
+ * ## Gemessen (17 Familiengraphen, 2026-09-12)
36
+ *
37
+ * 278 FUNCs liegen in mindestens einer Kette: 224 in genau einer, 37 in zwei, 6 in drei, 8 in
38
+ * vier, je eine in fuenf, sechs und elf. Ein duenner Schwanz ohne Plateau — aus der FORM dieser
39
+ * Verteilung ist keine Schwelle abzulesen (derselbe Befund wie bei `instability`, CR-SM-293).
40
+ * Ablesbar ist sie an ihrer WIRKUNG, und das steht bei `criticality` in der Policy.
41
+ *
42
+ * @author andreas@siglochconsulting
43
+ */
44
+ import type { OntologyGraph } from './ontology.js';
45
+ /** Eine Zeile je FUNC — mit Schwelle oder ohne, wie `ModuleMetrics` je MOD (CR-SM-232). */
46
+ export interface FunctionCriticality {
47
+ funcId: string;
48
+ funcName: string;
49
+ /**
50
+ * Wirkketten, die diese FUNC komponieren — R-30s Definition: direkte
51
+ * `FCHAIN -compose-> FUNC`-Kante, keine Vererbung ueber Vorfahren.
52
+ *
53
+ * `0` ist eine AUSSAGE, kein fehlender Wert: bei einem Blatt ist es R-30s Befund, bei einem
54
+ * zerlegten Block der von FC-03 erzwungene Normalzustand. Wer die beiden unterscheiden muss,
55
+ * fragt `decomposedFuncs()` — diese Datei tut es bewusst nicht (sie waere sonst an `rules.ts`
56
+ * gebunden und fuer `rules.ts` nicht mehr importierbar).
57
+ */
58
+ chains: number;
59
+ /**
60
+ * Use Cases, die ueber diese Ketten an der FUNC haengen (`UC -compose-> FCHAIN`).
61
+ *
62
+ * Nicht aus `chains` ableitbar und auch nicht dasselbe: eine FUNC in drei Ketten EINES Use
63
+ * Case ist etwas anderes als eine in drei Ketten dreier Use Cases. Gemessen an graphcode
64
+ * faellt die Zahl regelmaessig kleiner aus als `chains` (FUNC-mutate: 11 Ketten, 7 UCs).
65
+ */
66
+ useCases: number;
67
+ }
68
+ /**
69
+ * Kritikalitaet je FUNC, kritischste zuerst.
70
+ *
71
+ * Die Rangfolge IST das Signal (wie bei `moduleMetrics`): absteigend nach Ketten, dann nach Use
72
+ * Cases, dann stabil nach `funcId`. Eine Zeile je FUNC — auch fuer die mit 0, sonst waere die
73
+ * Abwesenheit eines Wertes von der Abwesenheit der Funktion nicht zu unterscheiden.
74
+ */
75
+ export declare function functionCriticality(graph: OntologyGraph): FunctionCriticality[];
@@ -0,0 +1,50 @@
1
+ import { indexOf } from './graph-index.js';
2
+ /**
3
+ * Kritikalitaet je FUNC, kritischste zuerst.
4
+ *
5
+ * Die Rangfolge IST das Signal (wie bei `moduleMetrics`): absteigend nach Ketten, dann nach Use
6
+ * Cases, dann stabil nach `funcId`. Eine Zeile je FUNC — auch fuer die mit 0, sonst waere die
7
+ * Abwesenheit eines Wertes von der Abwesenheit der Funktion nicht zu unterscheiden.
8
+ */
9
+ export function functionCriticality(graph) {
10
+ const idx = indexOf(graph);
11
+ const funcs = idx.elementsOfType('FUNC');
12
+ const typeOf = new Map(graph.elements.map((e) => [e.id, e.type]));
13
+ const funcIds = new Set(funcs.map((f) => f.id));
14
+ const compose = idx.tracesOfType('compose');
15
+ // FUNC -> seine Ketten. R-30s Fassung, woertlich.
16
+ const chainsOf = new Map();
17
+ for (const t of compose) {
18
+ if (typeOf.get(t.source) !== 'FCHAIN' || !funcIds.has(t.target))
19
+ continue;
20
+ let s = chainsOf.get(t.target);
21
+ if (!s) {
22
+ s = new Set();
23
+ chainsOf.set(t.target, s);
24
+ }
25
+ s.add(t.source);
26
+ }
27
+ // FCHAIN -> seine Use Cases.
28
+ const ucsOf = new Map();
29
+ for (const t of compose) {
30
+ if (typeOf.get(t.source) !== 'UC' || typeOf.get(t.target) !== 'FCHAIN')
31
+ continue;
32
+ let s = ucsOf.get(t.target);
33
+ if (!s) {
34
+ s = new Set();
35
+ ucsOf.set(t.target, s);
36
+ }
37
+ s.add(t.source);
38
+ }
39
+ const rows = funcs.map((fn) => {
40
+ const chains = chainsOf.get(fn.id) ?? new Set();
41
+ const ucs = new Set();
42
+ for (const ch of chains)
43
+ for (const uc of ucsOf.get(ch) ?? [])
44
+ ucs.add(uc);
45
+ return { funcId: fn.id, funcName: fn.name, chains: chains.size, useCases: ucs.size };
46
+ });
47
+ return rows.sort((a, b) => b.chains - a.chains ||
48
+ b.useCases - a.useCases ||
49
+ (a.funcId < b.funcId ? -1 : a.funcId > b.funcId ? 1 : 0));
50
+ }
@@ -13,16 +13,16 @@ export declare const GRAMMAR_SNAPSHOT: {
13
13
  readonly versions: {
14
14
  readonly ontology: "9.0.0";
15
15
  readonly metaModel: "5.0.0";
16
- readonly rules: "19.2.0";
16
+ readonly rules: "24.0.0";
17
17
  };
18
18
  readonly elementTypes: readonly ["ACTOR", "CR", "FCHAIN", "FLOW", "FUNC", "MOD", "MS", "REQ", "SCHEMA", "SYS", "TEST", "UC"];
19
19
  readonly traceTypes: readonly ["allocate", "compose", "io", "relation", "satisfy", "verify"];
20
20
  readonly patterns: readonly ["ACTOR -io-> FLOW", "CR -relation-> FUNC", "CR -relation-> MOD", "CR -relation-> MS", "CR -relation-> REQ", "CR -relation-> UC", "FCHAIN -compose-> FUNC [1..*]", "FCHAIN -satisfy-> REQ", "FLOW -io-> ACTOR", "FLOW -io-> FUNC", "FLOW -relation-> SCHEMA [1]", "FUNC -allocate-> MOD [0..1]", "FUNC -compose-> FUNC [0..*]", "FUNC -io-> FLOW", "FUNC -satisfy-> REQ where target.kinds in {functional,postcondition,precondition}", "MOD -compose-> MOD [0..*]", "MOD -satisfy-> REQ where target.kinds in {mitigation,non-functional,risk}", "MS -compose-> FUNC", "MS -compose-> REQ", "MS -compose-> UC", "MS -relation-> MS label=depends-on", "REQ -compose-> REQ [0..*]", "SYS -compose-> MOD [0..*]", "SYS -compose-> REQ [0..*]", "SYS -compose-> SYS [0..*]", "SYS -compose-> UC [1..*]", "SYS -satisfy-> REQ where target.kinds in {mitigation,non-functional,risk}", "TEST -verify-> REQ", "UC -compose-> FCHAIN [1..*]", "UC -compose-> REQ [1..*]"];
21
21
  readonly elementColumns: readonly ["OntologyElement.attributes", "OntologyElement.created_at", "OntologyElement.description", "OntologyElement.id", "OntologyElement.kinds", "OntologyElement.method", "OntologyElement.name", "OntologyElement.status", "OntologyElement.type", "OntologyElement.updated_at", "Trace.attributes", "Trace.created_at", "Trace.label", "Trace.source", "Trace.target", "Trace.type", "Trace.verified_at", "Trace.weight"];
22
22
  readonly attributes: readonly ["CR.rationale: string", "CR.spike: boolean", "CR.status: enum", "FLOW.protocol: string", "FLOW.qos: string", "FUNC.concept: boolean", "FUNC.external: boolean", "FUNC.measuredMs: number", "FUNC.realRef: object", "FUNC.safety_relevant: boolean", "FUNC.sourceFile: string", "FUNC.timingBudgetMs: number", "MOD.concept: boolean", "MOD.external: boolean", "MOD.kind: string", "MOD.path: string", "MOD.realRef: object", "REQ.detection: number", "REQ.occurrence: number", "REQ.severity: number", "SCHEMA.concept: boolean", "SCHEMA.contract: string", "SCHEMA.external: boolean", "SCHEMA.realRef: object", "TEST.concept: boolean", "TEST.sourceFile: string", "TEST.testRefs: array", "UC.operatingMode: string"];
23
- readonly rules: readonly ["AF-01 (warning) domain=[graph]", "AF-02 (warning) domain=[graph]", "AF-03 (warning) domain=[graph]", "AF-04 (warning) domain=[graph]", "AF-05 (warning) domain=[graph]", "BQ-01 (warning) domain=[REQ]", "BQ-02 (warning) domain=[REQ]", "BQ-04 (warning) domain=[REQ]", "BQ-06 (warning) domain=[REQ]", "BQ-07 (warning) domain=[REQ]", "BW-02 (warning) domain=[FUNC]", "CL-01 (warning) domain=[ACTOR]", "CR-01 (warning) domain=[MOD]", "CR-R01 (error) domain=[CR]", "CR-R02 (error) domain=[CR]", "CR-R03 (warning) domain=[all]", "FC-02 (warning) domain=[UC]", "FC-03 (warning) domain=[FUNC]", "FC-04 (warning) domain=[FCHAIN]", "FM-01 (warning) domain=[REQ]", "FM-02 (warning) domain=[REQ]", "FM-03 (error) domain=[REQ]", "IO-01 (warning) domain=[FUNC]", "MS-01 (warning) domain=[MS]", "MS-02 (error) domain=[MS]", "MS-03 (info) domain=[CR]", "MT-01 (warning) domain=[MOD]", "MT-02 (info) domain=[MOD]", "ND-01 (error) domain=[FUNC]", "ND-02 (error) domain=[SCHEMA]", "NFR-01 (warning) domain=[FCHAIN,FUNC,MOD]", "R-01 (error) domain=[REQ]", "R-02 (warning) domain=[FUNC]", "R-04 (warning) domain=[MOD]", "R-05 (warning) domain=[TEST]", "R-08 (error) domain=[all]", "R-10 (warning) domain=[FLOW]", "R-12 (warning) domain=[FUNC]", "R-15 (warning) domain=[FCHAIN]", "R-16 (warning) domain=[ACTOR]", "R-17 (warning) domain=[SYS]", "R-18 (error) domain=[all]", "R-19 (warning) domain=[TEST]", "R-20 (warning) domain=[FUNC]", "R-21 (warning) domain=[FCHAIN]", "R-22 (warning) domain=[FUNC]", "R-23 (warning) domain=[MOD]", "R-26 (warning) domain=[SCHEMA]", "R-29 (error) domain=[TEST]", "R-30 (warning) domain=[FUNC]", "R-31 (warning) domain=[FUNC]", "RC-01 (error) domain=[FUNC]", "RC-02 (error) domain=[TEST]", "RC-03 (error) domain=[SCHEMA]", "RC-04 (warning) domain=[SCHEMA]", "RC-05 (warning) domain=[MOD]", "RC-06 (warning) domain=[FUNC,MOD,SCHEMA]", "RD-01 (warning) domain=[REQ]", "RD-02 (warning) domain=[REQ]", "RD-03 (info) domain=[REQ]", "RD-04 (warning) domain=[FUNC,MOD,SYS]", "SC-02 (warning) domain=[SCHEMA]", "UC-01 (error) domain=[UC]", "UC-02 (error) domain=[UC]", "UC-03 (warning) domain=[UC]", "UC-04 (warning) domain=[UC]", "UC-05 (info) domain=[UC]", "UC-06 (info) domain=[UC]", "VR-01 (info) domain=[TEST]"];
23
+ readonly rules: readonly ["AF-01 (warning) domain=[graph]", "AF-02 (warning) domain=[graph]", "AF-03 (warning) domain=[graph]", "AF-04 (warning) domain=[graph]", "AF-05 (warning) domain=[graph]", "BQ-01 (warning) domain=[REQ]", "BQ-02 (warning) domain=[REQ]", "BQ-04 (warning) domain=[REQ]", "BQ-06 (warning) domain=[REQ]", "BQ-07 (warning) domain=[REQ]", "BW-02 (warning) domain=[FUNC]", "CL-01 (warning) domain=[ACTOR]", "CR-01 (warning) domain=[MOD]", "CR-R01 (error) domain=[CR]", "CR-R02 (error) domain=[CR]", "CR-R03 (warning) domain=[all]", "FC-02 (warning) domain=[UC]", "FC-03 (warning) domain=[FUNC]", "FC-04 (warning) domain=[FCHAIN]", "FM-01 (warning) domain=[REQ]", "FM-02 (warning) domain=[REQ]", "FM-03 (error) domain=[REQ]", "IO-01 (warning) domain=[FUNC]", "IO-02 (error) domain=[FLOW]", "MS-01 (warning) domain=[MS]", "MS-02 (error) domain=[MS]", "MS-03 (info) domain=[CR]", "MT-01 (warning) domain=[MOD]", "MT-02 (info) domain=[MOD]", "ND-01 (error) domain=[FUNC]", "ND-02 (error) domain=[SCHEMA]", "NFR-01 (warning) domain=[FCHAIN,FUNC,MOD]", "R-01 (error) domain=[REQ]", "R-02 (warning) domain=[FUNC]", "R-04 (warning) domain=[MOD]", "R-05 (warning) domain=[TEST]", "R-08 (error) domain=[all]", "R-10 (warning) domain=[FLOW]", "R-12 (warning) domain=[FUNC]", "R-15 (warning) domain=[FCHAIN]", "R-16 (warning) domain=[ACTOR]", "R-17 (warning) domain=[SYS]", "R-18 (error) domain=[all]", "R-19 (warning) domain=[TEST]", "R-20 (warning) domain=[FUNC]", "R-21 (warning) domain=[FCHAIN,FUNC]", "R-22 (warning) domain=[FUNC]", "R-23 (warning) domain=[MOD]", "R-26 (warning) domain=[SCHEMA]", "R-29 (error) domain=[TEST]", "R-30 (warning) domain=[FUNC]", "R-31 (warning) domain=[FUNC]", "RC-01 (error) domain=[FUNC]", "RC-02 (error) domain=[TEST]", "RC-03 (error) domain=[SCHEMA]", "RC-04 (warning) domain=[SCHEMA]", "RC-05 (warning) domain=[MOD]", "RC-06 (warning) domain=[FUNC,MOD,SCHEMA]", "RD-01 (warning) domain=[REQ]", "RD-02 (warning) domain=[REQ]", "RD-03 (info) domain=[REQ]", "RD-04 (warning) domain=[FUNC,MOD,SYS]", "RD-05 (warning) domain=[FUNC,MOD,SYS]", "SC-02 (warning) domain=[SCHEMA]", "UC-01 (error) domain=[UC]", "UC-02 (error) domain=[UC]", "UC-03 (warning) domain=[UC]", "UC-04 (warning) domain=[UC]", "UC-05 (info) domain=[UC]", "UC-06 (info) domain=[UC]", "VR-01 (info) domain=[TEST]"];
24
24
  readonly conformanceRules: readonly ["RC-01 (error)", "RC-02 (error)", "RC-03 (error)", "RC-04 (warning)", "RC-05 (warning)", "RC-06 (warning)"];
25
- readonly policy: readonly ["apTable = null", "boundaryWidth = {warning:5}", "crossingFlows = {warning:3}", "decompositionBreadth = {warning:9}", "instability = null", "lcom4 = {info:4,warning:6}", "moduleSize = {coupled:7,crossings:2,large:9}", "riskRpn = 100"];
26
- readonly ruleHelp: readonly ["AF-01", "AF-02", "AF-03", "AF-04", "AF-05", "BQ-01", "BQ-02", "BQ-04", "BQ-06", "BQ-07", "BW-02", "CL-01", "CR-01", "CR-R01", "CR-R02", "CR-R03", "FC-02", "FC-03", "FC-04", "FM-01", "FM-02", "FM-03", "IO-01", "MS-01", "MS-02", "MS-03", "MT-01", "MT-02", "ND-01", "ND-02", "NFR-01", "R-01", "R-02", "R-04", "R-05", "R-08", "R-10", "R-12", "R-15", "R-16", "R-17", "R-18", "R-19", "R-20", "R-21", "R-22", "R-23", "R-26", "R-29", "R-30", "R-31", "RC-01", "RC-02", "RC-03", "RC-04", "RC-05", "RC-06", "RD-01", "RD-02", "RD-03", "RD-04", "SC-02", "UC-01", "UC-02", "UC-03", "UC-04", "UC-05", "UC-06", "VR-01"];
27
- readonly exports: readonly ["AF_RULES", "ALL_RULE_DEFS", "AO_RULES", "ActionPriority", "AnalysisArtifactId", "AnalysisFreshnessStampSchema", "ApTableSchema", "BOUNDED_PATTERNS", "BQ_RULES", "CODE_CONFORMANCE_RULES", "CR_RULES", "CodeFactsSchema", "DEFAULT_METRIC_POLICY", "DIMENSION_READINESS_DELTA_NAME", "DIMENSION_READINESS_NAME", "ELEMENT_ATTRIBUTES", "ELEMENT_DESCRIPTIONS", "ElementType", "ElementUid", "FC_RULES", "FM_RULES", "FileFactsSchema", "ImportEdgeSchema", "MAX_SLUG_LENGTH", "META_MODEL_VERSION", "MODELING_ELEMENT_TYPES", "MT_RULES", "MetricPolicySchema", "ND_RULES", "ONTOLOGY_VERSION", "OntologyElement", "OntologyGraph", "PHASE_READINESS_NAME", "PhaseGate", "READINESS_SCORED_PROFILES", "REQUIRED_PATTERNS", "RULES_VERSION", "RULE_HELP", "RULE_TO_DIMENSION", "RULE_TO_PHASE", "ReadinessDimension", "ReadinessReport", "ReadinessScore", "RealRefSchema", "RepoRelativePathSchema", "ReqKind", "RuleSeverity", "RuleViolation", "SC_RULES", "TRACE_PATTERNS", "TestRefSchema", "TestRefsSchema", "TestResult", "Trace", "TraceType", "UC_RULES", "V3_RULES", "VIEW_RULES", "VerificationMethod", "ViolationCandidate", "ViolationContext", "actionPriority", "allocationCohesion", "apMethod", "attributeTypeOf", "bq01Unambiguous", "bq02Verifiable", "bq04Necessary", "bq06Conforming", "bq07Complete", "bw02WhiteboxWidth", "cl01ConopsCompleteness", "cr01CrossingFlowCount", "crossingContractCount", "decomposedFuncs", "evaluateAFRules", "evaluateAORules", "evaluateAllRules", "evaluateBQRules", "evaluateCRRules", "evaluateConformanceRules", "evaluateFCRules", "evaluateFMRules", "evaluateMTRules", "evaluateNDRules", "evaluateRules", "evaluateSCRules", "evaluateUCRules", "evaluateViewRules", "extractFormatE", "fc02LeafUcHasFchain", "fc03FchainFlat", "fc04ActorBounded", "fm01MissingFmeaAttributes", "fm02MissingMitigation", "fm03HighRiskUnverified", "funcSimilarity", "getRuleDefsForProfile", "hydrateAttrValue", "importCoverage", "indexOf", "io01CrossModuleCompleteness", "isElementUid", "isValidTrace", "jaccard", "maxOccurs", "minOccurs", "moduleCrossings", "moduleMetrics", "mt01Instability", "mt02Lcom4", "nd01FuncNearDuplicate", "nd02SchemaNearDuplicate", "nfr01BudgetOvershoot", "normalizeReqKinds", "pairsAbove", "parseElementUid", "parseFormatE", "sc02IsReferenced", "schemaSimilarity", "serializeToFormatE", "setBQ04SimilarityMatrix", "subtreeFuncs", "toElementUid", "toEvaluableGraph", "tokens", "traceRejection", "tryParseElementUid", "uc01HasRequirements", "uc02HasActor", "uc03HasScenario", "uc04GoalDefined", "uc05HasPostcondition", "uc06HasPrecondition", "vr01TestNoResult", "whiteboxContractCount"];
25
+ readonly policy: readonly ["apTable = null", "boundaryWidth = {warning:5}", "criticality = {infrastructure:3}", "crossingFlows = {warning:3}", "decompositionBreadth = {min:3,warning:9}", "instability = null", "lcom4 = {info:4,warning:6}", "riskRpn = 100"];
26
+ readonly ruleHelp: readonly ["AF-01", "AF-02", "AF-03", "AF-04", "AF-05", "BQ-01", "BQ-02", "BQ-04", "BQ-06", "BQ-07", "BW-02", "CL-01", "CR-01", "CR-R01", "CR-R02", "CR-R03", "FC-02", "FC-03", "FC-04", "FM-01", "FM-02", "FM-03", "IO-01", "IO-02", "MS-01", "MS-02", "MS-03", "MT-01", "MT-02", "ND-01", "ND-02", "NFR-01", "R-01", "R-02", "R-04", "R-05", "R-08", "R-10", "R-12", "R-15", "R-16", "R-17", "R-18", "R-19", "R-20", "R-21", "R-22", "R-23", "R-26", "R-29", "R-30", "R-31", "RC-01", "RC-02", "RC-03", "RC-04", "RC-05", "RC-06", "RD-01", "RD-02", "RD-03", "RD-04", "RD-05", "SC-02", "UC-01", "UC-02", "UC-03", "UC-04", "UC-05", "UC-06", "VR-01"];
27
+ readonly exports: readonly ["AF_RULES", "ALL_RULE_DEFS", "AO_RULES", "ActionPriority", "AnalysisArtifactId", "AnalysisFreshnessStampSchema", "ApTableSchema", "BOUNDED_PATTERNS", "BQ_RULES", "CODE_CONFORMANCE_RULES", "CR_RULES", "CodeFactsSchema", "DEFAULT_METRIC_POLICY", "DIMENSION_READINESS_DELTA_NAME", "DIMENSION_READINESS_NAME", "ELEMENT_ATTRIBUTES", "ELEMENT_DESCRIPTIONS", "ElementType", "ElementUid", "FC_RULES", "FM_RULES", "FileFactsSchema", "ImportEdgeSchema", "MAX_SLUG_LENGTH", "META_MODEL_VERSION", "MODELING_ELEMENT_TYPES", "MT_RULES", "MetricPolicySchema", "ND_RULES", "ONTOLOGY_VERSION", "OntologyElement", "OntologyGraph", "PHASE_READINESS_NAME", "PhaseGate", "READINESS_SCORED_PROFILES", "REQUIRED_PATTERNS", "RULES_VERSION", "RULE_HELP", "RULE_TO_DIMENSION", "RULE_TO_PHASE", "ReadinessDimension", "ReadinessReport", "ReadinessScore", "RealRefSchema", "RepoRelativePathSchema", "ReqKind", "RuleSeverity", "RuleViolation", "SC_RULES", "TRACE_PATTERNS", "TestRefSchema", "TestRefsSchema", "TestResult", "Trace", "TraceType", "UC_RULES", "V3_RULES", "VIEW_RULES", "VerificationMethod", "ViolationCandidate", "ViolationContext", "actionPriority", "allocationCohesion", "apMethod", "attributeTypeOf", "bq01Unambiguous", "bq02Verifiable", "bq04Necessary", "bq06Conforming", "bq07Complete", "bw02WhiteboxWidth", "cl01ConopsCompleteness", "cr01CrossingFlowCount", "crossingContractCount", "decomposedFuncs", "evaluateAFRules", "evaluateAORules", "evaluateAllRules", "evaluateBQRules", "evaluateCRRules", "evaluateConformanceRules", "evaluateFCRules", "evaluateFMRules", "evaluateMTRules", "evaluateNDRules", "evaluateRules", "evaluateSCRules", "evaluateUCRules", "evaluateViewRules", "extractFormatE", "fc02LeafUcHasFchain", "fc03FchainFlat", "fc04ActorBounded", "fm01MissingFmeaAttributes", "fm02MissingMitigation", "fm03HighRiskUnverified", "funcSimilarity", "functionCriticality", "getRuleDefsForProfile", "hydrateAttrValue", "importCoverage", "indexOf", "io01CrossModuleCompleteness", "isElementUid", "isValidTrace", "jaccard", "maxOccurs", "minOccurs", "moduleCrossings", "moduleMetrics", "mt01Instability", "mt02Lcom4", "nd01FuncNearDuplicate", "nd02SchemaNearDuplicate", "nfr01BudgetOvershoot", "normalizeReqKinds", "pairsAbove", "parseElementUid", "parseFormatE", "sc02IsReferenced", "schemaSimilarity", "serializeToFormatE", "setBQ04SimilarityMatrix", "subtreeFuncs", "toElementUid", "toEvaluableGraph", "tokens", "traceRejection", "tryParseElementUid", "uc01HasRequirements", "uc02HasActor", "uc03HasScenario", "uc04GoalDefined", "uc05HasPostcondition", "uc06HasPrecondition", "vr01TestNoResult", "whiteboxContractCount"];
28
28
  };
@@ -13,7 +13,7 @@ export const GRAMMAR_SNAPSHOT = {
13
13
  versions: {
14
14
  ontology: "9.0.0",
15
15
  metaModel: "5.0.0",
16
- rules: "19.2.0",
16
+ rules: "24.0.0",
17
17
  },
18
18
  elementTypes: [
19
19
  "ACTOR",
@@ -143,6 +143,7 @@ export const GRAMMAR_SNAPSHOT = {
143
143
  "FM-02 (warning) domain=[REQ]",
144
144
  "FM-03 (error) domain=[REQ]",
145
145
  "IO-01 (warning) domain=[FUNC]",
146
+ "IO-02 (error) domain=[FLOW]",
146
147
  "MS-01 (warning) domain=[MS]",
147
148
  "MS-02 (error) domain=[MS]",
148
149
  "MS-03 (info) domain=[CR]",
@@ -164,7 +165,7 @@ export const GRAMMAR_SNAPSHOT = {
164
165
  "R-18 (error) domain=[all]",
165
166
  "R-19 (warning) domain=[TEST]",
166
167
  "R-20 (warning) domain=[FUNC]",
167
- "R-21 (warning) domain=[FCHAIN]",
168
+ "R-21 (warning) domain=[FCHAIN,FUNC]",
168
169
  "R-22 (warning) domain=[FUNC]",
169
170
  "R-23 (warning) domain=[MOD]",
170
171
  "R-26 (warning) domain=[SCHEMA]",
@@ -181,6 +182,7 @@ export const GRAMMAR_SNAPSHOT = {
181
182
  "RD-02 (warning) domain=[REQ]",
182
183
  "RD-03 (info) domain=[REQ]",
183
184
  "RD-04 (warning) domain=[FUNC,MOD,SYS]",
185
+ "RD-05 (warning) domain=[FUNC,MOD,SYS]",
184
186
  "SC-02 (warning) domain=[SCHEMA]",
185
187
  "UC-01 (error) domain=[UC]",
186
188
  "UC-02 (error) domain=[UC]",
@@ -201,11 +203,11 @@ export const GRAMMAR_SNAPSHOT = {
201
203
  policy: [
202
204
  "apTable = null",
203
205
  "boundaryWidth = {warning:5}",
206
+ "criticality = {infrastructure:3}",
204
207
  "crossingFlows = {warning:3}",
205
- "decompositionBreadth = {warning:9}",
208
+ "decompositionBreadth = {min:3,warning:9}",
206
209
  "instability = null",
207
210
  "lcom4 = {info:4,warning:6}",
208
- "moduleSize = {coupled:7,crossings:2,large:9}",
209
211
  "riskRpn = 100",
210
212
  ],
211
213
  ruleHelp: [
@@ -232,6 +234,7 @@ export const GRAMMAR_SNAPSHOT = {
232
234
  "FM-02",
233
235
  "FM-03",
234
236
  "IO-01",
237
+ "IO-02",
235
238
  "MS-01",
236
239
  "MS-02",
237
240
  "MS-03",
@@ -270,6 +273,7 @@ export const GRAMMAR_SNAPSHOT = {
270
273
  "RD-02",
271
274
  "RD-03",
272
275
  "RD-04",
276
+ "RD-05",
273
277
  "SC-02",
274
278
  "UC-01",
275
279
  "UC-02",
@@ -377,6 +381,7 @@ export const GRAMMAR_SNAPSHOT = {
377
381
  "fm02MissingMitigation",
378
382
  "fm03HighRiskUnverified",
379
383
  "funcSimilarity",
384
+ "functionCriticality",
380
385
  "getRuleDefsForProfile",
381
386
  "hydrateAttrValue",
382
387
  "importCoverage",
@@ -5,7 +5,7 @@
5
5
  /** Ontology schema version (element types + trace types). */
6
6
  export declare const ONTOLOGY_VERSION = "9.0.0";
7
7
  /** Rules engine version (validation rules incl. RC conformance). */
8
- export declare const RULES_VERSION = "19.2.0";
8
+ export declare const RULES_VERSION = "24.0.0";
9
9
  /** Meta-model version (trace pattern constraints + format-e parser). */
10
10
  export declare const META_MODEL_VERSION = "5.0.0";
11
11
  export * from './ontology.js';
@@ -25,6 +25,12 @@ export * from './similarity.js';
25
25
  export * from './ao-rules.js';
26
26
  export * from './rule-help.js';
27
27
  export * from './module-crossings.js';
28
+ /**
29
+ * CR-SM-314: Kritikalitaet je FUNC (Ketten + Use Cases). Wie `module-crossings` eine reine
30
+ * Rechnung ohne Regelcode — deshalb darf `rules.ts` sie importieren (R-21s Infrastruktur-
31
+ * Ausnahme, CR-SM-313) und es gibt nur EINEN Kettenbegriff im Paket.
32
+ */
33
+ export * from './function-criticality.js';
28
34
  export * from './quality-rules.js';
29
35
  export * from './analysis-freshness-rules.js';
30
36
  export * from './evaluate-all.js';
package/dist/se/index.js CHANGED
@@ -5,7 +5,7 @@
5
5
  /** Ontology schema version (element types + trace types). */
6
6
  export const ONTOLOGY_VERSION = '9.0.0'; // BREAKING (CR-SM-294): das Feld `asil` entfaellt ersatzlos, zusammen mit dem Enum `AsilLevel`. Sein einziger Leser war R-03 (ASIL isolation), und die Messung ueber 19 Familiengraphen (4540 Elemente) zeigt: 0 von 305 MOD tragen `asil` ueberhaupt. Ein Feld ohne Schreiber ist eine Regel, die nie feuert -- Gate 3 der Grammatik-Review. Mit R-03 fallen im selben Zug RT-01, PH-01, R-27 (alle drei lesen `attributes.kind === 'physical'`, 0 von 305 MOD tragen es) und CA-01 (`attributes.requires` / `attributes.capability`, 0/0). Die physische Ebene der Ontologie war nie in Gebrauch: kein Mitglied der Familie modelliert Hardware oder funktionale Sicherheit. `kind`, `requires` und `capability` brauchen KEINEN Schema-Eingriff -- sie lagen im freien `attributes`-Sack und sind nach der Streichung ihrer Leser schlicht unbenutzte Schluessel. Migration: ein Graph, der `asil` traegt, verliert das Feld beim naechsten Reseed; kein Element und keine Kante faellt (0 -> 0 Befunde ueber alle 19 Graphen). Prior: BREAKING (CR-SM-266a): drei Enums schrumpfen, und alle drei sind ENTFERNUNGEN. (1) ElementType 13 -> 12: `SESSION` entfaellt. (2) TraceType 7 -> 6: `produces` entfaellt. Beide trugen genau EIN Pattern (`SESSION -produces-> *`), und die Messung hat es widerlegt: 0 produces-Kanten und 0 SESSION-Knoten in ALLEN 9 aktiven Familie-Graphen, auch im graphcode-Selbstmodell nach 195 gegateten Versionen. Die reale Provenance lebt seit CR-GC-347 in `.graphcode/audit.jsonl` -- audit_trail/audit_stats lesen die DATEI, nie den Graphen; einziger Schreiber war ein toter Pfad (CR-195d in graph-api-core), Leser mit Wirkung keiner. Der urspruengliche CR-Entwurf hielt das Pattern fuer die Provenance-Kante und wollte es BEHALTEN ('Nicht-Befund'); die Zaehlung hat den Nicht-Befund kassiert -- Gate 1 verlangt fuer das Wiederaufmachen einer Entscheidung eine Messung, und hier war sie es. (3) Mit `produces` faellt `TraceCategory` und `Trace.category`. Das ist die wichtigste Haelfte: R-08 und R-18 haben `category === 'audit'` UEBERSPRUNGEN, also haette nach D5 jede Kante mit diesem selbstgesetzten Attribut die komplette Pattern-Matrix UND die Referenzintegritaet umgangen. Ein Attribut, das eine Strukturpruefung abschaltet, ist die Klasse aus CR-SM-263/-257; hier zusaetzlich der billige Zweitweg aus CR-GC-366, nur ohne Kante. Gemessen: 0 von 0 Traces aller 9 Graphen tragen `category` -- die Entfernung nimmt niemandem etwas weg. (4) ReqKind 7 -> 6: `negative` entfaellt. Ein Verbots-REQ ist auch eine Anforderung -- die Implementierung muss Regeln abfragen oder Sicherungen einbauen, das ist `functional`. Der Wert hatte NULL Leser (anders als risk/mitigation -> FM-01..03, pre/postcondition -> UC-05/06, non-functional -> NFR-01) und haette durch die neuen where-Praedikate per AUSLASSUNG erstmals Wirkung bekommen: er stand in keiner der vier Listen und waere nur noch per FCHAIN erfuellbar gewesen. Die sechs verbleibenden Werte partitionieren die where-Listen VOLLSTAENDIG. Migration `negative`: 3 REQs, alle graphcode, Ziel-kind je REQ statt pauschal -- REQ-dashboard-readonly und REQ-readonly-bridge nach `non-functional` (Architektur-Constraint an der Modulgrenze, konsistent mit D2), REQ-no-extraction nach `functional`. Rest-Reject dabei: genau 1 (`FUNC-serve-sse -satisfy-> REQ-readonly-bridge`), aufzuloesen durch Loeschen oder Heben auf die FCHAIN. Prior: BREAKING (CR-GC-366): das Trace-Pattern `FUNC -satisfy-> UC` entfaellt ersatzlos. Es war ein zweiter Weg vom Verhalten zum Use Case neben dem einzig tragenden `UC -compose-> FCHAIN -compose-> FUNC` — und der billigere: eine FUNC konnte einen UC bedienen, ohne in dessen Wirkkette zu stehen, womit IO-01 (FLOW-Pfade je Kettenpaar) und R-21 (Integrationstest je Kette) sie nie sahen. Die Abkuerzung umging genau die Pruefungen, die die Anbindung belastbar machen. Am graphcode-Selbstmodell waren 10 der 26 solchen Kanten reine Dopplung (die FUNC stand ohnehin in der FCHAIN), die restlichen 13 sind der Migrationsanlass. Jeder Graph mit solchen Kanten faellt jetzt R-18 (error) zur Last und muss migriert werden: FUNC in die FCHAIN des UC aufnehmen, satisfy-Kante loeschen. Prior: BREAKING (CR-SM-231b): das knotenweite `testResult` entfaellt — das Ergebnis haengt PRO `testRefs`-Eintrag (`result`, `ranAt`, `evidence`), aus demselben Grund wie `tool`. Ein einzelnes Ergebnis am Knoten konnte bei n Laeufen nicht sagen, welcher gemeint ist, und "einer rot, einer gruen" war gar nicht darstellbar — genau der Zustand, den ein Gate wissen muss. FM-03 liest jetzt "jeder Eintrag passed": "irgendeiner gruen" haette einen gruenen Unit-Lauf einen roten Visual-Lauf verdecken lassen. Ein Eintrag ohne Ergebnis ist nicht bestanden. VR-01 meldet die Dateien ohne Ergebnis statt nur den Knoten. Prior: BREAKING (CR-SM-231): `testRef` (Objekt) entfaellt ersatzlos zugunsten von `testRefs` (Array, min 1) — eine Abnahme, n Testdateien. Kein Union, kein Alias: ein Graph mit dem alten Attribut faellt R-19 zur Last und muss migriert werden. Anlass war ein Zaehl-Audit (537 laufende Tests in 67 Dateien, 14 TEST-Knoten mit je genau einer Datei, 53 Dateien an keine Abnahme gebunden) plus ein belegter Ausweichweg: dieselbe Spec-Datei stand im testRef ZWEIER TEST-Knoten, womit die Relation faktisch n:m war — ein roter Lauf keiner Abnahme mehr eindeutig zuordenbar, der TRR-Gate zaehlte dieselbe Evidenz doppelt. `tool` bleibt PRO EINTRAG: eine Abnahme mischt real die Runner (vitest + playwright). +AttributeSpec.type 'array' (testRefs ist das erste Listen-Attribut). Prior: BREAKING: `SemanticId` (`Name.TypeAbbr.Counter`) deleted, `ElementUid` (`<TYPE>-<slug>`) is the family canon — the old canon was used by no production graph of the family while 626 of 1145 elements already carried TYPE-slug (CR-SM-217); realRef unifies codeRef+schemaRef (+physical-MOD CAD ref), symbol optional; testRef stays separate (CR-228 C); +RepoRelativePathSchema on testRef/realRef .file — no absolute/`..` paths (CR-GC-255)
7
7
  /** Rules engine version (validation rules incl. RC conformance). */
8
- export const RULES_VERSION = '19.2.0'; // MINOR (CR-SM-305): die sechs RC-Regeln stehen im KATALOG -- ALL_RULE_DEFS 63 -> 69, mit eigenem Profil `conformance`. Reine Ergaenzung: keine bestehende Regel, Severity oder Schwelle aendert sich, kein Graph faellt, das Golden-File der Regelausgabe steht zeichengleich. ROOT CAUSE: das Kongruenz-Urteil existierte, war getestet und erreichte KEINEN Produktionspfad. Gate (`harness.mutate`), Steuerung (fit-advisory, steering-snapshot) und `GET /api/graph/readiness` lesen alle ALL_RULE_DEFS; RC stand nicht darin, weil seine Signatur `(graph, facts: CodeFacts)` statt `(graph, policy)` ist -- der einzige Aufrufer von `scoreReadinessWithConformance` war ein Test. Ein Modell-Zug, der die Bindung an den Code bricht, passierte damit jedes Gate lautlos. `evaluateAllRules` fuehrt RC weiterhin NICHT aus (es hat keine CodeFacts), und genau diese Differenz ist das Signal: deklariert und nicht ausgewertet heisst NICHT GEPRUEFT; die vorhandene Ableitung `ALL_RULE_DEFS minus ausgewertet` macht es von selbst laut -- kein zweiter Zweig, die ND-fail-open-Lehre aus CR-SM-286. `domain` ist NEU an `ConformanceRuleDefinition` und fuer Konstruierer breaking; die Werte sind GEMESSEN statt gesetzt (der Entwurf des CR schlug `['code']`, `['code','interface']` vor -- `domain` ist aber die Grundgesamtheit, und 'code' ist kein Elementtyp): RC-01 10x FUNC, RC-04 4x SCHEMA, RC-05 5x MOD ueber fuenf Familien-Repos, RC-02/RC-03 aus der Schleife gelesen (TEST/SCHEMA), RC-06 aus ELEMENT_ATTRIBUTES -- nur FUNC/MOD/SCHEMA duerfen `external` + `realRef` tragen. NICHT in RULE_TO_DIMENSION/RULE_TO_PHASE, und das ist die zweite Korrektur am eigenen Entwurf: der readiness-Nenner summiert die Grundgesamtheiten der Regeln einer Dimension, eine deklarierte und nicht ausgewertete Regel HEBT also den Score. Gemessen ueber vier Repos, was das geschenkt haette: graph-view-edit `schema` +5,0 Punkte (Nenner 24 -> 40), graphify `arch` +2,1 und `ver` +1,8, alles Uebrige <= 0,2 -- der Ausschlag sitzt genau in der kleinen Dimension, wo die Zahl am meisten wiegt. `computeApplicable` ueberspringt eine Regel ohne Dimension bereits von selbst; die beiden Vollstaendigkeitstests sind auf die AUSGEWERTETEN Profile eingeengt (READINESS_SCORED_PROFILES) und haben das seit CR-228 in ihrem eigenen Kopf behauptet -- nur war der Katalog bis hierher mit den ausgewerteten Regeln identisch. Der Ausloese-Beleg (CR-SM-285) gilt auch fuer RC: sechs synthetische CodeFacts-Fixturen, kein Checkout, wie `CodeFactsSchema` es seit CR-211 zusagt ('testable without any filesystem') -- die Zahl der unbelegten Regeln bleibt 42 und ist nicht um sechs gestiegen. In se-engine deckt CLASS_MAP die sechs als Constraint ab: wohin eine verwaiste realRef zeigen SOLL, steht in keinem Feld, ein Operator daraus waere geraten. Prior: MINOR (CR-SM-300): `RULE_HELP` -- die Regelhilfe liegt neben der Regel. Reine ERGAENZUNG: kein Katalogeintrag, keine Severity, keine Schwelle aendert sich, kein bestehender Graph faellt. Warum sie umzieht: ein Hilfeeintrag ist keine Annotation UEBER einer Regel, er ist Teil ihrer LIEFERUNG -- eine Regel, deren Befund niemand lesen kann, ist nicht fertig. CR-GC-227 hat die Schicht 2025 bewusst in graphcode gelassen (Tempo: kein Bump, kein Familien-Review) und die Verschiebung ausdruecklich als spaetere Entscheidung notiert; Gate 1 verlangt fuer das Wiederaufmachen eine MESSUNG, und die lag am 2026-09-08 vor: von 73 Eintraegen in graphcodes `HELP_CONTENT` zeigten 11 auf Regel-IDs, die es nicht mehr gab (R-03, R-14, R-27, FC-01, SC-04, CR-R04, AO-D01, AO-D03, RT-01, PH-01, CA-01 -- AO-D03 seit drei Wochen), waehrend 8 Katalogregeln ganz ohne Eintrag dastanden (BW-02, BQ-01, BQ-02, BQ-04, BQ-06, BQ-07, ND-01, ND-02) -- darunter mit BW-02 eine der vier STEUERDIMENSIONEN: eine Regel, die rankt und sich nicht erklaert. Die Drift lief in beide Richtungen und ueber vier Releases, obwohl graphcode sehr wohl eine Vorwaertspruefung hat (`tests/help-content.test.ts`) -- sie war rot und blieb es, weil niemand graphcodes Suite las (CR-GC-488). Eine Pruefung im ungelesenen Repo ist keine Pruefung, und hinter einem Symlink gilt kein Versionsbereich (CR-GC-488 Paragraph 1a). Co-located zwingt die Grammatik selbst: eine Streichung nimmt den Eintrag mit, eine Neuanlage kann ohne ihn nicht landen. Umfang: 69 Eintraege (63 Katalog- + 6 Conformance-Regeln), 18,6 KB reine Strings -- der ./browser-Eintrag bleibt transitiv `node:`-frei (CR-SM-230). Im Golden-File steht nur die SCHLUESSELMENGE (`ruleHelp`), nicht die Prosa: 'Prosa ist ein Patch-Bump, keine Grammatik' (CR-SM-244) -- ein umformulierter Satz darf keinen Bump ausloesen, ein weggefallener Eintrag muss. Abgrenzung zu `fix_hint`, das an jeder Regeldefinition schon steht (Gate 3): `fix_hint` ist die Anweisung an den AGENTEN, `plain`/`se` die Erklaerung fuer den MENSCHEN -- verschiedene Leser, verschiedene Laenge, verschiedene Sprache. NICHT mitgezogen und bewusst in graphcode geblieben: Dashboard-Panels, Artefakte, das Vokabular und die sechs Metrik-Dimensionen (`HELP_METRICS`, CR-GC-458) -- sie haengen an graphcodes Oberflaeche, nicht am Regelkatalog. Prior: BREAKING (CR-SM-293): MT-01 zaehlt KOPPLUNG statt aller Kanten, dreht die Richtung um und urteilt per Default nicht mehr. (1) fan_in/fan_out lasen bis hierher JEDE Kante, deren eines Ende einem Modul gehoerte. Gemessen ueber 19 Familiengraphen: von 3281 gezaehlten Kanten sind nur 1117 (34 %) Kopplung. fan_in 2164 = 674 `allocate` -- das ist die MODULGROESSE, ein Modul wurde also stabiler, indem es Funktionen bekam -- plus 610 `FLOW -io-> FUNC`, 348 `FCHAIN -compose-> FUNC`, 255 `CR -relation-> FUNC`, 277 Rest. fan_out 1117 = 507 `FUNC -io-> FLOW` plus 501 `FUNC -satisfy-> REQ`; fast die halbe fan_out war SPEZIFIKATION, dieselbe Begruendung, mit der CR-SM-297 `satisfy` aus LCOM4 genommen hat. Dazu 70 Kanten ueber `MOD -io-> MOD`, ein Muster, das CR-SM-266 D2 aus TRACE_PATTERNS geloescht hat. MT-01 liest jetzt `moduleCrossings` -- DIESELBE Definition von Rand und dieselbe Zaehlbasis (verschiedene Vertraege), die CR-01, R-04 und BW-02 seit CR-SM-274/276 benutzen; MT-01 hatte bis hierher noch eine dritte. (2) RICHTUNG: der KONSUMENT haengt ab. Wer nach draussen liefert, wird gebraucht (afferent, fan_in); wer von draussen bezieht, haengt ab (efferent, fan_out). Das ist Martins Ca/Ce; die alte Lesart 'Kante zeigt weg = Abhaengigkeit' drehte jedes reine Verbrauchermodul auf I = 0 (maximal stabil), wo es 1 sein muss. Die Konvention stand vorher NIRGENDS im Repo -- sie war nie entschieden, nur codiert. (3) `DEFAULT_METRIC_POLICY.instability` 0.7 -> null, messen statt urteilen. Die Verteilung ist zweigipflig -- 44 von 149 messbaren Modulen exakt auf 0.00, 45 exakt auf 1.00, und 45 der 51 Befunde bei 0.7 sind genau diese 1.00 -- aus ihr laesst sich keine Schwelle ABLESEN (Praezedenz CR-SM-283/BW-02). Tiefer: I ist eine KOORDINATE, kein Defekt; Martins Satz lautet 'in Richtung Stabilitaet abhaengen', nicht 'I klein halten'. Damit fehlt MT-01 die Eigenschaft, die CR-SM-287 §2 von einer Steuerdimension verlangt (gerichtet, 'weniger ist besser, ohne Diskussion'), und (4) `STEER_RULES` schrumpft auf VIER. Praktisch war die Dimension ohnehin still: `steerScore` liest Verstoesse, und graphcodes Config setzt `instability: null` seit CR-GC-329 -- auf dem einzigen Repo, das taeglich steuert, lief der R5 laengst als R4. Der Nachfolger ist gemessen und angelegt (CR-SM-298): Martins Stable Dependencies Principle als gerichtete Groesse, Abhaengigkeiten 'bergauf' je Modul -- 34 von 305 (11 %), 132 von 146 Modulen bei null, Schwanz bis 6; lokal, gerichtet, budgetierbar, nicht entartet. Die Zahl `instability` bleibt vollstaendig erhalten: graph_metrics, Export und das gve-Dashboard rendern sie weiter, letzteres samt Hinweis 'Instabilitaet wird hier nur gemessen'. Prior: BREAKING (CR-SM-297, Teil 1): `satisfy` faellt aus der LCOM4-Verbindung von MT-02. Die Regel misst KOHAESION, und Kohaesion ist Datenkopplung -- geteilte `io`-Ziele und geteilte FLOW. `satisfy` ist dagegen eine SPEZIFIKATIONS-Beziehung: zwei FUNCs, die dasselbe REQ erfuellen, koennen zur Laufzeit vollstaendig entkoppelt sein. Die Wirkung ging dabei nur in EINE Richtung -- `satisfy` verbindet Gruppen, senkte also LCOM4 und liess Module kohaesiver aussehen, als ihr Datenfluss hergibt. Der Modellierungsgrund wiegt schwerer als der Messgrund: teilen sich zwei Funktionen ein Requirement, ist das REQUIREMENT zu zerlegen, nicht die Kohaesionsmessung zu beschoenigen; RD-02 sagt bereits das Verwandte. Gemessen ueber 19 Familiengraphen: MT-02 23 -> 24 Befunde. `satisfy` hat also fast nie zwei Gruppen verbunden -- die Aenderung ist prinzipiell richtig und praktisch billig. Golden neu verankert OHNE Zahlenaenderung: Totals und Reihenfolge bleiben an allen drei Eingaengen gleich (ssot 99, n700 1475, n3500 17495), nur die MT-02-shas wandern, weil die Meldung den gemessenen LCOM4-Wert woertlich nennt. NOCH OFFEN aus CR-SM-297: die beiden Container-Masse fuer den zerlegten FUNC (Kohaesion analog MT-02, Paar-Kopplung analog CR-01) -- ihre Schwellen sind aus der Verteilung ABZULESEN, nicht zu setzen (Praezedenz CR-SM-283/BW-02). Prior: BREAKING (CR-SM-296): je Frage eine Regel, je Schwelle ein Wert. (1) RD-04 gibt das Bein `FUNC -allocate-> MOD` an R-04 ab. Es zaehlte die allozierten FUNCs je Modul -- also die MODULGROESSE, und die misst R-04 auch. Beide feuerten am selben Modul im selben Batch, mit zwei Schwellen aus zwei Policy-Feldern (RD-04 > 11 gegen R-04 > 12), und welche recht hat, stand nirgends; dieselbe Ursache ging doppelt in den readiness-Nenner. R-04 ist die reichere Aussage -- Groesse GEGEN Kreuzungen, drei Urteile -- und liest ueber moduleCrossings ohnehin dieselben Daten. RD-04 bleibt fuer ZERLEGUNGSBREITE zustaendig: FUNC compose FUNC, SYS/MOD compose MOD und der FUNC-Wurzelwald am SYS (CR-SM-282). (2) Beide Schwellen sind an die Doktrin 7+-2 gebunden statt danebengesetzt: decompositionBreadth.warning 11 -> 9 (die Obergrenze der Doktrin) und moduleSize large 12 -> 9, coupled 8 -> 7. CR-SM-282 hatte gegen die Doktrin-Zahl 5 entschieden, weil `info` bei 5 den readiness-Nenner verwaessert haette -- das Argument traegt fuer die OBERgrenze nicht, denn RD-04 meldet als warning. Wirkung ueber 19 Familiengraphen, gemessen: RD-04 17 -> 15, R-04 9 -> 11, Summe 26 -> 26. Die Zahl bleibt, die Zustaendigkeit wird eindeutig. Prior: BREAKING (CR-SM-295): drei Regeln entfallen, eine wird neu gefasst -- Katalog 66 -> 63. (1) R-14 (UC must have compose) kann NIE ALLEIN feuern: sie prueft nur, ob ein UC irgendeine ausgehende compose-Kante hat, ohne den Zieltyp anzusehen, waehrend UC-01 den REQ und UC-03 die FCHAIN einzeln verlangen. Auf ELEMENTEBENE ueber 19 Familiengraphen gemessen, nicht auf Summen: R-14 (2 Befunde) ist eine echte Teilmenge von UC-01 vereinigt UC-03 (23 Befunde). Ihr Fixhinweis ('Add FCHAIN or REQ') versprach zudem eine Pruefung, die der Code nicht macht. (2) FC-01 (FCHAIN has actor boundary) ist von FC-04 gedeckt -- gemessen FC-01 (5) Teilmenge von FC-04 (27); FC-04 verlangt Eingang UND Ausgang, FC-01 nur eine Beruehrung. Dazu trug FC-01 einen TOTEN Ersatzweg: `hasDirectActorIO` prueft, ob der Eltern-UC actor-adjazent ist, und `ACTOR -io-> UC` hat CR-SM-266 (D1/D4) aus TRACE_PATTERNS entfernt -- der Zweig konnte seit drei Versionen nicht mehr wahr werden. (3) CR-R04 entfaellt und CR-R01 wird NEU GEFASST. Beide zielten daneben: CR-R01 akzeptierte jede relation-Kante, also auch einen CR, der nur an einem Meilenstein haengt (Termin, kein Umfang) -- 5 Befunde bei 49 CRs, die ausschliesslich auf ein MS zeigen. CR-R04 verlangte einen FUNC und war damit zu eng: 21 Befunde, darunter Daten-, Doku- und Konfigurationsaenderungen, die zu Recht keine Funktion beruehren. Die neue Fassung fragt nach dem UMFANG: mindestens eine relation auf FUNC, MOD, SCHEMA, REQ oder UC. Grundgesamtheit woertlich die von CR-R04 (CR-SM-255, nur offene CRs) -- an einem geschlossenen CR ist die Frage Archaeologie. Gemessen an 514 CR-Knoten: 54 ohne Umfangsbezug, davon 0 OFFEN, 52 done, 2 ohne Status. Die Verschaerfung laesst die Historie damit unangetastet und wirkt ab dem naechsten CR. MS-03 (CR ohne MS) bleibt unberuehrt -- sie stellt die komplementaere Frage. (4) R-17 behaelt ihre Pruefung und verliert ihren falschen Fixhinweis, der drei Zieltypen nannte, die sie nie angesehen hat. Prior: BREAKING (CR-SM-294): sechs Regeln entfallen ersatzlos, Katalog 72 -> 66. AO-D01 (RelayNodeDetection) ist STRUKTURELL unmoeglich -- sie verlangt `>= 2` ausgehende `io`-Kanten auf andere FUNC, und `FUNC -io-> FUNC` steht nicht in TRACE_PATTERNS; R-18 lehnt es als error ab. Das ist zeichengleich der Fall, fuer den CR-SM-283 AO-D03 geloescht hat -- AO-D03 fiel, AO-D01 blieb stehen. CA-01, RT-01, PH-01, R-27 und R-03 sind GEGENSTANDSLOS: sie lesen `asil`, `attributes.kind === 'physical'`, `attributes.requires` und `attributes.capability`, und ueber 19 Familiengraphen ist jedes dieser Attribute 0-mal gesetzt (305 MOD, 696 FUNC). Beleg beidseitig: `report:silence` meldet alle sechs mit 0 Befunden ueber den gesamten Bestand, UND keine von ihnen hat eine Ausloese-Fixture in se-rule-trigger-proof.test.ts (CR-SM-285) -- also weder feuert sie irgendwo, noch ist belegt, dass sie ueberhaupt feuern KANN. Genau die Doppelbedingung, die Gate 7 fuer eine Streichung verlangt. NICHT gestrichen wurde CL-01, obwohl derselbe Verdacht bestand: sie feuert 2x in 1 von 19 Graphen. Ihre Aussage waere anderswo besser aufgehoben -- jeder UC muss hinreichend distinct sein, und das saehe man an der FCHAIN -- aber diese Regel EXISTIERT NICHT: CR-SM-283 hat AO-D03 geloescht und dabei festgehalten, dass ihre Aufgabe ('zwei Wirkketten, die auf einer Ebene mitgliedsgleich werden') ungeloest bleibt. CL-01 zu streichen hiesse, eine feuernde Pruefung gegen eine nicht existierende zu tauschen. Wirkung auf die Bestandsgraphen: 0 -> 0 Befunde, kein Graph faellt. Die einzige messbare Folge ist der kleinere NENNER jeder readiness-Dimension -- sechs stumme Regeln hoben bisher jeden Score. Prior: BREAKING (CR-SM-286): ND-01/ND-02 rechnen ihre Aehnlichkeit SELBST; setND01SimilarityMatrix/setND02SimilarityMatrix/getND02SimilarityMatrix entfallen ersatzlos. Die Formel stand als Prosa in contracts und als Code in graphcode/src/kernel/measure/nd-similarity.ts, verbunden durch eine Injektionsnaht. Vier gemessene Kosten: (1) FAIL-OPEN -- ohne Injektion gaben zwei `error`-Regeln [] zurueck, ununterscheidbar von 'keine Duplikate'; moneyflow trug 16 ND-01-Befunde, die ausserhalb graphcodes NIEMAND sah (report:silence, die Spike-Skripte, jedes kuenftige Werkzeug). Die Familie hat fuer 'kann nicht urteilen' eine Konvention (policy.X = null, moduleMetrics.instability = null); ND fiel statt dessen nach gruen. (2) PROZESSWEITER MODULZUSTAND -- `let _nd01Matrix` ist global, graphcode brauchte die Klammer withNDMatrices mit finally-Reset, jeder andere Aufrufer nicht: Graph A urteilte ueber Graph B. (3) AO-D01 liegt IM Gate-Katalog und uebersprang seine Overlap-Pruefung ohne Matrix ('no matrix -> assume pass') -- eine Gate-Regel, die sich stillschweigend abschwaecht. (4) BQ-04 haengt am selben Muster und ist komplett tot. Jetzt: similarity.ts, je Graph WeakMap-gecacht, Formeln und Schwelle 0,85 zeichengleich uebernommen -- dasselbe Urteil an der richtigen Stelle, Praezedenz CR-SM-276 (moduleCrossings). DABEI GEFUNDEN UND BEHOBEN: die portierte Formel las Partner ueber ALLE Traces; se-rule-pair-legality (Pruefung A, CR-SM-278) meldete 35 Faelle, in denen ND-02 erst durch eine grammatikwidrige Kante auf SCHEMA anschlug (ACTOR -compose-> SCHEMA und Verwandte). `partners()` filtert jetzt ueber isValidTrace -- eine Regel darf ihr Verdikt nicht an einer Kante haengen, die R-18 als error ablehnt. Golden bewusst neu verankert und im Test-Header aufgeschluesselt: ssot 98 -> 98 UNVERAENDERT, n700 1038 -> 1478, n3500 5176 -> 17496, der gesamte Zuwachs ist ND-01 auf den synthetischen Eingaengen -- buildFixedSizeGraph KOPIERT den Basisgraphen 11x bzw. 56x, dort hat jedes Element 10 bzw. 55 exakte Duplikate; eine Duplikatsregel MUSS das melden. ND-02 bleibt ueberall 0. BQ-04 ausdruecklich NICHT angeschlossen: es verlangt 'pre-computed EMBEDDING similarity', und Embeddings kann ein reines Vertragspaket nicht rechnen; ein Ersatzmass haette ich erfinden muessen, und der Versuch (0.7*Beschreibung + 0.3*Name) lieferte 0 Befunde an allen neun Familiengraphen und 4950 an einer templatierten Fixture -- beide Gate-7-Ausreisser zugleich. BQ-04 ist damit der naechste AO-D03-Fall und ein eigener se-grammar-review-Vorgang. Prior: BREAKING (CR-SM-285): R-12 findet Zyklen JEDER Laenge statt nur Rueckkanten. Die Regel heisst 'No circular dependencies' und prueft bis hierher nur, ob eine Kante DIREKT zurueckliuft (A->B und B->A). Ein Dreierzyklus A->B->C->A ging vollstaendig durch das Gate, mit null Fehlern. Das ist die gefaehrlichere Haelfte der Fehlerklasse dieser Session: ein Absturz meldet sich, eine Pruefung die ihren Gegenstand nicht erreicht BESTAETIGT. Warum es zaehlt: jeder Rollup der Familie setzt Azyklizitaet voraus -- chainOf/funcChainOf (CR-SM-282/-283) tragen seen-Guards gegen genau diese Endlosschleife, moduleMetrics, die RD-04-Breitenzaehlung und gves Container-Sicht ebenso; unter einem Zyklus ist ein Rollup nicht falsch sondern UNDEFINIERT. Das R-18-Bein aus CR-SM-283 deckt es NICHT: bei A->B->C->A hat jeder Knoten genau EINEN compose-Elternteil, die Baum-Eigenschaft ist formal erfuellt und die Struktur trotzdem kein Baum. Gemessen ueber NEUN Familiengraphen: 0 Zyklen jeder Laenge, also 0 -> 0 Befunde -- die Schaerfung laesst keinen bestehenden Graphen fallen. Major nach Gate 8 trotzdem, weil eine verschaerfte Regel fremde Graphen fallen lassen KANN. EIN Befund je Zyklus statt je Kante (Klasse CR-SM-242), verankert am kleinsten Knoten des Rings und damit unabhaengig vom DFS-Einstieg (Gate 5); die Zweierring-Meldung ist woertlich unveraendert. Dazu zwei Haertungen OHNE Regelwirkung: descriptionOf() gab `el.description ?? ''` und starb an jedem Nicht-String (42/{}/[]/true -> '.trim is not a function', vier von fuenf Typen) -- am Gate hiess das Stacktrace statt block-Verdict; jetzt zaehlt nur ein echter String, alles andere ist 'keine Beschreibung'. Und tests/unit/se-rule-trigger-proof.test.ts ist die Umkehrung von report:silence: je Regel ein Graph der sie feuern LASSEN MUSS, 22 belegt, 50 als unbelegt ausgewiesen mit einer Zahl die nur sinken darf. Beim ersten Lauf fand er sofort einen Fall: ND-01/ND-02 sind `error` und beginnen mit `if (!_nd01Matrix) return []` -- ohne injizierte Aehnlichkeitsmatrix fallen sie nach GRUEN, ununterscheidbar von 'keine Duplikate'. Dokumentiert und eingefroren, nicht behoben: die Severity-/Signalfrage ist ein eigener se-grammar-review-Vorgang. Prior: BREAKING (CR-SM-283): AO-D03 entfaellt ERSATZLOS, BW-02 kommt -- Katalog 74 -> 74, kein Wachstum im Komplexitaetsbudget. (1) AO-D03 (`DuplicatePathDetection`) suchte `FUNC -io-> FUNC`, ein Paar das TRACE_PATTERNS nicht kennt und das R-18 als error ablehnt: die Regel konnte STRUKTURELL nicht feuern und hat es an keinem der fuenf Familiengraphen je getan. Eine Regel die nie feuert kostet trotzdem Nenner-Anteil, Katalogzeile, readiness-Zuordnung, Golden-File-Eintrag und einen Leser der sie unterscheiden muss. Die Aufgabe die sie tragen SOLLTE -- zwei Wirkketten die auf einer Ebene mitgliedsgleich werden -- bleibt ungeloest und ist ein eigener CR; BW-02 ersetzt sie nicht. (2) BW-02 misst die Randbreite der FUNC-WHITEBOX: verschiedene SCHEMA-Vertraege auf `FUNC -io-> FLOW -io-> FUNC` mit einem Endpunkt im compose-Teilbaum und einem ausserhalb. Gerechnet im selben Durchlauf wie `byModule` (module-crossings.ts), damit es EINE Definition von Rand gibt. Der Rollup ist hier nicht Genauigkeit sondern Existenz: ein zerlegter FUNC traegt selbst keine io-Kanten (die liegen an seinen Blaettern), ohne Teilbaum-Aufloesung waere die Zahl strukturell immer 0 -- gemessen FUNC-block-grounding 0 -> 19. Gate 6 des Grammatik-Reviews: 12 Befunde an graphcode, davon 0 in Ueberlappung mit irgendeiner FUNC-Regel (AO-D01 misst Relay-Knoten, IO-01 Kettenkohaerenz, R-31 blosse Verdrahtung). Schwelle 5 aus der Verteilung ABGELESEN, nicht gewaehlt: bok und graph-view-edit enden beide bei 3, graphcode geht bis 19, moneyflow bis 17. Kein info-Level -- bei Schwelle 3 truege bok 4 Befunde auf 9 Blackboxes, und readiness zaehlt info ungefiltert in den Nenner (MT-03-Praezedenz). (3) VIERTES BEIN von R-18: ein Element hat hoechstens einen compose-Elternteil (FUNC/FUNC und MOD/MOD), error. TRACE_PATTERNS begrenzt heute nur die KINDER einer Kante, nie die ELTERN -- graph_mutate legt eine zweite compose-Kante anstandslos an. 0 Verstoesse ueber fuenf Graphen, und trotzdem hart, weil jeder Rollup der Familie unter Verletzung nicht falsch sondern UNDEFINIERT ist (byModule/byFunc, RD-04-Breite, die Container-Sicht von gve). Keine eigene Regel-ID, Praezedenz CR-SM-271 Teil 2: dieselbe Aussage, derselbe naechste Schritt. EIN Befund je KIND, nicht je ueberzaehliger Kante. (4) `policy.boundaryWidth` neu (nullable, Default {warning: 5}) -- fuer Konstruierer einer MetricPolicy breaking. Prior: BREAKING (CR-SM-282): zwei Blackbox-Luecken geschlossen, und beide lassen bestehende Graphen fallen. (1) RD-04 zaehlt den FUNC-WURZELWALD, verankert am SYS-Knoten. Die Regel zaehlt Kinder je Parent; die Wurzeln des `FUNC -compose-> FUNC`-Waldes haben keinen, also war die oberste Funktionsebene unsichtbar -- gemessen: moneyflow hat 306 Wurzel-FUNCs und RD-04 meldete 0. Die MOD-Seite hatte das Problem nie, weil `SYS -compose-> MOD` eine echte Kante ist; die FUNC-Seite hat keine (`se:top-level`: der obere FUNC-Satz ist eine PROJEKTION, keine Kante). Anker ist der einzige SYS-Knoten; gibt es nicht genau einen, schweigt der Zweig statt zu raten (R-17 deckt den Fall). (2) `moduleCrossings.byModule` und die Groesse in R-04 lesen jetzt die BESITZKETTE (direkt alloziertes MOD + compose-Vorfahren) statt nur der direkten Allokation. Ein Eltern-MOD hat keine direkt allozierten FUNCs -- es kam in `byModule` nicht vor, `funcCount` war 0, und R-04 schwieg per `funcCount <= coupled` an genau der Whitebox, in die der Leser hineinklickt. `pairs` (CR-01) bleibt BEWUSST auf der direkten Zuordnung: 'wie stark haengen DIESE zwei Module aneinander' ist eine Blattebenen-Frage, ein Elternpaar meldete dieselbe Kopplung ein zweites Mal -- CR-01 ist an allen fuenf Familiengraphen zeichengleich geblieben (5/3/15/0/63). Delta gesamt: moneyflow RD-04 +1, sirail R-04 +1, bok/gve/graphcode unveraendert. (3) Die Schwelle steht in `policy.decompositionBreadth` statt als `DECOMPOSITION_BREADTH_MAX = 11` inline in rules.ts -- dieselbe Klasse, die CR-SM-236 fuer R-04 aufgeloest hat; `null` schaltet die Regel ab, Default 11 = verhaltensgleich. Das neue Pflichtfeld in MetricPolicy ist fuer Konstruierer breaking. Die Doktrin-Zahl 5 (`se:top-level`) ist bewusst NICHT der Default: `info` bei 5 haette an graphcode 20 zusaetzliche Befunde erzeugt, und readiness zaehlt `info` ungefiltert in den Nenner (`score = 1 - Verstoesse / applicable`) -- genau der Grund, aus dem MT-03 zur Messung statt zur Regel wurde (readiness.ts). Wer doktrin-streng fahren will, setzt 5. Prior: BREAKING (CR-SM-271 Teil 2): SC-04 entfaellt ersatzlos. Ein FLOW ohne SCHEMA ist kein schwaecherer Typ, sondern untypisiert — die Untergrenze ist Grammatik geworden (`FLOW -relation-> SCHEMA [1..1]`, META_MODEL 5.0.0) und meldet als drittes Bein von R-18 (error, mit candidate_targets im Kontext wie zuvor SC-04). Kein Regel-Loch: derselbe Sachverhalt, EIN Befund, nur haerter und am Gate durchgesetzt statt nur gemeldet. Der Katalog schrumpft um eine Zeile (Nenner, readiness-Zuordnung 'schema'/CDR und Golden-File-Eintrag fallen mit); der Fall zaehlt jetzt unter R-18/'arch'/PDR.
8
+ export const RULES_VERSION = '24.0.0'; // MAJOR (CR-SM-313): R-21 meldet die Uebergabe OHNE gemeinsame Kette -- Befund am SENDER, weil es ohne gemeinsame Kette keinen Kettenanker gibt. CR-GC-315 hatte diesen Fall stillgelegt, weil ein FLOW damals viele Produzenten haben durfte und die P·C-Ableitung Wiederverwendung quadratisch besteuerte; seit IO-02 (CR-SM-307) hat er genau EINEN, die Begruendung ist entfallen. DREI ZWEIGE: (1) ein Ende in GAR KEINER Kette -> still, das ist R-30s Aussage und nicht R-21s -- ohne die Klausel feuert die Regel an einem code-importierten Graphen wie moneyflow 219 von 219 Mal und meldet in Wahrheit 'dieses Repo hat keine Wirkketten', einmal je Kante; (2) gemeinsame Kette, keine davon geprueft -> Befund an der Kette, WOERTLICH unveraendert; (3) beide in Ketten, keine gemeinsame -> Befund am Sender, NEU. AUSNAHME: ist ein Ende Infrastruktur (chains >= policy.criticality.infrastructure, CR-SM-314), schweigt Zweig 3 -- eine Uebergabe an eine Funktion, die quer durch alle Ketten laeuft, ist ein Bus und kein fehlender Integrationstest. GEMESSEN ueber 12 Familiengraphen bei Modellstand 2026-09-12: 500 Uebergabepaare, davon 240 still durch Zweig 1 (219 allein moneyflow -- die Klausel ist der Unterschied zwischen einer Regel und einem Rauschen), 34 Altbefunde, 56 neu roh, 20 durch die Ausnahme freigestellt (alle graphcode, getragen von sechs FUNCs: mutate 11 Ketten, evaluate-rules 6, graph-impact/read-tools/graph-store je 4, list-elements 3), 36 netto neu. R-21 gesamt 34 -> 70. DOMAIN waechst von ['FCHAIN'] auf ['FCHAIN','FUNC']: Zweig 3 verankert am FUNC, und ein Zaehler, der seinen eigenen readiness-Nenner uebersteigt, ist die Fehlmessung aus CR-SM-242. GOLDEN UNVERAENDERT und das ist gemessen: der ssot-Eingang ist docs/graph/sigloch-modules.graph.json mit 0 Uebergabepaaren, n700/n3500 sind Kopien davon. R-21 hatte bis hierher KEINEN Ausloese-Beleg (eine der 42 als unbelegt ausgewiesenen Regeln) -- er kommt mit diesem CR, die Zahl sinkt auf 41. NOCH OFFEN: R-21 haelt fuer die Frage 'teilen diese beiden EINE Kette' weiterhin einen lokalen Ketten-Index, waehrend die ZAHL fuer die Ausnahme aus functionCriticality kommt; ein gemeinsamer Helfer chainsByFunc waere die siebte Datei gewesen und ist ein eigenes Item. Prior: MAJOR (CR-SM-314): neues PFLICHTFELD `criticality` in MetricPolicy (nullable, Default {infrastructure: 3}) und die neue Kennzahl `functionCriticality` -- je FUNC die Zahl der Wirkketten und der Use Cases darueber. MAJOR nach der Praezedenz CR-SM-283 (`boundaryWidth`): ein neues Pflichtfeld laesst JEDEN Konsumenten mit eigener Config hart abbrechen, auch wenn die Oberflaeche nur waechst. KEIN Verstoss aendert sich in dieser Version -- die Kennzahl urteilt nicht, und ihr erster Abnehmer (R-21s Infrastruktur-Ausnahme) landet getrennt als CR-SM-313. DIE DEFINITION IST GELIEHEN: "FUNC in einer Kette" heisst woertlich, was R-30 darunter versteht (direkte `FCHAIN -compose-> FUNC`), ohne Vererbung ueber compose-Vorfahren. Der Rollup wurde gemessen und verworfen -- ueber 17 Familiengraphen haette er die Grundgesamtheit von 224 auf 227 FUNCs gehoben, drei Stueck, und waere ein ZWEITER Kettenbegriff neben R-30s gewesen (die Drift-Klasse aus CR-SM-276). Ein zerlegter Block misst deshalb 0, und das ist richtig: FC-03 verbietet ein Kettenglied mit verschachtelter Zerlegung, er KANN die Kante nicht tragen. KEIN `infrastructure`-Flag an der Kennzahl: ein Flag waere ein Urteil in der Messung, die Schwelle gehoert in die Policy -- dieselbe Trennung, die `moduleMetrics` gegen MT-01/MT-02 haelt. SCHWELLE ABGELESEN, aber NICHT an der Verteilungsform: die ist ein duenner Schwanz (224 FUNCs in 1 Kette, 37 in 2, 6 in 3, 8 in 4, je 1 in 5/6/11) und gibt wie `instability` keine Schwelle her (CR-SM-293). Abgelesen ist die WIRKUNG: auf graphcodes 31 Uebergaben ohne gemeinsame Kette stellen die Schwellen 3 und 4 identisch 19 frei, 5 und 6 identisch 6 -- ein Plateau 3..4, innerhalb dessen die Wahl wirkungsfrei ist; der Startwert ist seine Untergrenze. Prior: MAJOR (CR-SM-312): R-04 misst nur noch die Randbreite des Moduls (verschiedene Vertraege ueber den Modulrand, Schwelle `boundaryWidth` wie BW-02) und steht in STEER_RULES; `policy.moduleSize` entfaellt ersatzlos -- Pflichtfeld weg, Befunde verschieben sich. Prior: MAJOR (CR-SM-311): RD-04 misst die Breite je Ebene als BAND -- neues Pflichtfeld `decompositionBreadth.min` (Default 3, Z3) und die neue Regel RD-05 fuer die Untergrenze (Katalog 70 -> 71), und ein MOD zaehlt Untermodule PLUS eigene Blatt-FUNC (das Allokations-Bein aus CR-SM-296 kommt als Breite zurueck; R-04 wird Kopplung, CR-SM-312). Breaking fuer jede Config (Pflichtfeld) und fuer Befunde (Untergrenze neu, MOD-Breite neu). Prior: MAJOR (CR-SM-307): IO-02 -- ein FLOW hat genau EINEN Produzenten, severity `error`. Katalog 69 -> 70. Reine Ergaenzung im Umfang, MAJOR nach Gate 8: eine neue error-Regel KANN fremde Graphen fallen lassen, und diese tut es -- gemessen ueber fuenf Systeme sind es graphcode 20/42 FLOWs, graph-view-edit 5/8, bok 3/15, waehrend moneyflow (0/220) und test_karp (0/21) sauber sind. Die beiden Referenzen erfuellen sogar die STRENGERE beidseitige Fassung; die Lockerung rettet sie nicht, sie brauchen sie nicht. WARUM NUR DIE PRODUZENTENSEITE: der Inhalt eines FLOW kommt von einer Stelle -- haengen zwei Quellen dran, sind es zwei Fluesse unter einem Namen und kein Leser weiss, welche Fassung er bekommt (Beleg aus dem Bestand: graphcodes FLOW-metric-policy trug ACTOR-owner UND FUNC-load-config, und seine eigene Beschreibung sagte es woertlich -- 'Dieselbe Form in zwei Fassungen, deshalb ein Vertrag'). Mehrere KONSUMENTEN sind dagegen die normale Gestalt eines geteilten Vertrags: eine Quelle, viele Leser, genau wie eine zentrale Konfiguration aussieht. Die beidseitige Fassung wurde GEMESSEN und VERWORFEN -- sie haette allein in graphcode 8 saubere 1:N-Muster bestraft (FLOW-dimension-readiness 1x7, FLOW-steering-snapshot 1x3, sechs weitere) und keinen einzigen zusaetzlichen echten Fehler gefunden; tests/unit/se-rule-trigger-proof.test.ts haelt die Gegenprobe fest, damit sie nicht unbemerkt zurueckkehrt. GATE 4, keine Doppelzaehlung: R-10 prueft die UNTERgrenze derselben Achse (mindestens ein Produzent, mindestens ein Konsument), IO-02 die OBERgrenze der Produzentenseite -- |P| = 0 und |P| > 1 sind disjunkt, die beiden koennen am selben FLOW nie zugleich feuern. Getrennte IDs, weil es getrennte Fragen sind (CR-SM-296): R-10 fragt 'hat dieser Fluss ueberhaupt Enden?', IO-02 'kommt sein Inhalt von einer Stelle?'. WARUM ES BIS HIERHER KEIN GATE PRUEFTE: die vier io-Zeilen in TRACE_PATTERNS tragen kein cardinality-Feld und fallen durch REQUIRED_PATTERNS (= TRACE_PATTERNS.filter(p => p.cardinality === '1')); die SCHEMA-Haelfte IST Grammatik (FLOW -relation-> SCHEMA [1..1], R-18 seit CR-SM-271 Teil 2), die io-Haelfte war nie deklariert -- kein wieder aufgerissenes Loch, die Frage wurde nie gestellt. EIN Befund je FLOW, nicht je ueberzaehliger Kante (Klasse CR-SM-242), value = Zahl der Produzenten, threshold 1 (CR-SM-288); dieselbe Quelle zweimal verdrahtet ist EINE Quelle. NICHT in STEER_RULES, und das ist die wichtigste Abgrenzung: das ist Hygiene, keine Steuerdimension, und der Unterschied ist GEGENLAEUFIG gemessen -- die Reparatur in graphcode (CR-GC-499/500, IO-02 20 -> 9) verbesserte fitAdvisory (coherence +0,111, modifiability +0,072) und verschlechterte den Chebyshev-Abstand um 0,25. KORREKTUR (CR-SM-308): nicht, weil ein aufgetrennter Fluss BW-02 hebt -- moduleCrossings zaehlt verschiedene SCHEMA je Rand, reines Auftrennen laesst BW-02 gleich (gemessen graphcode v257: 15 -> 15); der Anstieg kam von drei neuen SCHEMA und umgelegten Ketten. Die Einordnung als Hygiene statt Steuerdimension bleibt eine Entscheidung, kein gemessener Zielkonflikt. Die neun Befunde, die in graphcode bleiben, sind Busse mit 3 bis 19 Produzenten -- un-modellierte Entwurfsarbeit, kein Regelproblem, eigener Spike. Prior: MINOR (CR-SM-305): die sechs RC-Regeln stehen im KATALOG -- ALL_RULE_DEFS 63 -> 69, mit eigenem Profil `conformance`. Reine Ergaenzung: keine bestehende Regel, Severity oder Schwelle aendert sich, kein Graph faellt, das Golden-File der Regelausgabe steht zeichengleich. ROOT CAUSE: das Kongruenz-Urteil existierte, war getestet und erreichte KEINEN Produktionspfad. Gate (`harness.mutate`), Steuerung (fit-advisory, steering-snapshot) und `GET /api/graph/readiness` lesen alle ALL_RULE_DEFS; RC stand nicht darin, weil seine Signatur `(graph, facts: CodeFacts)` statt `(graph, policy)` ist -- der einzige Aufrufer von `scoreReadinessWithConformance` war ein Test. Ein Modell-Zug, der die Bindung an den Code bricht, passierte damit jedes Gate lautlos. `evaluateAllRules` fuehrt RC weiterhin NICHT aus (es hat keine CodeFacts), und genau diese Differenz ist das Signal: deklariert und nicht ausgewertet heisst NICHT GEPRUEFT; die vorhandene Ableitung `ALL_RULE_DEFS minus ausgewertet` macht es von selbst laut -- kein zweiter Zweig, die ND-fail-open-Lehre aus CR-SM-286. `domain` ist NEU an `ConformanceRuleDefinition` und fuer Konstruierer breaking; die Werte sind GEMESSEN statt gesetzt (der Entwurf des CR schlug `['code']`, `['code','interface']` vor -- `domain` ist aber die Grundgesamtheit, und 'code' ist kein Elementtyp): RC-01 10x FUNC, RC-04 4x SCHEMA, RC-05 5x MOD ueber fuenf Familien-Repos, RC-02/RC-03 aus der Schleife gelesen (TEST/SCHEMA), RC-06 aus ELEMENT_ATTRIBUTES -- nur FUNC/MOD/SCHEMA duerfen `external` + `realRef` tragen. NICHT in RULE_TO_DIMENSION/RULE_TO_PHASE, und das ist die zweite Korrektur am eigenen Entwurf: der readiness-Nenner summiert die Grundgesamtheiten der Regeln einer Dimension, eine deklarierte und nicht ausgewertete Regel HEBT also den Score. Gemessen ueber vier Repos, was das geschenkt haette: graph-view-edit `schema` +5,0 Punkte (Nenner 24 -> 40), graphify `arch` +2,1 und `ver` +1,8, alles Uebrige <= 0,2 -- der Ausschlag sitzt genau in der kleinen Dimension, wo die Zahl am meisten wiegt. `computeApplicable` ueberspringt eine Regel ohne Dimension bereits von selbst; die beiden Vollstaendigkeitstests sind auf die AUSGEWERTETEN Profile eingeengt (READINESS_SCORED_PROFILES) und haben das seit CR-228 in ihrem eigenen Kopf behauptet -- nur war der Katalog bis hierher mit den ausgewerteten Regeln identisch. Der Ausloese-Beleg (CR-SM-285) gilt auch fuer RC: sechs synthetische CodeFacts-Fixturen, kein Checkout, wie `CodeFactsSchema` es seit CR-211 zusagt ('testable without any filesystem') -- die Zahl der unbelegten Regeln bleibt 42 und ist nicht um sechs gestiegen. In se-engine deckt CLASS_MAP die sechs als Constraint ab: wohin eine verwaiste realRef zeigen SOLL, steht in keinem Feld, ein Operator daraus waere geraten. Prior: MINOR (CR-SM-300): `RULE_HELP` -- die Regelhilfe liegt neben der Regel. Reine ERGAENZUNG: kein Katalogeintrag, keine Severity, keine Schwelle aendert sich, kein bestehender Graph faellt. Warum sie umzieht: ein Hilfeeintrag ist keine Annotation UEBER einer Regel, er ist Teil ihrer LIEFERUNG -- eine Regel, deren Befund niemand lesen kann, ist nicht fertig. CR-GC-227 hat die Schicht 2025 bewusst in graphcode gelassen (Tempo: kein Bump, kein Familien-Review) und die Verschiebung ausdruecklich als spaetere Entscheidung notiert; Gate 1 verlangt fuer das Wiederaufmachen eine MESSUNG, und die lag am 2026-09-08 vor: von 73 Eintraegen in graphcodes `HELP_CONTENT` zeigten 11 auf Regel-IDs, die es nicht mehr gab (R-03, R-14, R-27, FC-01, SC-04, CR-R04, AO-D01, AO-D03, RT-01, PH-01, CA-01 -- AO-D03 seit drei Wochen), waehrend 8 Katalogregeln ganz ohne Eintrag dastanden (BW-02, BQ-01, BQ-02, BQ-04, BQ-06, BQ-07, ND-01, ND-02) -- darunter mit BW-02 eine der vier STEUERDIMENSIONEN: eine Regel, die rankt und sich nicht erklaert. Die Drift lief in beide Richtungen und ueber vier Releases, obwohl graphcode sehr wohl eine Vorwaertspruefung hat (`tests/help-content.test.ts`) -- sie war rot und blieb es, weil niemand graphcodes Suite las (CR-GC-488). Eine Pruefung im ungelesenen Repo ist keine Pruefung, und hinter einem Symlink gilt kein Versionsbereich (CR-GC-488 Paragraph 1a). Co-located zwingt die Grammatik selbst: eine Streichung nimmt den Eintrag mit, eine Neuanlage kann ohne ihn nicht landen. Umfang: 69 Eintraege (63 Katalog- + 6 Conformance-Regeln), 18,6 KB reine Strings -- der ./browser-Eintrag bleibt transitiv `node:`-frei (CR-SM-230). Im Golden-File steht nur die SCHLUESSELMENGE (`ruleHelp`), nicht die Prosa: 'Prosa ist ein Patch-Bump, keine Grammatik' (CR-SM-244) -- ein umformulierter Satz darf keinen Bump ausloesen, ein weggefallener Eintrag muss. Abgrenzung zu `fix_hint`, das an jeder Regeldefinition schon steht (Gate 3): `fix_hint` ist die Anweisung an den AGENTEN, `plain`/`se` die Erklaerung fuer den MENSCHEN -- verschiedene Leser, verschiedene Laenge, verschiedene Sprache. NICHT mitgezogen und bewusst in graphcode geblieben: Dashboard-Panels, Artefakte, das Vokabular und die sechs Metrik-Dimensionen (`HELP_METRICS`, CR-GC-458) -- sie haengen an graphcodes Oberflaeche, nicht am Regelkatalog. Prior: BREAKING (CR-SM-293): MT-01 zaehlt KOPPLUNG statt aller Kanten, dreht die Richtung um und urteilt per Default nicht mehr. (1) fan_in/fan_out lasen bis hierher JEDE Kante, deren eines Ende einem Modul gehoerte. Gemessen ueber 19 Familiengraphen: von 3281 gezaehlten Kanten sind nur 1117 (34 %) Kopplung. fan_in 2164 = 674 `allocate` -- das ist die MODULGROESSE, ein Modul wurde also stabiler, indem es Funktionen bekam -- plus 610 `FLOW -io-> FUNC`, 348 `FCHAIN -compose-> FUNC`, 255 `CR -relation-> FUNC`, 277 Rest. fan_out 1117 = 507 `FUNC -io-> FLOW` plus 501 `FUNC -satisfy-> REQ`; fast die halbe fan_out war SPEZIFIKATION, dieselbe Begruendung, mit der CR-SM-297 `satisfy` aus LCOM4 genommen hat. Dazu 70 Kanten ueber `MOD -io-> MOD`, ein Muster, das CR-SM-266 D2 aus TRACE_PATTERNS geloescht hat. MT-01 liest jetzt `moduleCrossings` -- DIESELBE Definition von Rand und dieselbe Zaehlbasis (verschiedene Vertraege), die CR-01, R-04 und BW-02 seit CR-SM-274/276 benutzen; MT-01 hatte bis hierher noch eine dritte. (2) RICHTUNG: der KONSUMENT haengt ab. Wer nach draussen liefert, wird gebraucht (afferent, fan_in); wer von draussen bezieht, haengt ab (efferent, fan_out). Das ist Martins Ca/Ce; die alte Lesart 'Kante zeigt weg = Abhaengigkeit' drehte jedes reine Verbrauchermodul auf I = 0 (maximal stabil), wo es 1 sein muss. Die Konvention stand vorher NIRGENDS im Repo -- sie war nie entschieden, nur codiert. (3) `DEFAULT_METRIC_POLICY.instability` 0.7 -> null, messen statt urteilen. Die Verteilung ist zweigipflig -- 44 von 149 messbaren Modulen exakt auf 0.00, 45 exakt auf 1.00, und 45 der 51 Befunde bei 0.7 sind genau diese 1.00 -- aus ihr laesst sich keine Schwelle ABLESEN (Praezedenz CR-SM-283/BW-02). Tiefer: I ist eine KOORDINATE, kein Defekt; Martins Satz lautet 'in Richtung Stabilitaet abhaengen', nicht 'I klein halten'. Damit fehlt MT-01 die Eigenschaft, die CR-SM-287 §2 von einer Steuerdimension verlangt (gerichtet, 'weniger ist besser, ohne Diskussion'), und (4) `STEER_RULES` schrumpft auf VIER. Praktisch war die Dimension ohnehin still: `steerScore` liest Verstoesse, und graphcodes Config setzt `instability: null` seit CR-GC-329 -- auf dem einzigen Repo, das taeglich steuert, lief der R5 laengst als R4. Der Nachfolger ist gemessen und angelegt (CR-SM-298): Martins Stable Dependencies Principle als gerichtete Groesse, Abhaengigkeiten 'bergauf' je Modul -- 34 von 305 (11 %), 132 von 146 Modulen bei null, Schwanz bis 6; lokal, gerichtet, budgetierbar, nicht entartet. Die Zahl `instability` bleibt vollstaendig erhalten: graph_metrics, Export und das gve-Dashboard rendern sie weiter, letzteres samt Hinweis 'Instabilitaet wird hier nur gemessen'. Prior: BREAKING (CR-SM-297, Teil 1): `satisfy` faellt aus der LCOM4-Verbindung von MT-02. Die Regel misst KOHAESION, und Kohaesion ist Datenkopplung -- geteilte `io`-Ziele und geteilte FLOW. `satisfy` ist dagegen eine SPEZIFIKATIONS-Beziehung: zwei FUNCs, die dasselbe REQ erfuellen, koennen zur Laufzeit vollstaendig entkoppelt sein. Die Wirkung ging dabei nur in EINE Richtung -- `satisfy` verbindet Gruppen, senkte also LCOM4 und liess Module kohaesiver aussehen, als ihr Datenfluss hergibt. Der Modellierungsgrund wiegt schwerer als der Messgrund: teilen sich zwei Funktionen ein Requirement, ist das REQUIREMENT zu zerlegen, nicht die Kohaesionsmessung zu beschoenigen; RD-02 sagt bereits das Verwandte. Gemessen ueber 19 Familiengraphen: MT-02 23 -> 24 Befunde. `satisfy` hat also fast nie zwei Gruppen verbunden -- die Aenderung ist prinzipiell richtig und praktisch billig. Golden neu verankert OHNE Zahlenaenderung: Totals und Reihenfolge bleiben an allen drei Eingaengen gleich (ssot 99, n700 1475, n3500 17495), nur die MT-02-shas wandern, weil die Meldung den gemessenen LCOM4-Wert woertlich nennt. NOCH OFFEN aus CR-SM-297: die beiden Container-Masse fuer den zerlegten FUNC (Kohaesion analog MT-02, Paar-Kopplung analog CR-01) -- ihre Schwellen sind aus der Verteilung ABZULESEN, nicht zu setzen (Praezedenz CR-SM-283/BW-02). Prior: BREAKING (CR-SM-296): je Frage eine Regel, je Schwelle ein Wert. (1) RD-04 gibt das Bein `FUNC -allocate-> MOD` an R-04 ab. Es zaehlte die allozierten FUNCs je Modul -- also die MODULGROESSE, und die misst R-04 auch. Beide feuerten am selben Modul im selben Batch, mit zwei Schwellen aus zwei Policy-Feldern (RD-04 > 11 gegen R-04 > 12), und welche recht hat, stand nirgends; dieselbe Ursache ging doppelt in den readiness-Nenner. R-04 ist die reichere Aussage -- Groesse GEGEN Kreuzungen, drei Urteile -- und liest ueber moduleCrossings ohnehin dieselben Daten. RD-04 bleibt fuer ZERLEGUNGSBREITE zustaendig: FUNC compose FUNC, SYS/MOD compose MOD und der FUNC-Wurzelwald am SYS (CR-SM-282). (2) Beide Schwellen sind an die Doktrin 7+-2 gebunden statt danebengesetzt: decompositionBreadth.warning 11 -> 9 (die Obergrenze der Doktrin) und moduleSize large 12 -> 9, coupled 8 -> 7. CR-SM-282 hatte gegen die Doktrin-Zahl 5 entschieden, weil `info` bei 5 den readiness-Nenner verwaessert haette -- das Argument traegt fuer die OBERgrenze nicht, denn RD-04 meldet als warning. Wirkung ueber 19 Familiengraphen, gemessen: RD-04 17 -> 15, R-04 9 -> 11, Summe 26 -> 26. Die Zahl bleibt, die Zustaendigkeit wird eindeutig. Prior: BREAKING (CR-SM-295): drei Regeln entfallen, eine wird neu gefasst -- Katalog 66 -> 63. (1) R-14 (UC must have compose) kann NIE ALLEIN feuern: sie prueft nur, ob ein UC irgendeine ausgehende compose-Kante hat, ohne den Zieltyp anzusehen, waehrend UC-01 den REQ und UC-03 die FCHAIN einzeln verlangen. Auf ELEMENTEBENE ueber 19 Familiengraphen gemessen, nicht auf Summen: R-14 (2 Befunde) ist eine echte Teilmenge von UC-01 vereinigt UC-03 (23 Befunde). Ihr Fixhinweis ('Add FCHAIN or REQ') versprach zudem eine Pruefung, die der Code nicht macht. (2) FC-01 (FCHAIN has actor boundary) ist von FC-04 gedeckt -- gemessen FC-01 (5) Teilmenge von FC-04 (27); FC-04 verlangt Eingang UND Ausgang, FC-01 nur eine Beruehrung. Dazu trug FC-01 einen TOTEN Ersatzweg: `hasDirectActorIO` prueft, ob der Eltern-UC actor-adjazent ist, und `ACTOR -io-> UC` hat CR-SM-266 (D1/D4) aus TRACE_PATTERNS entfernt -- der Zweig konnte seit drei Versionen nicht mehr wahr werden. (3) CR-R04 entfaellt und CR-R01 wird NEU GEFASST. Beide zielten daneben: CR-R01 akzeptierte jede relation-Kante, also auch einen CR, der nur an einem Meilenstein haengt (Termin, kein Umfang) -- 5 Befunde bei 49 CRs, die ausschliesslich auf ein MS zeigen. CR-R04 verlangte einen FUNC und war damit zu eng: 21 Befunde, darunter Daten-, Doku- und Konfigurationsaenderungen, die zu Recht keine Funktion beruehren. Die neue Fassung fragt nach dem UMFANG: mindestens eine relation auf FUNC, MOD, SCHEMA, REQ oder UC. Grundgesamtheit woertlich die von CR-R04 (CR-SM-255, nur offene CRs) -- an einem geschlossenen CR ist die Frage Archaeologie. Gemessen an 514 CR-Knoten: 54 ohne Umfangsbezug, davon 0 OFFEN, 52 done, 2 ohne Status. Die Verschaerfung laesst die Historie damit unangetastet und wirkt ab dem naechsten CR. MS-03 (CR ohne MS) bleibt unberuehrt -- sie stellt die komplementaere Frage. (4) R-17 behaelt ihre Pruefung und verliert ihren falschen Fixhinweis, der drei Zieltypen nannte, die sie nie angesehen hat. Prior: BREAKING (CR-SM-294): sechs Regeln entfallen ersatzlos, Katalog 72 -> 66. AO-D01 (RelayNodeDetection) ist STRUKTURELL unmoeglich -- sie verlangt `>= 2` ausgehende `io`-Kanten auf andere FUNC, und `FUNC -io-> FUNC` steht nicht in TRACE_PATTERNS; R-18 lehnt es als error ab. Das ist zeichengleich der Fall, fuer den CR-SM-283 AO-D03 geloescht hat -- AO-D03 fiel, AO-D01 blieb stehen. CA-01, RT-01, PH-01, R-27 und R-03 sind GEGENSTANDSLOS: sie lesen `asil`, `attributes.kind === 'physical'`, `attributes.requires` und `attributes.capability`, und ueber 19 Familiengraphen ist jedes dieser Attribute 0-mal gesetzt (305 MOD, 696 FUNC). Beleg beidseitig: `report:silence` meldet alle sechs mit 0 Befunden ueber den gesamten Bestand, UND keine von ihnen hat eine Ausloese-Fixture in se-rule-trigger-proof.test.ts (CR-SM-285) -- also weder feuert sie irgendwo, noch ist belegt, dass sie ueberhaupt feuern KANN. Genau die Doppelbedingung, die Gate 7 fuer eine Streichung verlangt. NICHT gestrichen wurde CL-01, obwohl derselbe Verdacht bestand: sie feuert 2x in 1 von 19 Graphen. Ihre Aussage waere anderswo besser aufgehoben -- jeder UC muss hinreichend distinct sein, und das saehe man an der FCHAIN -- aber diese Regel EXISTIERT NICHT: CR-SM-283 hat AO-D03 geloescht und dabei festgehalten, dass ihre Aufgabe ('zwei Wirkketten, die auf einer Ebene mitgliedsgleich werden') ungeloest bleibt. CL-01 zu streichen hiesse, eine feuernde Pruefung gegen eine nicht existierende zu tauschen. Wirkung auf die Bestandsgraphen: 0 -> 0 Befunde, kein Graph faellt. Die einzige messbare Folge ist der kleinere NENNER jeder readiness-Dimension -- sechs stumme Regeln hoben bisher jeden Score. Prior: BREAKING (CR-SM-286): ND-01/ND-02 rechnen ihre Aehnlichkeit SELBST; setND01SimilarityMatrix/setND02SimilarityMatrix/getND02SimilarityMatrix entfallen ersatzlos. Die Formel stand als Prosa in contracts und als Code in graphcode/src/kernel/measure/nd-similarity.ts, verbunden durch eine Injektionsnaht. Vier gemessene Kosten: (1) FAIL-OPEN -- ohne Injektion gaben zwei `error`-Regeln [] zurueck, ununterscheidbar von 'keine Duplikate'; moneyflow trug 16 ND-01-Befunde, die ausserhalb graphcodes NIEMAND sah (report:silence, die Spike-Skripte, jedes kuenftige Werkzeug). Die Familie hat fuer 'kann nicht urteilen' eine Konvention (policy.X = null, moduleMetrics.instability = null); ND fiel statt dessen nach gruen. (2) PROZESSWEITER MODULZUSTAND -- `let _nd01Matrix` ist global, graphcode brauchte die Klammer withNDMatrices mit finally-Reset, jeder andere Aufrufer nicht: Graph A urteilte ueber Graph B. (3) AO-D01 liegt IM Gate-Katalog und uebersprang seine Overlap-Pruefung ohne Matrix ('no matrix -> assume pass') -- eine Gate-Regel, die sich stillschweigend abschwaecht. (4) BQ-04 haengt am selben Muster und ist komplett tot. Jetzt: similarity.ts, je Graph WeakMap-gecacht, Formeln und Schwelle 0,85 zeichengleich uebernommen -- dasselbe Urteil an der richtigen Stelle, Praezedenz CR-SM-276 (moduleCrossings). DABEI GEFUNDEN UND BEHOBEN: die portierte Formel las Partner ueber ALLE Traces; se-rule-pair-legality (Pruefung A, CR-SM-278) meldete 35 Faelle, in denen ND-02 erst durch eine grammatikwidrige Kante auf SCHEMA anschlug (ACTOR -compose-> SCHEMA und Verwandte). `partners()` filtert jetzt ueber isValidTrace -- eine Regel darf ihr Verdikt nicht an einer Kante haengen, die R-18 als error ablehnt. Golden bewusst neu verankert und im Test-Header aufgeschluesselt: ssot 98 -> 98 UNVERAENDERT, n700 1038 -> 1478, n3500 5176 -> 17496, der gesamte Zuwachs ist ND-01 auf den synthetischen Eingaengen -- buildFixedSizeGraph KOPIERT den Basisgraphen 11x bzw. 56x, dort hat jedes Element 10 bzw. 55 exakte Duplikate; eine Duplikatsregel MUSS das melden. ND-02 bleibt ueberall 0. BQ-04 ausdruecklich NICHT angeschlossen: es verlangt 'pre-computed EMBEDDING similarity', und Embeddings kann ein reines Vertragspaket nicht rechnen; ein Ersatzmass haette ich erfinden muessen, und der Versuch (0.7*Beschreibung + 0.3*Name) lieferte 0 Befunde an allen neun Familiengraphen und 4950 an einer templatierten Fixture -- beide Gate-7-Ausreisser zugleich. BQ-04 ist damit der naechste AO-D03-Fall und ein eigener se-grammar-review-Vorgang. Prior: BREAKING (CR-SM-285): R-12 findet Zyklen JEDER Laenge statt nur Rueckkanten. Die Regel heisst 'No circular dependencies' und prueft bis hierher nur, ob eine Kante DIREKT zurueckliuft (A->B und B->A). Ein Dreierzyklus A->B->C->A ging vollstaendig durch das Gate, mit null Fehlern. Das ist die gefaehrlichere Haelfte der Fehlerklasse dieser Session: ein Absturz meldet sich, eine Pruefung die ihren Gegenstand nicht erreicht BESTAETIGT. Warum es zaehlt: jeder Rollup der Familie setzt Azyklizitaet voraus -- chainOf/funcChainOf (CR-SM-282/-283) tragen seen-Guards gegen genau diese Endlosschleife, moduleMetrics, die RD-04-Breitenzaehlung und gves Container-Sicht ebenso; unter einem Zyklus ist ein Rollup nicht falsch sondern UNDEFINIERT. Das R-18-Bein aus CR-SM-283 deckt es NICHT: bei A->B->C->A hat jeder Knoten genau EINEN compose-Elternteil, die Baum-Eigenschaft ist formal erfuellt und die Struktur trotzdem kein Baum. Gemessen ueber NEUN Familiengraphen: 0 Zyklen jeder Laenge, also 0 -> 0 Befunde -- die Schaerfung laesst keinen bestehenden Graphen fallen. Major nach Gate 8 trotzdem, weil eine verschaerfte Regel fremde Graphen fallen lassen KANN. EIN Befund je Zyklus statt je Kante (Klasse CR-SM-242), verankert am kleinsten Knoten des Rings und damit unabhaengig vom DFS-Einstieg (Gate 5); die Zweierring-Meldung ist woertlich unveraendert. Dazu zwei Haertungen OHNE Regelwirkung: descriptionOf() gab `el.description ?? ''` und starb an jedem Nicht-String (42/{}/[]/true -> '.trim is not a function', vier von fuenf Typen) -- am Gate hiess das Stacktrace statt block-Verdict; jetzt zaehlt nur ein echter String, alles andere ist 'keine Beschreibung'. Und tests/unit/se-rule-trigger-proof.test.ts ist die Umkehrung von report:silence: je Regel ein Graph der sie feuern LASSEN MUSS, 22 belegt, 50 als unbelegt ausgewiesen mit einer Zahl die nur sinken darf. Beim ersten Lauf fand er sofort einen Fall: ND-01/ND-02 sind `error` und beginnen mit `if (!_nd01Matrix) return []` -- ohne injizierte Aehnlichkeitsmatrix fallen sie nach GRUEN, ununterscheidbar von 'keine Duplikate'. Dokumentiert und eingefroren, nicht behoben: die Severity-/Signalfrage ist ein eigener se-grammar-review-Vorgang. Prior: BREAKING (CR-SM-283): AO-D03 entfaellt ERSATZLOS, BW-02 kommt -- Katalog 74 -> 74, kein Wachstum im Komplexitaetsbudget. (1) AO-D03 (`DuplicatePathDetection`) suchte `FUNC -io-> FUNC`, ein Paar das TRACE_PATTERNS nicht kennt und das R-18 als error ablehnt: die Regel konnte STRUKTURELL nicht feuern und hat es an keinem der fuenf Familiengraphen je getan. Eine Regel die nie feuert kostet trotzdem Nenner-Anteil, Katalogzeile, readiness-Zuordnung, Golden-File-Eintrag und einen Leser der sie unterscheiden muss. Die Aufgabe die sie tragen SOLLTE -- zwei Wirkketten die auf einer Ebene mitgliedsgleich werden -- bleibt ungeloest und ist ein eigener CR; BW-02 ersetzt sie nicht. (2) BW-02 misst die Randbreite der FUNC-WHITEBOX: verschiedene SCHEMA-Vertraege auf `FUNC -io-> FLOW -io-> FUNC` mit einem Endpunkt im compose-Teilbaum und einem ausserhalb. Gerechnet im selben Durchlauf wie `byModule` (module-crossings.ts), damit es EINE Definition von Rand gibt. Der Rollup ist hier nicht Genauigkeit sondern Existenz: ein zerlegter FUNC traegt selbst keine io-Kanten (die liegen an seinen Blaettern), ohne Teilbaum-Aufloesung waere die Zahl strukturell immer 0 -- gemessen FUNC-block-grounding 0 -> 19. Gate 6 des Grammatik-Reviews: 12 Befunde an graphcode, davon 0 in Ueberlappung mit irgendeiner FUNC-Regel (AO-D01 misst Relay-Knoten, IO-01 Kettenkohaerenz, R-31 blosse Verdrahtung). Schwelle 5 aus der Verteilung ABGELESEN, nicht gewaehlt: bok und graph-view-edit enden beide bei 3, graphcode geht bis 19, moneyflow bis 17. Kein info-Level -- bei Schwelle 3 truege bok 4 Befunde auf 9 Blackboxes, und readiness zaehlt info ungefiltert in den Nenner (MT-03-Praezedenz). (3) VIERTES BEIN von R-18: ein Element hat hoechstens einen compose-Elternteil (FUNC/FUNC und MOD/MOD), error. TRACE_PATTERNS begrenzt heute nur die KINDER einer Kante, nie die ELTERN -- graph_mutate legt eine zweite compose-Kante anstandslos an. 0 Verstoesse ueber fuenf Graphen, und trotzdem hart, weil jeder Rollup der Familie unter Verletzung nicht falsch sondern UNDEFINIERT ist (byModule/byFunc, RD-04-Breite, die Container-Sicht von gve). Keine eigene Regel-ID, Praezedenz CR-SM-271 Teil 2: dieselbe Aussage, derselbe naechste Schritt. EIN Befund je KIND, nicht je ueberzaehliger Kante. (4) `policy.boundaryWidth` neu (nullable, Default {warning: 5}) -- fuer Konstruierer einer MetricPolicy breaking. Prior: BREAKING (CR-SM-282): zwei Blackbox-Luecken geschlossen, und beide lassen bestehende Graphen fallen. (1) RD-04 zaehlt den FUNC-WURZELWALD, verankert am SYS-Knoten. Die Regel zaehlt Kinder je Parent; die Wurzeln des `FUNC -compose-> FUNC`-Waldes haben keinen, also war die oberste Funktionsebene unsichtbar -- gemessen: moneyflow hat 306 Wurzel-FUNCs und RD-04 meldete 0. Die MOD-Seite hatte das Problem nie, weil `SYS -compose-> MOD` eine echte Kante ist; die FUNC-Seite hat keine (`se:top-level`: der obere FUNC-Satz ist eine PROJEKTION, keine Kante). Anker ist der einzige SYS-Knoten; gibt es nicht genau einen, schweigt der Zweig statt zu raten (R-17 deckt den Fall). (2) `moduleCrossings.byModule` und die Groesse in R-04 lesen jetzt die BESITZKETTE (direkt alloziertes MOD + compose-Vorfahren) statt nur der direkten Allokation. Ein Eltern-MOD hat keine direkt allozierten FUNCs -- es kam in `byModule` nicht vor, `funcCount` war 0, und R-04 schwieg per `funcCount <= coupled` an genau der Whitebox, in die der Leser hineinklickt. `pairs` (CR-01) bleibt BEWUSST auf der direkten Zuordnung: 'wie stark haengen DIESE zwei Module aneinander' ist eine Blattebenen-Frage, ein Elternpaar meldete dieselbe Kopplung ein zweites Mal -- CR-01 ist an allen fuenf Familiengraphen zeichengleich geblieben (5/3/15/0/63). Delta gesamt: moneyflow RD-04 +1, sirail R-04 +1, bok/gve/graphcode unveraendert. (3) Die Schwelle steht in `policy.decompositionBreadth` statt als `DECOMPOSITION_BREADTH_MAX = 11` inline in rules.ts -- dieselbe Klasse, die CR-SM-236 fuer R-04 aufgeloest hat; `null` schaltet die Regel ab, Default 11 = verhaltensgleich. Das neue Pflichtfeld in MetricPolicy ist fuer Konstruierer breaking. Die Doktrin-Zahl 5 (`se:top-level`) ist bewusst NICHT der Default: `info` bei 5 haette an graphcode 20 zusaetzliche Befunde erzeugt, und readiness zaehlt `info` ungefiltert in den Nenner (`score = 1 - Verstoesse / applicable`) -- genau der Grund, aus dem MT-03 zur Messung statt zur Regel wurde (readiness.ts). Wer doktrin-streng fahren will, setzt 5. Prior: BREAKING (CR-SM-271 Teil 2): SC-04 entfaellt ersatzlos. Ein FLOW ohne SCHEMA ist kein schwaecherer Typ, sondern untypisiert — die Untergrenze ist Grammatik geworden (`FLOW -relation-> SCHEMA [1..1]`, META_MODEL 5.0.0) und meldet als drittes Bein von R-18 (error, mit candidate_targets im Kontext wie zuvor SC-04). Kein Regel-Loch: derselbe Sachverhalt, EIN Befund, nur haerter und am Gate durchgesetzt statt nur gemeldet. Der Katalog schrumpft um eine Zeile (Nenner, readiness-Zuordnung 'schema'/CDR und Golden-File-Eintrag fallen mit); der Fall zaehlt jetzt unter R-18/'arch'/PDR.
9
9
  /** Meta-model version (trace pattern constraints + format-e parser). */
10
10
  export const META_MODEL_VERSION = '5.0.0'; // BREAKING (CR-SM-271 Teil 2): `FLOW -relation-> SCHEMA` wird `1..1` — die erste DURCHGESETZTE Untergrenze (minOccurs/REQUIRED_PATTERNS; nur exakt-`1`, die `1..*`-compose-Untergrenzen bleiben Vollstaendigkeits-Warnungen ihrer Regeln). Migrationsbestand vorab abgetragen: graphcode 2, sigloch-modules 4, graph-view-edit 5 (je durchs Gate), urbanmobility-Fixture geloescht (39, Spike ohne Referenzen); sirail 4 offen (EXPORT_PENDING-Divergenz seit 2026-08-18).
11
11
  export * from './ontology.js';
@@ -33,6 +33,12 @@ export * from './rule-help.js';
33
33
  * parallele Pfad, gegen den `module-crossings.ts` gebaut wurde — sie ist ab jetzt erreichbar.
34
34
  */
35
35
  export * from './module-crossings.js';
36
+ /**
37
+ * CR-SM-314: Kritikalitaet je FUNC (Ketten + Use Cases). Wie `module-crossings` eine reine
38
+ * Rechnung ohne Regelcode — deshalb darf `rules.ts` sie importieren (R-21s Infrastruktur-
39
+ * Ausnahme, CR-SM-313) und es gibt nur EINEN Kettenbegriff im Paket.
40
+ */
41
+ export * from './function-criticality.js';
36
42
  export * from './quality-rules.js';
37
43
  export * from './analysis-freshness-rules.js';
38
44
  export * from './evaluate-all.js';
@@ -25,11 +25,15 @@ export declare const MetricPolicySchema: z.ZodObject<{
25
25
  warning: z.ZodNumber;
26
26
  }, z.core.$strip>>;
27
27
  decompositionBreadth: z.ZodNullable<z.ZodObject<{
28
+ min: z.ZodNumber;
28
29
  warning: z.ZodNumber;
29
30
  }, z.core.$strip>>;
30
31
  boundaryWidth: z.ZodNullable<z.ZodObject<{
31
32
  warning: z.ZodNumber;
32
33
  }, z.core.$strip>>;
34
+ criticality: z.ZodNullable<z.ZodObject<{
35
+ infrastructure: z.ZodNumber;
36
+ }, z.core.$strip>>;
33
37
  riskRpn: z.ZodNullable<z.ZodNumber>;
34
38
  apTable: z.ZodNullable<z.ZodObject<{
35
39
  severityBands: z.ZodArray<z.ZodObject<{
@@ -50,11 +54,6 @@ export declare const MetricPolicySchema: z.ZodObject<{
50
54
  Low: "Low";
51
55
  }>>>>;
52
56
  }, z.core.$strip>>;
53
- moduleSize: z.ZodNullable<z.ZodObject<{
54
- large: z.ZodNumber;
55
- coupled: z.ZodNumber;
56
- crossings: z.ZodNumber;
57
- }, z.core.$strip>>;
58
57
  }, z.core.$strip>;
59
58
  export type MetricPolicy = z.infer<typeof MetricPolicySchema>;
60
59
  /**
package/dist/se/policy.js CHANGED
@@ -45,8 +45,20 @@ export const MetricPolicySchema = z.object({
45
45
  * graphcode-Selbstmodell 20 zusaetzliche Befunde erzeugt, und `readiness` zaehlt `info`
46
46
  * ungefiltert in den Nenner (`score = 1 - Verstoesse / applicable`) — genau der Grund, aus dem
47
47
  * MT-03 zur Messung statt zur Regel wurde. Wer doktrin-streng fahren will, setzt 5.
48
+ *
49
+ * CR-SM-311: die Breite ist ein BAND. `min` ist die Untergrenze (Z3, ITEM-2026-053): eine Ebene
50
+ * mit weniger Elementen abstrahiert nichts. Beide Grenzen sind Pflicht, beide exklusiv — `min`
51
+ * und `warning` selbst liegen im Band.
48
52
  */
49
- decompositionBreadth: z.object({ warning: z.number().int().min(2) }).nullable(),
53
+ decompositionBreadth: z
54
+ .object({
55
+ /** Unter `min` Kindern ist eine Ebene entartet. */
56
+ min: z.number().int().min(1),
57
+ /** Ueber `warning` Kindern ist eine Ebene zu breit. */
58
+ warning: z.number().int().min(2),
59
+ })
60
+ .refine((b) => b.min <= b.warning, { message: 'decompositionBreadth.min must not exceed decompositionBreadth.warning' })
61
+ .nullable(),
50
62
  /**
51
63
  * CR-SM-283: Randbreite je Blackbox (BW-02) — verschiedene Vertraege, die den Rand queren.
52
64
  * Bewusst OHNE `info`-Stufe: die Verteilung gibt sie nicht her. bok und graph-view-edit enden
@@ -55,6 +67,26 @@ export const MetricPolicySchema = z.object({
55
67
  * aus dem MT-03 zur Messung statt zur Regel wurde. `null` -> messen, nicht urteilen.
56
68
  */
57
69
  boundaryWidth: z.object({ warning: z.number().int().min(1) }).nullable(),
70
+ /**
71
+ * CR-SM-314: ab wie vielen Wirkketten gilt eine FUNC als INFRASTRUKTUR.
72
+ *
73
+ * Die Zahl selbst (`functionCriticality`) urteilt nie — sie steht in jeder FUNC-Zeile. Diese
74
+ * Schwelle sagt nur, wo eine Regel sie als Ausnahme lesen darf; erster Abnehmer ist R-21
75
+ * (CR-SM-313): eine Uebergabe an eine Funktion, die quer durch alle Ketten laeuft, ist kein
76
+ * fehlender Integrationstest, sondern ein Bus.
77
+ *
78
+ * ABGELESEN, nicht gesetzt — aber nicht an der Verteilungsform. Die ist ein duenner Schwanz
79
+ * (17 Familiengraphen: 224 FUNCs in 1 Kette, 37 in 2, 6 in 3, 8 in 4, je 1 in 5/6/11) und gibt
80
+ * wie bei `instability` keine Schwelle her (CR-SM-293). Ablesbar ist die WIRKUNG: auf
81
+ * graphcodes 31 kettenlose Uebergaben stellen die Schwellen 3 und 4 identisch 19 frei, 5 und 6
82
+ * identisch 6. Ein Plateau von 3 bis 4 — innerhalb dessen die Wahl wirkungsfrei ist. Der
83
+ * Startwert ist seine Untergrenze.
84
+ *
85
+ * `min(2)`, nicht `min(1)`: bei 1 waere JEDE FUNC in einer Kette Infrastruktur, und die
86
+ * Ausnahme fraesse ihre eigene Regel. `null` -> keine FUNC gilt als Infrastruktur; die Zahl
87
+ * bleibt trotzdem gemessen.
88
+ */
89
+ criticality: z.object({ infrastructure: z.number().int().min(2) }).nullable(),
58
90
  /**
59
91
  * FM-03: RPN (severity · occurrence · detection), ab dem ein Risiko-REQ eine bestandene
60
92
  * Verifikation braucht (exklusiv). null → FM-03 feuert nie.
@@ -75,23 +107,6 @@ export const MetricPolicySchema = z.object({
75
107
  * exportierten Graphen steht sie nie** (Zellwerte sind normative Wertung, keine Fakten).
76
108
  */
77
109
  apTable: ApTableSchema.nullable(),
78
- /**
79
- * R-04: Modulgröße **gegen** Kreuzungen — die Regel wägt beides ab, ihr alter Name
80
- * („Max module size") gab das nicht her. null → R-04 feuert nie.
81
- *
82
- * Drei Werte, weil die Regel drei Urteile fällt. Ein einzelner Schwellwert hätte zwei davon
83
- * im Code gelassen — genau der Zustand, den dieser CR beseitigt.
84
- */
85
- moduleSize: z
86
- .object({
87
- /** > `large` FUNCs: mit Kreuzungen warning, ohne Kreuzungen info („kohäsiv, nur groß"). */
88
- large: z.number().int().min(1),
89
- /** > `coupled` FUNCs ist die Untergrenze, ab der die Regel überhaupt hinsieht. */
90
- coupled: z.number().int().min(1),
91
- /** Kreuzungen, ab denen ein nur `coupled`-großes Modul warnt (exklusiv). */
92
- crossings: z.number().int().min(0),
93
- })
94
- .nullable(),
95
110
  });
96
111
  /**
97
112
  * Der Startwert, sichtbar als solcher — **kein** Fallback in der Regel.
@@ -133,17 +148,13 @@ export const DEFAULT_METRIC_POLICY = {
133
148
  // weil `info` bei 5 den readiness-Nenner verwaessert haette; das Argument traegt fuer die
134
149
  // OBERgrenze nicht, denn RD-04 meldet als `warning`. Die Zahl steht jetzt an EINER Stelle und
135
150
  // gilt fuer beide Zerlegungsfragen -- Sub-FUNC und Sub-MOD.
136
- decompositionBreadth: { warning: 9 },
151
+ // CR-SM-311: Untergrenze 3 eine Ebene mit weniger Elementen ist entartet (Z3).
152
+ decompositionBreadth: { min: 3, warning: 9 },
137
153
  // CR-SM-283: aus der Verteilung abgelesen — bok/gve max 3, graphcode 19, moneyflow 17.
138
154
  boundaryWidth: { warning: 5 },
155
+ // CR-SM-314: Untergrenze des gemessenen Plateaus 3..4 (siehe Feldkommentar oben).
156
+ criticality: { infrastructure: 3 },
139
157
  riskRpn: 100,
140
- // `large`/`coupled`/`crossings` = die drei Literale aus `maxModuleSize`: vorher
141
- // `funcCount <= 8` → skip, `funcCount > 12` → groß, `crossings > 2` → gekoppelt.
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 },
147
158
  // CR-SM-229: kein Startwert möglich und keiner gewollt — die Tabelle steht nur im Handbook.
148
159
  // `null` heißt hier nicht „aus", sondern „markierter Übergang": bestätigte AP-Invarianten,
149
160
  // sonst RPN-Bänder. `apMethod()` macht das für den Leser sichtbar.
@@ -34,7 +34,6 @@ export declare const ReadinessScore: z.ZodObject<{
34
34
  violations: z.ZodNumber;
35
35
  applicable: z.ZodNumber;
36
36
  coreApplicable: z.ZodNumber;
37
- ready: z.ZodBoolean;
38
37
  }, z.core.$strip>;
39
38
  export type ReadinessScoreType = z.infer<typeof ReadinessScore>;
40
39
  /**
@@ -62,7 +61,6 @@ export declare const ReadinessReport: z.ZodObject<{
62
61
  violations: z.ZodNumber;
63
62
  applicable: z.ZodNumber;
64
63
  coreApplicable: z.ZodNumber;
65
- ready: z.ZodBoolean;
66
64
  }, z.core.$strip>>;
67
65
  timestamp: z.ZodISODateTime;
68
66
  }, z.core.$strip>;
@@ -44,10 +44,9 @@ export const ReadinessScore = z.object({
44
44
  * Unterscheidung, an der der dokumentierte Ausweg aus CR-SM-237 gescheitert ist.
45
45
  */
46
46
  coreApplicable: z.number().int(),
47
- // CR-SM-235: `score >= readyThreshold`. Die Schwelle steht bewusst NICHT hier sie ist
48
- // Eingabe von `computeReadiness`, und eine Zahl im Kommentar wäre die dritte Meinung dazu.
49
- // CR-SM-270: bei `score === null` ist `ready` immer `false` nicht messbar ist nicht bereit.
50
- ready: z.boolean(),
47
+ // CR-SM-310: kein `ready` mehr. Ob eine Dimension zu schwach ist, urteilt der Konsument mit
48
+ // SEINER Schwelle (graphcode: generate.ts, focusThreshold aus der Config). Die Messung traegt
49
+ // kein Urteil vorher stand es zweimal: hier als `ready` und beim Konsumenten.
51
50
  });
52
51
  /**
53
52
  * CR-SM-237: `overallScore` ist weg. Es war das **ungewichtete** Mittel der acht
@@ -96,6 +95,8 @@ export const RULE_TO_DIMENSION = {
96
95
  'RD-01': 'req', 'RD-02': 'req', 'RD-03': 'req',
97
96
  // RD-04 is decomposition *breadth* — an architecture concern, not a requirement one
98
97
  'RD-04': 'arch',
98
+ // CR-SM-311: die Untergrenze derselben Breite — ebenfalls Architektur
99
+ 'RD-05': 'arch',
99
100
  // trace/realization/allocation completeness rules (CR-228 D: previously unmapped → advisory fall-through)
100
101
  'R-18': 'arch', 'R-19': 'ver', 'R-20': 'arch', 'R-21': 'ver',
101
102
  // CR-SM-231: R-29 (Testdatei-Exklusivitaet) gehoert zu 'ver' wie R-19 — beide bewerten die
@@ -149,6 +150,7 @@ export const RULE_TO_DIMENSION = {
149
150
  'NFR-01': 'arch',
150
151
  // cross-module IO (CR-192)
151
152
  'IO-01': 'arch',
153
+ 'IO-02': 'arch', // CR-SM-307: Flusstopologie, dieselbe Dimension wie R-10/IO-01.
152
154
  // view rules (CR-184)
153
155
  'VR-01': 'ver',
154
156
  'CL-01': 'uc',
@@ -189,7 +191,7 @@ export const RULE_TO_PHASE = {
189
191
  'CR-R01': 'SRR', 'CR-R03': 'SRR',
190
192
  'MS-01': 'SRR', 'MS-02': 'SRR', 'MS-03': 'SRR',
191
193
  // PDR — architecture/functional completeness.
192
- 'RD-04': 'PDR',
194
+ 'RD-04': 'PDR', 'RD-05': 'PDR',
193
195
  'R-02': 'PDR', 'R-08': 'PDR', 'R-10': 'PDR', 'R-12': 'PDR', 'R-18': 'PDR',
194
196
  'R-04': 'PDR', 'R-22': 'PDR', 'R-23': 'PDR',
195
197
  'R-15': 'PDR',
@@ -199,7 +201,7 @@ export const RULE_TO_PHASE = {
199
201
  'R-30': 'PDR', 'R-31': 'PDR',
200
202
  'MT-01': 'PDR', 'MT-02': 'PDR',
201
203
  'ND-01': 'PDR',
202
- 'BW-02': 'PDR', 'CR-01': 'PDR', 'IO-01': 'PDR', // CR-SM-226: extended to all FCHAIN FUNC-pairs, added to the phase axis.
204
+ 'BW-02': 'PDR', 'CR-01': 'PDR', 'IO-01': 'PDR', 'IO-02': 'PDR', // CR-SM-226: extended to all FCHAIN FUNC-pairs, added to the phase axis.
203
205
  // CDR — critical design/schema completeness.
204
206
  'R-26': 'CDR', 'SC-02': 'CDR', // SC-04 entfiel mit CR-SM-271 — der FLOW-ohne-SCHEMA-Fall haengt an R-18/PDR
205
207
  'ND-02': 'CDR',
@@ -147,6 +147,10 @@ export const RULE_HELP = {
147
147
  plain: "Two steps in the same sequence have no described data passing between them → add the data one hands to the other.",
148
148
  se: "A `FUNC` pair inside one `FCHAIN` with no `io` path (`FUNC -io-> FLOW -io-> FUNC`) connecting them.",
149
149
  },
150
+ 'IO-02': {
151
+ plain: "Two different places write into the same flow, so a reader cannot tell which version arrives \u2192 give each source its own flow; they may keep sharing one contract.",
152
+ se: "`FLOW` with more than one producer (`FUNC`/`ACTOR -io-> FLOW`). R-10 checks the LOWER bound of the same axis (at least one producer, at least one consumer); the two can never fire on the same FLOW, so there is no double count. Only the producer side is bounded: several consumers are the normal shape of a shared contract \u2014 one source, many readers, which is exactly how a central configuration looks. One finding per FLOW, `value` = number of producers, `threshold` 1. Severity `error` (hygiene) and deliberately NOT in STEER_RULES — a decision, not a measured conflict: a split flow keeps its `SCHEMA`, so BW-02 (distinct contracts per boundary) does not rise. Whether a gate blocks a new finding is the consumer's gating choice, not part of the rule.",
153
+ },
150
154
  'MS-01': {
151
155
  plain: "A milestone has no work assigned to it → assign the work items that belong to it.",
152
156
  se: "`MS` with no `CR` `relation`.",
@@ -190,8 +194,8 @@ export const RULE_HELP = {
190
194
  se: "`FUNC` with no `satisfy` trace to a `REQ` (design→requirement traceability).",
191
195
  },
192
196
  'R-04': {
193
- plain: "A module does too much or is too tangledopen it and split it.",
194
- se: "`MOD` with >12 `FUNC`, or 8–12 `FUNC` with >2 flows crossing the module boundary (cohesion/coupling).",
197
+ plain: "Too many different data formats cross this module's boundarymerge formats, or move the functions that carry them.",
198
+ se: "`MOD` whose boundary is crossed by `metricPolicy.boundaryWidth.warning` or more DISTINCT contracts (`SCHEMA` on the io path `FUNC` ─io→ `FLOW` ─io→ `FUNC` across the boundary, union over all neighbours — CR-SM-276). Same question and same threshold as BW-02 on the FUNC tree (CR-SM-312); module SIZE is RD-04's question since CR-SM-311.",
195
199
  prompt: "se-view:arch",
196
200
  },
197
201
  'R-05': {
@@ -235,8 +239,8 @@ export const RULE_HELP = {
235
239
  se: "Realized `FUNC` with no valid `realRef` `{file, symbol}` (graph↔code binding); else `concept:true` / `external:true`.",
236
240
  },
237
241
  'R-21': {
238
- plain: "You grouped functions into a chain that passes data along, but nothing tests that hand-off → add an integration test that checks the chain works.",
239
- se: "FUNC↔FUNC connection (`FUNC` ─io→ `FLOW` ─io→ `FUNC`) whose endpoints DO share an `FCHAIN`, but no shared chain carries a verified integration test (`TEST` ─verify→ `REQ` ←satisfy─ `FCHAIN`). Pairs sharing no `FCHAIN` are silent: co-adjacency at a reused `FLOW` is not an asserted interface.",
242
+ plain: "Two functions hand data over, but they are not in one chain together or they are, and nothing tests the hand-off → put both into the same chain, and give that chain an integration test.",
243
+ se: "FUNC↔FUNC handover (`FUNC` ─io→ `FLOW` ─io→ `FUNC`), two findings. (a) endpoints share NO `FCHAIN` → finding at the PRODUCER: no integration scope is declared. (b) endpoints share one, but no shared chain carries a verified integration test (`TEST` ─verify→ `REQ` ←satisfy─ `FCHAIN`) → finding at the chain. SILENT when either endpoint is in no chain at all (that is R-30's statement) and when either is infrastructure (`chains >= policy.criticality.infrastructure`): a handover into a function that runs through every chain is a bus, not a missing test.",
240
244
  },
241
245
  'R-22': {
242
246
  plain: "A function isn't assigned to any building block, so it has no home in the structure → put it on a module.",
@@ -301,7 +305,11 @@ export const RULE_HELP = {
301
305
  },
302
306
  'RD-04': {
303
307
  plain: "One thing has more than 9 parts directly under it → group them, so each level stays readable.",
304
- se: "Decomposition breadth above `metricPolicy.decompositionBreadth.warning` children on one level — `FUNC` `compose` `FUNC`, `SYS`/`MOD` `compose` `MOD`, and the root `FUNC` forest anchored at `SYS` (CR-SM-282). Default 9, the upper end of 7±2 (CR-SM-296). The `FUNC` `allocate` `MOD` leg moved to R-04 in that CR: counting allocated FUNCs is module SIZE, and one question deserves one rule.",
308
+ se: "Decomposition breadth above `metricPolicy.decompositionBreadth.warning` children on one level — `FUNC` `compose` `FUNC`, `SYS` `compose` `MOD`, a `MOD`'s sub-`MOD`s plus its own leaf `FUNC` (`allocate`), and the root `FUNC` forest anchored at `SYS` (CR-SM-282). Default 9, the upper end of 7±2 (CR-SM-296). CR-SM-311: a module level counts its own functions again breadth, not size; module coupling is R-04. The lower bound is RD-05.",
309
+ },
310
+ 'RD-05': {
311
+ plain: "A level has only one or two parts under it → it groups nothing; fold it into the level above, or gather the parts that belong together under it.",
312
+ se: "Decomposition breadth below `metricPolicy.decompositionBreadth.min` (default 3, exclusive) on one level, counted exactly like RD-04 (CR-SM-311, Z3). A level abstracts nothing below it. The finding carries no `value`/`threshold` and does not enter the steering score: it is readability, not boundary width. A level with no children at all is not this rule (empty MOD: R-23; FUNC without children is a leaf).",
305
313
  },
306
314
  'SC-02': {
307
315
  plain: "A data format is defined but nothing uses it → connect it to the data it describes, or drop it.",
package/dist/se/rules.js CHANGED
@@ -7,7 +7,8 @@ import { z } from 'zod/v4';
7
7
  import { ElementType, TraceType, TestRefsSchema, RealRefSchema } from './ontology.js';
8
8
  import { traceRejection, BOUNDED_PATTERNS, REQUIRED_PATTERNS, maxOccurs } from './meta-model.js';
9
9
  import { indexOf } from './graph-index.js';
10
- import { crossingContractCount, subtreeFuncs } from './module-crossings.js';
10
+ import { moduleCrossings } from './module-crossings.js';
11
+ import { functionCriticality } from './function-criticality.js';
11
12
  export const RuleSeverity = z.enum(['error', 'warning', 'info']);
12
13
  /** Candidate target for resolving a violation (e.g. a REQ to satisfy, a TEST to link). */
13
14
  export const ViolationCandidate = z.object({
@@ -233,74 +234,45 @@ function funcMustSatisfyReq(graph) {
233
234
  // R-03: ASIL isolation
234
235
  // ---------------------------------------------------------------------------
235
236
  // ---------------------------------------------------------------------------
236
- // R-04: Modulgroesse GEGEN Kreuzungen (nicht „max module size")
237
+ // R-04: Randbreite des Moduls (CR-SM-312)
237
238
  //
238
- // CR-SM-236: die Regel waegt Groesse gegen kreuzende io-Flows ab ein grosses Modul ohne
239
- // Kreuzungen ist info („kohaesiv, nur gross"), dasselbe Modul mit Kreuzungen ist warning.
240
- // Der alte Name gab das nicht her, und die drei Schwellen (8/12/2) standen inline: eine
241
- // Urteilsschwelle, die sich weder per grep noch aus dem Regelnamen ablesen laesst, ist nicht
242
- // ueberpruefbar. Jetzt sind es `policy.moduleSize.{coupled,large,crossings}`.
243
- // `null` → messen, nicht urteilen: die Regel schweigt.
239
+ // Die Groessenfrage ist mit CR-SM-311 an RD-04 gegangen: eine Modul-Ebene zeigt Untermodule UND
240
+ // eigene Funktionen, das IST ihre Breite. Was bleibt, ist die Kopplung und die misst diese Regel
241
+ // jetzt allein: die VERSCHIEDENEN Vertraege, die den Modulrand queren (`moduleCrossings.byModule`,
242
+ // dieselbe Zaehlung wie CR-01 und BW-02, CR-SM-276).
244
243
  //
245
- // CR-SM-276: „Kreuzung" heisst hier dasselbe wie bei CR-01 ein Vertrag auf dem io-Pfad
246
- // `FUNC -io-> FLOW -io-> FUNC` UEBER die Modulgrenze (`module-crossings.ts`). Vorher zaehlte
247
- // die Regel jede io-Kante einer Modul-FUNC „mit einem Endpunkt ausserhalb der FUNC-Menge" —
248
- // FLOWs liegen nie in dieser Menge, also zaehlte auch jeder modulINTERNE Fluss mit. Das war der
249
- // io-Grad des Moduls und fuer jedes Modul mit Datenfluss trivial > `crossings`; damit war R-04
250
- // faktisch wieder „max module size", der Name, den CR-SM-236 gerade verworfen hatte.
251
- // ---------------------------------------------------------------------------
252
- function maxModuleSize(graph, policy) {
244
+ // Dieselbe Frage wie BW-02 auf dem zweiten Baum, also dieselbe Schwelle: `policy.boundaryWidth`.
245
+ // Damit faellt `policy.moduleSize` ersatzlos drei Zahlen fuer eine Frage, die es nicht mehr gibt.
246
+ //
247
+ // Das schliesst die Blindstelle, die `se-engine/steer.ts` seit CR-SM-292 benennt: ein KLEINES
248
+ // Modul mit breitem Rand war stumm, weil R-04 den Rand nur zusammen mit der Groesse ansah
249
+ // (gemessen: MOD-kernel-measure 9 Vertraege bei 7 Funktionen). Seit diesem CR steuert R-04 mit
250
+ // (`STEER_RULES`), denn der Befund ist lokal, gerichtet und budgetiert.
251
+ // ---------------------------------------------------------------------------
252
+ function moduleBoundaryWidth(graph, policy) {
253
+ const steps = policy.boundaryWidth;
254
+ if (steps === null)
255
+ return [];
253
256
  const idx = indexOf(graph);
254
257
  const violations = [];
255
- const steps = policy.moduleSize;
256
- if (steps === null)
257
- return violations;
258
- const modules = idx.elementsOfType('MOD');
259
- for (const mod of modules) {
260
- // CR-SM-282: die Groesse ist die des TEILBAUMS, nicht der direkten Allokation. Ein
261
- // Eltern-MOD (`MOD -compose-> MOD`) hat keine direkt allozierten FUNCs; `funcCount`
262
- // war dort 0 und die Regel schwieg an genau der Whitebox, in die man hineinklickt.
263
- // Fuer ein Blatt-MOD ist die Menge zeichengleich mit der bisherigen (nur `FUNC
264
- // -allocate-> MOD` existiert als Pattern) — Regressions-Invariante.
265
- const allocatedIds = [...subtreeFuncs(graph, mod.id)];
266
- const allocated = allocatedIds.map(id => idx.byId.get(id)).filter((e) => !!e);
267
- const funcCount = allocated.length;
268
- if (funcCount <= steps.coupled)
258
+ for (const [modId, contracts] of moduleCrossings(graph).byModule) {
259
+ if (contracts.size < steps.warning)
269
260
  continue;
270
- // Verschiedene Vertraege (SCHEMA) auf io-Pfaden ueber DIESEN Modulrand — CR-SM-276.
271
- const crossings = crossingContractCount(graph, mod.id);
272
- if (funcCount > steps.large && crossings === 0) {
273
- violations.push({
274
- rule_id: 'R-04',
275
- severity: 'info',
276
- element_id: mod.id,
277
- message: `${mod.id} has ${funcCount} functions but 0 contracts crossing its module boundary (cohesive, just large)`,
278
- fix_hint: `Size alone is not the finding: > ${steps.large} functions without crossing flows is cohesive. Split only if crossings appear`,
279
- context: { element_type: mod.type, element_name: mod.name, candidate_targets: toCandidates(allocated) },
280
- });
281
- }
282
- else if (funcCount > steps.large && crossings > 0) {
283
- violations.push({
284
- rule_id: 'R-04',
285
- severity: 'warning',
286
- element_id: mod.id,
287
- message: `${mod.id} has ${funcCount} functions and ${crossings} distinct contract(s) crossing its module boundary (split recommended)`,
288
- fix_hint: `Split module into smaller units to reduce coupling (> ${steps.large} functions AND crossing flows)`,
289
- context: { element_type: mod.type, element_name: mod.name, candidate_targets: toCandidates(allocated) },
290
- });
291
- }
292
- else if (funcCount > steps.coupled && crossings > steps.crossings) {
293
- violations.push({
294
- rule_id: 'R-04',
295
- severity: 'warning',
296
- element_id: mod.id,
297
- message: `${mod.id} has ${funcCount} functions and ${crossings} distinct contract(s) crossing its module boundary (high coupling)`,
298
- fix_hint: 'Reduce crossing flows or split module',
299
- context: { element_type: mod.type, element_name: mod.name, candidate_targets: toCandidates(allocated) },
300
- });
301
- }
261
+ const mod = idx.byId.get(modId);
262
+ violations.push({
263
+ rule_id: 'R-04',
264
+ severity: 'warning',
265
+ element_id: modId,
266
+ message: `${modId} exposes ${contracts.size} distinct contract(s) at its module boundary (>= ${steps.warning})`,
267
+ fix_hint: 'Reduce the contracts crossing this module boundary — merge contracts, or move the functions that carry them',
268
+ // CR-SM-288: `>= steps.warning` ist inklusiv, also ist `steps.warning - 1` der groesste Wert
269
+ // im Budget; die Masse `value - threshold` ist damit fuer jeden Befund > 0. Gleiche Konvention
270
+ // wie BW-02, dieselbe Schwelle.
271
+ context: { element_type: mod?.type, element_name: mod?.name, value: contracts.size, threshold: steps.warning - 1 },
272
+ });
302
273
  }
303
- return violations;
274
+ // Kanonische Reihenfolge wie bei BW-02 — sonst haengt die Befund-Sequenz an der Trace-Reihenfolge.
275
+ return violations.sort((a, b) => (a.element_id < b.element_id ? -1 : a.element_id > b.element_id ? 1 : 0));
304
276
  }
305
277
  // ---------------------------------------------------------------------------
306
278
  // R-05: Every TEST must verify at least one REQ
@@ -396,6 +368,95 @@ function flowCompleteness(graph) {
396
368
  return violations;
397
369
  }
398
370
  // ---------------------------------------------------------------------------
371
+ // IO-02: ein FLOW hat genau EINEN Produzenten (CR-SM-307).
372
+ //
373
+ // R-10 prueft die UNTERgrenze (mindestens ein Ende), IO-02 die OBERgrenze der
374
+ // Produzentenseite. Die beiden koennen am selben FLOW nie zugleich feuern — |P| = 0
375
+ // gegen |P| > 1 ist disjunkt — also keine Doppelzaehlung im Sinne von Gate 4. Getrennte
376
+ // IDs, weil es getrennte Fragen sind (CR-SM-296, "je Frage eine Regel"): R-10 fragt
377
+ // "hat dieser Fluss ueberhaupt Enden?", IO-02 fragt "kommt sein Inhalt von einer Stelle?".
378
+ //
379
+ // WARUM NUR DIE PRODUZENTENSEITE. Der Inhalt eines FLOW kommt von einer Stelle; haengen
380
+ // zwei Quellen dran, sind es zwei Fluesse unter einem Namen, und kein Leser kann wissen,
381
+ // welche Fassung er bekommt. Mehrere KONSUMENTEN sind dagegen normal — wer abholt, aendert
382
+ // am Fluss nichts. Das ist genau die zentrale Konfiguration: eine Quelle, viele Verbraucher.
383
+ // Eine beidseitige Fassung wurde gemessen und VERWORFEN: sie haette allein in graphcode
384
+ // 8 saubere 1:N-Muster bestraft (FLOW-dimension-readiness 1x7, FLOW-steering-snapshot 1x3,
385
+ // sechs weitere), ohne einen einzigen zusaetzlichen echten Fehler zu finden.
386
+ //
387
+ // BESTAND vor der Einfuehrung, fuenf Systeme (graphcode/rig/flow-cardinality):
388
+ // graphcode 20/42 · graph-view-edit 5/8 · bok 3/15 · moneyflow 0/220 · test_karp 0/21
389
+ // Die beiden Referenzen sind auch BEIDSEITIG sauber — sie erfuellen die strengere Fassung
390
+ // ohnehin, die Lockerung rettet sie nicht. graphcode wurde vor diesem CR auf 9 repariert
391
+ // (CR-GC-499/500); die 9 sind Busse mit 3 bis 19 Produzenten, un-modellierte Entwurfsarbeit.
392
+ //
393
+ // WARUM ES BIS HIERHER KEIN GATE PRUEFTE: die vier io-Zeilen in TRACE_PATTERNS tragen kein
394
+ // `cardinality`-Feld und fallen deshalb durch REQUIRED_PATTERNS
395
+ // (= TRACE_PATTERNS.filter(p => p.cardinality === '1')). Die SCHEMA-Haelfte IST Grammatik
396
+ // (`FLOW -relation-> SCHEMA [1..1]`, R-18 seit CR-SM-271 Teil 2) — die io-Haelfte war nie
397
+ // deklariert. Es ist kein Regel-Loch, das jemand geschlossen und wieder geoeffnet haette:
398
+ // die Frage wurde nie gestellt.
399
+ //
400
+ // EIN BEFUND JE FLOW, nicht je ueberzaehliger Kante (Klasse CR-SM-242): sonst uebertraefe
401
+ // der Zaehler seinen eigenen Nenner-Beitrag (`domain: ['FLOW']`). `value`/`threshold` nach
402
+ // CR-SM-288, damit der Befund budgetierbar ist.
403
+ //
404
+ // SEVERITY error, aber NICHT in STEER_RULES: das ist Hygiene, keine Steuerdimension
405
+ // (Entscheidung Auftraggeber 2026-09-10).
406
+ //
407
+ // KORREKTUR (CR-SM-308): Hier stand als Beleg, das Auftrennen eines Flusses hebe BW-02 ("ein
408
+ // aufgetrennter Fluss kreuzt zwei Grenzen statt einer"), eine rankende Regel zoege also gegen
409
+ // diese urteilende. Das stimmt nicht. moduleCrossings zaehlt je Rand VERSCHIEDENE SCHEMA; ein
410
+ // aufgetrennter FLOW behaelt sein SCHEMA und hat eine Teilmenge der Enden, er erzeugt keinen
411
+ // neuen Randdurchgang. Gemessen an graphcode v257: alle sechs Mehrfach-Produzenten-FLOW je
412
+ // Produzent getrennt, BW-02 15 -> 15, byFunc und byModule unveraendert, IO-02 6 -> 0. Der
413
+ // Anstieg bei CR-GC-499/500 (BW-02 13 -> 14, Chebyshev -0,25) kam von drei NEUEN SCHEMA und
414
+ // umgelegten Ketten, nicht vom Auftrennen. Die Entscheidung oben steht; ein gemessener
415
+ // Zielkonflikt mit BW-02 traegt sie nicht.
416
+ // ---------------------------------------------------------------------------
417
+ function flowSingleProducer(graph) {
418
+ const idx = indexOf(graph);
419
+ const flowIds = new Set(idx.idsOfType('FLOW'));
420
+ const isEndpoint = (id) => { const t = idx.typeOf(id); return t === 'FUNC' || t === 'ACTOR'; };
421
+ // Produzenten je FLOW als MENGE: zwei io-Kanten derselben Quelle auf denselben FLOW sind
422
+ // eine Quelle, kein Verstoss. Konsumenten werden bewusst nicht gezaehlt (s. Kopf).
423
+ const producers = new Map();
424
+ for (const t of idx.tracesOfType('io')) {
425
+ if (!flowIds.has(t.target) || !isEndpoint(t.source))
426
+ continue;
427
+ if (!producers.has(t.target))
428
+ producers.set(t.target, new Set());
429
+ producers.get(t.target).add(t.source);
430
+ }
431
+ const violations = [];
432
+ // Ueber die Elementliste laufen, nicht ueber die Map: die Reihenfolge eines Befundes darf
433
+ // nicht an der Trace-Reihenfolge haengen (CR-SM-244, Ordnung).
434
+ for (const f of idx.elementsOfType('FLOW')) {
435
+ const ps = producers.get(f.id);
436
+ if (!ps || ps.size <= 1)
437
+ continue;
438
+ const sorted = [...ps].sort();
439
+ violations.push({
440
+ rule_id: 'IO-02',
441
+ severity: 'error',
442
+ element_id: f.id,
443
+ message: `${f.id} has ${ps.size} producers (${sorted.join(', ')}) — the content of a FLOW comes from exactly one place`,
444
+ fix_hint: 'Give each source its own FLOW; the shared contract stays on the SCHEMA (n FLOW -> 1 SCHEMA is legal)',
445
+ context: {
446
+ element_type: f.type,
447
+ element_name: f.name,
448
+ value: ps.size,
449
+ threshold: 1,
450
+ candidate_targets: sorted.slice(0, 3).map(id => {
451
+ const e = idx.byId.get(id);
452
+ return { id, type: e?.type ?? 'FUNC', name: e?.name ?? id };
453
+ }),
454
+ },
455
+ });
456
+ }
457
+ return violations;
458
+ }
459
+ // ---------------------------------------------------------------------------
399
460
  // R-11: REMOVED — superseded by SC-02 (identical check, better severity).
400
461
  // ---------------------------------------------------------------------------
401
462
  // ---------------------------------------------------------------------------
@@ -564,54 +625,10 @@ function noPrematureDecomposition(graph) {
564
625
  context: { element_type: req.type, element_name: req.name },
565
626
  }));
566
627
  }
567
- // ---------------------------------------------------------------------------
568
- // RD-04: Decomposition breadth (CR-SM-221)
569
- // ---------------------------------------------------------------------------
570
- /**
571
- * Max children on one decomposition level. The 7±2 convention existed only as
572
- * prose ("darüber func-of-func"); below 7 is a guideline, not a violation, so only
573
- * the upper bound is a rule.
574
- *
575
- * Counted per (parent, kind) — a MOD that both holds 12 FUNCs and composes 12
576
- * sub-MODs has two breadth problems, not one. (The spike reference merged both into
577
- * a single counter on the MOD id and would have reported 24 under one kind.)
578
- *
579
- * CR-SM-282, zwei Aenderungen:
580
- *
581
- * 1. **Die Schwelle steht in `policy.decompositionBreadth`**, nicht mehr inline. `null`
582
- * schaltet die Regel ab (messen statt urteilen). Der Default war hier 11 (verhaltensgleich
583
- * zum vorherigen Literal) und ist seit CR-SM-296 **9** — die Obergrenze von 7±2.
584
- * 2. **Der FUNC-Wurzelwald zaehlt mit.** Die Regel zaehlt Kinder je *Parent*; die Wurzeln des
585
- * `FUNC -compose-> FUNC`-Waldes haben keinen — die oberste Funktionsebene war damit fuer
586
- * RD-04 unsichtbar. Gemessen: moneyflow hat 306 Wurzel-FUNCs und RD-04 meldete **0**.
587
- * Die MOD-Seite hatte das Problem nie, weil `SYS -compose-> MOD` eine echte Kante ist und
588
- * unten schon als `sub-MOD` gezaehlt wird; die FUNC-Seite hat keine solche Kante
589
- * (`se:top-level`: „the top FUNC set is a PROJECTION, not an edge").
590
- *
591
- * Anker ist der SYS-Knoten — determiniert, weil genau einer existiert; gibt es nicht genau
592
- * einen, schweigt dieser Zweig statt zu raten (R-17 deckt den Fall ab). Kein zweiter Weg zur
593
- * Wurzelmenge: es ist dieselbe Definition wie in `se:top-level` (FUNC ohne compose-Elternteil).
594
- */
595
- // CR-SM-296: das Bein `FUNC -allocate-> MOD` ist ENTFALLEN und an R-04 abgegeben.
596
- //
597
- // Es zaehlte die allozierten FUNCs je Modul — also die MODULGROESSE, und die misst R-04 auch.
598
- // Beide feuerten am selben Modul im selben Batch, mit zwei Schwellen aus zwei Policy-Feldern:
599
- // RD-04 > 11 allozierte FUNC-Kinder policy.decompositionBreadth
600
- // R-04 > 12 FUNCs (bzw. 8-12 mit Kreuzungen) policy.moduleSize
601
- // Ein Sachverhalt, zwei Regeln, zwei Zahlen — und welche recht hat, stand nirgends. Schlimmer:
602
- // dieselbe Ursache ging doppelt in den readiness-Nenner. R-04 ist die reichere Aussage (Groesse
603
- // GEGEN Kreuzungen, drei Urteile) und liest ueber `moduleCrossings` ohnehin dieselben Daten.
604
- //
605
- // RD-04 bleibt fuer ZERLEGUNGSBREITE zustaendig: `FUNC compose FUNC`, `SYS/MOD compose MOD` und
606
- // der FUNC-Wurzelwald am SYS (CR-SM-282). Je Frage eine Regel, je Frage eine Schwelle.
607
- function decompositionBreadth(graph, policy) {
608
- const max = policy.decompositionBreadth;
609
- if (max === null)
610
- return [];
628
+ function breadthCounts(graph) {
611
629
  const idx = indexOf(graph);
612
630
  // CR-SM-264: `typeOf` und `byId` baute diese Regel je Aufruf selbst — der Index hat beide.
613
631
  const typeOf = idx;
614
- const byId = idx.byId;
615
632
  const counts = new Map();
616
633
  const bump = (parentId, kind) => {
617
634
  const key = `${parentId}\u0000${kind}`;
@@ -627,9 +644,24 @@ function decompositionBreadth(graph, policy) {
627
644
  if (t.type === 'compose' && src === 'FUNC' && tgt === 'FUNC') {
628
645
  bump(t.source, 'sub-FUNC');
629
646
  }
630
- else if (t.type === 'compose' && (src === 'SYS' || src === 'MOD') && tgt === 'MOD') {
647
+ else if (t.type === 'compose' && src === 'SYS' && tgt === 'MOD') {
631
648
  bump(t.source, 'sub-MOD');
632
649
  }
650
+ else if (t.type === 'compose' && src === 'MOD' && tgt === 'MOD') {
651
+ bump(t.source, 'module element');
652
+ }
653
+ }
654
+ // CR-SM-311: eine MOD-Ebene liest sich aus Untermodulen UND eigenen Funktionen. Gezaehlt werden
655
+ // die BLAETTER — ein zerlegter FUNC ist ein Wertbaum-Knoten; seine Blaetter tragen den Code und
656
+ // stehen selbst im Modul, ihn mitzuzaehlen hiesse dieselbe Funktion zweimal.
657
+ const decomposedFuncs = new Set(graph.traces
658
+ .filter(t => t.type === 'compose' && typeOf.typeOf(t.source) === 'FUNC' && typeOf.typeOf(t.target) === 'FUNC')
659
+ .map(t => t.source));
660
+ for (const t of graph.traces) {
661
+ if (t.type !== 'allocate' || typeOf.typeOf(t.source) !== 'FUNC' || typeOf.typeOf(t.target) !== 'MOD')
662
+ continue;
663
+ if (!decomposedFuncs.has(t.source))
664
+ bump(t.target, 'module element');
633
665
  }
634
666
  // CR-SM-282: der FUNC-Wurzelwald als eigene Blackbox, verankert am SYS-Knoten.
635
667
  const composedFuncs = new Set(graph.traces
@@ -641,18 +673,56 @@ function decompositionBreadth(graph, policy) {
641
673
  if (rootFuncs > 0)
642
674
  counts.set(`${systems[0].id}\u0000root FUNC`, { parentId: systems[0].id, kind: 'root FUNC', n: rootFuncs });
643
675
  }
644
- return [...counts.values()]
645
- .filter(c => c.n > max.warning)
676
+ return [...counts.values()];
677
+ }
678
+ function decompositionBreadth(graph, policy) {
679
+ const band = policy.decompositionBreadth;
680
+ if (band === null)
681
+ return [];
682
+ const byId = indexOf(graph).byId;
683
+ return breadthCounts(graph)
684
+ .filter(c => c.n > band.warning)
646
685
  .map(c => {
647
686
  const parent = byId.get(c.parentId);
648
687
  return {
649
688
  rule_id: 'RD-04',
650
689
  severity: 'warning',
651
690
  element_id: c.parentId,
652
- message: `${c.parentId} has ${c.n} ${c.kind} children on one level (>${max.warning})`,
691
+ message: `${c.parentId} has ${c.n} ${c.kind} children on one level (>${band.warning})`,
653
692
  fix_hint: 'Introduce an intermediate level (func-of-func / sub-MOD)',
654
- // CR-SM-288: `> max.warning` ist exklusiv, also ist `max.warning` selbst noch im Budget.
655
- context: { element_type: parent?.type, element_name: parent?.name, value: c.n, threshold: max.warning },
693
+ // CR-SM-288: `> band.warning` ist exklusiv, also ist `band.warning` selbst noch im Budget.
694
+ context: { element_type: parent?.type, element_name: parent?.name, value: c.n, threshold: band.warning },
695
+ };
696
+ });
697
+ }
698
+ // ---------------------------------------------------------------------------
699
+ // RD-05: Zerlegung zu SCHMAL — eine Ebene mit weniger als `decompositionBreadth.min` Elementen
700
+ // abstrahiert nichts (CR-SM-311, Z3 aus ITEM-2026-053). Dieselbe Zaehlung wie RD-04.
701
+ //
702
+ // Eine eigene Regel und nicht die untere Haelfte von RD-04: RD-04 ist eine der messenden Regeln
703
+ // (CR-SM-288) — jeder ihrer Befunde traegt `value` und `threshold`, und `value > threshold`
704
+ // heisst warning. Eine Unterschreitung hat keinen Ueberschuss; als RD-04 haette sie diese Zusage
705
+ // fuer alle Konsumenten gebrochen und waere in `steerScore` als Masse 0 mitgelaufen. RD-05 fuehrt
706
+ // deshalb keine Zahl im Kontext und steuert nicht — sie ist Lesbarkeit, nicht Randbreite.
707
+ // Eine Ebene ohne jedes Kind ist nicht ihr Fall: ein leeres MOD meldet R-23, eine FUNC ohne Kinder
708
+ // ist ein Blatt.
709
+ // ---------------------------------------------------------------------------
710
+ function decompositionNarrow(graph, policy) {
711
+ const band = policy.decompositionBreadth;
712
+ if (band === null)
713
+ return [];
714
+ const byId = indexOf(graph).byId;
715
+ return breadthCounts(graph)
716
+ .filter(c => c.n < band.min)
717
+ .map(c => {
718
+ const parent = byId.get(c.parentId);
719
+ return {
720
+ rule_id: 'RD-05',
721
+ severity: 'warning',
722
+ element_id: c.parentId,
723
+ message: `${c.parentId} has only ${c.n} ${c.kind} children on one level (<${band.min})`,
724
+ fix_hint: 'Dissolve the level into its parent, or gather related elements under it',
725
+ context: { element_type: parent?.type, element_name: parent?.name },
656
726
  };
657
727
  });
658
728
  }
@@ -1171,12 +1241,12 @@ function funcMustHaveCodeBinding(graph) {
1171
1241
  // as one produced P·C findings per hub FLOW and made reuse the expensive
1172
1242
  // choice. The FCHAIN is the modelled claim; the test is owed on the claim.
1173
1243
  // ---------------------------------------------------------------------------
1174
- function fchainMustHaveIntegrationTest(graph) {
1244
+ function fchainMustHaveIntegrationTest(graph, policy) {
1175
1245
  const idx = indexOf(graph);
1176
1246
  const typeOf = new Map(graph.elements.map(e => [e.id, e.type]));
1177
1247
  const isFunc = (id) => typeOf.get(id) === 'FUNC';
1178
1248
  const io = idx.tracesOfType('io');
1179
- // FUNC↔FUNC connections via the FLOW hop: (producer FUNC) → FLOW → (consumer FUNC).
1249
+ // FUNC↔FUNC handovers via the FLOW hop: (producer FUNC) → FLOW → (consumer FUNC).
1180
1250
  const connections = [];
1181
1251
  for (const e of graph.elements) {
1182
1252
  if (e.type !== 'FLOW')
@@ -1190,7 +1260,8 @@ function fchainMustHaveIntegrationTest(graph) {
1190
1260
  }
1191
1261
  if (connections.length === 0)
1192
1262
  return [];
1193
- // FUNC → set of FCHAINs composing it.
1263
+ // FUNC → set of FCHAINs composing it. LOKALER Index fuer eine andere Frage als die Kennzahl:
1264
+ // "teilen diese beiden EINE Kette" braucht die Mengen, nicht die Zahl (CR-SM-313).
1194
1265
  const chainsOfFunc = new Map();
1195
1266
  for (const t of graph.traces) {
1196
1267
  if (t.type === 'compose' && typeOf.get(t.source) === 'FCHAIN' && isFunc(t.target)) {
@@ -1205,16 +1276,46 @@ function fchainMustHaveIntegrationTest(graph) {
1205
1276
  testedChains.add(t.source);
1206
1277
  }
1207
1278
  }
1279
+ // CR-SM-313: die Infrastruktur-Ausnahme. Die ZAHL kommt aus der Kennzahl (CR-SM-314), nicht
1280
+ // aus einer zweiten Rechnung — eine Uebergabe an eine Funktion, die quer durch alle Ketten
1281
+ // laeuft, ist kein fehlender Integrationstest, sondern ein Bus. `null` = keine Ausnahme.
1282
+ const infra = policy.criticality?.infrastructure ?? null;
1283
+ const chainCount = infra === null ? null : new Map(functionCriticality(graph).map(r => [r.funcId, r.chains]));
1284
+ const isInfrastructure = (id) => infra !== null && chainCount !== null && (chainCount.get(id) ?? 0) >= infra;
1208
1285
  const violations = [];
1209
1286
  const seen = new Set();
1210
1287
  for (const [p, c] of connections) {
1211
- const shared = [...(chainsOfFunc.get(p) ?? [])].filter(ch => chainsOfFunc.get(c)?.has(ch));
1212
- // No shared FCHAIN = no asserted integration (CR-GC-315). Sharing one FLOW
1213
- // between P producers and C consumers does not mean P·C interfaces exist
1214
- // deriving connections from FLOW adjacency alone taxed reuse quadratically.
1215
- // The FCHAIN is the declared integration scope; only that is held to a test.
1216
- if (shared.length === 0)
1288
+ const pChains = chainsOfFunc.get(p) ?? new Set();
1289
+ const cChains = chainsOfFunc.get(c) ?? new Set();
1290
+ // ZWEIG 1 ein Ende in GAR KEINER Kette: still. Das ist R-30s Aussage, nicht R-21s.
1291
+ // Ohne diese Klausel feuert die Regel an einem code-importierten Graphen wie moneyflow
1292
+ // 219 von 219 Mal und meldet in Wahrheit "dieses Repo hat keine Wirkketten", einmal je Kante.
1293
+ if (pChains.size === 0 || cChains.size === 0)
1294
+ continue;
1295
+ const shared = [...pChains].filter(ch => cChains.has(ch));
1296
+ // ZWEIG 3 — beide in Ketten, aber KEINE gemeinsame (CR-SM-313). CR-GC-315 hatte diesen Fall
1297
+ // stillgelegt, weil ein FLOW damals viele Produzenten haben durfte und die P·C-Ableitung
1298
+ // Wiederverwendung quadratisch besteuerte; seit IO-02 (CR-SM-307) hat er genau EINEN.
1299
+ // Befund am SENDER: ohne gemeinsame Kette gibt es keinen Kettenanker.
1300
+ if (shared.length === 0) {
1301
+ if (isInfrastructure(p) || isInfrastructure(c))
1302
+ continue;
1303
+ const key = `pair|${p}>${c}`;
1304
+ if (seen.has(key))
1305
+ continue;
1306
+ seen.add(key);
1307
+ const sender = idx.byId.get(p);
1308
+ violations.push({
1309
+ rule_id: 'R-21',
1310
+ severity: 'warning',
1311
+ element_id: p,
1312
+ message: `${p} hands over to ${c}, but the two share no FCHAIN — no integration scope is declared`,
1313
+ fix_hint: 'Put both functions into one FCHAIN, or route the handover through a function of an existing chain',
1314
+ context: { element_type: sender?.type, element_name: sender?.name },
1315
+ });
1217
1316
  continue;
1317
+ }
1318
+ // ZWEIG 2 — gemeinsame Kette, aber keine davon geprueft: Befund an der Kette. Unveraendert.
1218
1319
  if (shared.some(ch => testedChains.has(ch)))
1219
1320
  continue;
1220
1321
  const anchor = shared[0];
@@ -1444,7 +1545,7 @@ export const V3_RULES = [
1444
1545
  { id: 'R-02', name: 'FUNC must satisfy REQ', severity: 'warning', evaluate: funcMustSatisfyReq, domain: ['FUNC'] },
1445
1546
  // CR-SM-243: domain ist MOD, nicht FUNC — die Regel iteriert Module und meldet am Modul,
1446
1547
  // das ASIL-D und QM mischt. Die FUNCs sind der Anlass des Urteils, nicht seine Traeger.
1447
- { id: 'R-04', name: 'Module size relative to crossing flows', severity: 'warning', evaluate: maxModuleSize, domain: ['MOD'] },
1548
+ { id: 'R-04', name: 'Module boundary width', severity: 'warning', evaluate: moduleBoundaryWidth, domain: ['MOD'] },
1448
1549
  { id: 'R-05', name: 'TEST must verify REQ', severity: 'warning', evaluate: testMustVerifyReq, domain: ['TEST'] },
1449
1550
  { id: 'R-15', name: 'FCHAIN completeness', severity: 'warning', evaluate: fchainCompleteness, domain: ['FCHAIN'] },
1450
1551
  { id: 'R-16', name: 'ACTOR must have io', severity: 'warning', evaluate: actorMustHaveTrace, domain: ['ACTOR'] },
@@ -1454,12 +1555,14 @@ export const V3_RULES = [
1454
1555
  // Als ['FUNC'] deklariert erhoehte sie den Zaehler einer Grundgesamtheit, zu der ihre
1455
1556
  // Verstoesse nicht gehoerten (bis zu 2 Legs je FLOW gegen einen FUNC-Nenner).
1456
1557
  { id: 'R-10', name: 'FLOW completeness', severity: 'warning', evaluate: flowCompleteness, domain: ['FLOW'] },
1558
+ // Obergrenze zu R-10s Untergrenze — disjunkt, deshalb zwei IDs (CR-SM-307).
1559
+ { id: 'IO-02', name: 'FLOW single producer', severity: 'error', evaluate: flowSingleProducer, domain: ['FLOW'] },
1457
1560
  { id: 'R-12', name: 'No circular dependencies', severity: 'warning', evaluate: noDirectCircular, domain: ['FUNC'] },
1458
1561
  { id: 'R-18', name: 'Valid trace pattern', severity: 'error', evaluate: validTracePattern, domain: ['all'] },
1459
1562
  { id: 'R-19', name: 'Runnable TEST binding', severity: 'warning', evaluate: testMustHaveRunnableBinding, domain: ['TEST'] },
1460
1563
  { id: 'R-29', name: 'Test file exclusivity', severity: 'error', evaluate: testFileExclusivity, domain: ['TEST'] },
1461
1564
  { id: 'R-20', name: 'FUNC realRef binding', severity: 'warning', evaluate: funcMustHaveCodeBinding, domain: ['FUNC'] },
1462
- { id: 'R-21', name: 'FUNC↔FUNC connection needs integration test', severity: 'warning', evaluate: fchainMustHaveIntegrationTest, domain: ['FCHAIN'] },
1565
+ { id: 'R-21', name: 'FUNC↔FUNC handover needs a shared chain and an integration test', severity: 'warning', evaluate: fchainMustHaveIntegrationTest, domain: ['FCHAIN', 'FUNC'] },
1463
1566
  { id: 'R-22', name: 'FUNC must be allocated to MOD', severity: 'warning', evaluate: funcMustBeAllocated, domain: ['FUNC'] },
1464
1567
  { id: 'R-23', name: 'MOD must have allocated FUNC', severity: 'warning', evaluate: modMustHaveAllocatedFunc, domain: ['MOD'] },
1465
1568
  { id: 'R-26', name: 'SCHEMA must have realRef', severity: 'warning', evaluate: schemaMustHaveSchemaRef, domain: ['SCHEMA'] },
@@ -1467,6 +1570,7 @@ export const V3_RULES = [
1467
1570
  { id: 'RD-02', name: 'Decomposition consistency', severity: 'warning', evaluate: decompositionConsistency, domain: ['REQ'] },
1468
1571
  { id: 'RD-03', name: 'No premature decomposition', severity: 'info', evaluate: noPrematureDecomposition, domain: ['REQ'] },
1469
1572
  { id: 'RD-04', name: 'Decomposition breadth', severity: 'warning', evaluate: decompositionBreadth, domain: ['FUNC', 'MOD', 'SYS'] },
1573
+ { id: 'RD-05', name: 'Decomposition too narrow', severity: 'warning', evaluate: decompositionNarrow, domain: ['FUNC', 'MOD', 'SYS'] },
1470
1574
  { id: 'MS-01', name: 'Milestone empty scope', severity: 'warning', evaluate: msEmptyScope, domain: ['MS'] },
1471
1575
  { id: 'MS-02', name: 'Milestone dangling dependency', severity: 'error', evaluate: msDanglingDependency, domain: ['MS'] },
1472
1576
  { id: 'R-30', name: 'FUNC leaf must belong to a function chain', severity: 'warning', evaluate: funcMustBeInEffectChain, domain: ['FUNC'] },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sigloch/contracts",
3
- "version": "10.1.0",
3
+ "version": "10.2.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",