@sigloch/contracts 6.0.0 → 6.3.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,56 @@
1
+ /**
2
+ * CR-SM-264 — ein Index je Graph statt Vollscans je Regel.
3
+ *
4
+ * ## Der Befund, der ihn rechtfertigt
5
+ *
6
+ * Nach CR-SM-261 ist das Profil flach: keine Regel ueber 7,2 %, praktisch jede bei n^2,0
7
+ * (`npm run profile:rules -w @sigloch/se-engine`). Das ist keine einzelne teure Regel mehr,
8
+ * sondern EINE Form, die im ganzen Katalog steht — 58 Stellen der Bauart
9
+ *
10
+ * graph.elements.some(e => e.id === t.target && e.type === 'FLOW') // O(E), in einer T-Schleife
11
+ * graph.traces.filter(t => t.source === x && t.type === 'compose') // O(T), je Element
12
+ *
13
+ * Jede einzelne ist `O(E)` bzw. `O(T)` und steht in einer Schleife ueber die jeweils andere
14
+ * Menge. Der Index ersetzt beide durch konstante Lookups.
15
+ *
16
+ * ## Warum eine WeakMap und kein dritter Parameter an `evaluate`
17
+ *
18
+ * Die Alternative waere ein dritter Parameter an `RuleDefinition.evaluate` gewesen — 73
19
+ * Signaturen, und jede Regelfamilie muesste in einem Zug umgestellt werden. Diese Fassung
20
+ * kostet keine Signatur: eine Regel ruft `indexOf(graph)` und bekommt den Index, den die erste
21
+ * Regel desselben Laufs gebaut hat. Damit ist die Umstellung Regel fuer Regel moeglich
22
+ * (CR-SM-264 ist entlang der Familien geschnitten), und eine noch nicht umgestellte Regel
23
+ * funktioniert unveraendert weiter.
24
+ *
25
+ * Der Schluessel ist das Graph-OBJEKT. `applyRule` (se-engine) arbeitet auf einem
26
+ * `structuredClone` — ein anderes Objekt, also ein anderer Index, korrekt per Konstruktion.
27
+ * Eine `WeakMap` gibt den Eintrag frei, sobald der Graph es tut; es entsteht kein Cache, den
28
+ * jemand invalidieren muesste.
29
+ *
30
+ * ## Die Invariante, auf der das ruht
31
+ *
32
+ * **Regeln lesen, sie mutieren nicht.** Wuerde eine Regel `graph.elements` oder `graph.traces`
33
+ * in place aendern, waere der Index danach falsch — still, und nur bei den Regeln, die nach ihr
34
+ * laufen. `se-graph-index.test.ts` haelt das fest.
35
+ */
36
+ import type { OntologyElement, OntologyGraph, Trace } from './ontology.js';
37
+ export interface GraphIndex {
38
+ /** Element je id — ersetzt `elements.find(e => e.id === x)`. */
39
+ byId: ReadonlyMap<string, OntologyElement>;
40
+ /** Typ je id — ersetzt `elements.some(e => e.id === x && e.type === 'Y')`. */
41
+ typeOf(id: string): OntologyElement['type'] | undefined;
42
+ /** Elemente eines Typs, in GRAPH-Reihenfolge — ersetzt `elements.filter(e => e.type === 'X')`. */
43
+ elementsOfType(type: OntologyElement['type']): readonly OntologyElement[];
44
+ /** ids eines Typs, als Menge. */
45
+ idsOfType(type: OntologyElement['type']): ReadonlySet<string>;
46
+ /** Ausgehende Traces je (source, type), in Graph-Reihenfolge. */
47
+ out(source: string, type: Trace['type']): readonly Trace[];
48
+ /** Eingehende Traces je (target, type), in Graph-Reihenfolge. */
49
+ in(target: string, type: Trace['type']): readonly Trace[];
50
+ /** Alle Traces eines Typs, in Graph-Reihenfolge. */
51
+ tracesOfType(type: Trace['type']): readonly Trace[];
52
+ /** Alle ausgehenden Traces eines Elements, TYPUNABHAENGIG, in Graph-Reihenfolge. */
53
+ outAll(source: string): readonly Trace[];
54
+ }
55
+ /** Der Index dieses Graphen — einmal gebaut, danach konstante Lookups. */
56
+ export declare function indexOf(graph: OntologyGraph): GraphIndex;
@@ -0,0 +1,60 @@
1
+ const EMPTY_ELEMENTS = Object.freeze([]);
2
+ const EMPTY_TRACES = Object.freeze([]);
3
+ const EMPTY_IDS = Object.freeze(new Set());
4
+ const CACHE = new WeakMap();
5
+ function build(graph) {
6
+ const byId = new Map();
7
+ const byType = new Map();
8
+ const idsByType = new Map();
9
+ for (const e of graph.elements) {
10
+ // Erster Eintrag gewinnt — dieselbe Semantik wie `elements.find(e => e.id === x)`.
11
+ if (!byId.has(e.id))
12
+ byId.set(e.id, e);
13
+ const list = byType.get(e.type);
14
+ if (list)
15
+ list.push(e);
16
+ else
17
+ byType.set(e.type, [e]);
18
+ const ids = idsByType.get(e.type);
19
+ if (ids)
20
+ ids.add(e.id);
21
+ else
22
+ idsByType.set(e.type, new Set([e.id]));
23
+ }
24
+ const outBy = new Map();
25
+ const inBy = new Map();
26
+ const byTraceType = new Map();
27
+ const outAny = new Map();
28
+ const push = (m, key, t) => {
29
+ const list = m.get(key);
30
+ if (list)
31
+ list.push(t);
32
+ else
33
+ m.set(key, [t]);
34
+ };
35
+ for (const t of graph.traces) {
36
+ push(outBy, `${t.source} ${t.type}`, t);
37
+ push(inBy, `${t.target} ${t.type}`, t);
38
+ push(byTraceType, t.type, t);
39
+ push(outAny, t.source, t);
40
+ }
41
+ return {
42
+ byId,
43
+ typeOf: (id) => byId.get(id)?.type,
44
+ elementsOfType: (type) => byType.get(type) ?? EMPTY_ELEMENTS,
45
+ idsOfType: (type) => idsByType.get(type) ?? EMPTY_IDS,
46
+ out: (source, type) => outBy.get(`${source} ${type}`) ?? EMPTY_TRACES,
47
+ in: (target, type) => inBy.get(`${target} ${type}`) ?? EMPTY_TRACES,
48
+ tracesOfType: (type) => byTraceType.get(type) ?? EMPTY_TRACES,
49
+ outAll: (source) => outAny.get(source) ?? EMPTY_TRACES,
50
+ };
51
+ }
52
+ /** Der Index dieses Graphen — einmal gebaut, danach konstante Lookups. */
53
+ export function indexOf(graph) {
54
+ let idx = CACHE.get(graph);
55
+ if (idx === undefined) {
56
+ idx = build(graph);
57
+ CACHE.set(graph, idx);
58
+ }
59
+ return idx;
60
+ }
@@ -5,7 +5,7 @@
5
5
  /** Ontology schema version (element types + trace types). */
6
6
  export declare const ONTOLOGY_VERSION = "7.0.0";
7
7
  /** Rules engine version (validation rules incl. RC conformance). */
8
- export declare const RULES_VERSION = "6.0.0";
8
+ export declare const RULES_VERSION = "6.3.0";
9
9
  /** Meta-model version (trace pattern constraints + format-e parser). */
10
10
  export declare const META_MODEL_VERSION = "2.0.0";
11
11
  export * from './ontology.js';
@@ -25,6 +25,8 @@ export * from './ao-rules.js';
25
25
  export * from './quality-rules.js';
