@sigloch/se-engine 1.2.0 → 1.4.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.
@@ -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:
@@ -69,7 +112,10 @@ function relationToMentioned(types) {
69
112
  continue;
70
113
  if (hasTrace(g, el.id, target.id, 'relation'))
71
114
  continue;
72
- if (!isValidTrace({ source: el.type, target: target.type, type: 'relation' }))
115
+ if (!isValidTrace({
116
+ source: el.type, target: target.type, type: 'relation',
117
+ sourceKinds: el.kinds, targetKinds: target.kinds,
118
+ }))
73
119
  continue;
74
120
  return {
75
121
  op: 'add-trace',
@@ -109,8 +155,19 @@ function edgeToMentioned(opts) {
109
155
  const target = opts.direction === 'out' ? other : el;
110
156
  if (hasTrace(g, source.id, target.id, opts.traceType))
111
157
  continue;
112
- if (!isValidTrace({ source: source.type, target: target.type, type: opts.traceType }))
158
+ if (!isValidTrace({
159
+ source: source.type, target: target.type, type: opts.traceType,
160
+ sourceKinds: source.kinds, targetKinds: target.kinds,
161
+ }))
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')
113
169
  continue;
170
+ const file = retire ? realFileOf(source) : undefined;
114
171
  return {
115
172
  op: 'add-trace',
116
173
  source: source.id,
@@ -119,14 +176,17 @@ function edgeToMentioned(opts) {
119
176
  rationale: mentioned.length > 0
120
177
  ? `${other.id} ist im Text von ${el.id} genannt`
121
178
  : `${other.id} ist das einzige ${other.type} im Graphen`,
179
+ ...(retire ? { retire } : {}),
180
+ ...(retire && file ? { codeImpact: { file, targetModule: target.id } } : {}),
122
181
  };
123
182
  }
124
183
  return null;
125
184
  };
126
185
  }
127
186
  export const FIX_TEMPLATES = {
128
- 'CR-R01': relationToMentioned(['UC', 'REQ', 'FUNC', 'MOD']),
129
- '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']),
130
190
  // --- Architektur-Operatoren (CR-SM-241) ------------------------------------
131
191
  // Ohne sie hat auf `layer: 'arch'` — graphcodes DEFAULT-Messebene — jeder
132
192
  // Template-Edit Δm = 0, weil CR/MS/UC gar nicht im Teilgraphen liegen.
