@sigloch/se-engine 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sigloch Consulting
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,24 @@
1
+ import type { OntologyGraph, RuleViolation, TraceType, ElementType } from '@sigloch/contracts/se';
2
+ export interface SuggestedEdit {
3
+ op: 'add-trace';
4
+ source: string;
5
+ target: string;
6
+ type: TraceType;
7
+ /** Warum genau dieses Ziel — Fundstelle im Elementtext bzw. Eindeutigkeit. */
8
+ rationale: string;
9
+ }
10
+ type Element = OntologyGraph['elements'][number];
11
+ type FixTemplate = (v: RuleViolation, g: OntologyGraph) => SuggestedEdit | null;
12
+ /**
13
+ * Elemente der gegebenen Typen, deren id ODER name (Wortgrenze, ≥3 Zeichen,
14
+ * case-insensitive) im Text vorkommt. Deterministische Rangfolge:
15
+ * id-Treffer vor name-Treffer, längerer Name vor kürzerem, dann id-lexikografisch.
16
+ */
17
+ export declare function mentionedElements(text: string, g: OntologyGraph, types: readonly ElementType[]): Element[];
18
+ export declare const FIX_TEMPLATES: Record<string, FixTemplate>;
19
+ /**
20
+ * Rule-spezifischer Fix für eine Violation — oder null (Fund-Ebene reicht).
21
+ * Deterministisch; nie der generische applyRule-Trace.
22
+ */
23
+ export declare function fixFor(v: RuleViolation, g: OntologyGraph): SuggestedEdit | null;
24
+ export {};
@@ -0,0 +1,192 @@
1
+ /**
2
+ * Rule-spezifische Fix-Templates (CR-SM-225, Spike-2-Design-Konsequenz).
3
+ *
4
+ * Der Spike-2-Befund (aimpro docs/spike/revisit_suggestions.md): KEIN einziger
5
+ * generisch synthetisierter Edit war unverändert anwendbar — der generische
6
+ * applyRule-Trace taugt als Δm-Richtungssonde, nie als auszuliefernder Edit.
7
+ * Ausgeliefert wird ein Edit nur, wenn ein rule-spezifisches Template ihn aus
8
+ * dem Elementtext DETERMINISTISCH herleiten kann (Fundstelle = Begründung):
9
+ *
10
+ * - CR-R01: CR ohne relation → relation zu einem IM CR-TEXT genannten
11
+ * UC/REQ/FUNC/MOD (Spike: "Relation zu im CR-Text genannten Elementen")
12
+ * - CR-R04: CR ohne FUNC → relation zu einer im CR-Text genannten FUNC
13
+ * - MS-03 : CR ohne Milestone → relation zur im Text genannten MS,
14
+ * sonst zur einzigen MS im Graphen (eindeutig ⇒ herleitbar)
15
+ * - UC-02 : UC ohne Akteur → io von einem im UC-TEXT genannten ACTOR
16
+ * ("Als Entwickler…" → ACTOR-developer)
17
+ *
18
+ * CR-SM-241 ergänzt die ARCHITEKTUR-Operatoren — die einzigen, die auf
19
+ * `layer: 'arch'` überhaupt etwas bewegen können (CR/MS/UC liegen nicht im
20
+ * Teilgraphen, also war dort bis dahin jedes Δm = 0):
21
+ *
22
+ * - R-22 : FUNC ohne Modul → allocate zum im FUNC-TEXT genannten MOD,
23
+ * sonst zum einzigen MOD im Graphen (eindeutig ⇒ herleitbar)
24
+ * - R-23 : MOD ohne allozierte FUNC → allocate von der im MOD-TEXT
25
+ * genannten FUNC (kein Eindeutigkeits-Fallback, s. dort)
26
+ * - SC-02/SC-04: FLOW ohne Datenvertrag → relation zum im FLOW-TEXT
27
+ * genannten SCHEMA, sonst zum einzigen SCHEMA
28
+ *
29
+ * Regeln ohne Template (oder Template ohne Fund) liefern null → die Suggestion
30
+ * bleibt Fund-Ebene (Violation + Richtung + Δm), ohne Edit. Regeln, deren Fix
31
+ * Element-Erzeugung braucht (FCHAIN, REQ-Pre/Postcondition, TEST), sind mit
32
+ * additiven Kanten prinzipiell nicht ausdrückbar — bewusst kein Template.
33
+ */
34
+ import { isValidTrace } from '@sigloch/contracts/se';
35
+ function byId(g, id) {
36
+ return g.elements.find((e) => e.id === id);
37
+ }
38
+ function hasTrace(g, source, target, type) {
39
+ return g.traces.some((t) => t.source === source && t.target === target && t.type === type);
40
+ }
41
+ const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
42
+ /**
43
+ * Elemente der gegebenen Typen, deren id ODER name (Wortgrenze, ≥3 Zeichen,
44
+ * case-insensitive) im Text vorkommt. Deterministische Rangfolge:
45
+ * id-Treffer vor name-Treffer, längerer Name vor kürzerem, dann id-lexikografisch.
46
+ */
47
+ export function mentionedElements(text, g, types) {
48
+ const hits = [];
49
+ for (const el of g.elements) {
50
+ if (!types.includes(el.type))
51
+ continue;
52
+ const idHit = new RegExp(`(^|[^A-Za-z0-9-])${escapeRe(el.id)}([^A-Za-z0-9-]|$)`, 'i').test(text);
53
+ const nameHit = el.name.trim().length >= 3 && new RegExp(`\\b${escapeRe(el.name.trim())}\\b`, 'i').test(text);
54
+ if (idHit || nameHit)
55
+ hits.push({ el, idHit, nameLen: nameHit ? el.name.trim().length : 0 });
56
+ }
57
+ hits.sort((a, b) => Number(b.idHit) - Number(a.idHit) || b.nameLen - a.nameLen || a.el.id.localeCompare(b.el.id));
58
+ return hits.map((h) => h.el);
59
+ }
60
+ /** relation vom Violation-Element zu einem im Elementtext genannten Ziel-Typ. */
61
+ function relationToMentioned(types) {
62
+ return (v, g) => {
63
+ const el = byId(g, v.element_id);
64
+ if (!el)
65
+ return null;
66
+ const text = `${el.name} ${el.description ?? ''}`;
67
+ for (const target of mentionedElements(text, g, types)) {
68
+ if (target.id === el.id)
69
+ continue;
70
+ if (hasTrace(g, el.id, target.id, 'relation'))
71
+ continue;
72
+ if (!isValidTrace({ source: el.type, target: target.type, type: 'relation' }))
73
+ continue;
74
+ return {
75
+ op: 'add-trace',
76
+ source: el.id,
77
+ target: target.id,
78
+ type: 'relation',
79
+ rationale: `${target.id} ist im Text von ${el.id} genannt`,
80
+ };
81
+ }
82
+ return null;
83
+ };
84
+ }
85
+ /**
86
+ * CR-SM-241 — die Architektur-Operatoren. Verallgemeinerung von
87
+ * `relationToMentioned`: die Kante muss nicht vom Violation-Element ausgehen
88
+ * (R-23 sitzt am MOD, die legale Kante läuft aber FUNC→MOD), und der Trace-Typ
89
+ * ist nicht immer `relation`.
90
+ *
91
+ * `uniqueFallback` greift NUR, wenn der Text nichts nennt UND es genau einen
92
+ * Kandidaten des Zieltyps gibt. „Genau einer" ist eine Herleitung, „der erste
93
+ * von mehreren" wäre geraten — und ein falsch alloziertes FUNC verfälscht
94
+ * `coherence`/`modifiability`, also genau die Zahlen, auf denen das Ranking sitzt.
95
+ */
96
+ function edgeToMentioned(opts) {
97
+ return (v, g) => {
98
+ const el = byId(g, v.element_id);
99
+ if (!el)
100
+ return null;
101
+ const text = `${el.name} ${el.description ?? ''}`;
102
+ const mentioned = mentionedElements(text, g, opts.types);
103
+ const pool = g.elements.filter((e) => opts.types.includes(e.type));
104
+ const candidates = mentioned.length > 0 ? mentioned : opts.uniqueFallback && pool.length === 1 ? pool : [];
105
+ for (const other of candidates) {
106
+ if (other.id === el.id)
107
+ continue;
108
+ const source = opts.direction === 'out' ? el : other;
109
+ const target = opts.direction === 'out' ? other : el;
110
+ if (hasTrace(g, source.id, target.id, opts.traceType))
111
+ continue;
112
+ if (!isValidTrace({ source: source.type, target: target.type, type: opts.traceType }))
113
+ continue;
114
+ return {
115
+ op: 'add-trace',
116
+ source: source.id,
117
+ target: target.id,
118
+ type: opts.traceType,
119
+ rationale: mentioned.length > 0
120
+ ? `${other.id} ist im Text von ${el.id} genannt`
121
+ : `${other.id} ist das einzige ${other.type} im Graphen`,
122
+ };
123
+ }
124
+ return null;
125
+ };
126
+ }
127
+ export const FIX_TEMPLATES = {
128
+ 'CR-R01': relationToMentioned(['UC', 'REQ', 'FUNC', 'MOD']),
129
+ 'CR-R04': relationToMentioned(['FUNC']),
130
+ // --- Architektur-Operatoren (CR-SM-241) ------------------------------------
131
+ // Ohne sie hat auf `layer: 'arch'` — graphcodes DEFAULT-Messebene — jeder
132
+ // Template-Edit Δm = 0, weil CR/MS/UC gar nicht im Teilgraphen liegen.
133
+ // FUNC ohne Modul → das im FUNC-Text genannte MOD, sonst das einzige MOD.
134
+ 'R-22': edgeToMentioned({ types: ['MOD'], traceType: 'allocate', direction: 'out', uniqueFallback: true }),
135
+ // MOD ohne allozierte FUNC → das im MOD-Text genannte FUNC. Die Kante läuft
136
+ // weiterhin FUNC→MOD (einziges legales Muster), der Fund sitzt am MOD.
137
+ // KEIN uniqueFallback: „das einzige MOD" ist eine Herleitung (das FUNC muss
138
+ // irgendwohin), „das einzige FUNC" ist keine — ein FUNC darf legitim einem
139
+ // anderen Modul gehören, und dieses hier hätte dann einfach noch keins.
140
+ 'R-23': edgeToMentioned({ types: ['FUNC'], traceType: 'allocate', direction: 'in' }),
141
+ // FLOW ohne Datenvertrag → das im FLOW-Text genannte SCHEMA, sonst das einzige.
142
+ 'SC-02': edgeToMentioned({ types: ['SCHEMA'], traceType: 'relation', direction: 'out', uniqueFallback: true }),
143
+ 'SC-04': edgeToMentioned({ types: ['SCHEMA'], traceType: 'relation', direction: 'out', uniqueFallback: true }),
144
+ 'MS-03': (v, g) => {
145
+ const cr = byId(g, v.element_id);
146
+ if (!cr)
147
+ return null;
148
+ const milestones = g.elements.filter((e) => e.type === 'MS');
149
+ const mentioned = mentionedElements(`${cr.name} ${cr.description ?? ''}`, g, ['MS']);
150
+ const ms = mentioned[0] ?? (milestones.length === 1 ? milestones[0] : undefined);
151
+ if (!ms || hasTrace(g, cr.id, ms.id, 'relation'))
152
+ return null;
153
+ if (!isValidTrace({ source: cr.type, target: ms.type, type: 'relation' }))
154
+ return null;
155
+ return {
156
+ op: 'add-trace',
157
+ source: cr.id,
158
+ target: ms.id,
159
+ type: 'relation',
160
+ rationale: mentioned[0]
161
+ ? `${ms.id} ist im Text von ${cr.id} genannt`
162
+ : `${ms.id} ist der einzige Milestone im Graphen`,
163
+ };
164
+ },
165
+ 'UC-02': (v, g) => {
166
+ const uc = byId(g, v.element_id);
167
+ if (!uc)
168
+ return null;
169
+ const actors = mentionedElements(`${uc.name} ${uc.description ?? ''}`, g, ['ACTOR']);
170
+ 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
+ };
182
+ }
183
+ return null;
184
+ },
185
+ };
186
+ /**
187
+ * Rule-spezifischer Fix für eine Violation — oder null (Fund-Ebene reicht).
188
+ * Deterministisch; nie der generische applyRule-Trace.
189
+ */
190
+ export function fixFor(v, g) {
191
+ return FIX_TEMPLATES[v.rule_id]?.(v, g) ?? null;
192
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * @sigloch/se-engine — SE analysis over a governed graph.
3
+ *
4
+ * One package, two surfaces that were separate packages until CR-SM-248:
5
+ * metrics/… the ℝ⁶ metric vector, layer projection, rule apply, suggestions
6
+ * readiness-compute the readiness/steering computation
7
+ *
8
+ * They were merged because they share exactly one dependency (@sigloch/contracts),
9
+ * operate on the same graph, and had the same consumers — two packages with one
10
+ * consumer set is a publish cascade, not a boundary.
11
+ */
12
+ export * from './metrics.js';
13
+ export * from './readiness-compute.js';
package/dist/index.js ADDED
@@ -0,0 +1,13 @@
1
+ /**
2
+ * @sigloch/se-engine — SE analysis over a governed graph.
3
+ *
4
+ * One package, two surfaces that were separate packages until CR-SM-248:
5
+ * metrics/… the ℝ⁶ metric vector, layer projection, rule apply, suggestions
6
+ * readiness-compute the readiness/steering computation
7
+ *
8
+ * They were merged because they share exactly one dependency (@sigloch/contracts),
9
+ * operate on the same graph, and had the same consumers — two packages with one
10
+ * consumer set is a publish cascade, not a boundary.
11
+ */
12
+ export * from './metrics.js';
13
+ export * from './readiness-compute.js';
@@ -0,0 +1,29 @@
1
+ /**
2
+ * CR-AIM-235: Layer-Projektion + Knotengewicht-Producer.
3
+ *
4
+ * Reale governte Graphen bestehen zu 70–83 % aus Doku-Knoten (REQ/TEST/CR/MS);
5
+ * globale Metriken messen die Doku-Topologie, nicht die Architektur (Spike 2,
6
+ * aimpro docs/spike/revisit_layered.md §A). `projectLayer` liefert den
7
+ * Architektur-Teilgraphen; `weightNodes` mappt Datei-Massen (LOC, vom Aufrufer
8
+ * geliefert — metrics() bleibt pur, kein Filesystem-Zugriff) auf Knoten-Ids
9
+ * über deren realRef/codeRef-Bindung.
10
+ */
11
+ import type { OntologyGraph } from '@sigloch/contracts/se';
12
+ /** Architektur-Schicht: Struktur- und Schnittstellen-Typen (ohne REQ/TEST/CR/MS/SYS/UC/FCHAIN). */
13
+ export declare const ARCH_TYPES: ReadonlySet<string>;
14
+ export type MetricLayer = 'all' | 'arch';
15
+ /**
16
+ * Project a graph onto the requested layer. `'all'` returns the graph
17
+ * unchanged; `'arch'` keeps only ARCH_TYPES elements and the traces whose BOTH
18
+ * endpoints survive (Doku-Knoten und ihre Traces fallen weg).
19
+ */
20
+ export declare function projectLayer(graph: OntologyGraph, layer: MetricLayer): OntologyGraph;
21
+ /**
22
+ * Derive node weights from per-file masses (e.g. LOC) via each element's
23
+ * realRef/codeRef file binding. Elements without binding or without a mass
24
+ * entry get no weight (metrics() defaults their mass to 1). When several
25
+ * elements bind the same file, each carries the full file mass — the file's
26
+ * Masse ist an jedem gebundenen Knoten sichtbar (Spike-2-Befund: 713-LOC-File
27
+ * ist topologisch sonst ein Punkt).
28
+ */
29
+ export declare function weightNodes(graph: OntologyGraph, massByFile: Map<string, number>): Map<string, number>;
package/dist/layer.js ADDED
@@ -0,0 +1,36 @@
1
+ /** Architektur-Schicht: Struktur- und Schnittstellen-Typen (ohne REQ/TEST/CR/MS/SYS/UC/FCHAIN). */
2
+ export const ARCH_TYPES = new Set(['FUNC', 'FLOW', 'MOD', 'SCHEMA', 'ACTOR']);
3
+ /**
4
+ * Project a graph onto the requested layer. `'all'` returns the graph
5
+ * unchanged; `'arch'` keeps only ARCH_TYPES elements and the traces whose BOTH
6
+ * endpoints survive (Doku-Knoten und ihre Traces fallen weg).
7
+ */
8
+ export function projectLayer(graph, layer) {
9
+ if (layer === 'all')
10
+ return graph;
11
+ const ids = new Set(graph.elements.filter((e) => ARCH_TYPES.has(e.type)).map((e) => e.id));
12
+ return {
13
+ elements: graph.elements.filter((e) => ids.has(e.id)),
14
+ traces: graph.traces.filter((t) => ids.has(t.source) && ids.has(t.target)),
15
+ };
16
+ }
17
+ /**
18
+ * Derive node weights from per-file masses (e.g. LOC) via each element's
19
+ * realRef/codeRef file binding. Elements without binding or without a mass
20
+ * entry get no weight (metrics() defaults their mass to 1). When several
21
+ * elements bind the same file, each carries the full file mass — the file's
22
+ * Masse ist an jedem gebundenen Knoten sichtbar (Spike-2-Befund: 713-LOC-File
23
+ * ist topologisch sonst ein Punkt).
24
+ */
25
+ export function weightNodes(graph, massByFile) {
26
+ const weights = new Map();
27
+ for (const e of graph.elements) {
28
+ const file = e.realRef?.file ?? e.codeRef?.file ?? e.attributes?.realRef?.file;
29
+ if (!file)
30
+ continue;
31
+ const mass = massByFile.get(file);
32
+ if (mass !== undefined)
33
+ weights.set(e.id, mass);
34
+ }
35
+ return weights;
36
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * @sigloch/se-engine — `metrics(G, opts?): MetricVector` (CR-AIM-235).
3
+ *
4
+ * Die 6 Zielvektoren (aimpro docs/konzept/architekturgenerator-modell.md) als
5
+ * pure Funktion über OntologyGraph, nach dem se-steering-Muster: kein LLM,
6
+ * kein Store, kein Transport, kein Filesystem. Promotet aus aimpro
7
+ * src/harness/metrics.ts (CR-224) — MIT Layer-Projektion und optionalem
8
+ * Knotengewicht, denn promotet wird `metrics(G, layer)`, nicht das globale
9
+ * `metrics(G)` (sonst wird der Spike-2-blinde-Fleck familienweit verteilt).
10
+ *
11
+ * - modifiability = 5·clamp(Q, 0, 1) — Newman modularity Q (gewichtet, wenn nodeWeight gesetzt)
12
+ * - faultTolerance = 5·clamp(redundancyDensity) — cyclomatic redundancy
13
+ * - flowEfficiency = 5·(1/max(1, meanIO))·reachF — inverse mean Input→Output path length
14
+ * - coherence = 5·intraEdgeFraction — community-interner Kantenanteil (CR-229; gewichtet)
15
+ * - viability = 5·(largestComponentMass / totalMass) — Massenanteil der größten Komponente
16
+ * - scalability = 5·(1 − maxBetweenness) — die eine vom Konzept fixierte Formel
17
+ *
18
+ * Default (`layer:'all'`, kein nodeWeight) ist bit-identisch zum bisherigen
19
+ * globalen metrics(G) — bestehende Aufrufer unverändert (Regression-AC).
20
+ */
21
+ import { z } from 'zod';
22
+ import type { OntologyGraph } from '@sigloch/contracts/se';
23
+ import { type MetricLayer } from './layer.js';
24
+ export declare const MetricVector: z.ZodObject<{
25
+ modifiability: z.ZodNumber;
26
+ faultTolerance: z.ZodNumber;
27
+ flowEfficiency: z.ZodNumber;
28
+ coherence: z.ZodNumber;
29
+ viability: z.ZodNumber;
30
+ scalability: z.ZodNumber;
31
+ }, z.core.$strip>;
32
+ export type MetricVector = z.infer<typeof MetricVector>;
33
+ /**
34
+ * Ordered component keys — the canonical column order of the sensitivity
35
+ * matrix J (CR-227). Keep this order stable; downstream SVD indexes by it.
36
+ */
37
+ export declare const METRIC_DIMENSIONS: readonly ["modifiability", "faultTolerance", "flowEfficiency", "coherence", "viability", "scalability"];
38
+ /** Flatten a MetricVector into a fixed-order ℝ⁶ array (J-row / Δm). */
39
+ export declare function toArray(v: MetricVector): number[];
40
+ export interface MetricsOptions {
41
+ /** 'all' (Default, unverändertes Verhalten) oder 'arch' (Architektur-Teilgraph). */
42
+ layer?: MetricLayer;
43
+ /** Knotenmasse (z.B. LOC via layer.ts `weightNodes`); fehlende Knoten = Masse 1. */
44
+ nodeWeight?: Map<string, number>;
45
+ }
46
+ /** Measure a graph as a 6-dimensional topology vector (optional layer projection + node weight). */
47
+ export declare function metrics(graph: OntologyGraph, opts?: MetricsOptions): MetricVector;
48
+ export { projectLayer, weightNodes, ARCH_TYPES, type MetricLayer } from './layer.js';
49
+ export { CLASS_MAP, classOf, classifyAll, classificationStats, type RuleClass, type Classification, type ClassifiedRule, type ClassificationStats, } from './rule-classify.js';
50
+ export { applyRule, markEmptyDelta, type ApplyResult, type DeltaMark } from './rule-apply.js';
51
+ export { targetFor, suggestEdits, type Suggestion, type SuggestOptions } from './suggest.js';
52
+ export { fixFor, mentionedElements, FIX_TEMPLATES, type SuggestedEdit } from './fix-templates.js';
53
+ export { buildAdjacency, betweenness, maxBetweenness, detectCommunities, modularityOf, modularityQ, redundancyDensity, intraEdgeFraction, components, componentSizes, sourceSinkPaths, type Adjacency, } from './topology.js';
@@ -0,0 +1,97 @@
1
+ /**
2
+ * @sigloch/se-engine — `metrics(G, opts?): MetricVector` (CR-AIM-235).
3
+ *
4
+ * Die 6 Zielvektoren (aimpro docs/konzept/architekturgenerator-modell.md) als
5
+ * pure Funktion über OntologyGraph, nach dem se-steering-Muster: kein LLM,
6
+ * kein Store, kein Transport, kein Filesystem. Promotet aus aimpro
7
+ * src/harness/metrics.ts (CR-224) — MIT Layer-Projektion und optionalem
8
+ * Knotengewicht, denn promotet wird `metrics(G, layer)`, nicht das globale
9
+ * `metrics(G)` (sonst wird der Spike-2-blinde-Fleck familienweit verteilt).
10
+ *
11
+ * - modifiability = 5·clamp(Q, 0, 1) — Newman modularity Q (gewichtet, wenn nodeWeight gesetzt)
12
+ * - faultTolerance = 5·clamp(redundancyDensity) — cyclomatic redundancy
13
+ * - flowEfficiency = 5·(1/max(1, meanIO))·reachF — inverse mean Input→Output path length
14
+ * - coherence = 5·intraEdgeFraction — community-interner Kantenanteil (CR-229; gewichtet)
15
+ * - viability = 5·(largestComponentMass / totalMass) — Massenanteil der größten Komponente
16
+ * - scalability = 5·(1 − maxBetweenness) — die eine vom Konzept fixierte Formel
17
+ *
18
+ * Default (`layer:'all'`, kein nodeWeight) ist bit-identisch zum bisherigen
19
+ * globalen metrics(G) — bestehende Aufrufer unverändert (Regression-AC).
20
+ */
21
+ import { z } from 'zod';
22
+ import { buildAdjacency, maxBetweenness, detectCommunities, modularityOf, redundancyDensity, intraEdgeFraction, components, sourceSinkPaths, } from './topology.js';
23
+ import { projectLayer } from './layer.js';
24
+ export const MetricVector = z.object({
25
+ /** Änderbarkeit — Newman modularity Q (higher = more modular = easier to change). */
26
+ modifiability: z.number(),
27
+ /** Ausfalltoleranz — redundant-path density (cyclomatic redundancy). */
28
+ faultTolerance: z.number(),
29
+ /** Flusseffizienz — inverse mean Input→Output path length (shorter = more efficient). */
30
+ flowEfficiency: z.number(),
31
+ /** Kohärenz — community-internal edge share (cohesion / low coupling). */
32
+ coherence: z.number(),
33
+ /** Lebensfähigkeit unter Veränderung — largest-component mass share (stepwise buildability proxy). */
34
+ viability: z.number(),
35
+ /** Skalierbarkeit — SC = 5·(1 − maxBetweenness) (bottleneck-free share). */
36
+ scalability: z.number(),
37
+ });
38
+ /**
39
+ * Ordered component keys — the canonical column order of the sensitivity
40
+ * matrix J (CR-227). Keep this order stable; downstream SVD indexes by it.
41
+ */
42
+ export const METRIC_DIMENSIONS = [
43
+ 'modifiability',
44
+ 'faultTolerance',
45
+ 'flowEfficiency',
46
+ 'coherence',
47
+ 'viability',
48
+ 'scalability',
49
+ ];
50
+ /** Flatten a MetricVector into a fixed-order ℝ⁶ array (J-row / Δm). */
51
+ export function toArray(v) {
52
+ return METRIC_DIMENSIONS.map((d) => v[d]);
53
+ }
54
+ const clamp05 = (x) => Math.max(0, Math.min(5, x));
55
+ /** Measure a graph as a 6-dimensional topology vector (optional layer projection + node weight). */
56
+ export function metrics(graph, opts = {}) {
57
+ const g = projectLayer(graph, opts.layer ?? 'all');
58
+ const adj = buildAdjacency(g, opts.nodeWeight);
59
+ const n = adj.nodes.length;
60
+ if (n === 0) {
61
+ return {
62
+ modifiability: 0,
63
+ faultTolerance: 0,
64
+ flowEfficiency: 0,
65
+ coherence: 0,
66
+ viability: 0,
67
+ scalability: 0,
68
+ };
69
+ }
70
+ const io = sourceSinkPaths(adj);
71
+ const comps = components(adj);
72
+ let totalMass = 0;
73
+ let largestMass = 0;
74
+ for (const group of comps) {
75
+ let m = 0;
76
+ for (const id of group)
77
+ m += adj.mass.get(id);
78
+ totalMass += m;
79
+ if (m > largestMass)
80
+ largestMass = m;
81
+ }
82
+ const community = detectCommunities(adj);
83
+ return {
84
+ modifiability: clamp05(5 * modularityOf(adj, community)),
85
+ faultTolerance: clamp05(5 * redundancyDensity(adj)),
86
+ flowEfficiency: clamp05(5 * (1 / Math.max(1, io.meanLength)) * io.reachableFraction),
87
+ coherence: clamp05(5 * intraEdgeFraction(adj, community)),
88
+ viability: clamp05(5 * (largestMass / totalMass)),
89
+ scalability: clamp05(5 * (1 - maxBetweenness(adj))),
90
+ };
91
+ }
92
+ export { projectLayer, weightNodes, ARCH_TYPES } from './layer.js';
93
+ export { CLASS_MAP, classOf, classifyAll, classificationStats, } from './rule-classify.js';
94
+ export { applyRule, markEmptyDelta } from './rule-apply.js';
95
+ export { targetFor, suggestEdits } from './suggest.js';
96
+ export { fixFor, mentionedElements, FIX_TEMPLATES } from './fix-templates.js';
97
+ export { buildAdjacency, betweenness, maxBetweenness, detectCommunities, modularityOf, modularityQ, redundancyDensity, intraEdgeFraction, components, componentSizes, sourceSinkPaths, } from './topology.js';
@@ -0,0 +1,27 @@
1
+ /**
2
+ * CR-120 / CR-221: Compute readiness scores from graph state (dimension model).
3
+ * Readiness is emergent — no state machine, no blockers. Deterministic, F2-free.
4
+ * Relocated from aimpro (learning-engine/graph/readiness.ts) into @sigloch/se-engine (CR-SM-248).
5
+ */
6
+ import type { OntologyGraph, MetricPolicy } from '@sigloch/contracts/se';
7
+ import { type ReadinessReportType } from '@sigloch/contracts/se';
8
+ /**
9
+ * Compute readiness report from the current graph state.
10
+ *
11
+ * Jede Dimension: `score = 1 - Verstoesse / applicable`.
12
+ *
13
+ * **Was `applicable` zaehlt — und was nicht.** Der Nenner summiert je Regel der Dimension die
14
+ * Elemente ihrer Grundgesamtheit (`ALL_RULE_DEFS[].domain`, CR-SM-235). Eine Regel mit 72 REQ
15
+ * traegt 72 bei, zehn solche Regeln 720 — der Score ist damit **„Anteil nicht gerissener
16
+ * Pruefungen", nicht „Anteil sauberer Elemente"**. Das ist vertretbar, muss aber so heissen:
17
+ * als Fertigstellungsgrad gelesen ist die Zahl zu optimistisch.
18
+ *
19
+ * CR-SM-233: `policy` reicht bis zu den Regeln durch und hat keinen Default — sonst urteilte
20
+ * die Readiness mit einer anderen Schwelle als die, die der Host anzeigt.
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.
26
+ */
27
+ export declare function computeReadiness(graph: OntologyGraph, policy: MetricPolicy, readyThreshold: number): ReadinessReportType;
@@ -0,0 +1,119 @@
1
+ import { evaluateAllRules, ALL_RULE_DEFS } from '@sigloch/contracts/se';
2
+ import { ReadinessDimension, RULE_TO_DIMENSION, } from '@sigloch/contracts/se';
3
+ /**
4
+ * Compute readiness report from the current graph state.
5
+ *
6
+ * Jede Dimension: `score = 1 - Verstoesse / applicable`.
7
+ *
8
+ * **Was `applicable` zaehlt — und was nicht.** Der Nenner summiert je Regel der Dimension die
9
+ * Elemente ihrer Grundgesamtheit (`ALL_RULE_DEFS[].domain`, CR-SM-235). Eine Regel mit 72 REQ
10
+ * traegt 72 bei, zehn solche Regeln 720 — der Score ist damit **„Anteil nicht gerissener
11
+ * Pruefungen", nicht „Anteil sauberer Elemente"**. Das ist vertretbar, muss aber so heissen:
12
+ * als Fertigstellungsgrad gelesen ist die Zahl zu optimistisch.
13
+ *
14
+ * CR-SM-233: `policy` reicht bis zu den Regeln durch und hat keinen Default — sonst urteilte
15
+ * die Readiness mit einer anderen Schwelle als die, die der Host anzeigt.
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.
21
+ */
22
+ export function computeReadiness(graph, policy, readyThreshold) {
23
+ const allViolations = evaluateAllRules(graph, policy);
24
+ // Group violations by dimension
25
+ const violationsByDim = new Map();
26
+ for (const dim of ReadinessDimension.options) {
27
+ violationsByDim.set(dim, []);
28
+ }
29
+ // CR-SM-239: „wir messen den ganzen Graphen" und „wir messen diese Gruppe" sind zwei
30
+ // verschiedene Fragen — die Trennung steht seit CR-SM-235 im `domain`, war aber auf diese
31
+ // Regeln nicht angewandt.
32
+ //
33
+ // Die acht Themen-Dimensionen sind **Gruppen-Scores**: „wie sauber sind die REQ / die UC /
34
+ // die Module?". Ein Check, der EINMAL den ganzen Graphen prueft (AF-01..05 Freshness-Stempel,
35
+ // R-28 Ebenen-Praesenz), beantwortet diese Frage nicht — er gehoert auf die Gate-Achse
36
+ // (`RULE_TO_PHASE`, wo er bereits steht). Er zaehlt hier deshalb **weder im Zaehler noch im
37
+ // Nenner**.
38
+ //
39
+ // Warum das noetig war: als Elementregel deklariert wog ein Graph-Check so viel wie EIN
40
+ // Element. Auf kleinen Gruppen dominierte er damit — `ver` mit einer einzigen Pruefung
41
+ // (R-21 ueber 1 FCHAIN) las 50 %, weil ein FMEA-Stempel fehlte, und schlug `uc` mit 73 %
42
+ // echter Befunde. Der Executor haette einen Stempel gefordert statt der leeren FCHAIN.
43
+ // Die Verstoesse sind nicht verloren: sie stehen im Regelstrom und auf der Gate-Achse.
44
+ const graphLevelRules = new Set(ALL_RULE_DEFS.filter(d => d.domain.includes('graph')).map(d => d.id));
45
+ // Count elements by type — used for applicable counts AND phase gate checks
46
+ const els = graph.elements.filter(e => e.type !== 'SESSION');
47
+ const countByType = {};
48
+ for (const e of els) {
49
+ countByType[e.type] = (countByType[e.type] ?? 0) + 1;
50
+ }
51
+ countByType['all'] = els.length;
52
+ const applicableCounts = computeApplicable(countByType);
53
+ for (const v of allViolations) {
54
+ const dim = RULE_TO_DIMENSION[v.rule_id];
55
+ if (!dim)
56
+ continue;
57
+ // Graph-Checks gehoeren auf die Gate-Achse, nicht in einen Gruppen-Score.
58
+ if (graphLevelRules.has(v.rule_id))
59
+ continue;
60
+ violationsByDim.get(dim).push(v);
61
+ }
62
+ const scores = ReadinessDimension.options.map(dim => {
63
+ const violations = violationsByDim.get(dim).length;
64
+ const applicable = applicableCounts[dim];
65
+ const score = applicable === 0 ? 0.0 : Math.max(0, 1 - violations / applicable);
66
+ return {
67
+ dimension: dim,
68
+ score: Math.round(score * 1000) / 1000, // 3 decimal precision
69
+ violations,
70
+ applicable,
71
+ ready: score >= readyThreshold,
72
+ };
73
+ });
74
+ // CR-SM-237: `overallScore` faellt — das ungewichtete Mittel dieser Scores, von keinem
75
+ // Konsumenten gelesen und nicht interpretierbar (`ms` mit 3 Regeln zaehlte so viel wie
76
+ // `arch` mit ~20). Wer „begonnen" braucht, liest `applicable > 0` je Dimension; das ist
77
+ // dieselbe Information, die schon den Nenner bildet, und seit CR-SM-235 die einzige
78
+ // Phasenaussage dieses Moduls.
79
+ return {
80
+ scores,
81
+ timestamp: new Date().toISOString(),
82
+ };
83
+ }
84
+ /**
85
+ * CR-146 / CR-SM-235: Anzahl anwendbarer Pruefungen je Dimension.
86
+ *
87
+ * `applicable = Σ (Elemente der Grundgesamtheit)` ueber die Regeln der Dimension. Die
88
+ * Grundgesamtheit steht seit CR-SM-235 an der Regel selbst (`ALL_RULE_DEFS[].domain`) —
89
+ * vorher lag hier eine Handtabelle `RULE_ELEMENT_TYPE`, in der **18 von 71 Regeln** fehlten.
90
+ * Deren Verstoesse erhoehten den Zaehler, nie den Nenner; auf graph-view-edit waren das 38
91
+ * von 415 Verstoessen. Jede neue Regel senkte den Score automatisch, bis jemand die zweite
92
+ * Tabelle nachzog, und kein Test erzwang das.
93
+ *
94
+ * Eine Regel mit mehreren Typen (RD-04: FUNC, MOD, SYS) traegt deren Summe bei — dieselben
95
+ * Elemente, die sie auch pruefen kann.
96
+ */
97
+ function computeApplicable(countByType) {
98
+ const result = {};
99
+ for (const dim of ReadinessDimension.options) {
100
+ result[dim] = 0;
101
+ }
102
+ // CR-SM-239: zwei Beitragsarten, streng getrennt.
103
+ // - Elementregeln zaehlen ihre Gruppe (`domain: ['REQ']` → so viele REQ es gibt).
104
+ // - Graph-Regeln (`domain: ['graph']`) pruefen EINMAL den ganzen Graphen (AF-01..05, R-28).
105
+ // Sie tragen 1 bei — aber nur, wenn die Dimension ueberhaupt eine Elementmenge hat.
106
+ for (const def of ALL_RULE_DEFS) {
107
+ const dim = RULE_TO_DIMENSION[def.id];
108
+ if (!dim)
109
+ continue;
110
+ // `domain: ['graph']` → eine Pruefung ueber den ganzen Graphen, keine Gruppe. Sie traegt
111
+ // nichts zum Gruppen-Score bei; ihre Heimat ist die Gate-Achse (`RULE_TO_PHASE`).
112
+ if (def.domain.includes('graph'))
113
+ continue;
114
+ for (const type of def.domain) {
115
+ result[dim] += countByType[type] ?? 0;
116
+ }
117
+ }
118
+ return result;
119
+ }