26
26
  export * from './analysis-freshness-rules.js';
27
27
  export * from './evaluate-all.js';
28
+ export * from './flat-graph.js';
29
+ export * from './graph-index.js';
28
30
  export * from './readiness.js';
29
31
  export * from './meta-model.js';
30
32
  export * from './element-uid.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 = '7.0.0'; // 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 = '6.0.0'; // BREAKING (CR-SM-247): R-28 (Ebenen-Praesenz) entfaellt ersatzlos. Sie schloss das Vacuous-Complete-Loch — mit 0/1 FUNC feuert keine Pro-Element-Regel und jede Vollstaendigkeitsfrage liest 'fertig' —, ist seit CR-GC-366 aber doppelt: die FLOW-Haelfte traegt R-31 (je FUNC statt einmal je Graph) plus IO-01, die SCHEMA-Haelfte SC-04 (je FLOW). Probe (SYS+UC+FCHAIN+3 FUNC, ohne FLOW/SCHEMA): 28 Befunde aus 18 Regeln, ohne R-28 bleiben 17 — das Loch bleibt zu. Gemessen feuerte sie 1x/0x an den Selbstmodellen, dort neben 14x IO-01 und 10x R-31. Dimension und Gate bleiben besetzt (R-30/R-31 sind ebenfalls arch/PDR). Bewusst in Kauf genommen: die SCHEMA-Absenz rutscht von PDR nach CDR, weil SC-04 erst feuert, wenn FLOWs existieren. Als domain:['graph']-Regel trug sie keinen Nenner bei — kein Dimensions-Score verschiebt sich. Prior: MINOR (CR-SM-250): IO-01 prueft ERREICHBARKEIT im io-Teilgraphen der Kette statt Adjazenz jedes Paares. Die Regel forderte einen vollstaendigen Graphen (n·(n−1)/2 Paarungen), obwohl eine Wirkkette eine Sequenz ist — der Zaehler wuchs quadratisch mit der Kettenlaenge, der Nenner-Beitrag linear, und wer sie befriedigen wollte, erfand einen fachlich falschen FLOW oder baute einen Ketten-Bus. Dieselbe Korrektur wie CR-GC-315 fuer R-21, eine Ebene hoeher. Zwei Praezisierungen aus dem Grammar-Review: ein Befund je abgehaengtem Glied statt je Paar (sonst waeren zwei Komponenten der Groesse n/2 wieder n²/4 Befunde, Zaehler > Nenner-Beitrag), und gar nicht verdrahtete Glieder gehoeren R-31 — am sigloch-modules-Selbstmodell waren 6 der 7 IO-01-Elemente zugleich R-31-Elemente, also Doppelzaehlung derselben Ursache. Lockerung, kein heute gruener Graph wird rot. Prior: BREAKING (CR-SM-249): R-15 wird VERSCHAERFT und heisst jetzt 'FCHAIN completeness' — eine Kette braucht Funktionen UNTER sich UND einen UC UEBER sich. Zweites Bein unter derselben rule_id nach dem R-10-Muster (bis zu zwei Meldungen je FCHAIN), bewusst keine neue Regel-ID: eine ID kostet dauerhaft einen Nenner-Anteil, eine Katalogzeile, eine readiness-Zuordnung und einen Golden-File-Eintrag — fuer einen einzigen Befund ueber beide Selbstmodelle (Gate 6 des Grammatik-Reviews). Major, weil ein heute gruener Graph rot werden kann: seit Ontologie 7.0.0 ist `UC -compose-> FCHAIN -compose-> FUNC` der EINZIGE Pfad vom Verhalten zum Zweck, und geprueft war nur sein unteres Bein. Migration: eine Kette ohne UC unterstellen oder loeschen (graphcode: `FCHAIN-doc-export`, 1 Befund). Zweitens R-30: die Grundgesamtheit sind ab jetzt AUSSCHLIESSLICH Blaetter (FUNC ohne FUNC-Kinder), und die Aufwaerts-Vererbung aus CR-GC-366 entfaellt ersatzlos. Ein zerlegter FUNC ist ein Rollup seiner Kinder und DARF gar kein Kettenglied sein — FC-03 ('FCHAIN is flat') verbietet es; die erste Fassung forderte einen Zustand, den eine andere Regel untersagt (am graphcode-Gate quittiert: graphVersion 111, zurueckgenommen in 112). Die Vererbung konnte nur greifen, wenn ein Block Kettenglied ist, und haette sonst ein unverbundenes Blatt stumm geschaltet. R-30 allein waere ein Minor (es meldet weniger: 56 -> 43 am graphcode-Selbstmodell, die 13 Differenz sind exakt die Bloecke). ACHTUNG, Gate-Luecke: `check-grammar-version.mjs` sieht diese Aenderung NICHT — der Snapshot erfasst id/severity/domain, und alle drei bleiben gleich. Eine semantische Verschaerfung ist mit einem Oberflaechen-Abzug nicht greifbar; der Bump hier ist von Hand. Prior: BREAKING (CR-SM-245): MS-01 verliert den Legacy-Fallback `MS -compose-> *` ersatzlos — Scope ist ausschliesslich `CR -relation-> MS`. Der Fallback war ein zweiter Weg zu derselben Aussage: ein Meilenstein galt als befuellt, ohne dass ein CR auf ihn zeigte, womit MS-03 und jede Scope-Auswertung ihn nie sahen. Verschaerfung, deshalb major — gemessen faellt an den Selbstmodellen aber kein einziger Meilenstein (alle 7 in graphcode haben CR-Relationen, vier zusaetzlich compose; MS-01 bleibt 0 Befunde). Zweitens: '+MS-' in SE_PREFIXES — MS-01/02/03 lagen in KEINEM Profil (weder se noch coding) und liefen nur im default-Lauf. Die Dimension `ms` haengt an genau vier Regeln (MS-01/02/03 + AF-05), und AF-05 traegt als `domain: ['graph']`-Regel keinen Nenner bei — im se-Profil war `ms` damit ein strukturell leerer Topic-Score und das SRR-Gate um drei Legs aermer. Zweiter Fall der 'RD-'-Klasse (CR-SM-221), gefunden vom Profil-Test aus CR-SM-244. Prior: BREAKING (CR-GC-366): +R-30 Wirkketten-Bindung, +R-31 io-Verdrahtung, R-02 zieltyp-scharf. Die vier Pflichten einer FUNC (ein REQ erfuellen, verdrahtet sein, in einer Wirkkette haengen, in einem MOD wohnen) waren nur zu zweit geprueft: R-02 und R-22. R-30 schliesst die Kette — R-15 verlangte, dass eine FCHAIN Funktionen hat, niemand verlangte, dass eine Funktion zu einer FCHAIN gehoert; daran haengen aber IO-01 und R-21, die beide nur INNERHALB einer Kette pruefen. R-31 schliesst die Verdrahtung: R-10 stellt dieselbe Frage vom FLOW aus und sieht eine FUNC ohne jede io-Kante deshalb nie (sie kommt in der FLOW-Schleife nicht vor); nur ACTOR darf terminieren. Ein Befund je FUNC statt je fehlender Seite, sonst uebersteigt der Zaehler den Nenner-Beitrag (die Fehlmessung aus CR-SM-242). BREAKING ist R-02: der Filter war zieltyp-blind, ein `FUNC -satisfy-> UC` schaltete ihn stumm, obwohl kein Requirement erfuellt war — am Selbstmodell 6 von 33 Faellen verdeckt (gemeldet: 27). Messung am graphcode-Selbstmodell (82 FUNC): R-30 60, R-31 54 (52 ohne Eingang, 50 ohne Ausgang, 48 ohne beides), R-02 27->33, R-22 3, R-18 26 (die Migrationsschuld aus dem gestrichenen Pattern). Prior: CR-SM-242: R-10 (`['FUNC']`→`['FLOW']`), SC-04 (`['SCHEMA']`→`['FLOW']`) und IO-01 (`['MOD']`→`['FUNC']`) deklarieren jetzt den Typ, an dem sie WIRKLICH melden. Alle drei iterierten ueber einen anderen Typ als den deklarierten: ihre Verstoesse erhoehten den Zaehler einer Grundgesamtheit, zu der sie nicht gehoerten. Gemessen am graphcode-Graphen (46 FLOW / 55 FUNC / 20 SCHEMA / 10 MOD) hob das `schema` von 0.713 auf 0.783 — SC-04 zog 46 FLOW-Verstoesse gegen 20 SCHEMA; bei IO-01 uebertraf der Zaehler (21) den eigenen Nenner-Beitrag (10) schon, nur der Clamp verhinderte einen negativen Score. Kein Evaluator, keine Regel-Semantik und kein Gate-Urteil geaendert — nur die deklarierte Grundgesamtheit, wie bei CR-SM-239. Neu erzwungen durch `se-rule-domain-invariant.test.ts`: der Typ jedes gemeldeten Elements liegt in der `domain` der Regel (stumm fuer 29 der 72 Regeln, die auf den Fixtures nicht feuern). CR-SM-243 (dieselbe Auslieferung): genau dieser Test hat vier weitere Faelle gefunden, die von Hand nie auffielen — R-03 (`['FUNC']`→`['MOD']`, meldet am Modul, das ASIL-D und QM mischt), FC-03 (`['FCHAIN']`→`['FUNC']`, meldet am FUNC mit der verschachtelten Zerlegung), NFR-01 (`['FUNC']`→`['MOD','FUNC','FCHAIN']`, Budgets liegen physisch am MOD und verhaltensseitig an FUNC/FCHAIN) und CR-R03 (`['CR']`→`['all']`, meldet am getrackten Zielknoten beliebigen Typs — nach R-08/R-18 die dritte und bewusst begruendete `all`-Regel). Prior: CR-SM-239: AF-01..05 und R-28 deklarieren `domain: ['graph']` statt `['SYS']` — sie pruefen EINMAL den Graphen, nicht die SYS-Elemente. Als Ein-Element-Gruppe waren sie ein Nenner-Beitrag mit fast sicherem Verstoss und zogen jede sonst leere Dimension auf 0 %: ein Graph ohne eine einzige REQ las `req` = 0 % (1 - 2/2 aus AF-01/AF-03) statt „nichts zu messen", und die Fokuswahl schickte den Executor dorthin. Kein Regelverhalten geaendert — nur die deklarierte Grundgesamtheit. Prior: CR-SM-231: +R-29 Testdatei-Exklusivitaet (severity **error**) — jede Testdatei erscheint in hoechstens einem `testRefs`. Das ist die Haelfte, die 1:n ERZWINGT statt es nur zu erlauben; ohne sie driftet das Attribut zurueck nach n:m. Bewusst schaerfer als R-19/R-20 (beide warning): eine doppelt beanspruchte Datei macht Gate-Zahlen nachweislich falsch, das ist eine Fehlmessung und kein Vollstaendigkeits-Signal. Rein graph-strukturell, kein I/O. R-19 und RC-02 lesen `testRefs`; RC-02 iteriert die Eintraege und nennt den konkreten Pfad statt nur der Knoten-ID. Prior: CR-SM-236: die letzten drei Urteilsschwellen sind Policy-Parameter statt Literal — CR-01 (`crossingFlows`, erstmals mit Aus-Zustand: `null` unterdrueckt auch die info je Modulpaar, die bisher die arch-Dimension verduennte), FM-03 (`riskRpn`, vorher inline 100) und R-04 (`moduleSize.{large,coupled,crossings}`, vorher inline 12/8/2 — drei Urteile, nicht eins). R-04 heisst jetzt 'Module size relative to crossing flows': die Regel waegt Groesse GEGEN Kreuzungen ab, der alte Name 'Max module size' verschwieg die zweite Haelfte der Bedingung. `RuleDefinition.evaluate` nimmt die Policy als zweiten Parameter, damit keine Familie einen Sonderweg braucht; einstellige Regelfunktionen bleiben zuweisbar. Bewertung unter DEFAULT_METRIC_POLICY unveraendert. Prior: CR-GC-315: R-12 + R-21 no longer tax FLOW reuse. R-12 restricted to dependency-carrying traces (compose/allocate/relation) — on `io` the only reachable shape was FUNC ─io→ FLOW ─io→ the SAME FUNC, i.e. read-modify-write on a shared FLOW, so the check was pure false-positive there (real io-cycles span ≥2 FUNCs and a 2-cycle test cannot see them); satisfy/verify/produces 2-cycles are pattern-illegal and belong to R-18. Its dedup key is now direction-independent (`type|sorted pair`) — keying on the message never collapsed anything, so every cycle was reported twice. R-21 no longer derives FUNC↔FUNC connections from FLOW co-adjacency alone: a producer/consumer pair with NO shared FCHAIN is silent. One hub FLOW with P producers and C consumers manufactured P·C findings, each demanding its own FCHAIN+integration test including pairs that never interact — reuse was penalised quadratically. The FCHAIN is the declared integration scope; the test is owed on that declared claim only. Rule intent (CR-GC-240 gap: unit/UC tests do not cover FUNC↔FUNC wiring) unchanged. Prior: CR-SM-227: +AF-01..05 Analysis-Freshness-Legs (analysis-freshness-rules.ts) — the third leg-kind a review gate checks (rule legs, layer-presence legs [R-28], now analysis-freshness legs, docs/articles/07-the-scoring-landscape.md); presence-only rules (Presence/Resolution-Split like R-19/R-20/R-26/R-27 vs. RC-01..05) checking a `graphVersion` freshness stamp exists under SYS.attributes.analysisFreshness.<AnalysisArtifactId> for each of the 5 CR-GC-221 judgment artifacts (conops/trade/assumption-review/fmea/implplan) — staleness (stamp vs live graphVersion) stays a consumer/I-O concern (`CreationCurrencyProvider`, @sigloch/graphcode-client), NOT evaluated here; Familie-Review 2026-08-05 gate assignment → RULE_TO_PHASE: ConOps/Trade/Assumption-Review → PDR, FMEA → CDR, Implementation Plan → TRR (SRR gets none); +AnalysisArtifactId enum + AnalysisFreshnessStampSchema (ontology.ts) as the SSOT graphcode-client's ARTIFACT_CATALOG analysis ids must match; grober Graph-Versions-Stempel only, no scope-hash in this first cut (conscious CR-SM-227 limitation).
