@sigloch/se-engine 1.3.0 → 1.4.1

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.
@@ -1,11 +1,63 @@
1
1
  import type { OntologyGraph, RuleViolation, TraceType, ElementType } from '@sigloch/contracts/se';
2
2
  export interface SuggestedEdit {
3
- op: 'add-trace';
3
+ /**
4
+ * `'add-trace'` — die Kante `source -type-> target` wird angehängt (ggf. mit
5
+ * `retire`, s. u.).
6
+ *
7
+ * `'merge-nodes'` — CR-GC-444, der Konsolidierungs-Operator: `target`
8
+ * ABSORBIERT `source` (Semantik des gleichnamigen `MutateCommand`), `source`
9
+ * verschwindet. `type` benennt dann die Trace, über die die Kopplung läuft
10
+ * (heute `relation`, der Vertrag `FLOW -relation-> SCHEMA`); die gekoppelten
11
+ * Zusatz-Merges stehen in `merges`.
12
+ */
13
+ op: 'add-trace' | 'merge-nodes';
4
14
  source: string;
5
15
  target: string;
6
16
  type: TraceType;
7
17
  /** Warum genau dieses Ziel — Fundstelle im Elementtext bzw. Eindeutigkeit. */
8
18
  rationale: string;
19
+ /**
20
+ * CR-GC-435 — die EINE Kante, die weichen muss, damit der Edit die
21
+ * Kardinalitäts-Obergrenze des Meta-Modells (R-18, zweites Bein) einhält.
22
+ * Fehlt = reines Anhängen. Anwendung IMMER als EIN Batch
23
+ * [delete(retire), add(edit)] durchs Gate — nacheinander wäre der
24
+ * Zwischenzustand je nach Reihenfolge illegal oder unternormiert.
25
+ */
26
+ retire?: {
27
+ source: string;
28
+ target: string;
29
+ type: TraceType;
30
+ rationale: string;
31
+ };
32
+ /**
33
+ * CR-GC-435 — gesetzt NUR beim Umhängen (retire) einer realisierten Quelle
34
+ * (realRef/codeRef): die Allokation speist den Datei→Modul-Resolver
35
+ * (buildModResolver, RC-05/importCoverage) — der Graph-Edit zieht dann
36
+ * Code-Arbeit nach sich. Ohne Realisierung fehlt das Feld (kein Rauschen).
37
+ */
38
+ codeImpact?: {
39
+ file: string;
40
+ targetModule: string;
41
+ };
42
+ /**
43
+ * CR-GC-444 — die GEKOPPELTEN Knoten-Merges, die zusammen mit dem primären
44
+ * Merge (`source` → `target`) in EINEM Batch laufen MÜSSEN, damit der
45
+ * Endzustand die Kardinalitäts-Obergrenzen des Meta-Modells einhält.
46
+ *
47
+ * Die Kopplung ist keine Konvention, sondern Gate-Realität: `FLOW -relation->
48
+ * SCHEMA` ist `1..1` (contracts 10.0.0, drittes R-18-Bein). Werden zwei FLOWs
49
+ * zusammengelegt, trägt der überlebende FLOW zwei SCHEMAs — und wird
50
+ * abgewiesen, solange der SCHEMA-Merge nicht im selben Batch steht (in
51
+ * CR-GC-438 Kill 2 gemessen: 5 FLOWs mit > 1 SCHEMA, Zug tot).
52
+ *
53
+ * Reihenfolge: primärer Merge zuerst, dann diese Liste. Fehlt das Feld, ist
54
+ * keine Kopplung nötig (die Zusammengelegten teilen den Vertrag bereits).
55
+ */
56
+ merges?: {
57
+ source: string;
58
+ target: string;
59
+ rationale: string;
60
+ }[];
9
61
  }
10
62
  type Element = OntologyGraph['elements'][number];
11
63
  type FixTemplate = (v: RuleViolation, g: OntologyGraph) => SuggestedEdit | null;
@@ -21,4 +73,68 @@ export declare const FIX_TEMPLATES: Record<string, FixTemplate>;
21
73
  * Deterministisch; nie der generische applyRule-Trace.
22
74
  */
23
75
  export declare function fixFor(v: RuleViolation, g: OntologyGraph): SuggestedEdit | null;
76
+ /**
77
+ * Die Kennung der Konsolidierungs-Vorschläge im `Suggestion.ruleId`-Feld.
78
+ *
79
+ * AUSDRÜCKLICH KEINE contracts-Regel-ID. Der Operator hängt an keiner Violation,
80
+ * weil ein Merge keine Regel repariert — er bewegt die Metrik (Vertrags-
81
+ * konzentration, in CR-GC-438 als dieselbe Größe wie `flowEfficiency` gemessen,
82
+ * r = 0,89). Ein Fund wäre erfunden; die Kennung sagt stattdessen, woher der
83
+ * Vorschlag kommt.
84
+ */
85
+ export declare const MERGE_OPERATOR_ID = "OP-MERGE";
86
+ /**
87
+ * Ab welcher ND-02-Ähnlichkeit zwei VERSCHIEDENE SCHEMAs als DERSELBE Vertrag
88
+ * gelten (CR-GC-444).
89
+ *
90
+ * Bewusst ND-02s eigene DUPLIKAT-Schwelle (0.85) und nicht die lockere
91
+ * Overlap-Schwelle 0.5 aus AO-D01 (AO-D03 ist mit CR-SM-283 entfallen):
92
+ * ein Merge ist destruktiv, „überlappt"
93
+ * ist nicht „ist derselbe Vertrag". Die Zahl steht hier, weil contracts sie
94
+ * modul-lokal hält (`near-duplicate-rules.ts`) — sie wird NICHT umdefiniert.
95
+ */
96
+ export declare const MERGE_SIMILARITY_THRESHOLD = 0.85;
97
+ /**
98
+ * Tragen zwei SCHEMA-Knoten denselben Datenvertrag?
99
+ *
100
+ * Genau die Definition, die contracts schon fährt: identischer Knoten = 100 %,
101
+ * sonst ND-02s eigene Rechnung (`similarity.ts`, seit CR-SM-286 dort statt in
102
+ * einer injizierten Matrix). Kein zweites Ähnlichkeitsmaß, keine eigene Formel,
103
+ * kein ND-02-Fork.
104
+ *
105
+ * **CR-SM-289 — der Ähnlichkeits-Zweig ist heute UNERREICHBAR, nicht nur still.**
106
+ * ND-02s `usage_overlap` läuft über die direkten Partner eines SCHEMA, und
107
+ * `FLOW -relation-> SCHEMA [1..1]` ist das einzige Pattern, das SCHEMA berührt:
108
+ * zwei VERSCHIEDENE SCHEMAs hängen an zwei verschiedenen FLOWs, ihre
109
+ * Partnermengen sind disjunkt, `usage_overlap` ist 0. Deckel damit
110
+ * `0,50 + 0,30 = 0,80` gegen die Schwelle `0,85` — gemessen, nicht gerechnet.
111
+ * Bis CR-SM-286 war das ein „ohne Matrix"-Vorbehalt; seit die Injektion weg ist,
112
+ * gilt es immer.
113
+ *
114
+ * Der Zweig BLEIBT: er ist die Sicherung gegen eine echte
115
+ * R-18-Kardinalitätsverletzung und wird scharf, sobald Formel, Schwelle oder
116
+ * SCHEMA-Pattern wandern. `suggest.test.ts` nagelt beide Zahlen fest und schlägt
117
+ * dann an. Das zu reparieren ist eine contracts-Entscheidung, kein Fork hier.
118
+ */
119
+ export declare function contractSimilarity(g: OntologyGraph, a: string, b: string): number;
120
+ /**
121
+ * Konsolidierungs-Kandidaten: FLOW-Paare, die DENSELBEN Datenvertrag tragen
122
+ * (CR-GC-444, Zielbild „graphcode als Regelkreis", FLOWs 62 → 27).
123
+ *
124
+ * „Derselbe Vertrag" = `contractSimilarity` (identischer SCHEMA-Knoten oder
125
+ * ND-02-Duplikat) — EINE Definition, keine neue Zahl. Ein FLOW ohne oder mit
126
+ * mehr als einem SCHEMA nimmt nicht teil: er ist schon grammatikalisch kaputt
127
+ * und gehört R-18, nicht dem Optimierer.
128
+ *
129
+ * Pro Kandidaten-GRUPPE genau EIN Vorschlag (ein Greedy-Schritt, wie der Rest
130
+ * des Optimierers): der io-stärkste FLOW absorbiert den nächststärksten.
131
+ * Deterministisch — io-Grad absteigend, Tiebreak id aufsteigend.
132
+ */
133
+ export declare function mergeCandidates(g: OntologyGraph): SuggestedEdit[];
134
+ /**
135
+ * Den Merge-Verbund eines `SuggestedEdit` IN MEMORY anwenden — die Δm-Sonde des
136
+ * Operators (`suggestEdits`). Kein Ersatz fürs Gate: hier wird gemessen, dort
137
+ * geurteilt. Reihenfolge wie beim Batch: primärer Merge zuerst, dann `merges`.
138
+ */
139
+ export declare function applyMergeEdit(g: OntologyGraph, edit: SuggestedEdit): OntologyGraph;
24
140
  export {};
@@ -7,7 +7,7 @@
7
7
  * Ausgeliefert wird ein Edit nur, wenn ein rule-spezifisches Template ihn aus
