@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 +21 -0
- package/dist/fix-templates.d.ts +24 -0
- package/dist/fix-templates.js +192 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +13 -0
- package/dist/layer.d.ts +29 -0
- package/dist/layer.js +36 -0
- package/dist/metrics.d.ts +53 -0
- package/dist/metrics.js +97 -0
- package/dist/readiness-compute.d.ts +27 -0
- package/dist/readiness-compute.js +119 -0
- package/dist/rule-apply.d.ts +39 -0
- package/dist/rule-apply.js +136 -0
- package/dist/rule-classify.d.ts +43 -0
- package/dist/rule-classify.js +163 -0
- package/dist/suggest.d.ts +31 -0
- package/dist/suggest.js +63 -0
- package/dist/topology.d.ts +119 -0
- package/dist/topology.js +383 -0
- package/package.json +49 -0
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
|
+
}
|
package/dist/index.d.ts
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';
|
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';
|
package/dist/layer.d.ts
ADDED
|
@@ -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';
|
package/dist/metrics.js
ADDED
|
@@ -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
|
+
}
|