8
+ export const RULES_VERSION = '6.3.0'; // MINOR (CR-SM-262): +RC-06 'external realRef names a declared dependency' (severity warning) und +`CodeFacts.declaredDependencies`. RC-01..03 ueberspringen `external === true` zu Recht — der Pfad eines fremden Pakets ist im Konsumenten-Repo nicht aufloesbar. Die Folge war, dass externe Bindungen NIE geprueft wurden und still verrotteten. Gefunden von Hand am graphcode-Selbstmodell (2026-08-22), nicht von einer Regel: `SCHEMA-metric-vector` trug `realRef.file = packages/se-optimizer/src/metrics.ts`, und `@sigloch/se-optimizer` existiert seit CR-SM-248 nicht mehr (MetricVector liegt in se-engine). Der Pfad ist hier nicht entscheidbar, der PAKETNAME schon: `packages/<name>/…` behauptet ein Workspace-Paket, also muss `@sigloch/<name>` in dependencies oder devDependencies stehen. Andere Pfadformen bleiben UNGEPRUEFT statt geraten. Severity warning, nicht error: der Pfad darf aus ehrlichen Gruenden von der Konvention abweichen (Monorepo-Umbau, verlinkte Arbeitskopie), und ein error wuerde ueber die Delta-Semantik jede weitere Mutation blockieren — eine verrottete Bindung ist ein Vollstaendigkeitssignal, keine Fehlmessung (anders als R-29, wo die Zahl selbst falsch wird). `declaredDependencies` ist OPTIONAL und der fehlende Fall ist SCHWEIGEN, nicht ein Befund — die bewusste Umkehrung des `files`-Vertrags, wo ein fehlender Schluessel laut scheitern muss: ein fehlender Datei-Eintrag heisst 'nachgesehen und nichts gefunden', eine fehlende Dependency-Liste heisst 'gar nicht nachgesehen', und das als 'deklariert nichts' zu lesen wuerde jede externe Bindung des Graphen auf einmal melden. Der Extraktor liegt in graphcode (`src/conformance.ts`, `extractDeclaredDependencies`) — vorher las `extractCodeFacts` die package.json NIE, die urspruengliche Kostenschaetzung des CR war insofern falsch. Gemessen an den drei Selbstmodellen: 0 Befunde (graphcode 9 externe Knoten mit realRef, davon 7 in der Pfadform, alle deklariert; sigloch-modules und graphcodedemo tragen keine externen realRefs). Die Null ist der REPARIERTE Zustand — vor der Reparatur am selben Tag waere es genau 1 gewesen, der reale Drift-Fall. ACHTUNG, Gate-Luecke, hier groesser als sonst: RC-Regeln stehen UEBERHAUPT NICHT im Golden-File. `ALL_RULE_DEFS` (die Quelle der `rules`-Oberflaeche in `grammar-snapshot.ts`) enthaelt nur die Familien, die `evaluateAllRules` faehrt; die sechs RC-Regeln laufen ueber `evaluateConformanceRules` mit CodeFacts und sind damit von `check-grammar-version.mjs` gar nicht erfasst — auch eine ENTFERNUNG wuerde dort nicht auffallen. Der Bump hier ist von Hand; die Luecke selbst ist ein eigener Befund. Prior: MINOR (CR-SM-263): MT-02 ueberspringt ROLLUP-CONTAINER — MODs, deren allozierte FUNCs AUSNAHMSLOS zerlegte Bloecke sind. `lcom4Of` verbindet zwei FUNCs ueber ein geteiltes io/satisfy-ZIEL, und CR-SM-256 hat festgehalten, dass ein Rollup genau die nicht traegt (12 der 13 Bloecke am graphcode-Selbstmodell tragen keine einzige io/satisfy-Ausgangskante). Daraus folgt LCOM4 = Zahl der Bloecke, IMMER: die Regel war dort nicht streng, sondern unerfuellbar — gruen wird sie erst mit der Kante, die CR-SM-256 fuer falsch erklaert hat. Zwei Regeln desselben Katalogs forderten Gegensaetzliches, der gegenlaeufige Fall aus Gate 4. Gemessen an allen drei Selbstmodellen (graphVersion 181): es existiert genau EIN Container, `MOD-repo-root` (5/5 Bloecke) — MT-02 7 -> 6 Befunde, die Differenz ist eine info; sigloch-modules und graphcodedemo tragen keinen. Lockerung, kein heute gruener Graph wird rot, keine Migration. Die MESSUNG bleibt: `moduleMetrics()` liefert `lcom4` unveraendert weiter und markiert den Container mit `rollupContainer` — nur das URTEIL entfaellt, denn die Zahl ist wahr und die Schwelle an dieser Stelle bedeutungslos. EIN geteilter Helper: `decomposedFuncs` ist jetzt exportiert und hat mit MT-02 seinen vierten Nutzer, damit keine zweite Definition von 'Blatt' entsteht (Gate 2). ABGELEHNT im selben Durchlauf, hier festgehalten damit es nicht wiederkommt: die FASSADEN-Ausnahme ueber `MOD.kind` (Option A und B des CR). Fachlich ist der Einwand richtig — fuer eine Registry wie `MOD-mcp-tools` (9 orthogonale Werkzeuge, KEIN Block darunter) ist hoher LCOM4 das gewuenschte Ergebnis, und geteilte Ziele hinter einer API waeren der Verdachtsfall. Aber `kind` ist an 30 von 30 MODs aller drei Selbstmodelle unset: ein Attribut mit Leser (R-27) und ohne Schreiber, und eine Selbstauskunft, die eine Strukturregel abschaltet — genau die Klasse, die CR-SM-257 am selben Tag abgelehnt hat. Der Container ist aus dem Graphen ABLEITBAR, die Fassade ist es nicht (Gate 3); deshalb nur die eine Haelfte. Fuer die Fassade bleibt Option C: die Zahl steht da und wird gelesen. ACHTUNG, Gate-Luecke (wie CR-SM-249 und CR-SM-256): `check-grammar-version.mjs` sieht die Aenderung NICHT — der Snapshot erfasst id/severity/domain, alle drei bleiben gleich. Der Bump hier ist von Hand. Prior: MINOR (CR-SM-255 + CR-SM-256): zwei Grundgesamtheiten verengt, beide Lockerungen — kein heute gruener Graph wird rot, keine Migration, keine Kante aendert sich. CR-SM-255: CR-R04 fragt nur noch CRs mit status open/in-progress (und CRs ganz ohne status — ein fehlendes Attribut darf keine Regel stumm schalten); die Statusmenge ist woertlich die von CR-R03 zwoelf Zeilen darueber. 'Welche FUNC fasst dieser CR an' ist eine PLANUNGSfrage; am geschlossenen CR ist sie Archaeologie (Umfang steht im Commit und im Diff), und wer die Kanten nachtraeglich zieht, raet. Gemessen am graphcode-Selbstmodell (graphVersion 171): 41 meldende CRs, davon 40 done und 1 dropped, KEIN einziger offener — 41 unbearbeitbare Befunde. Nach der Aenderung 0, und die Grundgesamtheit ist nicht leer: die 7 offenen CRs tragen ihre FUNC-Kanten bereits, die Regel ist erfuellt statt gegenstandslos. CR-R01 ('ein CR trackt ueberhaupt etwas') bleibt fuer ALLE CRs gueltig. Regelname 'CR must have FUNC' -> 'Open CR must have FUNC'. CR-SM-256: R-02 und R-31 bekommen dieselbe Grundgesamtheit wie R-30 seit CR-SM-249 und R-20 seit CR-210 — ausschliesslich BLAETTER (FUNC ohne FUNC-Kind). Ein zerlegter FUNC ist ein Rollup: durch einen Blackbox-Block fliesst nichts, und er erfuellt keine REQ, die nicht schon ein Blatt erfuellt; wer die beiden Regeln an ihm befriedigen will, muss eine io-Kante an einem Knoten erfinden, an dem nichts anliegt, oder eine satisfy-Kante, die dieselbe REQ ein zweites Mal beansprucht. CR-GC-375 hatte das fuer R-31 ausdruecklich offengelassen ('ein Block ohne io ist auch als Rollup ein Befund … eine eigene Frage, hier bewusst nicht'); die Messung, die Gate 1 zum Wiederaufmachen verlangt, liegt vor: 12 der 13 Bloecke am graphcode-Selbstmodell melden R-02 UND R-31, keiner aus einem anderen Grund — dieselbe Ursache zweimal gezaehlt, die Klasse CR-SM-235/-242. R-02 22->10, R-31 29->17. R-30 unveraendert. EIN geteilter Helper `decomposedFuncs` fuer alle drei Regeln, damit keine zweite Definition von 'Blatt' entsteht (Gate 2). ABGELEHNT im selben Durchlauf, hier festgehalten damit es nicht wiederkommt (CR-SM-257): R-02/R-30/R-31 sollten zusaetzlich `attributes.external === true` ueberspringen. `external` ist ein REALISIERUNGS-Marker — genau so steht es in ELEMENT_ATTRIBUTES ('exempt from the R-20 realRef-binding requirement') und genau so nutzen es R-27 und RC-01..03. Struktur schuldet der Konsument trotzdem: 5 der 12 externen FUNC am graphcode-Selbstmodell haengen vollstaendig in einer lokalen Wirkkette und sind beidseitig verdrahtet. Entscheidend ist die Reihenschaltung am System-Rand: R-31 erzwingt den Grenz-FLOW, SC-04 erzwingt dessen SCHEMA, und `external` am SCHEMA sagt 'der Vertrag wird drueben veroeffentlicht' (33 von 33 Grenz-FLOWs in graphcode tragen so ein SCHEMA, 12 davon external). Eine Ausnahme in R-31 kappt das erste Glied: ohne geforderten FLOW feuert SC-04 nie, und am externen Interface wird NIE ein Schema-Vertrag verlangt. Die 21 Befunde, die der Vorschlag entfernt haette, sind der Hebel, der genau diese Vertraege entstehen laesst. ACHTUNG, Gate-Luecke (wie CR-SM-249): `check-grammar-version.mjs` sieht beide Aenderungen NICHT — der Snapshot erfasst id/severity/domain, und alle drei bleiben gleich. Der Bump hier ist von Hand. Prior: BREAKING (CR-SM-247): R-28 (Ebenen-Praesenz) entfaellt ersatzlos. Sie schloss das Vacuous-Complete-Loch — mit 0/1 FUNC feuert keine Pro-Element-Regel und jede Vollstaendigkeitsfrage liest 'fertig' —, ist seit CR-GC-366 aber doppelt: die FLOW-Haelfte traegt R-31 (je FUNC statt einmal je Graph) plus IO-01, die SCHEMA-Haelfte SC-04 (je FLOW). Probe (SYS+UC+FCHAIN+3 FUNC, ohne FLOW/SCHEMA): 28 Befunde aus 18 Regeln, ohne R-28 bleiben 17 — das Loch bleibt zu. Gemessen feuerte sie 1x/0x an den Selbstmodellen, dort neben 14x IO-01 und 10x R-31. Dimension und Gate bleiben besetzt (R-30/R-31 sind ebenfalls arch/PDR). Bewusst in Kauf genommen: die SCHEMA-Absenz rutscht von PDR nach CDR, weil SC-04 erst feuert, wenn FLOWs existieren. Als domain:['graph']-Regel trug sie keinen Nenner bei — kein Dimensions-Score verschiebt sich. Prior: MINOR (CR-SM-250): IO-01 prueft ERREICHBARKEIT im io-Teilgraphen der Kette statt Adjazenz jedes Paares. Die Regel forderte einen vollstaendigen Graphen (n·(n−1)/2 Paarungen), obwohl eine Wirkkette eine Sequenz ist — der Zaehler wuchs quadratisch mit der Kettenlaenge, der Nenner-Beitrag linear, und wer sie befriedigen wollte, erfand einen fachlich falschen FLOW oder baute einen Ketten-Bus. Dieselbe Korrektur wie CR-GC-315 fuer R-21, eine Ebene hoeher. Zwei Praezisierungen aus dem Grammar-Review: ein Befund je abgehaengtem Glied statt je Paar (sonst waeren zwei Komponenten der Groesse n/2 wieder n²/4 Befunde, Zaehler > Nenner-Beitrag), und gar nicht verdrahtete Glieder gehoeren R-31 — am sigloch-modules-Selbstmodell waren 6 der 7 IO-01-Elemente zugleich R-31-Elemente, also Doppelzaehlung derselben Ursache. Lockerung, kein heute gruener Graph wird rot. Prior: BREAKING (CR-SM-249): R-15 wird VERSCHAERFT und heisst jetzt 'FCHAIN completeness' — eine Kette braucht Funktionen UNTER sich UND einen UC UEBER sich. Zweites Bein unter derselben rule_id nach dem R-10-Muster (bis zu zwei Meldungen je FCHAIN), bewusst keine neue Regel-ID: eine ID kostet dauerhaft einen Nenner-Anteil, eine Katalogzeile, eine readiness-Zuordnung und einen Golden-File-Eintrag — fuer einen einzigen Befund ueber beide Selbstmodelle (Gate 6 des Grammatik-Reviews). Major, weil ein heute gruener Graph rot werden kann: seit Ontologie 7.0.0 ist `UC -compose-> FCHAIN -compose-> FUNC` der EINZIGE Pfad vom Verhalten zum Zweck, und geprueft war nur sein unteres Bein. Migration: eine Kette ohne UC unterstellen oder loeschen (graphcode: `FCHAIN-doc-export`, 1 Befund). Zweitens R-30: die Grundgesamtheit sind ab jetzt AUSSCHLIESSLICH Blaetter (FUNC ohne FUNC-Kinder), und die Aufwaerts-Vererbung aus CR-GC-366 entfaellt ersatzlos. Ein zerlegter FUNC ist ein Rollup seiner Kinder und DARF gar kein Kettenglied sein — FC-03 ('FCHAIN is flat') verbietet es; die erste Fassung forderte einen Zustand, den eine andere Regel untersagt (am graphcode-Gate quittiert: graphVersion 111, zurueckgenommen in 112). Die Vererbung konnte nur greifen, wenn ein Block Kettenglied ist, und haette sonst ein unverbundenes Blatt stumm geschaltet. R-30 allein waere ein Minor (es meldet weniger: 56 -> 43 am graphcode-Selbstmodell, die 13 Differenz sind exakt die Bloecke). ACHTUNG, Gate-Luecke: `check-grammar-version.mjs` sieht diese Aenderung NICHT — der Snapshot erfasst id/severity/domain, und alle drei bleiben gleich. Eine semantische Verschaerfung ist mit einem Oberflaechen-Abzug nicht greifbar; der Bump hier ist von Hand. Prior: BREAKING (CR-SM-245): MS-01 verliert den Legacy-Fallback `MS -compose-> *` ersatzlos — Scope ist ausschliesslich `CR -relation-> MS`. Der Fallback war ein zweiter Weg zu derselben Aussage: ein Meilenstein galt als befuellt, ohne dass ein CR auf ihn zeigte, womit MS-03 und jede Scope-Auswertung ihn nie sahen. Verschaerfung, deshalb major — gemessen faellt an den Selbstmodellen aber kein einziger Meilenstein (alle 7 in graphcode haben CR-Relationen, vier zusaetzlich compose; MS-01 bleibt 0 Befunde). Zweitens: '+MS-' in SE_PREFIXES — MS-01/02/03 lagen in KEINEM Profil (weder se noch coding) und liefen nur im default-Lauf. Die Dimension `ms` haengt an genau vier Regeln (MS-01/02/03 + AF-05), und AF-05 traegt als `domain: ['graph']`-Regel keinen Nenner bei — im se-Profil war `ms` damit ein strukturell leerer Topic-Score und das SRR-Gate um drei Legs aermer. Zweiter Fall der 'RD-'-Klasse (CR-SM-221), gefunden vom Profil-Test aus CR-SM-244. Prior: BREAKING (CR-GC-366): +R-30 Wirkketten-Bindung, +R-31 io-Verdrahtung, R-02 zieltyp-scharf. Die vier Pflichten einer FUNC (ein REQ erfuellen, verdrahtet sein, in einer Wirkkette haengen, in einem MOD wohnen) waren nur zu zweit geprueft: R-02 und R-22. R-30 schliesst die Kette — R-15 verlangte, dass eine FCHAIN Funktionen hat, niemand verlangte, dass eine Funktion zu einer FCHAIN gehoert; daran haengen aber IO-01 und R-21, die beide nur INNERHALB einer Kette pruefen. R-31 schliesst die Verdrahtung: R-10 stellt dieselbe Frage vom FLOW aus und sieht eine FUNC ohne jede io-Kante deshalb nie (sie kommt in der FLOW-Schleife nicht vor); nur ACTOR darf terminieren. Ein Befund je FUNC statt je fehlender Seite, sonst uebersteigt der Zaehler den Nenner-Beitrag (die Fehlmessung aus CR-SM-242). BREAKING ist R-02: der Filter war zieltyp-blind, ein `FUNC -satisfy-> UC` schaltete ihn stumm, obwohl kein Requirement erfuellt war — am Selbstmodell 6 von 33 Faellen verdeckt (gemeldet: 27). Messung am graphcode-Selbstmodell (82 FUNC): R-30 60, R-31 54 (52 ohne Eingang, 50 ohne Ausgang, 48 ohne beides), R-02 27->33, R-22 3, R-18 26 (die Migrationsschuld aus dem gestrichenen Pattern). Prior: CR-SM-242: R-10 (`['FUNC']`→`['FLOW']`), SC-04 (`['SCHEMA']`→`['FLOW']`) und IO-01 (`['MOD']`→`['FUNC']`) deklarieren jetzt den Typ, an dem sie WIRKLICH melden. Alle drei iterierten ueber einen anderen Typ als den deklarierten: ihre Verstoesse erhoehten den Zaehler einer Grundgesamtheit, zu der sie nicht gehoerten. Gemessen am graphcode-Graphen (46 FLOW / 55 FUNC / 20 SCHEMA / 10 MOD) hob das `schema` von 0.713 auf 0.783 — SC-04 zog 46 FLOW-Verstoesse gegen 20 SCHEMA; bei IO-01 uebertraf der Zaehler (21) den eigenen Nenner-Beitrag (10) schon, nur der Clamp verhinderte einen negativen Score. Kein Evaluator, keine Regel-Semantik und kein Gate-Urteil geaendert — nur die deklarierte Grundgesamtheit, wie bei CR-SM-239. Neu erzwungen durch `se-rule-domain-invariant.test.ts`: der Typ jedes gemeldeten Elements liegt in der `domain` der Regel (stumm fuer 29 der 72 Regeln, die auf den Fixtures nicht feuern). CR-SM-243 (dieselbe Auslieferung): genau dieser Test hat vier weitere Faelle gefunden, die von Hand nie auffielen — R-03 (`['FUNC']`→`['MOD']`, meldet am Modul, das ASIL-D und QM mischt), FC-03 (`['FCHAIN']`→`['FUNC']`, meldet am FUNC mit der verschachtelten Zerlegung), NFR-01 (`['FUNC']`→`['MOD','FUNC','FCHAIN']`, Budgets liegen physisch am MOD und verhaltensseitig an FUNC/FCHAIN) und CR-R03 (`['CR']`→`['all']`, meldet am getrackten Zielknoten beliebigen Typs — nach R-08/R-18 die dritte und bewusst begruendete `all`-Regel). Prior: CR-SM-239: AF-01..05 und R-28 deklarieren `domain: ['graph']` statt `['SYS']` — sie pruefen EINMAL den Graphen, nicht die SYS-Elemente. Als Ein-Element-Gruppe waren sie ein Nenner-Beitrag mit fast sicherem Verstoss und zogen jede sonst leere Dimension auf 0 %: ein Graph ohne eine einzige REQ las `req` = 0 % (1 - 2/2 aus AF-01/AF-03) statt „nichts zu messen", und die Fokuswahl schickte den Executor dorthin. Kein Regelverhalten geaendert — nur die deklarierte Grundgesamtheit. Prior: CR-SM-231: +R-29 Testdatei-Exklusivitaet (severity **error**) — jede Testdatei erscheint in hoechstens einem `testRefs`. Das ist die Haelfte, die 1:n ERZWINGT statt es nur zu erlauben; ohne sie driftet das Attribut zurueck nach n:m. Bewusst schaerfer als R-19/R-20 (beide warning): eine doppelt beanspruchte Datei macht Gate-Zahlen nachweislich falsch, das ist eine Fehlmessung und kein Vollstaendigkeits-Signal. Rein graph-strukturell, kein I/O. R-19 und RC-02 lesen `testRefs`; RC-02 iteriert die Eintraege und nennt den konkreten Pfad statt nur der Knoten-ID. Prior: CR-SM-236: die letzten drei Urteilsschwellen sind Policy-Parameter statt Literal — CR-01 (`crossingFlows`, erstmals mit Aus-Zustand: `null` unterdrueckt auch die info je Modulpaar, die bisher die arch-Dimension verduennte), FM-03 (`riskRpn`, vorher inline 100) und R-04 (`moduleSize.{large,coupled,crossings}`, vorher inline 12/8/2 — drei Urteile, nicht eins). R-04 heisst jetzt 'Module size relative to crossing flows': die Regel waegt Groesse GEGEN Kreuzungen ab, der alte Name 'Max module size' verschwieg die zweite Haelfte der Bedingung. `RuleDefinition.evaluate` nimmt die Policy als zweiten Parameter, damit keine Familie einen Sonderweg braucht; einstellige Regelfunktionen bleiben zuweisbar. Bewertung unter DEFAULT_METRIC_POLICY unveraendert. Prior: CR-GC-315: R-12 + R-21 no longer tax FLOW reuse. R-12 restricted to dependency-carrying traces (compose/allocate/relation) — on `io` the only reachable shape was FUNC ─io→ FLOW ─io→ the SAME FUNC, i.e. read-modify-write on a shared FLOW, so the check was pure false-positive there (real io-cycles span ≥2 FUNCs and a 2-cycle test cannot see them); satisfy/verify/produces 2-cycles are pattern-illegal and belong to R-18. Its dedup key is now direction-independent (`type|sorted pair`) — keying on the message never collapsed anything, so every cycle was reported twice. R-21 no longer derives FUNC↔FUNC connections from FLOW co-adjacency alone: a producer/consumer pair with NO shared FCHAIN is silent. One hub FLOW with P producers and C consumers manufactured P·C findings, each demanding its own FCHAIN+integration test including pairs that never interact — reuse was penalised quadratically. The FCHAIN is the declared integration scope; the test is owed on that declared claim only. Rule intent (CR-GC-240 gap: unit/UC tests do not cover FUNC↔FUNC wiring) unchanged. Prior: CR-SM-227: +AF-01..05 Analysis-Freshness-Legs (analysis-freshness-rules.ts) — the third leg-kind a review gate checks (rule legs, layer-presence legs [R-28], now analysis-freshness legs, docs/articles/07-the-scoring-landscape.md); presence-only rules (Presence/Resolution-Split like R-19/R-20/R-26/R-27 vs. RC-01..05) checking a `graphVersion` freshness stamp exists under SYS.attributes.analysisFreshness.<AnalysisArtifactId> for each of the 5 CR-GC-221 judgment artifacts (conops/trade/assumption-review/fmea/implplan) — staleness (stamp vs live graphVersion) stays a consumer/I-O concern (`CreationCurrencyProvider`, @sigloch/graphcode-client), NOT evaluated here; Familie-Review 2026-08-05 gate assignment → RULE_TO_PHASE: ConOps/Trade/Assumption-Review → PDR, FMEA → CDR, Implementation Plan → TRR (SRR gets none); +AnalysisArtifactId enum + AnalysisFreshnessStampSchema (ontology.ts) as the SSOT graphcode-client's ARTIFACT_CATALOG analysis ids must match; grober Graph-Versions-Stempel only, no scope-hash in this first cut (conscious CR-SM-227 limitation).
9
9
  /** Meta-model version (trace pattern constraints + format-e parser). */
