@sigloch/se-engine 1.1.0 → 1.3.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.
@@ -69,7 +69,10 @@ function relationToMentioned(types) {
69
69
  continue;
70
70
  if (hasTrace(g, el.id, target.id, 'relation'))
71
71
  continue;
72
- if (!isValidTrace({ source: el.type, target: target.type, type: 'relation' }))
72
+ if (!isValidTrace({
73
+ source: el.type, target: target.type, type: 'relation',
74
+ sourceKinds: el.kinds, targetKinds: target.kinds,
75
+ }))
73
76
  continue;
74
77
  return {
75
78
  op: 'add-trace',
@@ -109,7 +112,10 @@ function edgeToMentioned(opts) {
109
112
  const target = opts.direction === 'out' ? other : el;
110
113
  if (hasTrace(g, source.id, target.id, opts.traceType))
111
114
  continue;
112
- if (!isValidTrace({ source: source.type, target: target.type, type: opts.traceType }))
115
+ if (!isValidTrace({
116
+ source: source.type, target: target.type, type: opts.traceType,
117
+ sourceKinds: source.kinds, targetKinds: target.kinds,
118
+ }))
113
119
  continue;
114
120
  return {
115
121
  op: 'add-trace',
@@ -150,7 +156,10 @@ export const FIX_TEMPLATES = {
150
156
  const ms = mentioned[0] ?? (milestones.length === 1 ? milestones[0] : undefined);
151
157
  if (!ms || hasTrace(g, cr.id, ms.id, 'relation'))
152
158
  return null;
153
- if (!isValidTrace({ source: cr.type, target: ms.type, type: 'relation' }))
159
+ if (!isValidTrace({
160
+ source: cr.type, target: ms.type, type: 'relation',
161
+ sourceKinds: cr.kinds, targetKinds: ms.kinds,
162
+ }))
154
163
  return null;
155
164
  return {
156
165
  op: 'add-trace',
@@ -162,23 +171,50 @@ export const FIX_TEMPLATES = {
162
171
  : `${ms.id} ist der einzige Milestone im Graphen`,
163
172
  };
164
173
  },
174
+ /**
175
+ * CR-SM-266 D1: der Vorschlag geht an den FLOW, nicht mehr an den UC.
176
+ *
177
+ * Die alte Fassung schlug `ACTOR -io-> UC` vor. Mit dem Wegfall des Patterns waere sie
178
+ * still gestorben — `isValidTrace` haette jeden Kandidaten verworfen, die Schleife immer
179
+ * `null` geliefert, und UC-02 haette einen Fix-Template-Eintrag OHNE Wirkung behalten: ein
180
+ * Vorschlagspfad, der nichts mehr vorschlaegt, ist schlimmer als keiner, weil niemand ihn
181
+ * vermisst.
182
+ *
183
+ * Der tragende Pfad ist jetzt `ACTOR -io-> FLOW`, wobei der FLOW ein Kettenglied des UC
184
+ * speisen muss — sonst ist die Kante zwar gueltig, macht UC-02 aber nicht gruen und der
185
+ * Vorschlag waere eine Kante ins Leere. Deshalb wird der FLOW nicht geraten, sondern aus
186
+ * dem Graphen HERGELEITET: die FLOWs, die in eine FUNC einer FCHAIN dieses UC laufen.
187
+ * Ist keiner da, gibt das Template `null` — der fehlende FLOW ist dann die eigentliche
188
+ * Luecke und gehoert R-31, nicht einer erfundenen Actor-Kante.
189
+ */
165
190
  'UC-02': (v, g) => {
166
191
  const uc = byId(g, v.element_id);
167
192
  if (!uc)
168
193
  return null;
194
+ const chainMembers = new Set(g.traces
195
+ .filter((t) => t.type === 'compose' && t.source === uc.id)
196
+ .flatMap((chain) => g.traces.filter((t) => t.type === 'compose' && t.source === chain.target).map((t) => t.target)));
197
+ const feedingFlows = g.traces
198
+ .filter((t) => t.type === 'io' && chainMembers.has(t.target))
199
+ .map((t) => byId(g, t.source))
200
+ .filter((e) => e?.type === 'FLOW');
201
+ if (feedingFlows.length === 0)
202
+ return null;
169
203
  const actors = mentionedElements(`${uc.name} ${uc.description ?? ''}`, g, ['ACTOR']);
170
204
  for (const actor of actors) {
171
- if (hasTrace(g, actor.id, uc.id, 'io'))
172
- continue;
173
- if (!isValidTrace({ source: actor.type, target: uc.type, type: 'io' }))
174
- continue;
175
- return {
176
- op: 'add-trace',
177
- source: actor.id,
178
- target: uc.id,
179
- type: 'io',
180
- rationale: `${actor.id} ist im Text von ${uc.id} genannt`,
181
- };
205
+ for (const flow of feedingFlows) {
206
+ if (hasTrace(g, actor.id, flow.id, 'io'))
207
+ continue;
208
+ if (!isValidTrace({ source: actor.type, target: flow.type, type: 'io' }))
209
+ continue;
210
+ return {
211
+ op: 'add-trace',
212
+ source: actor.id,
213
+ target: flow.id,
214
+ type: 'io',
215
+ rationale: `${actor.id} ist im Text von ${uc.id} genannt, und ${flow.id} speist dessen Wirkkette`,
216
+ };
217
+ }
182
218
  }
183
219
  return null;
184
220
  },
@@ -4,7 +4,7 @@
4
4
  * Relocated from aimpro (learning-engine/graph/readiness.ts) into @sigloch/se-engine (CR-SM-248).
5
5
  */
6
6
  import type { OntologyGraph, MetricPolicy } from '@sigloch/contracts/se';
7
- import { type ReadinessReportType } from '@sigloch/contracts/se';
7
+ import { type ReadinessDimensionType, type ReadinessReportType } from '@sigloch/contracts/se';
8
8
  /**
9
9
  * Compute readiness report from the current graph state.
10
10
  *
@@ -25,3 +25,37 @@ import { type ReadinessReportType } from '@sigloch/contracts/se';
25
25
  * (CR-GC-329); diese Funktion macht nur die Zweitmeinung unmoeglich.
26
26
  */
27
27
  export declare function computeReadiness(graph: OntologyGraph, policy: MetricPolicy, readyThreshold: number): ReadinessReportType;
28
+ /**
29
+ * CR-146 / CR-SM-235: Anzahl anwendbarer Pruefungen je Dimension.
30
+ *
31
+ * `applicable = Σ (Elemente der Grundgesamtheit)` ueber die Regeln der Dimension. Die
32
+ * Grundgesamtheit steht seit CR-SM-235 an der Regel selbst (`ALL_RULE_DEFS[].domain`) —
33
+ * vorher lag hier eine Handtabelle `RULE_ELEMENT_TYPE`, in der **18 von 71 Regeln** fehlten.
34
+ * Deren Verstoesse erhoehten den Zaehler, nie den Nenner; auf graph-view-edit waren das 38
35
+ * von 415 Verstoessen. Jede neue Regel senkte den Score automatisch, bis jemand die zweite
36
+ * Tabelle nachzog, und kein Test erzwang das.
37
+ *
38
+ * Eine Regel mit mehreren Typen (RD-04: FUNC, MOD, SYS) traegt deren Summe bei — dieselben
39
+ * Elemente, die sie auch pruefen kann.
40
+ */
41
+ /**
42
+ * CR-SM-270: die KERNMENGE je Dimension — die Elemente der Typen, um die es der Dimension
43
+ * wirklich geht.
44
+ *
45
+ * Der Anlass, gemessen: `moneyflow` ist ein reiner Code-Import (0 UC, 0 FCHAIN, 0 ACTOR,
46
+ * 306 FUNC) und meldete `uc: 0,997, ready: true`. Von den 15 uc-Regeln haben dreizehn eine
47
+ * leere Grundgesamtheit — sie schweigen mangels Gegenstand —, R-17 traegt 1 (SYS) und FC-03
48
+ * traegt 306 (FUNC). 99,7 % des Nenners kamen also aus EINER Fremdtyp-Regel, und das Schweigen
49
+ * der dreizehn las sich als Erfolg. Je mehr importierte FUNCs ein Repo ohne Wozu-Ebene hat,
50
+ * desto besser sah seine uc-Readiness aus — der Score zeigte in die falsche Richtung, und
51
+ * `dimension_readiness` ist Steuergroesse fuer `graph_next_step`.
52
+ *
53
+ * ABGELEITET, nicht deklariert: der Kern sind die Typen, die die MEHRHEIT der Dimensionsregeln
54
+ * prueft. Eine zweite Tabelle neben `RULE_TO_DIMENSION` waere ein zweiter Speicher derselben
55
+ * Wahrheit und liefe auseinander, sobald jemand nur eine von beiden pflegt — genau der Defekt,
56
+ * den CR-SM-235 hier schon einmal beseitigt hat (`RULE_ELEMENT_TYPE`, 18 von 71 Regeln
57
+ * fehlten). Ein Attribut waere zusaetzlich eine Selbstauskunft (abgelehnt in CR-SM-263).
58
+ *
59
+ * Graph-Regeln zaehlen auch hier nicht mit: sie haben keine Gruppe (CR-SM-239).
60
+ */
61
+ export declare function coreTypesOf(dim: ReadinessDimensionType): string[];
@@ -43,7 +43,8 @@ export function computeReadiness(graph, policy, readyThreshold) {
43
43
  // Die Verstoesse sind nicht verloren: sie stehen im Regelstrom und auf der Gate-Achse.
44
44
  const graphLevelRules = new Set(ALL_RULE_DEFS.filter(d => d.domain.includes('graph')).map(d => d.id));
45
45
  // Count elements by type — used for applicable counts AND phase gate checks
46
- const els = graph.elements.filter(e => e.type !== 'SESSION');
46
+ // CR-SM-266 D5: kein SESSION-Ausschluss mehr den Elementtyp gibt es nicht.
47
+ const els = graph.elements;
47
48
  const countByType = {};
48
49
  for (const e of els) {
49
50
  countByType[e.type] = (countByType[e.type] ?? 0) + 1;
@@ -59,16 +60,31 @@ export function computeReadiness(graph, policy, readyThreshold) {
59
60
  continue;
60
61
  violationsByDim.get(dim).push(v);
61
62
  }
63
+ const coreCounts = computeCoreApplicable(countByType);
62
64
  const scores = ReadinessDimension.options.map(dim => {
63
65
  const violations = violationsByDim.get(dim).length;
64
66
  const applicable = applicableCounts[dim];
65
- const score = applicable === 0 ? 0.0 : Math.max(0, 1 - violations / applicable);
67
+ const coreApplicable = coreCounts[dim];
68
+ // CR-SM-270: der Interpretierbarkeits-Waechter VOR der Rechnung. Ist die Kernmenge leer,
69
+ // wird nicht gerechnet — `null`, nicht 0,997 und nicht 0,0. Der Nenner selbst bleibt
70
+ // unangetastet: wo die Kernmenge da ist, zaehlen die Fremdtyp-Beitraege weiter mit.
71
+ //
72
+ // Die Reihenfolge ist Absicht: `applicable === 0` behaelt seine 0,0. Der Fall ist mit
73
+ // demselben Argument angreifbar ("nichts zu pruefen ist nicht 0 %"), aber CR-SM-270
74
+ // verlangt ihn ausdruecklich UNVERAENDERT, und ein stiller Mitnahmeeffekt waere hier die
75
+ // schlechtere Wahl als eine eigene Entscheidung dazu. Der Waechter greift also nur im
76
+ // wirklich neuen Fall: Kernmenge leer, Fremdtyp-Menge gross.
77
+ const score = applicable === 0 ? 0.0
78
+ : coreApplicable === 0 ? null
79
+ : Math.max(0, 1 - violations / applicable);
66
80
  return {
67
81
  dimension: dim,
68
- score: Math.round(score * 1000) / 1000, // 3 decimal precision
82
+ score: score === null ? null : Math.round(score * 1000) / 1000, // 3 decimal precision
69
83
  violations,
70
84
  applicable,
71
- ready: score >= readyThreshold,
85
+ coreApplicable,
86
+ // Nicht messbar ist nicht bereit — nie `true` bei `null`.
87
+ ready: score !== null && score >= readyThreshold,
72
88
  };
73
89
  });
74
90
  // CR-SM-237: `overallScore` faellt — das ungewichtete Mittel dieser Scores, von keinem
@@ -94,6 +110,51 @@ export function computeReadiness(graph, policy, readyThreshold) {
94
110
  * Eine Regel mit mehreren Typen (RD-04: FUNC, MOD, SYS) traegt deren Summe bei — dieselben
95
111
  * Elemente, die sie auch pruefen kann.
96
112
  */
113
+ /**
114
+ * CR-SM-270: die KERNMENGE je Dimension — die Elemente der Typen, um die es der Dimension
115
+ * wirklich geht.
116
+ *
117
+ * Der Anlass, gemessen: `moneyflow` ist ein reiner Code-Import (0 UC, 0 FCHAIN, 0 ACTOR,
118
+ * 306 FUNC) und meldete `uc: 0,997, ready: true`. Von den 15 uc-Regeln haben dreizehn eine
119
+ * leere Grundgesamtheit — sie schweigen mangels Gegenstand —, R-17 traegt 1 (SYS) und FC-03
120
+ * traegt 306 (FUNC). 99,7 % des Nenners kamen also aus EINER Fremdtyp-Regel, und das Schweigen
121
+ * der dreizehn las sich als Erfolg. Je mehr importierte FUNCs ein Repo ohne Wozu-Ebene hat,
122
+ * desto besser sah seine uc-Readiness aus — der Score zeigte in die falsche Richtung, und
123
+ * `dimension_readiness` ist Steuergroesse fuer `graph_next_step`.
124
+ *
125
+ * ABGELEITET, nicht deklariert: der Kern sind die Typen, die die MEHRHEIT der Dimensionsregeln
126
+ * prueft. Eine zweite Tabelle neben `RULE_TO_DIMENSION` waere ein zweiter Speicher derselben
127
+ * Wahrheit und liefe auseinander, sobald jemand nur eine von beiden pflegt — genau der Defekt,
128
+ * den CR-SM-235 hier schon einmal beseitigt hat (`RULE_ELEMENT_TYPE`, 18 von 71 Regeln
129
+ * fehlten). Ein Attribut waere zusaetzlich eine Selbstauskunft (abgelehnt in CR-SM-263).
130
+ *
131
+ * Graph-Regeln zaehlen auch hier nicht mit: sie haben keine Gruppe (CR-SM-239).
132
+ */
133
+ export function coreTypesOf(dim) {
134
+ const defs = ALL_RULE_DEFS.filter(d => RULE_TO_DIMENSION[d.id] === dim && !d.domain.includes('graph'));
135
+ if (defs.length === 0)
136
+ return [];
137
+ const seenIn = {};
138
+ for (const d of defs) {
139
+ // Ein Typ zaehlt EINMAL je Regel, auch wenn die Regel mehrere Typen prueft (RD-04).
140
+ for (const type of new Set(d.domain))
141
+ seenIn[type] = (seenIn[type] ?? 0) + 1;
142
+ }
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();
150
+ }
151
+ function computeCoreApplicable(countByType) {
152
+ const result = {};
153
+ for (const dim of ReadinessDimension.options) {
154
+ result[dim] = coreTypesOf(dim).reduce((sum, type) => sum + (countByType[type] ?? 0), 0);
155
+ }
156
+ return result;
157
+ }
97
158
  function computeApplicable(countByType) {
98
159
  const result = {};
99
160
  for (const dim of ReadinessDimension.options) {
@@ -65,6 +65,10 @@ export function applyRule(rule, graph, knownViolations) {
65
65
  // elements' stored types (isValidTrace) and (b) links two currently-
66
66
  // unconnected nodes, so it genuinely moves the topology.
67
67
  const typeById = new Map(graph.elements.map((e) => [e.id, e.type]));
68
+ // CR-SM-266 B: die where-Patterns lesen die Kinds des Zielknotens mit. Ohne sie wuerde
69
+ // `isValidTrace` jede satisfy-Kante ablehnen und der Operator griffe stumm auf einen anderen
70
+ // Kantentyp aus — ein falscher Vorschlag statt keinem.
71
+ const kindsById = new Map(graph.elements.map((e) => [e.id, e.kinds]));
68
72
  const v = violations[0];
69
73
  const source = v.element_id;
70
74
  const srcType = typeById.get(source);
@@ -94,7 +98,10 @@ export function applyRule(rule, graph, knownViolations) {
94
98
  [source, srcType, partner, tgtType],
95
99
  [partner, tgtType, source, srcType],
96
100
  ]) {
97
- if (isValidTrace({ source: sType, target: tType, type: tt })) {
101
+ if (isValidTrace({
102
+ source: sType, target: tType, type: tt,
103
+ sourceKinds: kindsById.get(s), targetKinds: kindsById.get(t),
104
+ })) {
98
105
  return { graph: addTrace(graph, s, t, tt), applied: true, op: `+ ${s} -${tt}-> ${t}` };
99
106
  }
100
107
  }
@@ -120,7 +127,8 @@ export function markEmptyDelta(graph, measure) {
120
127
  });
121
128
  }
122
129
  /** Trace types the meta-model accepts (ontology TraceType enum). */
123
- const TRACE_KEYWORDS = ['verify', 'satisfy', 'allocate', 'compose', 'produces', 'io', 'relation'];
130
+ // CR-SM-266 D5: `produces` entfaellt der TraceType existiert nicht mehr.
131
+ const TRACE_KEYWORDS = ['verify', 'satisfy', 'allocate', 'compose', 'io', 'relation'];
124
132
  /**
125
133
  * Best-effort trace type from the violation's fix_hint (topology-only — the
126
134
  * exact type does not affect `metrics`, but a plausible one keeps the edit
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sigloch/se-engine",
3
- "version": "1.1.0",
3
+ "version": "1.3.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -17,6 +17,7 @@
17
17
  "scripts": {
18
18
  "build": "rm -rf dist && tsc",
19
19
  "test": "vitest run",
20
+ "profile:rules": "SE_PROFILE=1 vitest run tests/perf/rule-profile.test.ts",
20
21
  "prepublishOnly": "npm run build && npm run test"
21
22
  },
22
23
  "dependencies": {
@@ -41,9 +42,9 @@
41
42
  "access": "public"
42
43
  },
43
44
  "peerDependencies": {
44
- "@sigloch/contracts": ">=5 <7"
45
+ "@sigloch/contracts": ">=5 <10"
45
46
  },
46
47
  "devDependencies": {
47
- "@sigloch/contracts": "^6.0.0"
48
+ "@sigloch/contracts": ">=6 <10"
48
49
  }
49
50
  }