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