@sigloch/se-engine 1.4.1 → 1.6.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.
@@ -118,8 +118,16 @@ export declare const MERGE_SIMILARITY_THRESHOLD = 0.85;
118
118
  */
119
119
  export declare function contractSimilarity(g: OntologyGraph, a: string, b: string): number;
120
120
  /**
121
- * Konsolidierungs-Kandidaten: FLOW-Paare, die DENSELBEN Datenvertrag tragen
122
- * (CR-GC-444, Zielbild „graphcode als Regelkreis", FLOWs 62 27).
121
+ * Konsolidierungs-Kandidaten: FLOW-Paare, die DENSELBEN Datenvertrag tragen UND dieselbe
122
+ * Verbindung sind gleiche Produzenten, gleiche Konsumenten (CR-GC-444, CR-SM-309).
123
+ *
124
+ * CR-SM-309: vorher genuegte der gleiche Vertrag. Seit CR-GC-510 ist „ein FLOW je
125
+ * Verbindung, n FLOW -> 1 SCHEMA" das Modell, und IO-02 blockt am Gate. Ein Merge zweier
126
+ * FLOWs mit verschiedenen Produzenten legte zwei Quellen in einen Fluss (IO-02); einer mit
127
+ * gleichem Produzenten, aber verschiedenen Konsumenten behauptete Verbindungen, die es nicht
128
+ * gibt. Gemessen an graphcode v261: alle 11 Vorschlaege der alten Fassung verletzten IO-02
129
+ * und wurden am Gate abgewiesen. Echt verschmelzbar sind nur DUPLIKATE — derselbe Vertrag
130
+ * auf derselben Verbindung, zweimal modelliert.
123
131
  *
124
132
  * „Derselbe Vertrag" = `contractSimilarity` (identischer SCHEMA-Knoten oder
125
133
  * ND-02-Duplikat) — EINE Definition, keine neue Zahl. Ein FLOW ohne oder mit
@@ -439,9 +439,35 @@ function coupledMerges(g, drop, keep) {
439
439
  function ioDegree(g, id) {
440
440
  return g.traces.filter((t) => t.type === 'io' && (t.source === id || t.target === id)).length;
441
441
  }
442
+ /** Produzenten- und Konsumentenmenge eines FLOW als Signatur (FUNC/ACTOR-Enden, sortiert). */
443
+ function endpointSignature(g, flowId) {
444
+ const isEnd = (id) => {
445
+ const t = byId(g, id)?.type;
446
+ return t === 'FUNC' || t === 'ACTOR';
447
+ };
448
+ const producers = new Set();
449
+ const consumers = new Set();
450
+ for (const t of g.traces) {
451
+ if (t.type !== 'io')
452
+ continue;
453
+ if (t.target === flowId && isEnd(t.source))
454
+ producers.add(t.source);
455
+ if (t.source === flowId && isEnd(t.target))
456
+ consumers.add(t.target);
457
+ }
458
+ return `${[...producers].sort().join(',')}|${[...consumers].sort().join(',')}`;
459
+ }
442
460
  /**
443
- * Konsolidierungs-Kandidaten: FLOW-Paare, die DENSELBEN Datenvertrag tragen
444
- * (CR-GC-444, Zielbild „graphcode als Regelkreis", FLOWs 62 27).
461
+ * Konsolidierungs-Kandidaten: FLOW-Paare, die DENSELBEN Datenvertrag tragen UND dieselbe
462
+ * Verbindung sind gleiche Produzenten, gleiche Konsumenten (CR-GC-444, CR-SM-309).
463
+ *
464
+ * CR-SM-309: vorher genuegte der gleiche Vertrag. Seit CR-GC-510 ist „ein FLOW je
465
+ * Verbindung, n FLOW -> 1 SCHEMA" das Modell, und IO-02 blockt am Gate. Ein Merge zweier
466
+ * FLOWs mit verschiedenen Produzenten legte zwei Quellen in einen Fluss (IO-02); einer mit
467
+ * gleichem Produzenten, aber verschiedenen Konsumenten behauptete Verbindungen, die es nicht
468
+ * gibt. Gemessen an graphcode v261: alle 11 Vorschlaege der alten Fassung verletzten IO-02
469
+ * und wurden am Gate abgewiesen. Echt verschmelzbar sind nur DUPLIKATE — derselbe Vertrag
470
+ * auf derselben Verbindung, zweimal modelliert.
445
471
  *
446
472
  * „Derselbe Vertrag" = `contractSimilarity` (identischer SCHEMA-Knoten oder
447
473
  * ND-02-Duplikat) — EINE Definition, keine neue Zahl. Ein FLOW ohne oder mit
@@ -492,11 +518,18 @@ export function mergeCandidates(g) {
492
518
  }
493
519
  const edits = [];
494
520
  for (const key of [...grouped.keys()].sort()) {
495
- const members = grouped.get(key)
521
+ // CR-SM-309: innerhalb des Vertrags nur FLOWs derselben Verbindung (Endpunkt-Signatur).
522
+ const byEnds = new Map();
523
+ for (const f of grouped.get(key)) {
524
+ const sig = endpointSignature(g, f.id);
525
+ byEnds.set(sig, [...(byEnds.get(sig) ?? []), f]);
526
+ }
527
+ const duplicates = [...byEnds.keys()].sort().map((sig) => byEnds.get(sig)).find((m) => m.length >= 2);
528
+ if (!duplicates)
529
+ continue;
530
+ const members = duplicates
496
531
  .slice()
497
532
  .sort((a, b) => ioDegree(g, b.id) - ioDegree(g, a.id) || a.id.localeCompare(b.id));
498
- if (members.length < 2)
499
- continue;
500
533
  const [keep, drop] = members;
501
534
  const merges = coupledMerges(g, drop, keep);
502
535
  if (merges === null)
@@ -19,12 +19,12 @@ import { type ReadinessDimensionType, type ReadinessReportType } from '@sigloch/
19
19
  * CR-SM-233: `policy` reicht bis zu den Regeln durch und hat keinen Default — sonst urteilte
20
20
  * die Readiness mit einer anderen Schwelle als die, die der Host anzeigt.
21
21
  *
22
- * CR-SM-235: `readyThreshold` ebenso. Vorher stand hier `0.7` und im graphcode-Treiber `0.8`
23
- * (`generate.ts`, Default-Parameter) — zwei Werte fuer dieselbe Frage „ist diese Dimension zu
24
- * schwach?", und der Konsument konnte nicht wissen, welcher gilt. Den Wert liefert die Config
25
- * (CR-GC-329); diese Funktion macht nur die Zweitmeinung unmoeglich.
22
+ * CR-SM-310: keine Schwelle mehr. CR-SM-235 hatte `readyThreshold` hereingereicht, damit es nur
23
+ * EINEN Wert fuer „ist diese Dimension zu schwach?" gibt — das Urteil stand danach trotzdem
24
+ * zweimal, hier als `ready` und beim Konsumenten (graphcode generate.ts). Messen und Urteilen
25
+ * sind getrennt: diese Funktion misst, der Konsument urteilt mit seiner Config.
26
26
  */