@@ -138,9 +198,46 @@ export const FIX_TEMPLATES = {
138
198
  // irgendwohin), „das einzige FUNC" ist keine — ein FUNC darf legitim einem
139
199
  // anderen Modul gehören, und dieses hier hätte dann einfach noch keins.
140
200
  'R-23': edgeToMentioned({ types: ['FUNC'], traceType: 'allocate', direction: 'in' }),
141
- // FLOW ohne Datenvertrag → das im FLOW-Text genannte SCHEMA, sonst das einzige.
201
+ // SCHEMA-Nutzung → das im Text genannte SCHEMA, sonst das einzige.
142
202
  'SC-02': edgeToMentioned({ types: ['SCHEMA'], traceType: 'relation', direction: 'out', uniqueFallback: true }),
143
- '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
+ },
144
241
  'MS-03': (v, g) => {
145
242
  const cr = byId(g, v.element_id);
146
243
  if (!cr)
@@ -150,7 +247,10 @@ export const FIX_TEMPLATES = {
150
247
  const ms = mentioned[0] ?? (milestones.length === 1 ? milestones[0] : undefined);
151
248
  if (!ms || hasTrace(g, cr.id, ms.id, 'relation'))
152
249
  return null;
153
- if (!isValidTrace({ source: cr.type, target: ms.type, type: 'relation' }))
250
+ if (!isValidTrace({
251
+ source: cr.type, target: ms.type, type: 'relation',
252
+ sourceKinds: cr.kinds, targetKinds: ms.kinds,
253
+ }))
154
254
  return null;
155
255
  return {
156
256
  op: 'add-trace',
@@ -162,23 +262,50 @@ export const FIX_TEMPLATES = {
162
262
  : `${ms.id} ist der einzige Milestone im Graphen`,
163
263
  };
164
264
  },
265
+ /**
266
+ * CR-SM-266 D1: der Vorschlag geht an den FLOW, nicht mehr an den UC.
267
+ *
268
+ * Die alte Fassung schlug `ACTOR -io-> UC` vor. Mit dem Wegfall des Patterns waere sie
269
+ * still gestorben — `isValidTrace` haette jeden Kandidaten verworfen, die Schleife immer
270
+ * `null` geliefert, und UC-02 haette einen Fix-Template-Eintrag OHNE Wirkung behalten: ein
271
+ * Vorschlagspfad, der nichts mehr vorschlaegt, ist schlimmer als keiner, weil niemand ihn
272
+ * vermisst.
273
+ *
274
+ * Der tragende Pfad ist jetzt `ACTOR -io-> FLOW`, wobei der FLOW ein Kettenglied des UC
275
+ * speisen muss — sonst ist die Kante zwar gueltig, macht UC-02 aber nicht gruen und der
276
+ * Vorschlag waere eine Kante ins Leere. Deshalb wird der FLOW nicht geraten, sondern aus
277
+ * dem Graphen HERGELEITET: die FLOWs, die in eine FUNC einer FCHAIN dieses UC laufen.
278
+ * Ist keiner da, gibt das Template `null` — der fehlende FLOW ist dann die eigentliche
279
+ * Luecke und gehoert R-31, nicht einer erfundenen Actor-Kante.
280
+ */
165
281
  'UC-02': (v, g) => {
166
282
  const uc = byId(g, v.element_id);
167
283
  if (!uc)
168
284
  return null;
285
+ const chainMembers = new Set(g.traces
286
+ .filter((t) => t.type === 'compose' && t.source === uc.id)
287
+ .flatMap((chain) => g.traces.filter((t) => t.type === 'compose' && t.source === chain.target).map((t) => t.target)));
288
+ const feedingFlows = g.traces
289
+ .filter((t) => t.type === 'io' && chainMembers.has(t.target))
290
+ .map((t) => byId(g, t.source))
291
+ .filter((e) => e?.type === 'FLOW');
292
+ if (feedingFlows.length === 0)
293
+ return null;
169
294
  const actors = mentionedElements(`${uc.name} ${uc.description ?? ''}`, g, ['ACTOR']);
170
295
  for (const actor of actors) {
171
- if (hasTrace(g, actor.id, uc.id, 'io'))
172
- continue;
173
- if (!isValidTrace({ source: actor.type, target: uc.type, type: 'io' }))
174
- continue;
175
- return {
176
- op: 'add-trace',
177
- source: actor.id,
178
- target: uc.id,
179
- type: 'io',
180
- rationale: `${actor.id} ist im Text von ${uc.id} genannt`,
181
- };
296
+ for (const flow of feedingFlows) {
297
+ if (hasTrace(g, actor.id, flow.id, 'io'))
298
+ continue;
299
+ if (!isValidTrace({ source: actor.type, target: flow.type, type: 'io' }))
300
+ continue;
301
+ return {
302
+ op: 'add-trace',
303
+ source: actor.id,
304
+ target: flow.id,
305
+ type: 'io',
306
+ rationale: `${actor.id} ist im Text von ${uc.id} genannt, und ${flow.id} speist dessen Wirkkette`,
307
+ };
308
+ }
182
309
  }
183
310
  return null;
184
311
  },
@@ -190,3 +317,234 @@ export const FIX_TEMPLATES = {
190
317
  export function fixFor(v, g) {
191
318
  return FIX_TEMPLATES[v.rule_id]?.(v, g) ?? null;
192
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';