10
10
  export const META_MODEL_VERSION = '2.0.0'; // BREAKING, zwei Anlaesse in einer Zahl. (1) Nachtrag zu CR-GC-366: dort entfiel das Trace-Pattern `FUNC -satisfy-> UC`, gebumpt wurde aber nur ONTOLOGY_VERSION — nach der in CR-SM-244 festgeschriebenen Zustaendigkeit (meta-model.ts + format-e-Parser -> META_MODEL_VERSION) fehlte der Bump hier. Eine Entfernung ist major; dass kein Konsument semver-maessig auf diese Konstante verzweigt (schema-guard.ts keyt bewusst auf einen DDL-Fingerprint), macht sie nicht kleiner, nur folgenlos fuer die Bump-Kette. (2) CR-SM-251: `hydrateAttrValue` hydriert typ-geleitet ueber ELEMENT_ATTRIBUTES — boolean/number-Attribute kamen ueber BEIDE Format-E-Pfade als String an, womit die elf `=== true`-Vergleiche in den Regeln jede Ausnahme (concept/external/safety_relevant/spike) ins Leere laufen liessen und ein Export-Reimport bestehende Ausnahmen still entwertete. Verhaltensaenderung am Parser, also hier verbucht. Prior: -REQ→MOD allocate pattern removed (CR-228 A); +FUNC→FUNC compose (blackbox function decomposition)