27
- export declare function computeReadiness(graph: OntologyGraph, policy: MetricPolicy, readyThreshold: number): ReadinessReportType;
27
+ export declare function computeReadiness(graph: OntologyGraph, policy: MetricPolicy): ReadinessReportType;
28
28
  /**
29
29
  * CR-146 / CR-SM-235: Anzahl anwendbarer Pruefungen je Dimension.
30
30
  *
@@ -14,12 +14,12 @@ import { ReadinessDimension, RULE_TO_DIMENSION, } from '@sigloch/contracts/se';
14
14
  * CR-SM-233: `policy` reicht bis zu den Regeln durch und hat keinen Default — sonst urteilte
15
15
  * die Readiness mit einer anderen Schwelle als die, die der Host anzeigt.
16
16
  *
17
- * CR-SM-235: `readyThreshold` ebenso. Vorher stand hier `0.7` und im graphcode-Treiber `0.8`
18
- * (`generate.ts`, Default-Parameter) — zwei Werte fuer dieselbe Frage „ist diese Dimension zu
19
- * schwach?", und der Konsument konnte nicht wissen, welcher gilt. Den Wert liefert die Config
20
- * (CR-GC-329); diese Funktion macht nur die Zweitmeinung unmoeglich.
17
+ * CR-SM-310: keine Schwelle mehr. CR-SM-235 hatte `readyThreshold` hereingereicht, damit es nur
18
+ * EINEN Wert fuer „ist diese Dimension zu schwach?" gibt — das Urteil stand danach trotzdem
19
+ * zweimal, hier als `ready` und beim Konsumenten (graphcode generate.ts). Messen und Urteilen
20
+ * sind getrennt: diese Funktion misst, der Konsument urteilt mit seiner Config.
21
21
  */
22
- export function computeReadiness(graph, policy, readyThreshold) {
22
+ export function computeReadiness(graph, policy) {
23
23
  const allViolations = evaluateAllRules(graph, policy);
24
24
  // Group violations by dimension
25
25
  const violationsByDim = new Map();
@@ -83,8 +83,6 @@ export function computeReadiness(graph, policy, readyThreshold) {
83
83
  violations,
84
84
  applicable,
85
85
  coreApplicable,
86
- // Nicht messbar ist nicht bereit — nie `true` bei `null`.
87
- ready: score !== null && score >= readyThreshold,
88
86
  };
89
87
  });
90
88
  // CR-SM-237: `overallScore` faellt — das ungewichtete Mittel dieser Scores, von keinem
@@ -140,13 +138,25 @@ export function coreTypesOf(dim) {
140
138
  for (const type of new Set(d.domain))
141
139
  seenIn[type] = (seenIn[type] ?? 0) + 1;
142
140
  }