8
8
  * dem Elementtext DETERMINISTISCH herleiten kann (Fundstelle = Begründung):
9
9
  *
10
- * - CR-R01: CR ohne relation → relation zu einem IM CR-TEXT genannten
10
+ * - CR-R01: CR ohne Umfangsbezug → relation zu einem IM CR-TEXT genannten
11
11
  * UC/REQ/FUNC/MOD (Spike: "Relation zu im CR-Text genannten Elementen")
12
12
  * - CR-R04: CR ohne FUNC → relation zu einer im CR-Text genannten FUNC
13
13
  * - MS-03 : CR ohne Milestone → relation zur im Text genannten MS,
@@ -23,15 +23,18 @@
23
23
  * sonst zum einzigen MOD im Graphen (eindeutig ⇒ herleitbar)
24
24
  * - R-23 : MOD ohne allozierte FUNC → allocate von der im MOD-TEXT
25
25
  * genannten FUNC (kein Eindeutigkeits-Fallback, s. dort)
26
- * - SC-02/SC-04: FLOW ohne Datenvertrag → relation zum im FLOW-TEXT
27
- * genannten SCHEMA, sonst zum einzigen SCHEMA
26
+ * - SC-02: fehlende SCHEMA-Nutzung → relation zum im TEXT genannten
27
+ * SCHEMA, sonst zum einzigen SCHEMA
28
+ * - R-18 : NUR das Untergrenzen-Bein (CR-SM-302) → die vom Meta-Modell
29
+ * geforderte Kante zum im TEXT genannten Ziel, sonst zum einzigen
30
+ * (heute `FLOW -relation-> SCHEMA [1..1]`, aus REQUIRED_PATTERNS gelesen)
28
31
  *
29
32
  * Regeln ohne Template (oder Template ohne Fund) liefern null → die Suggestion
30
33
  * bleibt Fund-Ebene (Violation + Richtung + Δm), ohne Edit. Regeln, deren Fix
31
34
  * Element-Erzeugung braucht (FCHAIN, REQ-Pre/Postcondition, TEST), sind mit
32
35
  * additiven Kanten prinzipiell nicht ausdrückbar — bewusst kein Template.
33
36
  */
34
- import { isValidTrace } from '@sigloch/contracts/se';
37
+ import { isValidTrace, BOUNDED_PATTERNS, REQUIRED_PATTERNS, maxOccurs, schemaSimilarity } from '@sigloch/contracts/se';
35
38
  function byId(g, id) {
36
39
  return g.elements.find((e) => e.id === id);
37
40
  }
@@ -39,6 +42,46 @@ function hasTrace(g, source, target, type) {
39
42
  return g.traces.some((t) => t.source === source && t.target === target && t.type === type);
40
43
  }
41
44
  const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
