@sigloch/se-engine 1.5.0 → 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.
@@ -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;
@@ -62,7 +62,11 @@ export const CLASS_MAP = {
62
62
  'R-15': { class: 'Operator', rationale: 'add compose trace FCHAIN→FUNC' },
63
63
  'R-16': { class: 'Operator', rationale: 'add io trace ACTOR→UC/FLOW' },
64
64
  'R-17': { class: 'Operator', rationale: 'add compose trace SYS→child' },
65
- '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' },
66
70
  'R-22': { class: 'Operator', rationale: 'add allocate trace FUNC→MOD' },
67
71
  'R-23': { class: 'Operator', rationale: 'add allocate trace MOD←FUNC' },
68
72
  'RD-01': { class: 'Operator', rationale: 'resolve REQ by adding satisfy/decompose trace' },
@@ -159,6 +163,43 @@ const UNKNOWN = { class: 'Constraint', rationale: 'unclassified — not in CLASS
159
163
  export function classOf(ruleId) {
160
164
  return CLASS_MAP[ruleId] ?? UNKNOWN;
161
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
+ }
162
203
  /** Classify the entire live rule catalog (ALL_RULE_DEFS). */
163
204
  export function classifyAll() {
164
205
  return ALL_RULE_DEFS.map((r) => {
package/dist/steer.d.ts CHANGED
@@ -80,6 +80,34 @@ export interface SteerScore {
80
80
  /** Zahl der Blackboxes, die in die Rechnung eingegangen sind. */
81
81
  measured: number;
82
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[];
83
111
  /** Der Chebyshev-Score eines Zustands, aus seinem Regelstrom. Rein. */
84
112
  export declare function steerScore(violations: readonly RuleViolation[]): SteerScore;
85
113
  /** Ein Kandidat, so weit der Ranker ihn kennen muss. */
package/dist/steer.js CHANGED
@@ -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.5.0",
3
+ "version": "1.6.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",