11
11
  export * from './ontology.js';
@@ -25,6 +25,8 @@ export * from './ao-rules.js';
25
25
  export * from './quality-rules.js';
26
26
  export * from './analysis-freshness-rules.js';
27
27
  export * from './evaluate-all.js';
28
+ export * from './flat-graph.js';
29
+ export * from './graph-index.js';
28
30
  export * from './readiness.js';
29
31
  export * from './meta-model.js';
30
32
  export * from './element-uid.js';
@@ -71,6 +71,14 @@ export interface ModuleMetrics {
71
71
  instability: number | null;
72
72
  /** MT-02 core: connected-component count. null below 2 allocated FUNCs (not measurable). */
73
73
  lcom4: number | null;
74
+ /**
75
+ * CR-SM-263: JEDE allozierte FUNC ist ein zerlegter Block (Rollup), das MOD also ein
76
+ * Container statt eines Implementierungsmoduls. Dann ist `lcom4` = Zahl der Bloecke,
77
+ * garantiert durch CR-SM-256 — eine wahre Zahl ohne Aussage ueber Kohaesion. MT-02
78
+ * urteilt hier nicht; der Wert steht trotzdem da, damit der Leser ihn deuten kann.
79
+ * `false` unterhalb von 2 allozierten FUNCs (kein Container, nur zu klein zum Messen).
80
+ */
81
+ rollupContainer: boolean;
74
82
  /** The CR-SM-223 measurement, deliberately threshold-free. null where contracts omits it. */
75
83
  cohesion: {
76
84
  internal: number;
@@ -1,3 +1,5 @@
1
+ import { decomposedFuncs } from './rules.js';
2
+ import { indexOf } from './graph-index.js';
1
3
  /**
2
4
  * MT-01: Module Instability (CR-165: indirect via allocate-path).
3
5
  * I = fan_out / (fan_in + fan_out) > `policy.instability` → warning.
@@ -46,6 +48,13 @@ export function mt02Lcom4(graph, policy) {
46
48
  for (const m of measureModules(graph)) {
47
49
  if (m.lcom4 === null)
48
50
  continue;
51
+ // CR-SM-263: ein MOD aus lauter zerlegten Bloecken ist per Konstruktion zerfallen —
52
+ // LCOM4 verbindet ueber geteilte io/satisfy-ZIELE, und CR-SM-256 hat festgehalten, dass
53
+ // ein Rollup genau die nicht traegt. Die Regel war dort nicht streng, sondern
54
+ // unerfuellbar: gruen wird sie erst mit der Kante, die CR-SM-256 fuer falsch erklaert.
55
+ // Die MESSUNG bleibt (`moduleMetrics` liefert `lcom4` unveraendert) — nur das Urteil faellt.
56
+ if (m.rollupContainer)
57
+ continue;
49
58
  const message = `${m.moduleName} has LCOM4=${m.lcom4} (${m.allocatedFuncs} FUNCs in ${m.lcom4} disconnected groups)`;
50
59
  if (m.lcom4 >= steps.info && m.lcom4 < steps.warning) {
51
60
  violations.push({ rule_id: 'MT-02', severity: 'info', element_id: m.moduleId, message });
@@ -120,30 +129,86 @@ function connectionPairs(graph) {
120
129
  * Every value that is not measurable is `null`, never 0: a module with one allocated
121
130
  * FUNC has no LCOM4, and a 1 there would be an invented statement about cohesion.
122
131
  */
132
+ const MEASURE_CACHE = new WeakMap();
133
+ /**
134
+ * CR-SM-264: die Rechnung EINMAL je Graph.
135
+ *
136
+ * `mt01Instability` und `mt02Lcom4` riefen `measureModules` beide auf, also lief die
137
+ * vollstaendige Modulmessung je Auswertungslauf zweimal — im Profil aus CR-SM-260 stehen sie
138
+ * mit 7,1 % und 7,0 % nebeneinander, und die zweite Zahl ist reine Wiederholung der ersten.
139
+ * Schluessel ist das Graph-OBJEKT (dieselbe Begruendung wie in `graph-index.ts`).
140
+ *
141
+ * Der Cache gibt seine EIGENE Liste heraus. Wer sie sortieren will, kopiert vorher —
142
+ * `moduleMetrics` tut das; ein `.sort()` auf dem Cache-Eintrag wuerde ihn dauerhaft umordnen.
143
+ */
123
144
  function measureModules(graph) {
124
- const mods = graph.elements.filter(e => e.type === 'MOD');
145
+ const cached = MEASURE_CACHE.get(graph);
146
+ if (cached !== undefined)
147
+ return cached;
148
+ const rowsForCache = measureModulesUncached(graph);
149
+ MEASURE_CACHE.set(graph, rowsForCache);
150
+ return rowsForCache;
151
+ }
152
+ function measureModulesUncached(graph) {
153
+ const idx = indexOf(graph);
154
+ const mods = idx.elementsOfType('MOD');
125
155
  const pairs = [...connectionPairs(graph)].map(p => p.split('|'));
156
+ // CR-SM-263: EIN Helper fuer "Blatt" — derselbe, den R-02/R-30/R-31 seit CR-SM-256 lesen.
157
+ // Eine zweite Definition hier waere ein zweiter Weg zu derselben Aussage (Gate 2).
158
+ const decomposed = decomposedFuncs(graph);
126
159
  const rows = [];
160
+ // --- CR-SM-264: die indirekte Kopplung UMGEDREHT ---
161
+ //
162
+ // Vorher lief je MOD eine Schleife ueber ALLE Traces (`MOD x T`) — nach Teil c die teuerste
163
+ // verbliebene Form im Katalog, und sie traegt MT-01 UND MT-02, weil beide dieselbe Rechnung
164
+ // lesen. Der Index loest sie nicht auf: die Frage ist nicht "welche Kanten haengen an diesem
165
+ // Knoten", sondern "welche Kanten kreuzen diese Modulgrenze".
166
+ //
167
+ // Also einmal ueber die Traces statt einmal je Modul. Je Kante steht ueber die
168
+ // allocate-Zuordnung fest, welche Module ihre Enden besitzen; nur die zaehlen mit. Ein
169
+ // Element allokiert praktisch immer auf hoechstens ein MOD, die innere Schleife ist damit
170
+ // kurz — und das Ergebnis ist Zaehler fuer Zaehler dasselbe.
171
+ const modIds = idx.idsOfType('MOD');
172
+ const ownersOf = new Map(); // alloziertes Element -> seine MODs
173
+ const membersOf = new Map(); // MOD -> seine allozierten Elemente
174
+ for (const t of idx.tracesOfType('allocate')) {
175
+ if (!modIds.has(t.target))
176
+ continue;
177
+ const owners = ownersOf.get(t.source);
178
+ if (owners)
179
+ owners.push(t.target);
180
+ else
181
+ ownersOf.set(t.source, [t.target]);
182
+ const members = membersOf.get(t.target);
183
+ if (members)
184
+ members.add(t.source);
185
+ else
186
+ membersOf.set(t.target, new Set([t.source]));
187
+ }
188
+ const indirectOutOf = new Map();
189
+ const indirectInOf = new Map();
190
+ const bump = (m, key) => { m.set(key, (m.get(key) ?? 0) + 1); };
191
+ for (const t of graph.traces) {
192
+ if (t.type === 'allocate')
193
+ continue; // allocate itself doesn't count as coupling
194
+ for (const owner of ownersOf.get(t.source) ?? []) {
195
+ if (!membersOf.get(owner).has(t.target) && t.target !== owner)
196
+ bump(indirectOutOf, owner);
197
+ }
198
+ for (const owner of ownersOf.get(t.target) ?? []) {
199
+ if (!membersOf.get(owner).has(t.source) && t.source !== owner)
200
+ bump(indirectInOf, owner);
201
+ }
202
+ }
127
203
  for (const mod of mods) {
128
- const allocated = graph.traces
129
- .filter(t => t.type === 'allocate' && t.target === mod.id)
130
- .map(t => t.source);
204
+ const allocated = idx.in(mod.id, 'allocate').map(t => t.source);
131
205
  const modFuncIds = new Set(allocated);
132
206
  // --- MT-01: fan-in / fan-out (CR-165, indirect via the allocate path) ---
133
- const directOut = graph.traces.filter(t => t.source === mod.id && (t.type === 'io' || t.type === 'compose') && t.target !== mod.id).length;
134
- const directIn = graph.traces.filter(t => t.target === mod.id && (t.type === 'io' || t.type === 'compose' || t.type === 'allocate')).length;
135
- let indirectOut = 0;
136
- let indirectIn = 0;
137
- for (const t of graph.traces) {
138
- if (t.type === 'allocate')
139
- continue; // allocate itself doesn't count as coupling
140
- if (modFuncIds.has(t.source) && !modFuncIds.has(t.target) && t.target !== mod.id)
141
- indirectOut++;
142
- if (modFuncIds.has(t.target) && !modFuncIds.has(t.source) && t.source !== mod.id)
143
- indirectIn++;
144
- }
145
- const fanOut = directOut + indirectOut;
146
- const fanIn = directIn + indirectIn;
207
+ const directOut = idx.out(mod.id, 'io').filter(t => t.target !== mod.id).length +
208
+ idx.out(mod.id, 'compose').filter(t => t.target !== mod.id).length;
209
+ const directIn = idx.in(mod.id, 'io').length + idx.in(mod.id, 'compose').length + idx.in(mod.id, 'allocate').length;
210
+ const fanOut = directOut + (indirectOutOf.get(mod.id) ?? 0);
211
+ const fanIn = directIn + (indirectInOf.get(mod.id) ?? 0);
147
212
  const instability = fanIn + fanOut === 0 ? null : fanOut / (fanIn + fanOut);
148
213
  rows.push({
149
214
  moduleId: mod.id,
@@ -153,6 +218,7 @@ function measureModules(graph) {
153
218
  fanOut,
154
219
  instability,
155
220
  lcom4: lcom4Of(graph, allocated),
221
+ rollupContainer: allocated.length >= 2 && allocated.every(f => decomposed.has(f)),
156
222
  cohesion: cohesionOf(pairs, modFuncIds),
157
223
  });
158
224
  }
@@ -163,13 +229,19 @@ function lcom4Of(graph, funcIds) {
163
229
  if (funcIds.length < 2)
164
230
  return null;
165
231
  // Two FUNCs are connected if they share a common io/satisfy target.
232
+ // CR-SM-264: die ausgehenden Kanten kommen aus dem Index. Vorher lief je alloziertem FUNC
233
+ // ein Vollscan ueber alle Traces — MOD x FUNC x T. Die Einfuegereihenfolge in `targets`
234
+ // bleibt identisch (io vor satisfy war sie nicht; der Index liefert beide Listen in
235
+ // Graph-Reihenfolge, und `targets` ist ein Set, dessen Inhalt hier nur auf Mitgliedschaft
236
+ // geprueft wird).
237
+ const idx = indexOf(graph);
166
238
  const funcTargets = new Map();
167
239
  for (const fid of funcIds) {
168
240
  const targets = new Set();
169
- for (const t of graph.traces) {
170
- if (t.source === fid && (t.type === 'io' || t.type === 'satisfy'))
171
- targets.add(t.target);
172
- }
241
+ for (const t of idx.out(fid, 'io'))
242
+ targets.add(t.target);
243
+ for (const t of idx.out(fid, 'satisfy'))
244
+ targets.add(t.target);
173
245
  funcTargets.set(fid, targets);
174
246
  }
175
247
  const parent = new Map();
@@ -262,7 +334,7 @@ function cohesionOf(pairs, funcIds) {
262
334
  */
263
335
  export function moduleMetrics(graph) {
264
336
  const byId = (a, b) => a.moduleId < b.moduleId ? -1 : a.moduleId > b.moduleId ? 1 : 0;
265
- return measureModules(graph).sort((a, b) => {
337
+ return [...measureModules(graph)].sort((a, b) => {
266
338
  if (a.cohesion && b.cohesion)
267
339
  return a.cohesion.ratio - b.cohesion.ratio || byId(a, b);
268
340
  if (a.cohesion)
@@ -186,6 +186,18 @@ export interface RuleDefinition {
186
186
  */
187
187
  domain: readonly string[];
188
188
  }
189
+ /** Find the MOD a FUNC/SCHEMA is allocated to. */
190
+ /**
191
+ * CR-SM-256: die EINE Definition von "zerlegt". Ein FUNC mit FUNC-Kindern ist ein Rollup
192
+ * seiner Kinder — ein Blackbox-Block, durch den nichts fliesst und der keine Anforderung
193
+ * erfuellt, die nicht schon ein Blatt erfuellt. Die Pflichten liegen vollstaendig auf den
194
+ * Blaettern (so schon R-30 seit CR-SM-249, so R-20 seit CR-210).
195
+ *
196
+ * Bewusst EIN Helper statt drei Kopien: eine zweite Definition von "Blatt" waere ein
197
+ * zweiter Weg zu derselben Aussage, und der billigere gewinnt immer. CR-SM-263 haengt als
198
+ * vierter Nutzer daran (MT-02, `metric-rules.ts`) — deshalb exportiert.
199
+ */
200
+ export declare function decomposedFuncs(graph: OntologyGraph): Set<string>;
189
201
  export declare const V3_RULES: RuleDefinition[];
190
202
  /** Run all rules against a graph. CR-SM-236: `policy` ist Pflicht — wie bei `evaluateAllRules`. */
191
203
  export declare function evaluateRules(graph: OntologyGraph, policy: MetricPolicy): RuleViolation[];