143
- const majority = defs.length / 2;
144
- const core = Object.keys(seenIn).filter(type => seenIn[type] > majority).sort();
145
- // Keine Mehrheit ableitbar (die Regeln verteilen sich gleichmaessig ueber mehrere Typen):
146
- // dann ist der Kern ALLES, was die Dimension prueft. Der Waechter greift damit nur noch,
147
- // wenn auch `applicable` 0 waere das Verhalten bleibt exakt wie vor CR-SM-270. Lieber
148
- // nicht greifen als am falschen Ort greifen.
149
- return core.length > 0 ? core : [...new Set(defs.flatMap(d => d.domain))].sort();
141
+ // CR-SM-328: die RELATIVE Mehrheit mit eindeutigem Sieger, nicht die absolute.
142
+ //
143
+ // Die absolute Mehrheit (> n/2) haengt an einer Zahl, die mit dem KATALOG waechst und mit
144
+ // dem Gegenstand der Dimension nichts zu tun hat. Gemessen an `ver`: sieben Regeln mit TEST
145
+ // in vier ergaben 4 > 3,5 und den Kern [TEST]; nach CR-SM-319 (R-32, domain SCHEMA) waren es
146
+ // acht Regeln mit TEST in vier, also 4 > 4 = falsch — der Fallback machte ALLES zum Kern und
147
+ // legte den Waechter dieser Dimension still, ohne dass sich an `ver` selbst etwas geaendert
148
+ // haette. Eine einzige neue Regel mit fremder Grundgesamtheit genuegte.
149
+ //
150
+ // Die relative Mehrheit beantwortet dieselbe Frage stabil: welcher Typ wird von den MEISTEN
151
+ // Regeln dieser Dimension geprueft. Sie ist eine Abschwaechung, keine Umkehr — eine absolute
152
+ // Mehrheit IST immer das eindeutige Maximum, das Ergebnis kann sich also nur dort aendern,
153
+ // wo vorher der Fallback stand.
154
+ const top = Math.max(...Object.values(seenIn));
155
+ const core = Object.keys(seenIn).filter(type => seenIn[type] === top).sort();
156
+ // Gleichstand an der Spitze: kein Typ ist DER Gegenstand, also ist der Kern ALLES, was die
157
+ // Dimension prueft. Der Waechter greift dann nur noch, wenn auch `applicable` 0 waere —
158
+ // Verhalten wie vor CR-SM-270. Lieber nicht greifen als am falschen Ort greifen.
159
+ return core.length === 1 ? core : [...new Set(defs.flatMap(d => d.domain))].sort();
150
160
  }
