@sigloch/se-engine 1.4.0 → 1.5.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.
@@ -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
- * (CR-GC-444, Zielbild „graphcode als Regelkreis", FLOWs 62 27).
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
@@ -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
- * (CR-GC-444, Zielbild „graphcode als Regelkreis", FLOWs 62 27).
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
- const members = grouped.get(key)
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-235: `readyThreshold` ebenso. Vorher stand hier `0.7` und im graphcode-Treiber `0.8`
23
- * (`generate.ts`, Default-Parameter) — zwei Werte fuer dieselbe Frage „ist diese Dimension zu
24
- * schwach?", und der Konsument konnte nicht wissen, welcher gilt. Den Wert liefert die Config
25
- * (CR-GC-329); diese Funktion macht nur die Zweitmeinung unmoeglich.
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, readyThreshold: number): ReadinessReportType;
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-235: `readyThreshold` ebenso. Vorher stand hier `0.7` und im graphcode-Treiber `0.8`
18
- * (`generate.ts`, Default-Parameter) — zwei Werte fuer dieselbe Frage „ist diese Dimension zu
19
- * schwach?", und der Konsument konnte nicht wissen, welcher gilt. Den Wert liefert die Config
20
- * (CR-GC-329); diese Funktion macht nur die Zweitmeinung unmoeglich.
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, readyThreshold) {
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
- const majority = defs.length / 2;
144
- const core = Object.keys(seenIn).filter(type => seenIn[type] > majority).sort();
145
- // Keine Mehrheit ableitbar (die Regeln verteilen sich gleichmaessig ueber mehrere Typen):
146
- // dann ist der Kern ALLES, was die Dimension prueft. Der Waechter greift damit nur noch,
147
- // wenn auch `applicable` 0 waere das Verhalten bleibt exakt wie vor CR-SM-270. Lieber
148
- // nicht greifen als am falschen Ort greifen.
149
- return core.length > 0 ? core : [...new Set(defs.flatMap(d => d.domain))].sort();
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 = {};
@@ -53,6 +53,9 @@ 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' },
@@ -73,6 +76,10 @@ export const CLASS_MAP = {
73
76
  'FC-02': { class: 'Operator', rationale: 'add FCHAIN via compose trace to leaf UC' },
74
77
  'SC-02': { class: 'Operator', rationale: 'link FLOW→SCHEMA via relation trace' },
75
78
  'IO-01': { class: 'Operator', rationale: 'add FLOW element + io traces between the FUNC pair' },
79
+ // CR-SM-309: IO-02 (contracts 20) — die Reparatur ist ein Auftrennen je Verbindung. Welcher
80
+ // Produzent welche Konsumenten behaelt, steht in keinem Feld, sondern im Code; ein Operator
81
+ // daraus waere geraten (Praezedenz RC-*, CR-SM-305).
82
+ 'IO-02': { class: 'Constraint', rationale: 'one producer per FLOW — the split per connection needs code evidence, no deterministic operator' },
76
83
  // CR-GC-366: beide Fixes fuegen eine Trace hinzu, also Operator wie R-15 (compose) und R-10 (io).
77
84
  'R-30': { class: 'Operator', rationale: 'add compose trace FCHAIN→FUNC to bind the function into a chain' },
78
85
  'R-31': { class: 'Operator', rationale: 'add io traces FLOW→FUNC / FUNC→FLOW to wire the function up' },
@@ -81,7 +88,7 @@ export const CLASS_MAP = {
81
88
  'FM-02': { class: 'Operator', rationale: 'create mitigation REQ + compose trace' },
82
89
  'FM-03': { class: 'Operator', rationale: 'add TEST(passed) + verify trace to risk REQ' },
83
90
  // --- Constraints: remove/repair/reduce, or attribute/text-only -------------
84
- 'R-04': { class: 'Constraint', rationale: 'module too largesplit, non-additive' },
91
+ 'R-04': { class: 'Constraint', rationale: 'module boundary too widemerge contracts or move functions, non-additive' },
85
92
  'R-08': { class: 'Constraint', rationale: 'repair dangling trace endpoint' },
86
93
  'R-12': { class: 'Constraint', rationale: 'break dependency cycle — remove an edge' },
87
94
  'R-18': { class: 'Constraint', rationale: 'invalid trace pattern — change/remove trace' },
@@ -95,11 +102,15 @@ export const CLASS_MAP = {
95
102
  'RD-02': { class: 'Constraint', rationale: 'decomposition consistency — repair existing' },
96
103
  'RD-03': { class: 'Constraint', rationale: 'premature decomposition — remove children' },
97
104
  'RD-04': { class: 'Constraint', rationale: 'decomposition breadth 7±2 — split level, non-additive' },
105
+ 'RD-05': { class: 'Constraint', rationale: 'degenerate level (< min children) — dissolve into parent, non-additive' },
98
106
  'MS-02': { class: 'Constraint', rationale: 'dangling dependency — fix relation target' },
99
107
  'UC-04': { class: 'Constraint', rationale: 'goal = description text — no topology change' },
100
108
  'FC-03': { class: 'Constraint', rationale: 'flatten chain — move nested funcs' },
101
109
  'MT-01': { class: 'Constraint', rationale: 'instability threshold — restructure' },
102
110
  'MT-02': { class: 'Constraint', rationale: 'cohesion (LCOM4) threshold — restructure' },
111
+ // CR-SM-327: wie MT-02 — der Fix zerlegt oder verdrahtet um, er fuegt keine einzelne
112
+ // Kante hinzu, die der Operator raten koennte.
113
+ 'MT-04': { class: 'Constraint', rationale: 'split the whitebox or wire its groups — a restructuring, not one additive trace' },
103
114
  'BQ-01': { class: 'Constraint', rationale: 'unambiguous — text quality' },
104
115
  'BQ-02': { class: 'Constraint', rationale: 'verifiable — text quality' },
105
116
  'BQ-04': { class: 'Constraint', rationale: 'necessary — text/scope quality' },
@@ -124,6 +135,24 @@ export const CLASS_MAP = {
124
135
  // --- Ambiguous (mixed-intent fix) ------------------------------------------
125
136
  'CL-01': { class: 'Constraint', rationale: 'ConOps completeness — attribute vs added element unclear', ambiguous: true },
126
137
  'FM-01': { class: 'Constraint', rationale: 'FMEA S/O/D attributes vs added mitigation — mixed', ambiguous: true },
138
+ // --- Kongruenz (CR-SM-305): der Code hat recht, nicht der Graph ------------
139
+ //
140
+ // Alle sechs sind **Constraints**, und der Grund ist derselbe für alle: der Befund sagt, dass
141
+ // die Bindung an den Code nicht mehr stimmt — und die Antwort darauf ist entweder eine
142
+ // Änderung AM CODE (den der Optimizer nicht anfasst) oder ein Umbinden der Referenz auf den
143
+ // Stand, den der Code inzwischen hat. Beides ist keine Topologie-Änderung am Graphen, und
144
+ // beides kann der Optimizer nicht aus dem Elementtext ableiten: wohin eine verwaiste `realRef`
145
+ // ZEIGEN SOLL, steht in keinem Feld. Ein Operator daraus wäre geraten, nicht abgeleitet.
146
+ //
147
+ // `applyRule`/`suggestEdits` verwerfen alles, was nicht `Operator` ist — RC bleibt damit
148
+ // Bericht und Gate-Signal, so wie es gemeint ist.
149
+ 'RC-01': { class: 'Constraint', rationale: 'realRef zeigt ins Leere — umbinden oder Code nachziehen, keine Kante' },
150
+ 'RC-02': { class: 'Constraint', rationale: 'testRefs zeigt ins Leere — Testdatei/Fall umbinden, keine Kante' },
151
+ 'RC-03': { class: 'Constraint', rationale: 'SCHEMA-realRef zeigt ins Leere — umbinden, keine Kante' },
152
+ 'RC-04': { class: 'Constraint', rationale: 'SCHEMA wird an seiner Schnittstelle nicht geparst — Codeänderung' },
153
+ 'RC-05': { class: 'Constraint', rationale: 'Import quert eine undokumentierte Modulgrenze — Import entfernen oder Fluss modellieren', ambiguous: true },
154
+ 'RC-06': { class: 'Constraint', rationale: 'externe Bindung nennt ein nicht deklariertes Paket — dependency oder realRef' },
155
+ 'RC-07': { class: 'Constraint', rationale: 'CR-Knoten widerspricht docs/cr — Status angleichen oder Knoten anlegen, das Verzeichnis entscheidet' },
127
156
  };
128
157
  const UNKNOWN = { class: 'Constraint', rationale: 'unclassified — not in CLASS_MAP', ambiguous: true };
129
158
  /** Classify a single rule id (falls back to ambiguous-unknown). */
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 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.
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 {
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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sigloch/se-engine",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",