45
+ /** Datei-Bindung einer Quelle — dieselbe Zugriffsfolge wie weightNodes (layer.ts). */
46
+ function realFileOf(e) {
47
+ const el = e;
48
+ return el.realRef?.file ?? el.codeRef?.file ?? el.attributes?.realRef?.file ?? el.attributes?.codeRef?.file;
49
+ }
50
+ /**
51
+ * CR-GC-435 — das Kardinalitäts-Bewusstsein der Templates. `isValidTrace` prüft
52
+ * die PAAR-Legalität; die MENGEN-Bedingung (BOUNDED_PATTERNS, z.B.
53
+ * `FUNC -allocate-> MOD [0..1]`) sah bis dahin kein Template — es schlug Edits
54
+ * vor, die das Gate über R-18s zweites Bein sicher abweist (der verifizierte
55
+ * Repo-Befund: KEIN Architektur-Vorschlag anwendbar, sobald alloziert ist).
56
+ *
57
+ * Rückgabe: `undefined` = kein retire nötig (Anhängen bleibt Anhängen);
58
+ * eine Kante = GENAU DIE muss weichen, damit der Edit legal wird (Umhängen);
59
+ * `'blocked'` = kein EINZELNES retire macht den Edit legal (die Quelle verletzt
60
+ * die Obergrenze schon jetzt) — dann Option C: kein Edit statt eines sicher
61
+ * scheiternden (der Aufrufer probiert den nächsten Kandidaten bzw. liefert null).
62
+ */
63
+ function retireFor(g, source, target, type) {
64
+ const pattern = BOUNDED_PATTERNS.find((p) => p.source === source.type && p.target === target.type && p.type === type);
65
+ if (!pattern)
66
+ return undefined;
67
+ const max = maxOccurs(pattern.cardinality);
68
+ const existing = g.traces.filter((t) => t.type === type &&
69
+ t.source === source.id &&
70
+ t.target !== target.id &&
71
+ byId(g, t.target)?.type === pattern.target);
72
+ if (existing.length + 1 <= max)
73
+ return undefined;
74
+ if (existing.length + 1 - max > 1)
75
+ return 'blocked';
76
+ const old = existing[0];
77
+ return {
78
+ source: old.source,
79
+ target: old.target,
80
+ type,
81
+ rationale: `${source.id} trägt bereits ${type} → ${old.target}; das Meta-Modell erlaubt höchstens ` +
82
+ `${max} (${pattern.cardinality}) — diese Kante muss weichen, damit ${target.id} legal wird`,
83
+ };
84
+ }
42
85
  /**
43
86
  * Elemente der gegebenen Typen, deren id ODER name (Wortgrenze, ≥3 Zeichen,
44
87
  * case-insensitive) im Text vorkommt. Deterministische Rangfolge:
@@ -117,6 +160,14 @@ function edgeToMentioned(opts) {
117
160
  sourceKinds: source.kinds, targetKinds: target.kinds,
118
161
  }))
119
162
  continue;
163
+ // CR-GC-435: Paar legal ≠ Menge legal — die Kardinalitäts-Obergrenze
164
+ // entscheidet, ob der Edit ein Anhängen (kein retire), ein Umhängen
165
+ // (genau ein retire) oder nicht ausdrückbar ist ('blocked' → nächster
166
+ // Kandidat; Option C statt eines Edits, den das Gate sicher abweist).
167
+ const retire = retireFor(g, source, target, opts.traceType);
168
+ if (retire === 'blocked')
169
+ continue;
170
+ const file = retire ? realFileOf(source) : undefined;
120
171
  return {
121
172
  op: 'add-trace',
122
173
  source: source.id,
@@ -125,14 +176,17 @@ function edgeToMentioned(opts) {
125
176
  rationale: mentioned.length > 0
126
177
  ? `${other.id} ist im Text von ${el.id} genannt`
127
178
  : `${other.id} ist das einzige ${other.type} im Graphen`,
179
+ ...(retire ? { retire } : {}),
180
+ ...(retire && file ? { codeImpact: { file, targetModule: target.id } } : {}),
128
181
  };
129
182
  }
130
183
  return null;
131
184
  };
132
185
  }
133
186
  export const FIX_TEMPLATES = {
134
- 'CR-R01': relationToMentioned(['UC', 'REQ', 'FUNC', 'MOD']),
135
- 'CR-R04': relationToMentioned(['FUNC']),
187
+ // CR-SM-295: SCHEMA gehoert zum Umfang — ein CR, der nur einen Datenvertrag aendert,
188
+ // ist ein vollstaendiger CR. Die Liste ist woertlich SCOPE_TYPES aus cr-quality-rules.
189
+ 'CR-R01': relationToMentioned(['UC', 'REQ', 'FUNC', 'MOD', 'SCHEMA']),
136
190
  // --- Architektur-Operatoren (CR-SM-241) ------------------------------------
137
191
  // Ohne sie hat auf `layer: 'arch'` — graphcodes DEFAULT-Messebene — jeder
138
192
  // Template-Edit Δm = 0, weil CR/MS/UC gar nicht im Teilgraphen liegen.
@@ -144,9 +198,46 @@ export const FIX_TEMPLATES = {
144
198
  // irgendwohin), „das einzige FUNC" ist keine — ein FUNC darf legitim einem
145
199
  // anderen Modul gehören, und dieses hier hätte dann einfach noch keins.
146
200
  'R-23': edgeToMentioned({ types: ['FUNC'], traceType: 'allocate', direction: 'in' }),
147
- // FLOW ohne Datenvertrag → das im FLOW-Text genannte SCHEMA, sonst das einzige.
201
+ // SCHEMA-Nutzung → das im Text genannte SCHEMA, sonst das einzige.
148
202
  'SC-02': edgeToMentioned({ types: ['SCHEMA'], traceType: 'relation', direction: 'out', uniqueFallback: true }),
149
- 'SC-04': edgeToMentioned({ types: ['SCHEMA'], traceType: 'relation', direction: 'out', uniqueFallback: true }),
203
+ /**
204
+ * CR-SM-302 — das UNTERGRENZEN-Bein von R-18, und ausschliesslich dieses.
205
+ *
206
+ * CR-SM-271 hat SC-04 gestrichen und den Fall zur Grammatik gemacht; mit der Regel fiel
207
+ * ihr Template, mit der Begruendung "R-18 meldet zu viele verschiedene Faelle". Der Satz
208
+ * stimmt fuer drei der vier Beine — und genau die drei sind hier ausgeschlossen:
209
+ *
210
+ * illegales Paar · Kardinalitaets-OBERgrenze · compose-Baum
211
+ *
212
+ * Ihr Fix ist ein LOESCHEN, und ein additives Template drueckt das nicht aus. Das vierte
213
+ * Bein ist das Gegenteil: es fehlt genau EINE Kante, deren Typ und Ziel-Elementtyp das
214
+ * Meta-Modell vorgibt. Erkennungsmerkmal ist `candidate_targets` im Kontext — das setzt
215
+ * nur `cardinalityViolations`' Untergrenzen-Schleife (contracts `rules.ts`).
216
+ *
217
+ * Gemessen (CR-SM-302): an einem Graphen IM BAU (graphcodes DIVERGENCE_FIXTURE, 5 FLOWs)
218
+ * sind 3 von 4 Befunden herleitbar — der FLOW-Text nennt sein SCHEMA. An den migrierten
219
+ * Selbstmodellen sind es 0 von 6, aber diese Zahl misst Ueberlebende: ein R-18-Befund ist
220
+ * ein `error`, ein Graph, der sein Gate besteht, hat per Konstruktion keinen.
221
+ *
222
+ * Das Muster kommt aus REQUIRED_PATTERNS, nicht aus einer Konstanten: kommt eine zweite
223
+ * durchgesetzte Untergrenze dazu, traegt dieses Template sie ohne Aenderung.
224
+ */
225
+ 'R-18': (v, g) => {
226
+ if (!Array.isArray(v.context?.candidate_targets))
227
+ return null;
228
+ const el = byId(g, v.element_id);
229
+ if (!el)
230
+ return null;
231
+ const pattern = REQUIRED_PATTERNS.find((p) => p.source === el.type);
232
+ if (!pattern)
233
+ return null;
234
+ return edgeToMentioned({
235
+ types: [pattern.target],
236
+ traceType: pattern.type,
237
+ direction: 'out',
238
+ uniqueFallback: true,
239
+ })(v, g);
240
+ },
150
241
  'MS-03': (v, g) => {
151
242
  const cr = byId(g, v.element_id);
152
243
  if (!cr)
@@ -226,3 +317,234 @@ export const FIX_TEMPLATES = {
226
317
  export function fixFor(v, g) {
227
318
  return FIX_TEMPLATES[v.rule_id]?.(v, g) ?? null;
228
319
  }
320
+ // ---------------------------------------------------------------------------
321
+ // CR-GC-444 — der Konsolidierungs-Operator (Merge)
322
+ // ---------------------------------------------------------------------------
323
+ /**
324
+ * Die Kennung der Konsolidierungs-Vorschläge im `Suggestion.ruleId`-Feld.
325
+ *
326
+ * AUSDRÜCKLICH KEINE contracts-Regel-ID. Der Operator hängt an keiner Violation,
327
+ * weil ein Merge keine Regel repariert — er bewegt die Metrik (Vertrags-
328
+ * konzentration, in CR-GC-438 als dieselbe Größe wie `flowEfficiency` gemessen,
329
+ * r = 0,89). Ein Fund wäre erfunden; die Kennung sagt stattdessen, woher der
330
+ * Vorschlag kommt.
331
+ */
332
+ export const MERGE_OPERATOR_ID = 'OP-MERGE';
333
+ /**
334
+ * Ab welcher ND-02-Ähnlichkeit zwei VERSCHIEDENE SCHEMAs als DERSELBE Vertrag
335
+ * gelten (CR-GC-444).
336
+ *
337
+ * Bewusst ND-02s eigene DUPLIKAT-Schwelle (0.85) und nicht die lockere
338
+ * Overlap-Schwelle 0.5 aus AO-D01 (AO-D03 ist mit CR-SM-283 entfallen):
339
+ * ein Merge ist destruktiv, „überlappt"
340
+ * ist nicht „ist derselbe Vertrag". Die Zahl steht hier, weil contracts sie
341
+ * modul-lokal hält (`near-duplicate-rules.ts`) — sie wird NICHT umdefiniert.
342
+ */
343
+ export const MERGE_SIMILARITY_THRESHOLD = 0.85;
344
+ /**
345
+ * Tragen zwei SCHEMA-Knoten denselben Datenvertrag?
346
+ *
347
+ * Genau die Definition, die contracts schon fährt: identischer Knoten = 100 %,
348
+ * sonst ND-02s eigene Rechnung (`similarity.ts`, seit CR-SM-286 dort statt in
349
+ * einer injizierten Matrix). Kein zweites Ähnlichkeitsmaß, keine eigene Formel,
350
+ * kein ND-02-Fork.
351
+ *
352
+ * **CR-SM-289 — der Ähnlichkeits-Zweig ist heute UNERREICHBAR, nicht nur still.**
353
+ * ND-02s `usage_overlap` läuft über die direkten Partner eines SCHEMA, und
354
+ * `FLOW -relation-> SCHEMA [1..1]` ist das einzige Pattern, das SCHEMA berührt:
355
+ * zwei VERSCHIEDENE SCHEMAs hängen an zwei verschiedenen FLOWs, ihre
356
+ * Partnermengen sind disjunkt, `usage_overlap` ist 0. Deckel damit
357
+ * `0,50 + 0,30 = 0,80` gegen die Schwelle `0,85` — gemessen, nicht gerechnet.
358
+ * Bis CR-SM-286 war das ein „ohne Matrix"-Vorbehalt; seit die Injektion weg ist,
359
+ * gilt es immer.
360
+ *
361
+ * Der Zweig BLEIBT: er ist die Sicherung gegen eine echte
362
+ * R-18-Kardinalitätsverletzung und wird scharf, sobald Formel, Schwelle oder
363
+ * SCHEMA-Pattern wandern. `suggest.test.ts` nagelt beide Zahlen fest und schlägt
364
+ * dann an. Das zu reparieren ist eine contracts-Entscheidung, kein Fork hier.
365
+ */
366
+ export function contractSimilarity(g, a, b) {
367
+ if (a === b)
368
+ return 1;
369
+ // CR-SM-286: der Graph ist jetzt Parameter. Vorher las die Funktion den contracts-Modulzustand
370
+ // und gab ohne Injektion 0 — also "nicht aehnlich" statt "nicht beurteilbar", und das an einer
371
+ // Stelle, die ueber einen destruktiven Merge entscheidet.
372
+ const sim = schemaSimilarity(g);
373
+ const i = sim.ids.indexOf(a);
374
+ const j = sim.ids.indexOf(b);
375
+ if (i < 0 || j < 0)
376
+ return 0;
377
+ return sim.matrix[i]?.[j] ?? 0;
378
+ }
379
+ /** Eindeutige Ziele einer Kante `id -type-> <targetType>`, in Graph-Reihenfolge. */
380
+ function outTargets(g, id, type, targetType) {
381
+ const out = [];
382
+ for (const t of g.traces) {
383
+ if (t.type !== type || t.source !== id)
384
+ continue;
385
+ if (byId(g, t.target)?.type !== targetType)
386
+ continue;
387
+ if (!out.includes(t.target))
388
+ out.push(t.target);
389
+ }
390
+ return out;
391
+ }
392
+ /**
393
+ * Die Merges, die MITLAUFEN müssen, wenn `drop` in `keep` aufgeht — hergeleitet
394
+ * aus der GRAMMATIK, nicht konventioniert.
395
+ *
396
+ * Für jedes `BOUNDED_PATTERNS`-Muster, dessen QUELLE der gemergte Typ ist
397
+ * (Kardinalität zählt ausgehend, wie R-18s zweites Bein), wird geprüft, ob die
398
+ * Vereinigung der Ziele von `keep` und `drop` die Obergrenze reißt. Genau dann
399
+ * fällt ein gekoppelter Merge an. Heute trifft das exakt einen Fall:
400
+ * `FLOW -relation-> SCHEMA [1..1]`.
401
+ *
402
+ * `null` = nicht eindeutig auflösbar ⇒ KEIN Vorschlag (Option C aus CR-GC-435:
403
+ * lieber kein Edit als einer, den das Gate sicher abweist). Das gilt, wenn die
404
+ * Obergrenze > 1 ist, wenn `keep` selbst schon überzählt, oder wenn die weichenden
405
+ * Ziele nicht nachweislich derselbe Vertrag sind.
406
+ */
407
+ function coupledMerges(g, drop, keep) {
408
+ const out = [];
409
+ for (const p of BOUNDED_PATTERNS) {
410
+ if (p.source !== drop.type || p.target === '*')
411
+ continue;
412
+ const targetType = p.target;
413
+ const keepTargets = outTargets(g, keep.id, p.type, targetType);
414
+ const dropTargets = outTargets(g, drop.id, p.type, targetType);
415
+ const union = [...keepTargets, ...dropTargets.filter((t) => !keepTargets.includes(t))];
416
+ if (union.length <= maxOccurs(p.cardinality))
417
+ continue;
418
+ if (maxOccurs(p.cardinality) !== 1 || keepTargets.length !== 1)
419
+ return null;
420
+ const survivor = keepTargets[0];
421
+ for (const extra of union) {
422
+ if (extra === survivor)
423
+ continue;
424
+ // Nur SCHEMA lässt sich hier belegen — für alles andere fehlt der Nachweis,
425
+ // dass die beiden Ziele dasselbe sind, also gibt es keinen Vorschlag.
426
+ if (targetType !== 'SCHEMA' || contractSimilarity(g, extra, survivor) < MERGE_SIMILARITY_THRESHOLD)
427
+ return null;
428
+ out.push({
429
+ source: extra,
430
+ target: survivor,
431
+ rationale: `${keep.id} trüge nach dem Merge ${union.length} ${targetType} über ${p.type}, ` +
432
+ `die Grammatik erlaubt ${p.cardinality} — ${extra} muss im SELBEN Batch in ${survivor} aufgehen`,
433
+ });
434
+ }
435
+ }
436
+ return out;
437
+ }
438
+ /** io-Grad eines Knotens — der besser verdrahtete Vertrag überlebt den Merge. */
439
+ function ioDegree(g, id) {
440
+ return g.traces.filter((t) => t.type === 'io' && (t.source === id || t.target === id)).length;
441
+ }
442
+ /**
443
+ * Konsolidierungs-Kandidaten: FLOW-Paare, die DENSELBEN Datenvertrag tragen
444
+ * (CR-GC-444, Zielbild „graphcode als Regelkreis", FLOWs 62 → 27).
445
+ *
446
+ * „Derselbe Vertrag" = `contractSimilarity` (identischer SCHEMA-Knoten oder
447
+ * ND-02-Duplikat) — EINE Definition, keine neue Zahl. Ein FLOW ohne oder mit
448
+ * mehr als einem SCHEMA nimmt nicht teil: er ist schon grammatikalisch kaputt
449
+ * und gehört R-18, nicht dem Optimierer.
450
+ *
451
+ * Pro Kandidaten-GRUPPE genau EIN Vorschlag (ein Greedy-Schritt, wie der Rest
452
+ * des Optimierers): der io-stärkste FLOW absorbiert den nächststärksten.
453
+ * Deterministisch — io-Grad absteigend, Tiebreak id aufsteigend.
454
+ */
455
+ export function mergeCandidates(g) {
456
+ const flows = g.elements.filter((e) => e.type === 'FLOW');
457
+ // Gruppen zunächst nach identischem SCHEMA-Knoten …
458
+ const bySchema = new Map();
459
+ for (const f of flows) {
460
+ const schemas = outTargets(g, f.id, 'relation', 'SCHEMA');
461
+ if (schemas.length !== 1)
462
+ continue;
463
+ const group = bySchema.get(schemas[0]);
464
+ if (group)
465
+ group.push(f);
466
+ else
467
+ bySchema.set(schemas[0], [f]);
468
+ }
469
+ // … dann die Gruppen verschmelzen, deren SCHEMAs ND-02-Duplikate sind.
470
+ const schemaIds = [...bySchema.keys()].sort();
471
+ const groupOf = new Map(schemaIds.map((s) => [s, s]));
472
+ const root = (s) => {
473
+ let x = s;
474
+ for (let n = 0; n < 50 && groupOf.get(x) !== x; n += 1)
475
+ x = groupOf.get(x);
476
+ return x;
477
+ };
478
+ for (let i = 0; i < schemaIds.length; i += 1) {
479
+ for (let j = i + 1; j < schemaIds.length; j += 1) {
480
+ if (contractSimilarity(g, schemaIds[i], schemaIds[j]) < MERGE_SIMILARITY_THRESHOLD)
481
+ continue;
482
+ const a = root(schemaIds[i]);
483
+ const b = root(schemaIds[j]);
484
+ if (a !== b)
485
+ groupOf.set(b, a);
486
+ }
487
+ }
488
+ const grouped = new Map();
489
+ for (const s of schemaIds) {
490
+ const key = root(s);
491
+ grouped.set(key, [...(grouped.get(key) ?? []), ...bySchema.get(s)]);
492
+ }
493
+ const edits = [];
494
+ for (const key of [...grouped.keys()].sort()) {
495
+ const members = grouped.get(key)
496
+ .slice()
497
+ .sort((a, b) => ioDegree(g, b.id) - ioDegree(g, a.id) || a.id.localeCompare(b.id));
498
+ if (members.length < 2)
499
+ continue;
500
+ const [keep, drop] = members;
501
+ const merges = coupledMerges(g, drop, keep);
502
+ if (merges === null)
503
+ continue;
504
+ const keepSchema = outTargets(g, keep.id, 'relation', 'SCHEMA')[0];
505
+ const dropSchema = outTargets(g, drop.id, 'relation', 'SCHEMA')[0];
506
+ edits.push({
507
+ op: 'merge-nodes',
508
+ source: drop.id,
509
+ target: keep.id,
510
+ type: 'relation',
511
+ rationale: keepSchema === dropSchema
512
+ ? `${drop.id} und ${keep.id} tragen denselben Vertrag ${keepSchema} — ${keep.id} absorbiert ${drop.id}`
513
+ : `${drop.id} und ${keep.id} tragen mit ${dropSchema} / ${keepSchema} ein ND-02-Duplikat ` +
514
+ `(${Math.round(contractSimilarity(g, dropSchema, keepSchema) * 100)} %) — ${keep.id} absorbiert ${drop.id}`,
515
+ ...(merges.length ? { merges } : {}),
516
+ });
517
+ }
518
+ return edits;
519
+ }
520
+ /**
521
+ * Den Merge-Verbund eines `SuggestedEdit` IN MEMORY anwenden — die Δm-Sonde des
522
+ * Operators (`suggestEdits`). Kein Ersatz fürs Gate: hier wird gemessen, dort
523
+ * geurteilt. Reihenfolge wie beim Batch: primärer Merge zuerst, dann `merges`.
524
+ */
525
+ export function applyMergeEdit(g, edit) {
526
+ const absorbedBy = new Map([
527
+ [edit.source, edit.target],
528
+ ...(edit.merges ?? []).map((m) => [m.source, m.target]),
529
+ ]);
530
+ const resolve = (id) => {
531
+ let x = id;
532
+ for (let n = 0; n < 50 && absorbedBy.has(x); n += 1)
533
+ x = absorbedBy.get(x);
534
+ return x;
535
+ };
536
+ const seen = new Set();
537
+ const traces = [];
538
+ for (const t of g.traces) {
539
+ const source = resolve(t.source);
540
+ const target = resolve(t.target);
541
+ if (source === target)
542
+ continue;
543
+ const key = `${source}>${t.type}>${target}`;
544
+ if (seen.has(key))
545
+ continue;
546
+ seen.add(key);
547
+ traces.push({ ...t, source, target });
548
+ }
549
+ return { ...g, elements: g.elements.filter((e) => !absorbedBy.has(e.id)), traces };
550
+ }
package/dist/metrics.d.ts CHANGED
@@ -48,6 +48,7 @@ export declare function metrics(graph: OntologyGraph, opts?: MetricsOptions): Me
48
48
  export { projectLayer, weightNodes, ARCH_TYPES, type MetricLayer } from './layer.js';
49
49
  export { CLASS_MAP, classOf, classifyAll, classificationStats, type RuleClass, type Classification, type ClassifiedRule, type ClassificationStats, } from './rule-classify.js';
50
50
  export { applyRule, markEmptyDelta, type ApplyResult, type DeltaMark } from './rule-apply.js';
51
- export { targetFor, suggestEdits, type Suggestion, type SuggestOptions } from './suggest.js';
52
- export { fixFor, mentionedElements, FIX_TEMPLATES, type SuggestedEdit } from './fix-templates.js';
51
+ export { suggestEdits, type Suggestion, type SuggestOptions } from './suggest.js';
52
+ export { steerScore, beatsBySteer, STEER_RULES, EPS_AUGMENT, type SteerScore, type SteerCandidate } from './steer.js';
53
+ export { fixFor, mentionedElements, FIX_TEMPLATES, mergeCandidates, applyMergeEdit, contractSimilarity, MERGE_OPERATOR_ID, MERGE_SIMILARITY_THRESHOLD, type SuggestedEdit, } from './fix-templates.js';
53
54
  export { buildAdjacency, betweenness, maxBetweenness, detectCommunities, modularityOf, modularityQ, redundancyDensity, intraEdgeFraction, components, componentSizes, sourceSinkPaths, type Adjacency, } from './topology.js';
package/dist/metrics.js CHANGED
@@ -92,6 +92,8 @@ export function metrics(graph, opts = {}) {
92
92
  export { projectLayer, weightNodes, ARCH_TYPES } from './layer.js';
93
93
  export { CLASS_MAP, classOf, classifyAll, classificationStats, } from './rule-classify.js';
94
94
  export { applyRule, markEmptyDelta } from './rule-apply.js';
95
- export { targetFor, suggestEdits } from './suggest.js';
96
- export { fixFor, mentionedElements, FIX_TEMPLATES } from './fix-templates.js';
95
+ export { suggestEdits } from './suggest.js';
96
+ // CR-SM-292: der Ranking-Score. `targetFor` ist mit dem ℝ⁶-Ranking ersatzlos entfallen.
97
+ export { steerScore, beatsBySteer, STEER_RULES, EPS_AUGMENT } from './steer.js';
98
+ export { fixFor, mentionedElements, FIX_TEMPLATES, mergeCandidates, applyMergeEdit, contractSimilarity, MERGE_OPERATOR_ID, MERGE_SIMILARITY_THRESHOLD, } from './fix-templates.js';
97
99
  export { buildAdjacency, betweenness, maxBetweenness, detectCommunities, modularityOf, modularityQ, redundancyDensity, intraEdgeFraction, components, componentSizes, sourceSinkPaths, } from './topology.js';
@@ -19,7 +19,8 @@
19
19
  *
20
20
  * Delta zum Spike-Original: RD-04 (Dekompositionsbreite, CR-SM-221) ergänzt;
21
21
  * SC-01/SC-03 (BOK-CR-026) und MT-03 (CR-SM-223) sind aus dem Katalog gelöscht
22
- * und daher hier entfernt. CR-SM-226: +R-28/FC-04/SC-04 (neue Regeln; R-28 mit CR-SM-247 wieder entfallen); IO-01
22
+ * und daher hier entfernt. CR-SM-226: +R-28/FC-04/SC-04 (neue Regeln; R-28 mit CR-SM-247,
23
+ * SC-04 mit CR-SM-271 wieder entfallen — Untergrenze ist Grammatik/R-18-Bein); IO-01
23
24
  * ist nicht mehr cross-module-only (Rationale-Text angepasst, Klasse unverändert).
24
25
  * CR-GC-366: +R-30 (Wirkketten-Bindung) und +R-31 (io-Verdrahtung), beide Operator —
25
26
  * ihr Fix fügt eine Trace hinzu und bewegt damit die Topologie.
@@ -55,7 +56,6 @@ export const CLASS_MAP = {
55
56
  'R-02': { class: 'Operator', rationale: 'add satisfy trace FUNC→REQ' },
56
57
  'R-05': { class: 'Operator', rationale: 'add verify trace TEST→REQ' },
57
58
  'R-10': { class: 'Operator', rationale: 'add io traces to complete the FLOW' },
58
- 'R-14': { class: 'Operator', rationale: 'add compose trace UC→FCHAIN/REQ' },
59
59
  'R-15': { class: 'Operator', rationale: 'add compose trace FCHAIN→FUNC' },
60
60
  'R-16': { class: 'Operator', rationale: 'add io trace ACTOR→UC/FLOW' },
61
61
  'R-17': { class: 'Operator', rationale: 'add compose trace SYS→child' },
@@ -70,22 +70,17 @@ export const CLASS_MAP = {
70
70
  'UC-03': { class: 'Operator', rationale: 'add compose trace UC→FCHAIN (scenario)' },
71
71
  'UC-05': { class: 'Operator', rationale: 'add REQ(postcondition) via compose trace' },
72
72
  'UC-06': { class: 'Operator', rationale: 'add REQ(precondition) via compose trace' },
73
- 'FC-01': { class: 'Operator', rationale: 'connect chain to ACTOR via FLOW io trace' },
74
73
  'FC-02': { class: 'Operator', rationale: 'add FCHAIN via compose trace to leaf UC' },
75
74
  'SC-02': { class: 'Operator', rationale: 'link FLOW→SCHEMA via relation trace' },
76
- 'PH-01': { class: 'Operator', rationale: 'add logical MOD via compose trace' },
77
75
  'IO-01': { class: 'Operator', rationale: 'add FLOW element + io traces between the FUNC pair' },
78
76
  // CR-GC-366: beide Fixes fuegen eine Trace hinzu, also Operator wie R-15 (compose) und R-10 (io).
79
77
  'R-30': { class: 'Operator', rationale: 'add compose trace FCHAIN→FUNC to bind the function into a chain' },
80
78
  'R-31': { class: 'Operator', rationale: 'add io traces FLOW→FUNC / FUNC→FLOW to wire the function up' },
81
79
  'FC-04': { class: 'Operator', rationale: 'add ACTOR→FLOW entry + FUNC→FLOW exit io traces' },
82
- 'SC-04': { class: 'Operator', rationale: 'link FLOW→SCHEMA via relation trace' },
83
80
  'CR-R01': { class: 'Operator', rationale: 'add relation traces CR→affected elements' },
84
- 'CR-R04': { class: 'Operator', rationale: 'add relation trace CR→FUNC' },
85
81
  'FM-02': { class: 'Operator', rationale: 'create mitigation REQ + compose trace' },
86
82
  'FM-03': { class: 'Operator', rationale: 'add TEST(passed) + verify trace to risk REQ' },
87
83
  // --- Constraints: remove/repair/reduce, or attribute/text-only -------------
88
- 'R-03': { class: 'Constraint', rationale: 'ASIL isolation — separate mixed levels, non-additive' },
89
84
  'R-04': { class: 'Constraint', rationale: 'module too large — split, non-additive' },
90
85
  'R-08': { class: 'Constraint', rationale: 'repair dangling trace endpoint' },
91
86
  'R-12': { class: 'Constraint', rationale: 'break dependency cycle — remove an edge' },
@@ -97,10 +92,9 @@ export const CLASS_MAP = {
97
92
  'R-29': { class: 'Constraint', rationale: 'test file claimed twice — which acceptance owns it is a judgement call' },
98
93
  'R-20': { class: 'Constraint', rationale: 'add realRef attribute — no topology change' },
99
94
  'R-26': { class: 'Constraint', rationale: 'add realRef attribute — no topology change' },
100
- 'R-27': { class: 'Constraint', rationale: 'physical MOD add realRef attribute — no topology change' },
101
95
  'RD-02': { class: 'Constraint', rationale: 'decomposition consistency — repair existing' },
102
96
  'RD-03': { class: 'Constraint', rationale: 'premature decomposition — remove children' },
103
- 'RD-04': { class: 'Constraint', rationale: 'decomposition breadth 7–11 — split level, non-additive' },
97
+ 'RD-04': { class: 'Constraint', rationale: 'decomposition breadth 7±2 — split level, non-additive' },
104
98
  'MS-02': { class: 'Constraint', rationale: 'dangling dependency — fix relation target' },
105
99
  'UC-04': { class: 'Constraint', rationale: 'goal = description text — no topology change' },
106
100
  'FC-03': { class: 'Constraint', rationale: 'flatten chain — move nested funcs' },
@@ -115,10 +109,8 @@ export const CLASS_MAP = {
115
109
  'ND-02': { class: 'Constraint', rationale: 'near-duplicate SCHEMA — merge/differentiate' },
116
110
  'CR-R02': { class: 'Constraint', rationale: 'done requires commitRef — attribute' },
117
111
  'CR-R03': { class: 'Constraint', rationale: 'concurrent mutation — coordinate, non-additive' },
118
- 'AO-D01': { class: 'Constraint', rationale: 'relay nodeeliminate/rewire, advisory' },
119
- 'AO-D03': { class: 'Constraint', rationale: 'duplicate path — unify, advisory' },
112
+ 'BW-02': { class: 'Constraint', rationale: 'whitebox boundary width consolidate contracts or add a level' },
120
113
  'CR-01': { class: 'Constraint', rationale: 'crossing-flow coupling — reduce, advisory' },
121
- 'RT-01': { class: 'Constraint', rationale: 'physical boundary integrity — repair' },
122
114
  'NFR-01': { class: 'Constraint', rationale: 'budget overshoot — reduce measured/raise budget' },
123
115
  'VR-01': { class: 'Constraint', rationale: 'missing testResult attribute — no topology change' },
124
116
  // CR-SM-227 hat AF-01..05 in den Katalog gelegt, ohne sie hier zu klassifizieren;
@@ -130,9 +122,25 @@ export const CLASS_MAP = {
130
122
  'AF-04': { class: 'Constraint', rationale: 'FMEA freshness stamp — attribute, no topology change' },
131
123
  'AF-05': { class: 'Constraint', rationale: 'Implementation Plan freshness stamp — attribute, no topology change' },
132
124
  // --- Ambiguous (mixed-intent fix) ------------------------------------------
133
- 'CA-01': { class: 'Constraint', rationale: 'add capabilities OR move FUNC — mixed', ambiguous: true },
134
125
  'CL-01': { class: 'Constraint', rationale: 'ConOps completeness — attribute vs added element unclear', ambiguous: true },
135
126
  'FM-01': { class: 'Constraint', rationale: 'FMEA S/O/D attributes vs added mitigation — mixed', ambiguous: true },
127
+ // --- Kongruenz (CR-SM-305): der Code hat recht, nicht der Graph ------------
128
+ //
129
+ // Alle sechs sind **Constraints**, und der Grund ist derselbe für alle: der Befund sagt, dass
130
+ // die Bindung an den Code nicht mehr stimmt — und die Antwort darauf ist entweder eine
131
+ // Änderung AM CODE (den der Optimizer nicht anfasst) oder ein Umbinden der Referenz auf den
132
+ // Stand, den der Code inzwischen hat. Beides ist keine Topologie-Änderung am Graphen, und
133
+ // beides kann der Optimizer nicht aus dem Elementtext ableiten: wohin eine verwaiste `realRef`
134
+ // ZEIGEN SOLL, steht in keinem Feld. Ein Operator daraus wäre geraten, nicht abgeleitet.
135
+ //
136
+ // `applyRule`/`suggestEdits` verwerfen alles, was nicht `Operator` ist — RC bleibt damit
137
+ // Bericht und Gate-Signal, so wie es gemeint ist.
138
+ 'RC-01': { class: 'Constraint', rationale: 'realRef zeigt ins Leere — umbinden oder Code nachziehen, keine Kante' },
139
+ 'RC-02': { class: 'Constraint', rationale: 'testRefs zeigt ins Leere — Testdatei/Fall umbinden, keine Kante' },
140
+ 'RC-03': { class: 'Constraint', rationale: 'SCHEMA-realRef zeigt ins Leere — umbinden, keine Kante' },
141
+ 'RC-04': { class: 'Constraint', rationale: 'SCHEMA wird an seiner Schnittstelle nicht geparst — Codeänderung' },
142
+ 'RC-05': { class: 'Constraint', rationale: 'Import quert eine undokumentierte Modulgrenze — Import entfernen oder Fluss modellieren', ambiguous: true },
143
+ 'RC-06': { class: 'Constraint', rationale: 'externe Bindung nennt ein nicht deklariertes Paket — dependency oder realRef' },
136
144
  };
137
145
  const UNKNOWN = { class: 'Constraint', rationale: 'unclassified — not in CLASS_MAP', ambiguous: true };
138
146
  /** Classify a single rule id (falls back to ambiguous-unknown). */
@@ -0,0 +1,104 @@
1
+ /**
2
+ * steer.ts — der Ranking-Score der Familie (CR-SM-292).
3
+ *
4
+ * Er ersetzt `Δm · t̂` aus `metrics()`, das dreimal an gemessenen Gegenbeispielen gefallen ist
5
+ * (CR-SM-281, CR-SM-287 §10/§11): eine gewichtete Summe über sechs Zahlen aus dem UNTYPISIERTEN
6
+ * Graphen kann Enthaltensein nicht von Fluss und nicht von Besitz unterscheiden — und rankte
7
+ * deshalb einen Code-Import mit 306 flachen Wurzeln über jedes strukturierte Modell der Familie.
8
+ *
9
+ * ## Die Rechnung
10
+ *
11
+ * score = max_i( (wert_i − budget_i)/budget_i ) + ε · mittel_i( (wert_i − budget_i)/budget_i )
12
+ *
13
+ * über die fünf messenden Regeln. **Kleiner ist besser**; 0 heisst „jede Blackbox innerhalb
14
+ * ihres Budgets".
15
+ *
16
+ * ## Warum das Maximum und nicht die Summe
17
+ *
18
+ * Summe und Lexikographie sind derselbe Fehler an entgegengesetzten Enden. Die Summe ist
19
+ * KOMPENSATORISCH — eine gute Dimension kauft eine schlechte frei (CR-SM-281: `faultTolerance`
20
+ * +2,11 kaufte drei Regressionen frei). Die Lexikographie ist DIKTATORISCH — die erste Stufe,
21
+ * die sich unterscheidet, entscheidet allein (CR-SM-287 §11: RD-04 entschied, BW-02 kam nie zu
22
+ * Wort, und ein bedeutungsloser Zufallsschnitt gewann gegen den bestätigten).
23
+ *
24
+ * Chebyshev (Wierzbickis achievement scalarizing function) sitzt dazwischen: **man ist so gut wie
25
+ * die eigene schlechteste Stelle.** Kein Freikauf, kein Diktat. Und es ist dieselbe Logik, nach
26
+ * der das Gate schon immer urteilt — „ein neuer `error` blockiert" ist ein Maximum über
27
+ * Severities, nie ein Mittelwert.
28
+ *
29
+ * ## Warum es keine neuen freien Zahlen gibt
30
+ *
31
+ * Normiert wird gegen `context.threshold`, also gegen die **Regelschwelle selbst**
32
+ * (`policy.decompositionBreadth`, `boundaryWidth`, `crossingFlows`, `instability`, `lcom4`) —
33
+ * aus der Verteilung realer Modelle abgelesen, unter `RULES_VERSION` versioniert, an einer Stelle
34
+ * grep-bar. Genau daran ist CR-SM-281 gestorben: sechs Gewichte, die nie bestimmbar waren
35
+ * (R² = 0,91 bei R²_LOO = −40, N = 9 auf 6 Prädiktoren). Hier erbt die Steuerung die Schwellen
36
+ * der Regeln, statt eigene zu führen.
37
+ *
38
+ * `EPS_AUGMENT` ist die einzige eigene Zahl und kein Urteil: ohne sie sind zwei Kandidaten mit
39
+ * gleichem Maximum ununterscheidbar (schwach pareto-optimal) und die Rangfolge hinge an der
40
+ * Sortier-Stabilität.
41
+ *
42
+ * ## Was der Score NICHT kann — gemessen, nicht vermutet (CR-SM-291 §7.2)
43
+ *
44
+ * 1. **Er misst Form, nie Substanz.** Ein Kandidat, der Elemente LÖSCHT, senkt ihn zuverlässig.
45
+ * Dagegen hilft kein besserer Skalar, sondern `beatsBySteer` — die Nebenbedingung unten.
46
+ * 2. **Innerhalb aller Budgets ist er blind** (Score 0 für alles). Dort führt `readiness`, die
47
+ * weiter zählt, was fehlt. Die beiden sind komplementär: readiness misst ABDECKUNG (wie
48
+ * viele Stellen sind erledigt), dieser Score AUSPRÄGUNG (wie schlimm ist die schlimmste
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.
54
+ */
55
+ import type { RuleViolation } from '@sigloch/contracts/se';
56
+ /**
57
+ * Die fünf messenden Regeln — der theoriegestützte Satz, der im Katalog schon stand: Simon
58
+ * (Breite je Container), Parnas (Randbreite je Whitebox), Baldwin/Clark (Kopplung je Modulpaar),
59
+ * Martin (Instabilität), LCOM4 (Kohäsion).
60
+ *
61
+ * Bewusst eine geschlossene Liste und NICHT aus `context.value !== undefined` abgeleitet: eine
62
+ * künftige Regel, die zufällig eine Zahl mitführt, würde sonst still zum Steuersignal.
63
+ */
64
+ export declare const STEER_RULES: readonly ["RD-04", "BW-02", "CR-01", "MT-02"];
65
+ /** Der Ausgleichsterm. Klein genug, dass er ein echtes Maximum nie überstimmt. */
66
+ export declare const EPS_AUGMENT = 0.001;
67
+ export interface SteerScore {
68
+ /** Der schlimmste normierte Überschuss. 0 = jede Blackbox innerhalb ihres Budgets. */
69
+ worst: number;
70
+ /** Wo er sitzt — die Begründung, nicht nur die Zahl. `null`, wenn nichts überschreitet. */
71
+ worstAt: {
72
+ ruleId: string;
73
+ elementId: string;
74
+ } | null;
75
+ /** Mittel über alle gemessenen Blackboxes; Grundlage des Ausgleichsterms. */
76
+ mean: number;
77
+ /** `worst + EPS_AUGMENT * mean` — der Vergleichswert. **Kleiner ist besser.** */
78
+ score: number;
79
+ /** Zahl der Blackboxes, die in die Rechnung eingegangen sind. */
80
+ measured: number;
81
+ }
82
+ /** Der Chebyshev-Score eines Zustands, aus seinem Regelstrom. Rein. */
83
+ export declare function steerScore(violations: readonly RuleViolation[]): SteerScore;
84
+ /** Ein Kandidat, so weit der Ranker ihn kennen muss. */
85
+ export interface SteerCandidate {
86
+ /** Der Score des Zustands NACH dem Zug. */
87
+ score: SteerScore;
88
+ /**
89
+ * Entfernt der Zug Elemente? Gemessen, nicht deklariert: Elementzahl vorher gegen nachher.
90
+ */
91
+ removesElements: boolean;
92
+ }
93
+ /**
94
+ * Vergleicht zwei Kandidaten. `< 0` heisst **a ist besser**; direkt sortierbar.
95
+ *
96
+ * Die Zerstörungs-Sperre steht VOR dem Score und ist keine Gewichtung: gemessen (CR-SM-291
97
+ * Satz F) rankt „18 Kinder löschen" unter jeder Massen-Ablesung vor jeder echten Reparatur, weil
98
+ * der ℝ⁵ Form misst und nie Substanz. Ein Score, der Zerstörung „einpreist", bräuchte genau das
99
+ * Gewicht, das niemand bestimmen kann — deshalb eine Nebenbedingung statt einer Zahl.
100
+ *
101
+ * Zwei löschende Kandidaten werden untereinander wieder nach Score verglichen: die Sperre soll
102
+ * Löschen nicht belohnen, aber auch nicht unvergleichbar machen.
103
+ */
104
+ export declare function beatsBySteer(a: SteerCandidate, b: SteerCandidate): number;
package/dist/steer.js ADDED
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Die fünf messenden Regeln — der theoriegestützte Satz, der im Katalog schon stand: Simon
3
+ * (Breite je Container), Parnas (Randbreite je Whitebox), Baldwin/Clark (Kopplung je Modulpaar),
4
+ * Martin (Instabilität), LCOM4 (Kohäsion).
5
+ *
6
+ * Bewusst eine geschlossene Liste und NICHT aus `context.value !== undefined` abgeleitet: eine
7
+ * künftige Regel, die zufällig eine Zahl mitführt, würde sonst still zum Steuersignal.
8
+ */
9
+ /*
10
+ * CR-SM-293: **MT-01 ist raus.** Der Satz oben nennt vier Theorien und beschreibt damit den
11
+ * Stand; Martin fehlt vorerst, und das ist eine Luecke mit Namen, kein stiller Verzicht.
12
+ *
13
+ * Der Grund ist das Kriterium aus CR-SM-287 §2 selbst — lokal, GERICHTET ("weniger ist besser,
14
+ * ohne Diskussion"), BUDGETIERT ("Schwelle aus der Verteilung abgelesen"). MT-01 erfuellt nur
15
+ * das erste. Martins Aussage ist "in Richtung Stabilitaet abhaengen", nicht "I klein halten":
16
+ * ein Blattmodul mit I = 1.0 ist richtig. Und die Verteilung gibt keine Schwelle her (44 Module
17
+ * exakt auf 0.00, 45 exakt auf 1.00 von 149). MT-01 kam ueber die Theorie-TABELLE in §3 herein
18
+ * und wurde nie gegen die drei Kriterien gehalten, die derselbe CR eine Seite vorher aufstellt.
19
+ *
20
+ * Praktisch war die Dimension ohnehin still: `steerScore` liest VERSTOESSE, und graphcodes
21
+ * Config setzt `instability: null` seit CR-GC-329 — auf dem einzigen Repo, das taeglich steuert,
22
+ * lief der R5 laengst als R4. Diese Zeile sagt es jetzt, statt es zu verbergen.
23
+ *
24
+ * Der Nachfolger ist gemessen und angelegt (CR-SM-298): Martins Stable Dependencies Principle
25
+ * als GERICHTETE Groesse — Abhaengigkeiten "bergauf" je Modul. Ueber 19 Familiengraphen 34 von
26
+ * 305 Abhaengigkeiten (11 %), 132 von 146 Modulen bei null, Schwanz bis 6. Lokal, gerichtet,
27
+ * budgetierbar, nicht entartet: drei von drei.
28
+ */
29
+ export const STEER_RULES = ['RD-04', 'BW-02', 'CR-01', 'MT-02'];
30
+ /** Der Ausgleichsterm. Klein genug, dass er ein echtes Maximum nie überstimmt. */
31
+ export const EPS_AUGMENT = 1e-3;
32
+ const NEUTRAL = { worst: 0, worstAt: null, mean: 0, score: 0, measured: 0 };
33
+ /**
34
+ * Normierter Überschuss je Befund: `(wert − budget) / budget`, dimensionslos.
35
+ *
36
+ * Ein Befund ohne `value`/`threshold` wird ÜBERSPRUNGEN, nicht als 0 gezählt — „keine Messung"
37
+ * ist nicht „keine Überschreitung". Dieselbe Konvention wie `policy.X = null` und
38
+ * `moduleMetrics.instability = null`.
39
+ */
40
+ function overshootOf(v) {
41
+ const value = v.context?.value;
42
+ const budget = v.context?.threshold;
43
+ if (typeof value !== 'number' || typeof budget !== 'number' || budget <= 0)
44
+ return null;
45
+ return Math.max(0, (value - budget) / budget);
46
+ }
47
+ /** Der Chebyshev-Score eines Zustands, aus seinem Regelstrom. Rein. */
48
+ export function steerScore(violations) {
49
+ const rules = STEER_RULES;
50
+ let worst = 0;
51
+ let worstAt = null;
52
+ 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;
62
+ // `>` statt `>=`: bei Gleichstand gewinnt der zuerst gesehene, und die Befund-Sequenz ist
63
+ // 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 };
67
+ }
68
+ }
69
+ if (measured === 0)
70
+ return NEUTRAL;
71
+ const mean = sum / measured;
72
+ return { worst, worstAt, mean, score: worst + EPS_AUGMENT * mean, measured };
73
+ }
74
+ /**
75
+ * Vergleicht zwei Kandidaten. `< 0` heisst **a ist besser**; direkt sortierbar.
76
+ *
77
+ * Die Zerstörungs-Sperre steht VOR dem Score und ist keine Gewichtung: gemessen (CR-SM-291
78
+ * Satz F) rankt „18 Kinder löschen" unter jeder Massen-Ablesung vor jeder echten Reparatur, weil
79
+ * der ℝ⁵ Form misst und nie Substanz. Ein Score, der Zerstörung „einpreist", bräuchte genau das
80
+ * Gewicht, das niemand bestimmen kann — deshalb eine Nebenbedingung statt einer Zahl.
81
+ *
82
+ * Zwei löschende Kandidaten werden untereinander wieder nach Score verglichen: die Sperre soll
83
+ * Löschen nicht belohnen, aber auch nicht unvergleichbar machen.
84
+ */
85
+ export function beatsBySteer(a, b) {
86
+ if (a.removesElements !== b.removesElements)
87
+ return a.removesElements ? 1 : -1;
88
+ return a.score.score - b.score.score;
89
+ }
package/dist/suggest.d.ts CHANGED
@@ -1,9 +1,7 @@
1
1
  import type { OntologyGraph } from '@sigloch/contracts/se';
2
- import { type MetricVector } from './metrics.js';
2
+ import { type SteerScore } from './steer.js';
3
3
  import type { MetricLayer } from './layer.js';
4
4
  import { type SuggestedEdit } from './fix-templates.js';
5
- /** Build a target vector (ℝ⁶, canonical dimension order) from named metric weights. */
6
- export declare function targetFor(weights: Partial<Record<keyof MetricVector, number>>): number[];
7
5
  export interface Suggestion {
8
6
  ruleId: string;
9
7
  /** Fund: das verletzte Element. */
@@ -11,10 +9,21 @@ export interface Suggestion {
11
9
  /** Fund: die Regel-Botschaft (erste Violation der Regel). */
12
10
  message: string;
13
11
  fixHint?: string;
14
- /** Δm der generischen Richtungssonde (ℝ⁶, kanonische Ordnung). */
12
+ /**
13
+ * Δm der generischen Richtungssonde (ℝ⁶, kanonische Ordnung) — **Ablesung, kein Ranking**.
14
+ * Seit CR-SM-292 rankt niemand mehr danach; die Zahl bleibt im Bericht, weil `graph_metrics`
15
+ * und das Fit-Advisory sie zeigen.
16
+ */
15
17
  delta: number[];
16
- /** Projektion von Δm auf das Einheitsziel: >0 hin, <0 weg. */
18
+ /**
19
+ * **Senkung des Chebyshev-Scores** durch diesen Zug: `steer(basis) − steer(nachher)`.
20
+ * Positiv = besser, absteigend sortiert. Ersetzt `Δm · t̂` (CR-SM-292).
21
+ */
17
22
  score: number;
23
+ /** Der Zustands-Score NACH dem Zug — trägt `worstAt`, also das WO, nicht nur das WIEVIEL. */
24
+ steer: SteerScore;
25
+ /** Entfernt der Zug Elemente? Trägt die Zerstörungs-Sperre (`beatsBySteer`). */
26
+ removesElements: boolean;
18
27
  /** Rule-spezifischer Template-Edit; fehlt = Fund-Ebene ohne Edit. */
19
28
  edit?: SuggestedEdit;
20
29
  }
@@ -25,7 +34,11 @@ export interface SuggestOptions {
25
34
  layer?: MetricLayer;
26
35
  }
27
36
  /**
28
- * Rank the firing Operator rules by how much their probe edit moves `graph`
29
- * toward `target`. Deterministic, score-descending, tiebreak ruleId.
37
+ * Rankt die feuernden Operator-Regeln danach, wie weit ihr Sonden-Edit den **Chebyshev-Score**
38
+ * senkt (CR-SM-292). Deterministisch, score-absteigend, Tiebreak ruleId/elementId.
39
+ *
40
+ * Vorher stand hier `Δm · t̂` gegen einen Zielvektor aus `target-profile.json`. Der Parameter ist
41
+ * mit diesem CR ersatzlos entfallen — dreimal widerlegt, und ein zweiter Pfad daneben wäre genau
42
+ * das, was der Regelsatz verbietet.
30
43
  */
31
- export declare function suggestEdits(graph: OntologyGraph, target: number[], opts?: SuggestOptions): Suggestion[];
44
+ export declare function suggestEdits(graph: OntologyGraph, opts?: SuggestOptions): Suggestion[];
package/dist/suggest.js CHANGED
@@ -2,39 +2,43 @@
2
2
  * Greedy one-step optimization suggestions (promotet aus aimpro
3
3
  * src/spike/suggest.ts, CR-234 → CR-SM-225) — MIT dem Spike-2-Redesign:
4
4
  *
5
- * AUSGELIEFERT wird die Fund-Ebene: Violation (Element + Message) + Richtung
6
- * (score = Δm·t̂) + Δm. Der generische applyRule-Trace bleibt eine interne
7
- * Richtungssonde als Edit ausgeliefert wird NUR, was ein rule-spezifisches
8
- * Fix-Template (fix-templates.ts) deterministisch aus dem Elementtext herleitet.
9
- * Δm rankt, es gibt nie frei: Anwendung läuft immer durchs Gate (3-Tier-Verdict,
10
- * graphcode graph_suggest → dryRun-Preview), nie auto-apply.
5
+ * AUSGELIEFERT wird die Fund-Ebene: Violation (Element + Message) + Richtung. Der generische
6
+ * applyRule-Trace bleibt eine interne Richtungssonde als Edit ausgeliefert wird NUR, was ein
7
+ * rule-spezifisches Fix-Template (fix-templates.ts) deterministisch aus dem Elementtext
8
+ * herleitet. Der Score rankt, er gibt nie frei: Anwendung läuft immer durchs Gate
9
+ * (3-Tier-Verdict, graphcode graph_suggest dryRun-Preview), nie auto-apply.
10
+ *
11
+ * CR-SM-292: gerankt wird nach der Senkung des CHEBYSHEV-Scores (`steer.ts`), nicht mehr nach
12
+ * `Δm · t̂`. Δm bleibt als Ablesung im Ergebnis, rankt aber nichts mehr — die Begründung steht
13
+ * vollständig im Kopf von `steer.ts`.
11
14
  *
12
15
  * Bewusst EIN Schritt von der Baseline — kein Sequencing, keine Interaktion,
13
16
  * keine Pareto-Front, keine Suche (Fahrplan-Schritt 5).
14
17
  */
15
18
  import { evaluateAllRules } from '@sigloch/contracts/se';
16
- import { metrics, toArray, METRIC_DIMENSIONS } from './metrics.js';
19
+ import { metrics, toArray } from './metrics.js';
20
+ import { steerScore, beatsBySteer } from './steer.js';
17
21
  import { applyRule } from './rule-apply.js';
18
22
  import { classOf, MT_IRRELEVANT_POLICY } from './rule-classify.js';
19
- import { fixFor } from './fix-templates.js';
20
- /** Build a target vector (ℝ⁶, canonical dimension order) from named metric weights. */
21
- export function targetFor(weights) {
22
- return METRIC_DIMENSIONS.map((d) => weights[d] ?? 0);
23
- }
24
- function l2(v) {
25
- return Math.sqrt(v.reduce((s, x) => s + x * x, 0));
26
- }
23
+ import { fixFor, mergeCandidates, applyMergeEdit, MERGE_OPERATOR_ID } from './fix-templates.js';
27
24
  /**
28
- * Rank the firing Operator rules by how much their probe edit moves `graph`
29
- * toward `target`. Deterministic, score-descending, tiebreak ruleId.
25
+ * Rankt die feuernden Operator-Regeln danach, wie weit ihr Sonden-Edit den **Chebyshev-Score**
26
+ * senkt (CR-SM-292). Deterministisch, score-absteigend, Tiebreak ruleId/elementId.
27
+ *
28
+ * Vorher stand hier `Δm · t̂` gegen einen Zielvektor aus `target-profile.json`. Der Parameter ist
29
+ * mit diesem CR ersatzlos entfallen — dreimal widerlegt, und ein zweiter Pfad daneben wäre genau
30
+ * das, was der Regelsatz verbietet.
30
31
  */
31
- export function suggestEdits(graph, target, opts = {}) {
32
+ export function suggestEdits(graph, opts = {}) {
32
33
  const { k = Infinity, layer = 'all' } = opts;
33
- const tn = l2(target);
34
- const t = tn < 1e-12 ? target : target.map((x) => x / tn);
35
34
  const measure = (g) => toArray(metrics(g, { layer }));
36
35
  const base = measure(graph);
37
36
  const violations = evaluateAllRules(graph, MT_IRRELEVANT_POLICY);
37
+ // Der Score braucht die Befunde MIT ihren Schwellen; `MT_IRRELEVANT_POLICY` legt MT-01/MT-02
38
+ // still (`null`), sie tragen dort also keine `context.threshold` und fallen im Score
39
+ // automatisch weg — dieselbe Konvention wie in `steerScore`, kein Sonderfall hier.
40
+ const baseSteer = steerScore(violations);
41
+ const steerOf = (g) => steerScore(evaluateAllRules(g, MT_IRRELEVANT_POLICY));
38
42
  const firstByRule = new Map();
39
43
  for (const v of violations)
40
44
  if (!firstByRule.has(v.rule_id))
@@ -47,17 +51,90 @@ export function suggestEdits(graph, target, opts = {}) {
47
51
  if (!probe.applied)
48
52
  continue;
49
53
  const delta = measure(probe.graph).map((x, i) => x - base[i]);
50
- const score = delta.reduce((s, x, i) => s + x * t[i], 0);
54
+ const steer = steerOf(probe.graph);
51
55
  suggestions.push({
52
56
  ruleId,
53
57
  elementId: v.element_id,
54
58
  message: v.message,
55
59
  fixHint: v.fix_hint,
56
60
  delta,
57
- score,
61
+ score: baseSteer.score - steer.score,
62
+ steer,
63
+ removesElements: probe.graph.elements.length < graph.elements.length,
58
64
  edit: fixFor(v, graph) ?? undefined,
59
65
  });
60
66
  }
61
- suggestions.sort((a, b) => b.score - a.score || a.ruleId.localeCompare(b.ruleId));
67
+ // CR-SM-302 — die Regel, deren KLASSE nicht reicht. `CLASS_MAP` ist je `rule_id`; R-18
68
+ // meldet vier Beine, und drei davon (illegales Paar, Kardinalitaets-Obergrenze,
69
+ // compose-Baum) heilt nur ein Loeschen — deshalb steht die Regel dort zu Recht als
70
+ // `Constraint`. Das vierte Bein ist das Gegenteil: es fehlt genau EINE Kante, deren Typ und
71
+ // Zieltyp das Meta-Modell vorgibt. Trennen kann das nicht die Klasse, sondern nur das
72
+ // TEMPLATE — es liefert genau dort einen Edit und sonst `null`.
73
+ //
74
+ // Deshalb ist hier der gelieferte Edit das Kriterium, und Δm kommt aus IHM statt aus der
75
+ // generischen Sonde: dieselbe Linie wie der Merge-Operator unten und wie CR-GC-431
76
+ // ("gerankt wird, was ausgeliefert wird"). Die Sonde ist fuer eine Constraint-Regel
77
+ // ohnehin nicht anwendbar — vor diesem Block fiel der Fund deshalb ganz heraus, mitsamt
78
+ // dem vertragsbauenden Arm des Aktionsraums (CR-GC-430: COHESIVE 5 -> 2 Schritte).
79
+ for (const [ruleId, v] of firstByRule) {
80
+ if (classOf(ruleId).class === 'Operator')
81
+ continue;
82
+ const edit = fixFor(v, graph);
83
+ if (!edit || edit.op !== 'add-trace')
84
+ continue;
85
+ const kept = edit.retire
86
+ ? graph.traces.filter((t) => !(t.source === edit.retire.source && t.target === edit.retire.target && t.type === edit.retire.type))
87
+ : graph.traces;
88
+ const after = {
89
+ ...graph,
90
+ traces: [...kept, { source: edit.source, target: edit.target, type: edit.type }],
91
+ };
92
+ const steer = steerOf(after);
93
+ suggestions.push({
94
+ ruleId,
95
+ elementId: v.element_id,
96
+ message: v.message,
97
+ fixHint: v.fix_hint,
98
+ delta: measure(after).map((x, i) => x - base[i]),
99
+ score: baseSteer.score - steer.score,
100
+ steer,
101
+ removesElements: false,
102
+ edit,
103
+ });
104
+ }
105
+ // CR-GC-444 — der KONSOLIDIERUNGS-Operator, die eine Quelle ohne Violation.
106
+ // Ein Merge repariert keine Regel (deshalb kein Fund, deshalb `MERGE_OPERATOR_ID`
107
+ // statt einer erfundenen Regel-ID) — er legt zwei Knoten zusammen, die denselben
108
+ // Vertrag tragen, und bewegt damit die Metrik. Δm kommt hier NICHT aus der
109
+ // generischen `applyRule`-Sonde, sondern aus dem echten In-Memory-Merge: das
110
+ // Δm des Zuges, der auch ausgeliefert wird.
111
+ for (const edit of mergeCandidates(graph)) {
112
+ const merged = applyMergeEdit(graph, edit);
113
+ const delta = measure(merged).map((x, i) => x - base[i]);
114
+ const steer = steerOf(merged);
115
+ suggestions.push({
116
+ ruleId: MERGE_OPERATOR_ID,
117
+ elementId: edit.source,
118
+ message: edit.rationale,
119
+ fixHint: 'Als EIN graph_mutate-Batch anwenden: merge-nodes(source→target) plus die gekoppelten ' +
120
+ '`merges` — getrennt weist das Gate den FLOW-Merge über R-18 ab (FLOW -relation-> SCHEMA ist 1..1).',
121
+ delta,
122
+ score: baseSteer.score - steer.score,
123
+ steer,
124
+ // Ein Merge entfernt per Definition einen Knoten. Die Sperre (CR-SM-291 §7.2 Grenze 1)
125
+ // trifft ihn damit bewusst: er bleibt Kandidat, rankt aber nie über einem Zug, der
126
+ // nichts wegnimmt.
127
+ removesElements: true,
128
+ edit,
129
+ });
130
+ }
131
+ // Tiebreak bis auf elementId: seit dem Konsolidierungs-Operator kann DIESELBE
132
+ // Kennung mehrfach vorkommen, und eine stabile Ordnung ist Teil des Vertrags.
133
+ // `beatsBySteer` trägt die Zerstörungs-Sperre und den Score; die Kennungen sind der
134
+ // Determinismus-Anker.
135
+ const asCandidate = (x) => ({ score: x.steer, removesElements: x.removesElements });
136
+ suggestions.sort((a, b) => beatsBySteer(asCandidate(a), asCandidate(b)) ||
137
+ a.ruleId.localeCompare(b.ruleId) ||
138
+ a.elementId.localeCompare(b.elementId));
62
139
  return Number.isFinite(k) ? suggestions.slice(0, k) : suggestions;
63
140
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sigloch/se-engine",
3
- "version": "1.3.0",
3
+ "version": "1.4.1",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -42,9 +42,9 @@
42
42
  "access": "public"
43
43
  },
44
44
  "peerDependencies": {
45
- "@sigloch/contracts": ">=5 <10"
45
+ "@sigloch/contracts": ">=10 <11"
46
46
  },
47
47
  "devDependencies": {
48
- "@sigloch/contracts": ">=6 <10"
48
+ "@sigloch/contracts": ">=10 <11"
49
49
  }
50
50
  }