@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.
- package/dist/fix-templates.d.ts +117 -1
- package/dist/fix-templates.js +330 -8
- package/dist/metrics.d.ts +3 -2
- package/dist/metrics.js +4 -2
- package/dist/rule-classify.js +21 -13
- package/dist/steer.d.ts +104 -0
- package/dist/steer.js +89 -0
- package/dist/suggest.d.ts +21 -8
- package/dist/suggest.js +100 -23
- package/package.json +3 -3
package/dist/fix-templates.d.ts
CHANGED
|
@@ -1,11 +1,63 @@
|
|
|
1
1
|
import type { OntologyGraph, RuleViolation, TraceType, ElementType } from '@sigloch/contracts/se';
|
|
2
2
|
export interface SuggestedEdit {
|
|
3
|
-
|
|
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 {};
|
package/dist/fix-templates.js
CHANGED
|
@@ -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
|
|
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
|
|
27
|
-
*
|
|
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
|
-
|
|
135
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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 {
|
|
52
|
-
export {
|
|
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 {
|
|
96
|
-
|
|
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';
|
package/dist/rule-classify.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
'
|
|
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). */
|
package/dist/steer.d.ts
ADDED
|
@@ -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
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
29
|
-
*
|
|
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,
|
|
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
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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
|
|
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
|
-
*
|
|
29
|
-
*
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
+
"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": ">=
|
|
45
|
+
"@sigloch/contracts": ">=10 <11"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
|
-
"@sigloch/contracts": ">=
|
|
48
|
+
"@sigloch/contracts": ">=10 <11"
|
|
49
49
|
}
|
|
50
50
|
}
|