@sigloch/contracts 3.2.0 → 4.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/dist/harness/index.d.ts +2 -2
- package/dist/se/action-priority.d.ts +101 -0
- package/dist/se/action-priority.js +124 -0
- package/dist/se/analysis-freshness-rules.d.ts +5 -0
- package/dist/se/analysis-freshness-rules.js +5 -5
- package/dist/se/ao-rules.d.ts +5 -39
- package/dist/se/ao-rules.js +20 -13
- package/dist/se/conformance-rules.d.ts +2 -2
- package/dist/se/conformance-rules.js +36 -30
- package/dist/se/cr-quality-rules.d.ts +2 -1
- package/dist/se/cr-quality-rules.js +9 -7
- package/dist/se/evaluate-all.d.ts +18 -3
- package/dist/se/evaluate-all.js +29 -26
- package/dist/se/fchain-quality-rules.d.ts +2 -1
- package/dist/se/fchain-quality-rules.js +8 -6
- package/dist/se/fmea-rules.d.ts +3 -11
- package/dist/se/fmea-rules.js +53 -16
- package/dist/se/format-e-parser.d.ts +14 -1
- package/dist/se/format-e-parser.js +8 -3
- package/dist/se/index.d.ts +4 -2
- package/dist/se/index.js +4 -2
- package/dist/se/metric-rules.d.ts +47 -5
- package/dist/se/metric-rules.js +199 -171
- package/dist/se/near-duplicate-rules.d.ts +2 -0
- package/dist/se/near-duplicate-rules.js +2 -2
- package/dist/se/ontology.d.ts +55 -4
- package/dist/se/ontology.js +46 -6
- package/dist/se/policy.d.ts +65 -0
- package/dist/se/policy.js +100 -0
- package/dist/se/quality-rules.d.ts +2 -1
- package/dist/se/quality-rules.js +9 -7
- package/dist/se/readiness.d.ts +9 -1
- package/dist/se/readiness.js +18 -2
- package/dist/se/rules.d.ts +25 -5
- package/dist/se/rules.js +112 -47
- package/dist/se/schema-quality-rules.d.ts +2 -1
- package/dist/se/schema-quality-rules.js +6 -4
- package/dist/se/uc-quality-rules.d.ts +2 -1
- package/dist/se/uc-quality-rules.js +10 -8
- package/dist/se/view-rules.d.ts +2 -6
- package/dist/se/view-rules.js +40 -13
- package/package.json +2 -1
package/dist/se/metric-rules.js
CHANGED
|
@@ -1,46 +1,30 @@
|
|
|
1
|
-
const INSTABILITY_THRESHOLD = 0.7;
|
|
2
1
|
/**
|
|
3
2
|
* MT-01: Module Instability (CR-165: indirect via allocate-path).
|
|
4
|
-
* I = fan_out / (fan_in + fan_out) >
|
|
3
|
+
* I = fan_out / (fan_in + fan_out) > `policy.instability` → warning.
|
|
5
4
|
* fan_out = traces from FUNCs-in-module pointing to elements OUTSIDE the module.
|
|
6
5
|
* fan_in = traces from OUTSIDE pointing to FUNCs-in-module.
|
|
7
6
|
* Direct MOD→MOD io/compose traces also count.
|
|
7
|
+
*
|
|
8
|
+
* CR-SM-233: die Schwelle ist Eingabe, nicht Konstante — `policy.instability === null`
|
|
9
|
+
* heißt messen, nicht urteilen (`moduleMetrics` liefert den Wert unverändert weiter).
|
|
8
10
|
*/
|
|
9
|
-
export function mt01Instability(graph) {
|
|
11
|
+
export function mt01Instability(graph, policy) {
|
|
10
12
|
const violations = [];
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const directIn = graph.traces.filter(t => t.target === mod.id && (t.type === 'io' || t.type === 'compose' || t.type === 'allocate')).length;
|
|
20
|
-
// Indirect: traces from module FUNCs to outside elements (and vice versa)
|
|
21
|
-
let indirectOut = 0;
|
|
22
|
-
let indirectIn = 0;
|
|
23
|
-
for (const t of graph.traces) {
|
|
24
|
-
if (t.type === 'allocate')
|
|
25
|
-
continue; // allocate itself doesn't count as coupling
|
|
26
|
-
if (modFuncIds.has(t.source) && !modFuncIds.has(t.target) && t.target !== mod.id) {
|
|
27
|
-
indirectOut++;
|
|
28
|
-
}
|
|
29
|
-
if (modFuncIds.has(t.target) && !modFuncIds.has(t.source) && t.source !== mod.id) {
|
|
30
|
-
indirectIn++;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
const fanOut = directOut + indirectOut;
|
|
34
|
-
const fanIn = directIn + indirectIn;
|
|
35
|
-
if (fanIn + fanOut === 0)
|
|
13
|
+
const threshold = policy.instability;
|
|
14
|
+
if (threshold === null)
|
|
15
|
+
return violations;
|
|
16
|
+
// CR-SM-232: EINE Rechnung, zwei Ausgaben. Die fan_in/fan_out-Formel steht in
|
|
17
|
+
// `measureModules` — hier wird nur noch geschwellt. Sonst stünde die Formel
|
|
18
|
+
// doppelt im selben File und der Export wäre eine zweite Implementierung.
|
|
19
|
+
for (const m of measureModules(graph)) {
|
|
20
|
+
if (m.instability === null)
|
|
36
21
|
continue;
|
|
37
|
-
|
|
38
|
-
if (instability > INSTABILITY_THRESHOLD) {
|
|
22
|
+
if (m.instability > threshold) {
|
|
39
23
|
violations.push({
|
|
40
24
|
rule_id: 'MT-01',
|
|
41
25
|
severity: 'warning',
|
|
42
|
-
element_id:
|
|
43
|
-
message: `${
|
|
26
|
+
element_id: m.moduleId,
|
|
27
|
+
message: `${m.moduleName} has instability ${Math.round(m.instability * 100)}% (>${threshold * 100}%). fan_in=${m.fanIn}, fan_out=${m.fanOut}`,
|
|
44
28
|
});
|
|
45
29
|
}
|
|
46
30
|
}
|
|
@@ -49,103 +33,25 @@ export function mt01Instability(graph) {
|
|
|
49
33
|
/**
|
|
50
34
|
* MT-02: LCOM4 (Lack of Cohesion — component count).
|
|
51
35
|
* MOD with allocated FUNCs that share no common io/satisfy targets → cohesion problem.
|
|
52
|
-
*
|
|
36
|
+
* `policy.lcom4.info` ≤ LCOM4 < `policy.lcom4.warning` → info, darüber warning.
|
|
37
|
+
*
|
|
38
|
+
* CR-SM-233: die Stufen sind Eingabe, `null` heißt messen statt urteilen.
|
|
53
39
|
*/
|
|
54
|
-
export function mt02Lcom4(graph) {
|
|
40
|
+
export function mt02Lcom4(graph, policy) {
|
|
55
41
|
const violations = [];
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
if (
|
|
42
|
+
const steps = policy.lcom4;
|
|
43
|
+
if (steps === null)
|
|
44
|
+
return violations;
|
|
45
|
+
// CR-SM-232: die Union-Find-Rechnung steht in `measureModules`; hier nur die Stufen.
|
|
46
|
+
for (const m of measureModules(graph)) {
|
|
47
|
+
if (m.lcom4 === null)
|
|
62
48
|
continue;
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const targets = new Set();
|
|
67
|
-
for (const t of graph.traces) {
|
|
68
|
-
if (t.source === fid && (t.type === 'io' || t.type === 'satisfy')) {
|
|
69
|
-
targets.add(t.target);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
funcTargets.set(fid, targets);
|
|
73
|
-
}
|
|
74
|
-
// Count connected components via union-find
|
|
75
|
-
const parent = new Map();
|
|
76
|
-
for (const fid of funcIds)
|
|
77
|
-
parent.set(fid, fid);
|
|
78
|
-
function find(x) {
|
|
79
|
-
while (parent.get(x) !== x) {
|
|
80
|
-
x = parent.get(x);
|
|
81
|
-
}
|
|
82
|
-
return x;
|
|
83
|
-
}
|
|
84
|
-
function union(a, b) {
|
|
85
|
-
const ra = find(a), rb = find(b);
|
|
86
|
-
if (ra !== rb)
|
|
87
|
-
parent.set(ra, rb);
|
|
88
|
-
}
|
|
89
|
-
for (let i = 0; i < funcIds.length; i++) {
|
|
90
|
-
for (let j = i + 1; j < funcIds.length; j++) {
|
|
91
|
-
const ti = funcTargets.get(funcIds[i]);
|
|
92
|
-
const tj = funcTargets.get(funcIds[j]);
|
|
93
|
-
// Check if they share any target
|
|
94
|
-
for (const t of ti) {
|
|
95
|
-
if (tj.has(t)) {
|
|
96
|
-
union(funcIds[i], funcIds[j]);
|
|
97
|
-
break;
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
// CR-165/CR-171: FUNCs sharing the same FLOW (via io in either direction) are connected
|
|
103
|
-
const funcIdSet = new Set(funcIds);
|
|
104
|
-
const flowToFuncs = new Map();
|
|
105
|
-
for (const t of graph.traces) {
|
|
106
|
-
if (t.type !== 'io')
|
|
107
|
-
continue;
|
|
108
|
-
// FLOW→FUNC direction
|
|
109
|
-
if (funcIdSet.has(t.target)) {
|
|
110
|
-
const srcEl = graph.elements.find(e => e.id === t.source);
|
|
111
|
-
if (srcEl?.type === 'FLOW') {
|
|
112
|
-
if (!flowToFuncs.has(t.source))
|
|
113
|
-
flowToFuncs.set(t.source, new Set());
|
|
114
|
-
flowToFuncs.get(t.source).add(t.target);
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
// FUNC→FLOW direction (CR-171)
|
|
118
|
-
if (funcIdSet.has(t.source)) {
|
|
119
|
-
const tgtEl = graph.elements.find(e => e.id === t.target);
|
|
120
|
-
if (tgtEl?.type === 'FLOW') {
|
|
121
|
-
if (!flowToFuncs.has(t.target))
|
|
122
|
-
flowToFuncs.set(t.target, new Set());
|
|
123
|
-
flowToFuncs.get(t.target).add(t.source);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
for (const funcsInFlow of flowToFuncs.values()) {
|
|
128
|
-
const arr = [...funcsInFlow];
|
|
129
|
-
for (let i = 1; i < arr.length; i++)
|
|
130
|
-
union(arr[0], arr[i]);
|
|
49
|
+
const message = `${m.moduleName} has LCOM4=${m.lcom4} (${m.allocatedFuncs} FUNCs in ${m.lcom4} disconnected groups)`;
|
|
50
|
+
if (m.lcom4 >= steps.info && m.lcom4 < steps.warning) {
|
|
51
|
+
violations.push({ rule_id: 'MT-02', severity: 'info', element_id: m.moduleId, message });
|
|
131
52
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
if (lcom4 >= 4 && lcom4 <= 5) {
|
|
135
|
-
violations.push({
|
|
136
|
-
rule_id: 'MT-02',
|
|
137
|
-
severity: 'info',
|
|
138
|
-
element_id: mod.id,
|
|
139
|
-
message: `${mod.name} has LCOM4=${lcom4} (${funcIds.length} FUNCs in ${lcom4} disconnected groups)`,
|
|
140
|
-
});
|
|
141
|
-
}
|
|
142
|
-
else if (lcom4 > 5) {
|
|
143
|
-
violations.push({
|
|
144
|
-
rule_id: 'MT-02',
|
|
145
|
-
severity: 'warning',
|
|
146
|
-
element_id: mod.id,
|
|
147
|
-
message: `${mod.name} has LCOM4=${lcom4} (${funcIds.length} FUNCs in ${lcom4} disconnected groups)`,
|
|
148
|
-
});
|
|
53
|
+
else if (m.lcom4 >= steps.warning) {
|
|
54
|
+
violations.push({ rule_id: 'MT-02', severity: 'warning', element_id: m.moduleId, message });
|
|
149
55
|
}
|
|
150
56
|
}
|
|
151
57
|
return violations;
|
|
@@ -204,65 +110,187 @@ function connectionPairs(graph) {
|
|
|
204
110
|
return pairs;
|
|
205
111
|
}
|
|
206
112
|
/**
|
|
207
|
-
*
|
|
208
|
-
*
|
|
209
|
-
* It used to be MT-03 with an 80 % threshold, and it fired on nearly every module of
|
|
210
|
-
* every real graph: 6 of 7 on graphcode, 4 of 4 on graph-view-edit, 10 of 11 on the
|
|
211
|
-
* family graph. `CR-SM-221` first suspected the edge definition and made it
|
|
212
|
-
* FLOW-transitive — the hit rate did not move. The threshold was the miscalibration:
|
|
213
|
-
* in a flow-routed layered architecture, module interaction crosses boundaries by
|
|
214
|
-
* design, so "80 % of interaction is internal" describes a monolith, not a healthy
|
|
215
|
-
* module.
|
|
216
|
-
*
|
|
217
|
-
* Rather than fit a cut-off to 11 data points, this reports the number and lets the
|
|
218
|
-
* architect judge. Returned worst-first, so the head of the list is where to look.
|
|
219
|
-
* Modules with fewer than two allocated FUNCs, or with no external connection at all,
|
|
220
|
-
* carry no signal and are omitted.
|
|
113
|
+
* The one per-module computation (CR-SM-232) — in GRAPH ORDER, unsorted.
|
|
221
114
|
*
|
|
222
|
-
*
|
|
223
|
-
*
|
|
224
|
-
* the
|
|
115
|
+
* `mt01Instability` and `mt02Lcom4` consume this and only threshold; `moduleMetrics()`
|
|
116
|
+
* exports a sorted copy. Keeping the internal list in `graph.elements` order is what
|
|
117
|
+
* makes the violation ORDER of both rules byte-identical to the pre-CR implementation
|
|
118
|
+
* — the export's worst-first ranking must not leak into the rule output.
|
|
225
119
|
*
|
|
226
|
-
*
|
|
227
|
-
*
|
|
120
|
+
* Every value that is not measurable is `null`, never 0: a module with one allocated
|
|
121
|
+
* FUNC has no LCOM4, and a 1 there would be an invented statement about cohesion.
|
|
228
122
|
*/
|
|
229
|
-
|
|
230
|
-
const measurements = [];
|
|
123
|
+
function measureModules(graph) {
|
|
231
124
|
const mods = graph.elements.filter(e => e.type === 'MOD');
|
|
232
125
|
const pairs = [...connectionPairs(graph)].map(p => p.split('|'));
|
|
126
|
+
const rows = [];
|
|
233
127
|
for (const mod of mods) {
|
|
234
|
-
const
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
128
|
+
const allocated = graph.traces
|
|
129
|
+
.filter(t => t.type === 'allocate' && t.target === mod.id)
|
|
130
|
+
.map(t => t.source);
|
|
131
|
+
const modFuncIds = new Set(allocated);
|
|
132
|
+
// --- MT-01: fan-in / fan-out (CR-165, indirect via the allocate path) ---
|
|
133
|
+
const directOut = graph.traces.filter(t => t.source === mod.id && (t.type === 'io' || t.type === 'compose') && t.target !== mod.id).length;
|
|
134
|
+
const directIn = graph.traces.filter(t => t.target === mod.id && (t.type === 'io' || t.type === 'compose' || t.type === 'allocate')).length;
|
|
135
|
+
let indirectOut = 0;
|
|
136
|
+
let indirectIn = 0;
|
|
137
|
+
for (const t of graph.traces) {
|
|
138
|
+
if (t.type === 'allocate')
|
|
139
|
+
continue; // allocate itself doesn't count as coupling
|
|
140
|
+
if (modFuncIds.has(t.source) && !modFuncIds.has(t.target) && t.target !== mod.id)
|
|
141
|
+
indirectOut++;
|
|
142
|
+
if (modFuncIds.has(t.target) && !modFuncIds.has(t.source) && t.source !== mod.id)
|
|
143
|
+
indirectIn++;
|
|
246
144
|
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
145
|
+
const fanOut = directOut + indirectOut;
|
|
146
|
+
const fanIn = directIn + indirectIn;
|
|
147
|
+
const instability = fanIn + fanOut === 0 ? null : fanOut / (fanIn + fanOut);
|
|
148
|
+
rows.push({
|
|
251
149
|
moduleId: mod.id,
|
|
252
150
|
moduleName: mod.name,
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
151
|
+
allocatedFuncs: allocated.length,
|
|
152
|
+
fanIn,
|
|
153
|
+
fanOut,
|
|
154
|
+
instability,
|
|
155
|
+
lcom4: lcom4Of(graph, allocated),
|
|
156
|
+
cohesion: cohesionOf(pairs, modFuncIds),
|
|
256
157
|
});
|
|
257
158
|
}
|
|
258
|
-
|
|
259
|
-
|
|
159
|
+
return rows;
|
|
160
|
+
}
|
|
161
|
+
/** MT-02 core: connected components over the allocated FUNCs. null below 2 FUNCs. */
|
|
162
|
+
function lcom4Of(graph, funcIds) {
|
|
163
|
+
if (funcIds.length < 2)
|
|
164
|
+
return null;
|
|
165
|
+
// Two FUNCs are connected if they share a common io/satisfy target.
|
|
166
|
+
const funcTargets = new Map();
|
|
167
|
+
for (const fid of funcIds) {
|
|
168
|
+
const targets = new Set();
|
|
169
|
+
for (const t of graph.traces) {
|
|
170
|
+
if (t.source === fid && (t.type === 'io' || t.type === 'satisfy'))
|
|
171
|
+
targets.add(t.target);
|
|
172
|
+
}
|
|
173
|
+
funcTargets.set(fid, targets);
|
|
174
|
+
}
|
|
175
|
+
const parent = new Map();
|
|
176
|
+
for (const fid of funcIds)
|
|
177
|
+
parent.set(fid, fid);
|
|
178
|
+
function find(x) {
|
|
179
|
+
while (parent.get(x) !== x) {
|
|
180
|
+
x = parent.get(x);
|
|
181
|
+
}
|
|
182
|
+
return x;
|
|
183
|
+
}
|
|
184
|
+
function union(a, b) {
|
|
185
|
+
const ra = find(a), rb = find(b);
|
|
186
|
+
if (ra !== rb)
|
|
187
|
+
parent.set(ra, rb);
|
|
188
|
+
}
|
|
189
|
+
for (let i = 0; i < funcIds.length; i++) {
|
|
190
|
+
for (let j = i + 1; j < funcIds.length; j++) {
|
|
191
|
+
const ti = funcTargets.get(funcIds[i]);
|
|
192
|
+
const tj = funcTargets.get(funcIds[j]);
|
|
193
|
+
for (const t of ti) {
|
|
194
|
+
if (tj.has(t)) {
|
|
195
|
+
union(funcIds[i], funcIds[j]);
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
// CR-165/CR-171: FUNCs sharing the same FLOW (via io in either direction) are connected.
|
|
202
|
+
const funcIdSet = new Set(funcIds);
|
|
203
|
+
const flowToFuncs = new Map();
|
|
204
|
+
for (const t of graph.traces) {
|
|
205
|
+
if (t.type !== 'io')
|
|
206
|
+
continue;
|
|
207
|
+
if (funcIdSet.has(t.target)) {
|
|
208
|
+
const srcEl = graph.elements.find(e => e.id === t.source);
|
|
209
|
+
if (srcEl?.type === 'FLOW') {
|
|
210
|
+
if (!flowToFuncs.has(t.source))
|
|
211
|
+
flowToFuncs.set(t.source, new Set());
|
|
212
|
+
flowToFuncs.get(t.source).add(t.target);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (funcIdSet.has(t.source)) {
|
|
216
|
+
const tgtEl = graph.elements.find(e => e.id === t.target);
|
|
217
|
+
if (tgtEl?.type === 'FLOW') {
|
|
218
|
+
if (!flowToFuncs.has(t.target))
|
|
219
|
+
flowToFuncs.set(t.target, new Set());
|
|
220
|
+
flowToFuncs.get(t.target).add(t.source);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
for (const funcsInFlow of flowToFuncs.values()) {
|
|
225
|
+
const arr = [...funcsInFlow];
|
|
226
|
+
for (let i = 1; i < arr.length; i++)
|
|
227
|
+
union(arr[0], arr[i]);
|
|
228
|
+
}
|
|
229
|
+
return new Set(funcIds.map(find)).size;
|
|
230
|
+
}
|
|
231
|
+
/** CR-SM-223 core: internal vs. external connection pairs. null where there is no signal. */
|
|
232
|
+
function cohesionOf(pairs, funcIds) {
|
|
233
|
+
if (funcIds.size < 2)
|
|
234
|
+
return null;
|
|
235
|
+
let internal = 0;
|
|
236
|
+
let external = 0;
|
|
237
|
+
for (const [a, b] of pairs) {
|
|
238
|
+
const aIn = funcIds.has(a);
|
|
239
|
+
const bIn = funcIds.has(b);
|
|
240
|
+
if (aIn && bIn)
|
|
241
|
+
internal++;
|
|
242
|
+
else if (aIn || bIn)
|
|
243
|
+
external++;
|
|
244
|
+
}
|
|
245
|
+
// No external connections → nothing to compare against, no signal.
|
|
246
|
+
if (external === 0)
|
|
247
|
+
return null;
|
|
248
|
+
return { internal, external, ratio: internal / (internal + external) };
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Per-module architecture metrics as NUMBERS — one row per MOD, threshold or not
|
|
252
|
+
* (CR-SM-232).
|
|
253
|
+
*
|
|
254
|
+
* MT-01 only ever reported the modules above 70 %, MT-02 only those with ≥ 4
|
|
255
|
+
* components, and both only inside a prose `message`. For every module below the
|
|
256
|
+
* threshold there was no value, there was nothing — so a trend ("was 62 %, is 68 %"),
|
|
257
|
+
* the actual steering signal, was unobtainable. This exports what the rules already
|
|
258
|
+
* compute; it invents no metric and calibrates no threshold.
|
|
259
|
+
*
|
|
260
|
+
* Sorted worst cohesion first (as `allocationCohesion` does — the ranking IS the
|
|
261
|
+
* signal); modules without a cohesion measurement follow, stable by `moduleId`.
|
|
262
|
+
*/
|
|
263
|
+
export function moduleMetrics(graph) {
|
|
264
|
+
const byId = (a, b) => a.moduleId < b.moduleId ? -1 : a.moduleId > b.moduleId ? 1 : 0;
|
|
265
|
+
return measureModules(graph).sort((a, b) => {
|
|
266
|
+
if (a.cohesion && b.cohesion)
|
|
267
|
+
return a.cohesion.ratio - b.cohesion.ratio || byId(a, b);
|
|
268
|
+
if (a.cohesion)
|
|
269
|
+
return -1;
|
|
270
|
+
if (b.cohesion)
|
|
271
|
+
return 1;
|
|
272
|
+
return byId(a, b);
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
export function allocationCohesion(graph) {
|
|
276
|
+
// CR-SM-232: dieselbe Rechnung wie `moduleMetrics` — hier nur die Projektion auf
|
|
277
|
+
// die Module MIT Messwert. Zwei Implementierungen derselben Kohäsion wären genau
|
|
278
|
+
// die Drift, gegen die dieser CR angetreten ist.
|
|
279
|
+
return moduleMetrics(graph)
|
|
280
|
+
.filter((m) => m.cohesion !== null)
|
|
281
|
+
.map((m) => ({
|
|
282
|
+
moduleId: m.moduleId,
|
|
283
|
+
moduleName: m.moduleName,
|
|
284
|
+
internal: m.cohesion.internal,
|
|
285
|
+
external: m.cohesion.external,
|
|
286
|
+
cohesion: m.cohesion.ratio,
|
|
287
|
+
}));
|
|
260
288
|
}
|
|
261
289
|
export const MT_RULES = [
|
|
262
|
-
{ id: 'MT-01', name: 'Module instability', severity: 'warning', evaluate: mt01Instability },
|
|
263
|
-
{ id: 'MT-02', name: 'Module cohesion (LCOM4)', severity: 'info', evaluate: mt02Lcom4 },
|
|
290
|
+
{ id: 'MT-01', name: 'Module instability', severity: 'warning', evaluate: mt01Instability, domain: ['MOD'] },
|
|
291
|
+
{ id: 'MT-02', name: 'Module cohesion (LCOM4)', severity: 'info', evaluate: mt02Lcom4, domain: ['MOD'] },
|
|
264
292
|
// MT-03 retired as a rule (CR-SM-223) — see `allocationCohesion` above.
|
|
265
293
|
];
|
|
266
|
-
export function evaluateMTRules(graph) {
|
|
267
|
-
return MT_RULES.flatMap(r => r.evaluate(graph));
|
|
294
|
+
export function evaluateMTRules(graph, policy) {
|
|
295
|
+
return MT_RULES.flatMap(r => r.evaluate(graph, policy));
|
|
268
296
|
}
|
|
@@ -34,11 +34,13 @@ export declare const ND_RULES: readonly [{
|
|
|
34
34
|
readonly name: "FuncNearDuplicate";
|
|
35
35
|
readonly severity: "error";
|
|
36
36
|
readonly evaluate: typeof nd01FuncNearDuplicate;
|
|
37
|
+
readonly domain: readonly ["FUNC"];
|
|
37
38
|
}, {
|
|
38
39
|
readonly id: "ND-02";
|
|
39
40
|
readonly name: "SchemaNearDuplicate";
|
|
40
41
|
readonly severity: "error";
|
|
41
42
|
readonly evaluate: typeof nd02SchemaNearDuplicate;
|
|
43
|
+
readonly domain: readonly ["SCHEMA"];
|
|
42
44
|
}];
|
|
43
45
|
/** Evaluate all near-duplicate rules. */
|
|
44
46
|
export declare function evaluateNDRules(graph: OntologyGraph): RuleViolation[];
|
|
@@ -97,8 +97,8 @@ export function nd02SchemaNearDuplicate(graph) {
|
|
|
97
97
|
// Aggregate
|
|
98
98
|
// ---------------------------------------------------------------------------
|
|
99
99
|
export const ND_RULES = [
|
|
100
|
-
{ id: 'ND-01', name: 'FuncNearDuplicate', severity: 'error', evaluate: nd01FuncNearDuplicate },
|
|
101
|
-
{ id: 'ND-02', name: 'SchemaNearDuplicate', severity: 'error', evaluate: nd02SchemaNearDuplicate },
|
|
100
|
+
{ id: 'ND-01', name: 'FuncNearDuplicate', severity: 'error', evaluate: nd01FuncNearDuplicate, domain: ['FUNC'] },
|
|
101
|
+
{ id: 'ND-02', name: 'SchemaNearDuplicate', severity: 'error', evaluate: nd02SchemaNearDuplicate, domain: ['SCHEMA'] },
|
|
102
102
|
];
|
|
103
103
|
/** Evaluate all near-duplicate rules. */
|
|
104
104
|
export function evaluateNDRules(graph) {
|
package/dist/se/ontology.d.ts
CHANGED
|
@@ -95,26 +95,72 @@ export type RepoRelativePath = z.infer<typeof RepoRelativePathSchema>;
|
|
|
95
95
|
* Runnable binding for a TEST element (CR-GC-134, ontology bump 3.4.0).
|
|
96
96
|
* Resolves a TEST node to the concrete artefact a runner can execute, enabling
|
|
97
97
|
* bottom-up selective test deduction (`graph_tests`): change → impacted TESTs →
|
|
98
|
-
* `
|
|
98
|
+
* `testRefs` → minimal selective run command.
|
|
99
99
|
* - `file` : test file path, e.g. `tests/foo.test.ts` (the runner target).
|
|
100
100
|
* - `case` : optional named case/`describe`/`it` block within the file.
|
|
101
101
|
* - `tool` : the runner, e.g. `vitest`, `playwright`, `pytest`.
|
|
102
102
|
* - `level` : optional level, e.g. `unit`, `integration`, `validation`.
|
|
103
|
-
*
|
|
103
|
+
*
|
|
104
|
+
* `tool` steht bewusst **pro Eintrag**, nicht hochgezogen: eine Abnahme mischt real die
|
|
105
|
+
* Runner — ein TEST kann aus `tests/dashboard.test.mjs` (vitest) *und*
|
|
106
|
+
* `tests/visual/dashboard-render.spec.mjs` (playwright) bestehen. Ein Runner je TEST-Knoten
|
|
107
|
+
* wäre eine Lüge, die das alte 1:1-Schema nur deshalb nicht produzierte, weil es die zweite
|
|
108
|
+
* Datei gar nicht erst zuliess.
|
|
104
109
|
*/
|
|
105
110
|
export declare const TestRefSchema: z.ZodObject<{
|
|
106
111
|
file: z.ZodString;
|
|
107
112
|
case: z.ZodOptional<z.ZodString>;
|
|
108
113
|
tool: z.ZodString;
|
|
109
114
|
level: z.ZodOptional<z.ZodString>;
|
|
115
|
+
result: z.ZodOptional<z.ZodEnum<{
|
|
116
|
+
passed: "passed";
|
|
117
|
+
failed: "failed";
|
|
118
|
+
skipped: "skipped";
|
|
119
|
+
pending: "pending";
|
|
120
|
+
}>>;
|
|
121
|
+
ranAt: z.ZodOptional<z.ZodISODateTime>;
|
|
122
|
+
evidence: z.ZodOptional<z.ZodString>;
|
|
110
123
|
}, z.core.$strip>;
|
|
111
124
|
export type TestRef = z.infer<typeof TestRefSchema>;
|
|
125
|
+
/**
|
|
126
|
+
* CR-SM-231: eine Abnahme, **n** Testdateien — 1:n, nie n:m.
|
|
127
|
+
*
|
|
128
|
+
* Vorher war `testRef` ein einzelnes Objekt: ein TEST-Knoten war an genau eine Datei bindbar.
|
|
129
|
+
* Eine Abnahme aus Unit- *und* Visual-Lauf konnte ihre Evidenz damit nicht vollstaendig
|
|
130
|
+
* deklarieren, und der Ausweichweg war belegt — dieselbe Spec-Datei stand im `testRef` zweier
|
|
131
|
+
* TEST-Knoten. Damit war die Relation faktisch n:m: ein roter Lauf war keiner Abnahme mehr
|
|
132
|
+
* eindeutig zuordenbar, waehrend der TRR-Gate dieselbe Evidenz doppelt zaehlte.
|
|
133
|
+
*
|
|
134
|
+
* **Warum n:m hier falsch ist.** n:m zwischen Abnahme und Anforderung ist bereits korrekt
|
|
135
|
+
* modelliert und in Gebrauch (`TEST -[verify]-> REQ`) — dafuer ist eine Kante da. Die Bindung
|
|
136
|
+
* an die Laufdatei ist etwas anderes: sie ist **Evidenz-Adresse**, kein Traceability-Link. Sie
|
|
137
|
+
* gehoert ins Attribut und bleibt 1:n — ein TEST kennt n Dateien, eine Datei gehoert zu
|
|
138
|
+
* hoechstens einem TEST. Die zweite Haelfte erzwingt R-29.
|
|
139
|
+
*
|
|
140
|
+
* Gespeichert unter `OntologyElement.attributes.testRefs`. Das alte `testRef` entfaellt
|
|
141
|
+
* ersatzlos — kein Union, kein Alias, keine parallelen Pfade.
|
|
142
|
+
*/
|
|
143
|
+
export declare const TestRefsSchema: z.ZodArray<z.ZodObject<{
|
|
144
|
+
file: z.ZodString;
|
|
145
|
+
case: z.ZodOptional<z.ZodString>;
|
|
146
|
+
tool: z.ZodString;
|
|
147
|
+
level: z.ZodOptional<z.ZodString>;
|
|
148
|
+
result: z.ZodOptional<z.ZodEnum<{
|
|
149
|
+
passed: "passed";
|
|
150
|
+
failed: "failed";
|
|
151
|
+
skipped: "skipped";
|
|
152
|
+
pending: "pending";
|
|
153
|
+
}>>;
|
|
154
|
+
ranAt: z.ZodOptional<z.ZodISODateTime>;
|
|
155
|
+
evidence: z.ZodOptional<z.ZodString>;
|
|
156
|
+
}, z.core.$strip>>;
|
|
157
|
+
export type TestRefs = z.infer<typeof TestRefsSchema>;
|
|
112
158
|
/**
|
|
113
159
|
* Realization binding for an element (CR-228, ontology bump 3.9.0) — unifies the
|
|
114
160
|
* byte-identical former `codeRef` (FUNC → code symbol) and `schemaRef` (SCHEMA →
|
|
115
161
|
* Zod export), and extends to MOD (physical part → CAD/geometry artefact). The
|
|
116
162
|
* *type* of the pointing element disambiguates what kind of realization it is:
|
|
117
|
-
* FUNC→code, SCHEMA→Zod-def, physical MOD→CAD. TEST keeps its own `
|
|
163
|
+
* FUNC→code, SCHEMA→Zod-def, physical MOD→CAD. TEST keeps its own `testRefs`
|
|
118
164
|
* (a TEST is not just located but *executed* — the runner is the extra value).
|
|
119
165
|
* - `file` : realization file path, e.g. `src/harness.ts`, `part.step`.
|
|
120
166
|
* - `symbol` : the realizing symbol (function/class/Zod export). Optional — a
|
|
@@ -330,7 +376,12 @@ export declare const ELEMENT_DESCRIPTIONS: Record<ElementType, string>;
|
|
|
330
376
|
export declare const MODELING_ELEMENT_TYPES: ElementType[];
|
|
331
377
|
export interface AttributeSpec {
|
|
332
378
|
key: string;
|
|
333
|
-
|
|
379
|
+
/**
|
|
380
|
+
* CR-SM-231: `array` kommt dazu — `testRefs` ist das erste Attribut, das eine Liste ist.
|
|
381
|
+
* Als `object` deklariert waere es fuer jeden Konsumenten, der diese Spezifikation liest
|
|
382
|
+
* (Format-E-Editor, Doku-Generator), formal falsch.
|
|
383
|
+
*/
|
|
384
|
+
type: 'string' | 'number' | 'boolean' | 'enum' | 'object' | 'array';
|
|
334
385
|
enumValues?: readonly string[];
|
|
335
386
|
description: string;
|
|
336
387
|
}
|
package/dist/se/ontology.js
CHANGED
|
@@ -58,25 +58,65 @@ export const RepoRelativePathSchema = z
|
|
|
58
58
|
* Runnable binding for a TEST element (CR-GC-134, ontology bump 3.4.0).
|
|
59
59
|
* Resolves a TEST node to the concrete artefact a runner can execute, enabling
|
|
60
60
|
* bottom-up selective test deduction (`graph_tests`): change → impacted TESTs →
|
|
61
|
-
* `
|
|
61
|
+
* `testRefs` → minimal selective run command.
|
|
62
62
|
* - `file` : test file path, e.g. `tests/foo.test.ts` (the runner target).
|
|
63
63
|
* - `case` : optional named case/`describe`/`it` block within the file.
|
|
64
64
|
* - `tool` : the runner, e.g. `vitest`, `playwright`, `pytest`.
|
|
65
65
|
* - `level` : optional level, e.g. `unit`, `integration`, `validation`.
|
|
66
|
-
*
|
|
66
|
+
*
|
|
67
|
+
* `tool` steht bewusst **pro Eintrag**, nicht hochgezogen: eine Abnahme mischt real die
|
|
68
|
+
* Runner — ein TEST kann aus `tests/dashboard.test.mjs` (vitest) *und*
|
|
69
|
+
* `tests/visual/dashboard-render.spec.mjs` (playwright) bestehen. Ein Runner je TEST-Knoten
|
|
70
|
+
* wäre eine Lüge, die das alte 1:1-Schema nur deshalb nicht produzierte, weil es die zweite
|
|
71
|
+
* Datei gar nicht erst zuliess.
|
|
67
72
|
*/
|
|
68
73
|
export const TestRefSchema = z.object({
|
|
69
74
|
file: RepoRelativePathSchema,
|
|
70
75
|
case: z.string().optional(),
|
|
71
76
|
tool: z.string(),
|
|
72
77
|
level: z.string().optional(),
|
|
78
|
+
/**
|
|
79
|
+
* CR-SM-231b: das Ergebnis haengt **pro Eintrag**, aus demselben Grund wie `tool`.
|
|
80
|
+
*
|
|
81
|
+
* Vorher lag `testResult` am TEST-**Knoten**. Mit n Eintraegen ist das mehrdeutig: laeuft eine
|
|
82
|
+
* Abnahme als vitest *und* playwright, kann ein einzelnes Ergebnis nicht sagen, welcher Lauf
|
|
83
|
+
* gemeint ist — und „einer rot, einer gruen" ist gar nicht darstellbar. Genau der Zustand,
|
|
84
|
+
* den ein Gate wissen muss.
|
|
85
|
+
*
|
|
86
|
+
* Optional: ein Eintrag ohne Ergebnis ist noch nicht gelaufen, nicht „bestanden". VR-01
|
|
87
|
+
* meldet das.
|
|
88
|
+
*/
|
|
89
|
+
result: TestResult.optional(),
|
|
90
|
+
/** ISO-Zeitstempel des Laufs, der `result` erzeugt hat. Ohne ihn ist ein Ergebnis undatiert. */
|
|
91
|
+
ranAt: z.iso.datetime().optional(),
|
|
92
|
+
/** Repo-relativer Pfad zum Lauf-Artefakt (Report, Screenshot, Trace) — die Evidenz zur Aussage. */
|
|
93
|
+
evidence: RepoRelativePathSchema.optional(),
|
|
73
94
|
});
|
|
95
|
+
/**
|
|
96
|
+
* CR-SM-231: eine Abnahme, **n** Testdateien — 1:n, nie n:m.
|
|
97
|
+
*
|
|
98
|
+
* Vorher war `testRef` ein einzelnes Objekt: ein TEST-Knoten war an genau eine Datei bindbar.
|
|
99
|
+
* Eine Abnahme aus Unit- *und* Visual-Lauf konnte ihre Evidenz damit nicht vollstaendig
|
|
100
|
+
* deklarieren, und der Ausweichweg war belegt — dieselbe Spec-Datei stand im `testRef` zweier
|
|
101
|
+
* TEST-Knoten. Damit war die Relation faktisch n:m: ein roter Lauf war keiner Abnahme mehr
|
|
102
|
+
* eindeutig zuordenbar, waehrend der TRR-Gate dieselbe Evidenz doppelt zaehlte.
|
|
103
|
+
*
|
|
104
|
+
* **Warum n:m hier falsch ist.** n:m zwischen Abnahme und Anforderung ist bereits korrekt
|
|
105
|
+
* modelliert und in Gebrauch (`TEST -[verify]-> REQ`) — dafuer ist eine Kante da. Die Bindung
|
|
106
|
+
* an die Laufdatei ist etwas anderes: sie ist **Evidenz-Adresse**, kein Traceability-Link. Sie
|
|
107
|
+
* gehoert ins Attribut und bleibt 1:n — ein TEST kennt n Dateien, eine Datei gehoert zu
|
|
108
|
+
* hoechstens einem TEST. Die zweite Haelfte erzwingt R-29.
|
|
109
|
+
*
|
|
110
|
+
* Gespeichert unter `OntologyElement.attributes.testRefs`. Das alte `testRef` entfaellt
|
|
111
|
+
* ersatzlos — kein Union, kein Alias, keine parallelen Pfade.
|
|
112
|
+
*/
|
|
113
|
+
export const TestRefsSchema = z.array(TestRefSchema).min(1);
|
|
74
114
|
/**
|
|
75
115
|
* Realization binding for an element (CR-228, ontology bump 3.9.0) — unifies the
|
|
76
116
|
* byte-identical former `codeRef` (FUNC → code symbol) and `schemaRef` (SCHEMA →
|
|
77
117
|
* Zod export), and extends to MOD (physical part → CAD/geometry artefact). The
|
|
78
118
|
* *type* of the pointing element disambiguates what kind of realization it is:
|
|
79
|
-
* FUNC→code, SCHEMA→Zod-def, physical MOD→CAD. TEST keeps its own `
|
|
119
|
+
* FUNC→code, SCHEMA→Zod-def, physical MOD→CAD. TEST keeps its own `testRefs`
|
|
80
120
|
* (a TEST is not just located but *executed* — the runner is the extra value).
|
|
81
121
|
* - `file` : realization file path, e.g. `src/harness.ts`, `part.step`.
|
|
82
122
|
* - `symbol` : the realizing symbol (function/class/Zod export). Optional — a
|
|
@@ -181,10 +221,10 @@ export const MODELING_ELEMENT_TYPES = ElementType.options.filter(t => t !== 'SES
|
|
|
181
221
|
*/
|
|
182
222
|
export const ELEMENT_ATTRIBUTES = {
|
|
183
223
|
TEST: [
|
|
184
|
-
|
|
224
|
+
// CR-SM-231b: das knotenweite `testResult` ist weg — das Ergebnis haengt pro testRefs-Eintrag.
|
|
185
225
|
{ key: 'sourceFile', type: 'string', description: 'Test file path (e.g. tests/foo.test.ts)' },
|
|
186
|
-
{ key: '
|
|
187
|
-
{ key: 'concept', type: 'boolean', description: 'Concept-only TEST: no run artifact yet; exempt from the R-19
|
|
226
|
+
{ key: 'testRefs', type: 'array', description: 'Runnable bindings [{file, case?, tool, level?, result?, ranAt?, evidence?}, …] — one acceptance, n test files (1:n), each with its own outcome. See TestRefsSchema (CR-SM-231)' },
|
|
227
|
+
{ key: 'concept', type: 'boolean', description: 'Concept-only TEST: no run artifact yet; exempt from the R-19 testRefs-binding requirement (CR-GC-205)' },
|
|
188
228
|
],
|
|
189
229
|
REQ: [
|
|
190
230
|
{ key: 'severity', type: 'number', description: 'FMEA severity (1-10)' },
|