151
161
  function computeCoreApplicable(countByType) {
152
162
  const result = {};
@@ -18,7 +18,7 @@
18
18
  * Edit; Vorschläge laufen durchs Gate (nie auto-apply).
19
19
  */
20
20
  import { evaluateAllRules, isValidTrace } from '@sigloch/contracts/se';
21
- import { classOf, MT_IRRELEVANT_POLICY } from './rule-classify.js';
21
+ import { classOf, classOfViolation, MT_IRRELEVANT_POLICY } from './rule-classify.js';
22
22
  /** Deterministic timestamp for synthesized traces (no Date.now — keeps metrics stable). */
23
23
  const APPLY_TS = '2026-07-26T00:00:00.000Z';
24
24
  /**
@@ -52,19 +52,25 @@ function addTrace(graph, source, target, type) {
52
52
  * @returns graph' + whether an edit was applied. Deterministic.
53
53
  */
54
54
  export function applyRule(rule, graph, knownViolations) {
55
- const cls = classOf(rule.id);
56
55
  const allViolations = knownViolations ?? evaluateAllRules(graph, MT_IRRELEVANT_POLICY);
57
- const violations = allViolations.filter((v) => v.rule_id === rule.id);
58
- if (violations.length === 0) {
56
+ const typeById = new Map(graph.elements.map((e) => [e.id, e.type]));
57
+ const forRule = allViolations.filter((v) => v.rule_id === rule.id);
58
+ if (forRule.length === 0) {
59
59
  return { graph, applied: false, reason: 'rule does not fire on this graph (empty Δm)' };
60
60
  }
61
- if (cls.class !== 'Operator') {
61
+ // CR-SM-334: die Klasse haengt bei R-21 am Ankerknoten des BEFUNDS, nicht nur an der Regel —
62
+ // ein Befund am Sender-FUNC (Zweig 3, keine gemeinsame Kette) ist kein Operator, auch wenn
63
+ // die Regel als Ganzes einer ist. Sonst schlaegt der Optimizer einen Test vor, wo die
64
+ // Modellierung fehlt.
65
+ const violations = forRule.filter((v) => classOfViolation(rule.id, typeById.get(v.element_id)).class === 'Operator');
66
+ if (violations.length === 0) {
67
+ const cls = classOfViolation(rule.id, typeById.get(forRule[0].element_id));
62
68
  return { graph, applied: false, reason: `class=${cls.class}: not a transforming operator` };
63
69
  }
64
70
  // Find a single additive edge that (a) is meta-model valid for the two
65
71
  // elements' stored types (isValidTrace) and (b) links two currently-
66
72
  // unconnected nodes, so it genuinely moves the topology.
67
- const typeById = new Map(graph.elements.map((e) => [e.id, e.type]));
73
+ // (`typeById` steht seit CR-SM-334 weiter oben — die Klassifikation je Befund braucht es.)
68
74
  // CR-SM-266 B: die where-Patterns lesen die Kinds des Zielknotens mit. Ohne sie wuerde
69
75
  // `isValidTrace` jede satisfy-Kante ablehnen und der Operator griffe stumm auf einen anderen
70
76
  // Kantentyp aus — ein falscher Vorschlag statt keinem.
@@ -21,6 +21,33 @@ export interface Classification {
21
21
  export declare const CLASS_MAP: Record<string, Classification>;
22
22
  /** Classify a single rule id (falls back to ambiguous-unknown). */
23
23
  export declare function classOf(ruleId: string): Classification;
24
+ /**
25
+ * CR-SM-334 (ITEM-2026-095) — die Klasse einer Regel, die MEHRERE Fragen stellt, haengt am
26
+ * ANKERKNOTEN des Befunds, nicht nur an der Regel-ID.
27
+ *
28
+ * Bisher einziger Fall: R-21 seit seiner scharfen Fassung (CR-SM-313). Die Regel hat drei
29
+ * Zweige, und ihre `domain` waechst genau deshalb auf ['FCHAIN','FUNC']:
30
+ * 1. ein Ende in GAR KEINER Kette -> still, das ist R-30s Aussage
31
+ * 2. gemeinsame Kette, aber keine geprueft -> Befund an der KETTE (FCHAIN)
32
+ * 3. beide in Ketten, aber KEINE gemeinsame -> Befund am SENDER (FUNC)
33
+ *
34
+ * Zweig 2 ist ein Operator: es fehlt ein Integrationstest, der Fix fuegt TEST + verify hinzu.
35
+ * Zweig 3 ist etwas anderes, und der Unterschied ist teuer. Dort fehlt kein Test, sondern die
36
+ * gemeinsame KETTE — zwei Funktionen reichen einander etwas, ohne dass das Modell sagt, in
37
+ * welchem Ablauf das passiert. Ein Operator, der hier einen Test vorschlaegt, erzeugt
38
+ * Testarbeit an einer Stelle, an der Modellierungsarbeit fehlt: der Test wuerde eine
39
+ * Uebergabe belegen, die im Modell gar nicht vorgesehen ist.
40
+ *
41
+ * ENTSCHEIDUNG ZUR KLASSE (Kriterium 3 des CR): Zweig 3 ist **Constraint, ambiguous**. Der
42
+ * Fix waere zwar additiv (compose-Kanten), aber WELCHE Kette die beiden teilen sollen —
43
+ * die des Senders verlaengern, die des Empfaengers, oder eine neue aufmachen — steht in
44
+ * keinem Feld; das ist Modellierungsabsicht. Ein Operator daraus waere geraten, nicht
45
+ * abgeleitet, und wuerde falsche compose-Kanten erzeugen. Praezedenz: IO-02 ("welcher
46
+ * Produzent welche Konsumenten behaelt, steht in keinem Feld") und die sechs RC-Regeln
47
+ * (CR-SM-305). `applyRule`/`suggestEdits` verwerfen Nicht-Operatoren — Zweig 3 bleibt damit
48
+ * Befund und Steuersignal, was er ist.
49
+ */
50
+ export declare function classOfViolation(ruleId: string, elementType?: string): Classification;
24
51
  export interface ClassifiedRule {
25
52
  id: string;
26
53
  name: string;
@@ -53,13 +53,20 @@ export const MT_IRRELEVANT_POLICY = {
53
53
  export const CLASS_MAP = {
54
54
  // --- Operators: fix adds a trace/element (topology-changing) ---------------
55
55
  'R-01': { class: 'Operator', rationale: 'add verify trace REQ←TEST' },
56
+ // CR-SM-328 (nachgezogen aus CR-SM-319): derselbe Fix wie R-01, nur am SCHEMA —
57
+ // eine `verify`-Kante kommt hinzu, die Topologie bewegt sich.
58
+ 'R-32': { class: 'Operator', rationale: 'add verify trace SCHEMA←TEST — the contract test is missing, not mis-shaped' },
56
59
  'R-02': { class: 'Operator', rationale: 'add satisfy trace FUNC→REQ' },
57
60
  'R-05': { class: 'Operator', rationale: 'add verify trace TEST→REQ' },
58
61
  'R-10': { class: 'Operator', rationale: 'add io traces to complete the FLOW' },
59
62
  'R-15': { class: 'Operator', rationale: 'add compose trace FCHAIN→FUNC' },
60
63
  'R-16': { class: 'Operator', rationale: 'add io trace ACTOR→UC/FLOW' },
61
64
  'R-17': { class: 'Operator', rationale: 'add compose trace SYS→child' },
62
- 'R-21': { class: 'Operator', rationale: 'add integration TEST + verify for FUNC↔FUNC link' },
65
+ // CR-SM-334 (ITEM-2026-095, zugesagt in CR-SM-313): dieser Eintrag gilt fuer ZWEIG 2
66
+ // Befund an der KETTE, beide Enden teilen eine Kette, nur geprueft ist sie nicht. Zweig 3
67
+ // (Befund am SENDER-FUNC) verlangt einen anderen Fix und wird in `classOfViolation`
68
+ // unterschieden; die Klasse allein kann das nicht tragen, denn sie haengt am Ankerknoten.
69
+ 'R-21': { class: 'Operator', rationale: 'chain has no integration TEST — add TEST + verify for the shared chain' },
63
70
  'R-22': { class: 'Operator', rationale: 'add allocate trace FUNC→MOD' },
64
71
  'R-23': { class: 'Operator', rationale: 'add allocate trace MOD←FUNC' },
65
72
  'RD-01': { class: 'Operator', rationale: 'resolve REQ by adding satisfy/decompose trace' },
@@ -73,6 +80,10 @@ export const CLASS_MAP = {
73
80
  'FC-02': { class: 'Operator', rationale: 'add FCHAIN via compose trace to leaf UC' },
74
81
  'SC-02': { class: 'Operator', rationale: 'link FLOW→SCHEMA via relation trace' },
75
82
  'IO-01': { class: 'Operator', rationale: 'add FLOW element + io traces between the FUNC pair' },
83
+ // CR-SM-309: IO-02 (contracts 20) — die Reparatur ist ein Auftrennen je Verbindung. Welcher
84
+ // Produzent welche Konsumenten behaelt, steht in keinem Feld, sondern im Code; ein Operator
85
+ // daraus waere geraten (Praezedenz RC-*, CR-SM-305).
86
+ 'IO-02': { class: 'Constraint', rationale: 'one producer per FLOW — the split per connection needs code evidence, no deterministic operator' },
76
87
  // CR-GC-366: beide Fixes fuegen eine Trace hinzu, also Operator wie R-15 (compose) und R-10 (io).
77
88
  'R-30': { class: 'Operator', rationale: 'add compose trace FCHAIN→FUNC to bind the function into a chain' },
78
89
  'R-31': { class: 'Operator', rationale: 'add io traces FLOW→FUNC / FUNC→FLOW to wire the function up' },
@@ -81,7 +92,7 @@ export const CLASS_MAP = {
81
92
  'FM-02': { class: 'Operator', rationale: 'create mitigation REQ + compose trace' },
82
93
  'FM-03': { class: 'Operator', rationale: 'add TEST(passed) + verify trace to risk REQ' },
83
94
  // --- Constraints: remove/repair/reduce, or attribute/text-only -------------
84
- 'R-04': { class: 'Constraint', rationale: 'module too largesplit, non-additive' },
95
+ 'R-04': { class: 'Constraint', rationale: 'module boundary too widemerge contracts or move functions, non-additive' },
85
96
  'R-08': { class: 'Constraint', rationale: 'repair dangling trace endpoint' },
86
97
  'R-12': { class: 'Constraint', rationale: 'break dependency cycle — remove an edge' },
87
98
  'R-18': { class: 'Constraint', rationale: 'invalid trace pattern — change/remove trace' },
@@ -95,11 +106,15 @@ export const CLASS_MAP = {
95
106
  'RD-02': { class: 'Constraint', rationale: 'decomposition consistency — repair existing' },
96
107
  'RD-03': { class: 'Constraint', rationale: 'premature decomposition — remove children' },
97
108
  'RD-04': { class: 'Constraint', rationale: 'decomposition breadth 7±2 — split level, non-additive' },
109
+ 'RD-05': { class: 'Constraint', rationale: 'degenerate level (< min children) — dissolve into parent, non-additive' },
98
110
  'MS-02': { class: 'Constraint', rationale: 'dangling dependency — fix relation target' },
99
111
  'UC-04': { class: 'Constraint', rationale: 'goal = description text — no topology change' },
100
112
  'FC-03': { class: 'Constraint', rationale: 'flatten chain — move nested funcs' },
101
113
  'MT-01': { class: 'Constraint', rationale: 'instability threshold — restructure' },
102
114
  'MT-02': { class: 'Constraint', rationale: 'cohesion (LCOM4) threshold — restructure' },
115
+ // CR-SM-327: wie MT-02 — der Fix zerlegt oder verdrahtet um, er fuegt keine einzelne
116
+ // Kante hinzu, die der Operator raten koennte.
117
+ 'MT-04': { class: 'Constraint', rationale: 'split the whitebox or wire its groups — a restructuring, not one additive trace' },
103
118
  'BQ-01': { class: 'Constraint', rationale: 'unambiguous — text quality' },
104
119
  'BQ-02': { class: 'Constraint', rationale: 'verifiable — text quality' },
105
120
  'BQ-04': { class: 'Constraint', rationale: 'necessary — text/scope quality' },
@@ -141,12 +156,50 @@ export const CLASS_MAP = {
141
156
  'RC-04': { class: 'Constraint', rationale: 'SCHEMA wird an seiner Schnittstelle nicht geparst — Codeänderung' },
142
157
  'RC-05': { class: 'Constraint', rationale: 'Import quert eine undokumentierte Modulgrenze — Import entfernen oder Fluss modellieren', ambiguous: true },
143
158
  'RC-06': { class: 'Constraint', rationale: 'externe Bindung nennt ein nicht deklariertes Paket — dependency oder realRef' },
159
+ 'RC-07': { class: 'Constraint', rationale: 'CR-Knoten widerspricht docs/cr — Status angleichen oder Knoten anlegen, das Verzeichnis entscheidet' },
144
160
  };
145
161
  const UNKNOWN = { class: 'Constraint', rationale: 'unclassified — not in CLASS_MAP', ambiguous: true };
146
162
  /** Classify a single rule id (falls back to ambiguous-unknown). */
147
163
  export function classOf(ruleId) {
148
164
  return CLASS_MAP[ruleId] ?? UNKNOWN;
149
165
  }
166
+ /**
167
+ * CR-SM-334 (ITEM-2026-095) — die Klasse einer Regel, die MEHRERE Fragen stellt, haengt am
168
+ * ANKERKNOTEN des Befunds, nicht nur an der Regel-ID.
169
+ *
170
+ * Bisher einziger Fall: R-21 seit seiner scharfen Fassung (CR-SM-313). Die Regel hat drei
171
+ * Zweige, und ihre `domain` waechst genau deshalb auf ['FCHAIN','FUNC']:
172
+ * 1. ein Ende in GAR KEINER Kette -> still, das ist R-30s Aussage
173
+ * 2. gemeinsame Kette, aber keine geprueft -> Befund an der KETTE (FCHAIN)
174
+ * 3. beide in Ketten, aber KEINE gemeinsame -> Befund am SENDER (FUNC)
175
+ *
176
+ * Zweig 2 ist ein Operator: es fehlt ein Integrationstest, der Fix fuegt TEST + verify hinzu.
177
+ * Zweig 3 ist etwas anderes, und der Unterschied ist teuer. Dort fehlt kein Test, sondern die
178
+ * gemeinsame KETTE — zwei Funktionen reichen einander etwas, ohne dass das Modell sagt, in
179
+ * welchem Ablauf das passiert. Ein Operator, der hier einen Test vorschlaegt, erzeugt
180
+ * Testarbeit an einer Stelle, an der Modellierungsarbeit fehlt: der Test wuerde eine
181
+ * Uebergabe belegen, die im Modell gar nicht vorgesehen ist.
182
+ *
183
+ * ENTSCHEIDUNG ZUR KLASSE (Kriterium 3 des CR): Zweig 3 ist **Constraint, ambiguous**. Der
184
+ * Fix waere zwar additiv (compose-Kanten), aber WELCHE Kette die beiden teilen sollen —
185
+ * die des Senders verlaengern, die des Empfaengers, oder eine neue aufmachen — steht in
186
+ * keinem Feld; das ist Modellierungsabsicht. Ein Operator daraus waere geraten, nicht
187
+ * abgeleitet, und wuerde falsche compose-Kanten erzeugen. Praezedenz: IO-02 ("welcher
188
+ * Produzent welche Konsumenten behaelt, steht in keinem Feld") und die sechs RC-Regeln
189
+ * (CR-SM-305). `applyRule`/`suggestEdits` verwerfen Nicht-Operatoren — Zweig 3 bleibt damit
190
+ * Befund und Steuersignal, was er ist.
191
+ */
192
+ export function classOfViolation(ruleId, elementType) {
193
+ if (ruleId === 'R-21' && elementType === 'FUNC') {
194
+ return {
195
+ class: 'Constraint',
196
+ rationale: 'handover without a shared chain — model the common FCHAIN; which chain the two '
197
+ + 'functions should share is modelling intent, not derivable, so no operator',
198
+ ambiguous: true,
199
+ };
200
+ }
201
+ return classOf(ruleId);
202
+ }
150
203
  /** Classify the entire live rule catalog (ALL_RULE_DEFS). */
151
204
  export function classifyAll() {
152
205
  return ALL_RULE_DEFS.map((r) => {
package/dist/steer.d.ts CHANGED
@@ -47,10 +47,11 @@
47
47
  * weiter zählt, was fehlt. Die beiden sind komplementär: readiness misst ABDECKUNG (wie
48
48
  * viele Stellen sind erledigt), dieser Score AUSPRÄGUNG (wie schlimm ist die schlimmste
49
49
  * offene). Beide lesen denselben Regelstrom.
50
- * 3. Die MOD-Whitebox hat keine eigene Randbreiten-Regel R-04 sieht den Rand nur zusammen mit
51
- * der Grösse, also ist ein kleines Modul mit breitem Rand stumm (gemessen:
52
- * `MOD-kernel-measure` 10 Verträge, `mod_api_server_ts` 17). Der Score erbt den blinden
53
- * Fleck; ihn zu schliessen ist ein eigener Grammatik-Vorgang.
50
+ * 3. ~~Die MOD-Whitebox hat keine eigene Randbreiten-Regel.~~ GESCHLOSSEN mit CR-SM-312: R-04
51
+ * misst jetzt allein den Modulrand, gegen dieselbe Schwelle wie BW-02, und steht unten in
52
+ * `STEER_RULES`. Vorher sah R-04 den Rand nur zusammen mit der Grösse, also war ein kleines
53
+ * Modul mit breitem Rand stumm (gemessen: `MOD-kernel-measure` 9 Verträge bei 7 Funktionen,
54
+ * `mod_api_server_ts` 17). Die Grösse ist seit CR-SM-311 RD-04s Frage.
54
55
  */
55
56
  import type { RuleViolation } from '@sigloch/contracts/se';
56
57
  /**
@@ -61,7 +62,7 @@ import type { RuleViolation } from '@sigloch/contracts/se';
61
62
  * Bewusst eine geschlossene Liste und NICHT aus `context.value !== undefined` abgeleitet: eine
62
63
  * künftige Regel, die zufällig eine Zahl mitführt, würde sonst still zum Steuersignal.
63
64
  */
64
- export declare const STEER_RULES: readonly ["RD-04", "BW-02", "CR-01", "MT-02"];
65
+ export declare const STEER_RULES: readonly ["RD-04", "BW-02", "R-04", "CR-01", "MT-02"];
65
66
  /** Der Ausgleichsterm. Klein genug, dass er ein echtes Maximum nie überstimmt. */
66
67
  export declare const EPS_AUGMENT = 0.001;
67
68
  export interface SteerScore {
@@ -79,6 +80,34 @@ export interface SteerScore {
79
80
  /** Zahl der Blackboxes, die in die Rechnung eingegangen sind. */
80
81
  measured: number;
81
82
  }
83
+ /**
84
+ * CR-SM-337 (ITEM-2026-059): ein TERM je gemessener Blackbox — die Zwischenrechnung, aus der
85
+ * `worst` und `mean` entstehen.
86
+ *
87
+ * Bis hierher gab `steerScore` nur das Ergebnis heraus. Wer die Terme brauchte — der Host, um
88
+ * den Steuerungsraum zu exportieren, das GVE-Dashboard, um ihn zu zeigen — musste sie selbst
89
+ * aus dem Regelstrom ableiten, also die Normierung ein zweites Mal schreiben. Eine zweite
90
+ * Rechnung ist eine zweite Wahrheit; hier ist sie vermeidbar, weil die Zwischenschritte schon
91
+ * existierten und nur nicht herausgereicht wurden.
92
+ *
93
+ * Die Form spiegelt `SteerTerm` in @sigloch/contracts (`se/readiness.ts`) — dort der Vertrag,
94
+ * hier die Rechnung.
95
+ */
96
+ export interface SteerTerm {
97
+ ruleId: string;
98
+ elementId: string;
99
+ value: number;
100
+ threshold: number;
101
+ /** `max(0, (value − threshold) / threshold)`, dimensionslos. */
102
+ overshoot: number;
103
+ }
104
+ /**
105
+ * Die Terme des Steuerungsraums, in kanonischer Befund-Reihenfolge (CR-SM-240). Rein.
106
+ *
107
+ * DIE Quelle für alles Weitere: `steerScore` rechnet daraus, der Host exportiert sie
108
+ * unverändert. Es gibt genau eine Normierung und genau eine Regel-Liste.
109
+ */
110
+ export declare function steerTerms(violations: readonly RuleViolation[]): SteerTerm[];
82
111
  /** Der Chebyshev-Score eines Zustands, aus seinem Regelstrom. Rein. */
83
112
  export declare function steerScore(violations: readonly RuleViolation[]): SteerScore;
84
113
  /** Ein Kandidat, so weit der Ranker ihn kennen muss. */
package/dist/steer.js CHANGED
@@ -26,7 +26,7 @@
26
26
  * 305 Abhaengigkeiten (11 %), 132 von 146 Modulen bei null, Schwanz bis 6. Lokal, gerichtet,
27
27
  * budgetierbar, nicht entartet: drei von drei.
28
28
  */
29
- export const STEER_RULES = ['RD-04', 'BW-02', 'CR-01', 'MT-02'];
29
+ export const STEER_RULES = ['RD-04', 'BW-02', 'R-04', 'CR-01', 'MT-02'];
30
30
  /** Der Ausgleichsterm. Klein genug, dass er ein echtes Maximum nie überstimmt. */
31
31
  export const EPS_AUGMENT = 1e-3;
32
32
  const NEUTRAL = { worst: 0, worstAt: null, mean: 0, score: 0, measured: 0 };
@@ -37,39 +37,56 @@ const NEUTRAL = { worst: 0, worstAt: null, mean: 0, score: 0, measured: 0 };
37
37
  * ist nicht „keine Überschreitung". Dieselbe Konvention wie `policy.X = null` und
38
38
  * `moduleMetrics.instability = null`.
39
39
  */
40
- function overshootOf(v) {
40
+ function termOf(v) {
41
41
  const value = v.context?.value;
42
42
  const budget = v.context?.threshold;
43
43
  if (typeof value !== 'number' || typeof budget !== 'number' || budget <= 0)
44
44
  return null;
45
- return Math.max(0, (value - budget) / budget);
45
+ return {
46
+ ruleId: v.rule_id,
47
+ elementId: v.element_id,
48
+ value,
49
+ threshold: budget,
50
+ overshoot: Math.max(0, (value - budget) / budget),
51
+ };
52
+ }
53
+ /**
54
+ * Die Terme des Steuerungsraums, in kanonischer Befund-Reihenfolge (CR-SM-240). Rein.
55
+ *
56
+ * DIE Quelle für alles Weitere: `steerScore` rechnet daraus, der Host exportiert sie
57
+ * unverändert. Es gibt genau eine Normierung und genau eine Regel-Liste.
58
+ */
59
+ export function steerTerms(violations) {
60
+ const rules = STEER_RULES;
61
+ const terms = [];
62
+ for (const v of violations) {
63
+ if (!rules.includes(v.rule_id))
64
+ continue;
65
+ const t = termOf(v);
66
+ if (t !== null)
67
+ terms.push(t);
68
+ }
69
+ return terms;
46
70
  }
47
71
  /** Der Chebyshev-Score eines Zustands, aus seinem Regelstrom. Rein. */
48
72
  export function steerScore(violations) {
49
- const rules = STEER_RULES;
73
+ const terms = steerTerms(violations);
74
+ if (terms.length === 0)
75
+ return NEUTRAL;
50
76
  let worst = 0;
51
77
  let worstAt = null;
52
78
  let sum = 0;
53
- let measured = 0;
54
- for (const v of violations) {
55
- if (!rules.includes(v.rule_id))
56
- continue;
57
- const d = overshootOf(v);
58
- if (d === null)
59
- continue;
60
- measured += 1;
61
- sum += d;
79
+ for (const t of terms) {
80
+ sum += t.overshoot;
62
81
  // `>` statt `>=`: bei Gleichstand gewinnt der zuerst gesehene, und die Befund-Sequenz ist
63
82
  // seit CR-SM-240 kanonisch — damit ist `worstAt` deterministisch.
64
- if (d > worst) {
65
- worst = d;
66
- worstAt = { ruleId: v.rule_id, elementId: v.element_id };
83
+ if (t.overshoot > worst) {
84
+ worst = t.overshoot;
85
+ worstAt = { ruleId: t.ruleId, elementId: t.elementId };
67
86
  }
68
87
  }
69
- if (measured === 0)
70
- return NEUTRAL;
71
- const mean = sum / measured;
72
- return { worst, worstAt, mean, score: worst + EPS_AUGMENT * mean, measured };
88
+ const mean = sum / terms.length;
89
+ return { worst, worstAt, mean, score: worst + EPS_AUGMENT * mean, measured: terms.length };
73
90
  }
74
91
  /**
75
92
  * Vergleicht zwei Kandidaten. `< 0` heisst **a ist besser**; direkt sortierbar.
package/dist/suggest.js CHANGED
@@ -19,7 +19,7 @@ import { evaluateAllRules } from '@sigloch/contracts/se';
19
19
  import { metrics, toArray } from './metrics.js';
20
20
  import { steerScore, beatsBySteer } from './steer.js';
21
21
  import { applyRule } from './rule-apply.js';
22
- import { classOf, MT_IRRELEVANT_POLICY } from './rule-classify.js';
22
+ import { classOfViolation, MT_IRRELEVANT_POLICY } from './rule-classify.js';
23
23
  import { fixFor, mergeCandidates, applyMergeEdit, MERGE_OPERATOR_ID } from './fix-templates.js';
24
24
  /**
25
25
  * Rankt die feuernden Operator-Regeln danach, wie weit ihr Sonden-Edit den **Chebyshev-Score**
@@ -43,9 +43,14 @@ export function suggestEdits(graph, opts = {}) {
43
43
  for (const v of violations)
44
44
  if (!firstByRule.has(v.rule_id))
45
45
  firstByRule.set(v.rule_id, v);
46
+ // CR-SM-334: die Klasse haengt bei R-21 am Ankerknoten des Befunds. Ohne den Typ schlaegt
47
+ // der Vorschlag fuer Zweig 3 (Uebergabe ohne gemeinsame Kette) einen Integrationstest vor,
48
+ // wo die gemeinsame Kette fehlt — Testarbeit statt Modellierungsarbeit.
49
+ const typeOfElement = new Map(graph.elements.map((e) => [e.id, e.type]));
50
+ const classOfFinding = (ruleId, elementId) => classOfViolation(ruleId, typeOfElement.get(elementId));
46
51
  const suggestions = [];
47
52
  for (const [ruleId, v] of firstByRule) {
48
- if (classOf(ruleId).class !== 'Operator')
53
+ if (classOfFinding(ruleId, v.element_id).class !== 'Operator')
49
54
  continue;
50
55
  const probe = applyRule({ id: ruleId }, graph, violations);
51
56
  if (!probe.applied)
@@ -77,7 +82,7 @@ export function suggestEdits(graph, opts = {}) {
77
82
  // ohnehin nicht anwendbar — vor diesem Block fiel der Fund deshalb ganz heraus, mitsamt
78
83
  // dem vertragsbauenden Arm des Aktionsraums (CR-GC-430: COHESIVE 5 -> 2 Schritte).
79
84
  for (const [ruleId, v] of firstByRule) {
80
- if (classOf(ruleId).class === 'Operator')
85
+ if (classOfFinding(ruleId, v.element_id).class === 'Operator')
81
86
  continue;
82
87
  const edit = fixFor(v, graph);
83
88
  if (!edit || edit.op !== 'add-trace')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sigloch/se-engine",
3
- "version": "1.4.1",
3
+ "version": "1.6.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",