@sigloch/contracts 10.4.0 → 10.6.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/se/conformance-rules.d.ts +4 -0
- package/dist/se/conformance-rules.js +63 -0
- package/dist/se/cr-quality-rules.d.ts +5 -0
- package/dist/se/cr-quality-rules.js +5 -2
- package/dist/se/format-e-parser.d.ts +9 -78
- package/dist/se/format-e-parser.js +34 -299
- package/dist/se/function-criticality.d.ts +16 -0
- package/dist/se/function-criticality.js +27 -6
- package/dist/se/grammar-snapshot.d.ts +5 -5
- package/dist/se/grammar-snapshot.js +13 -3
- package/dist/se/index.d.ts +1 -1
- package/dist/se/index.js +1 -1
- package/dist/se/metric-rules.d.ts +47 -4
- package/dist/se/metric-rules.js +140 -85
- package/dist/se/module-crossings.d.ts +17 -0
- package/dist/se/module-crossings.js +58 -4
- package/dist/se/readiness.d.ts +62 -0
- package/dist/se/readiness.js +64 -1
- package/dist/se/rule-help.js +9 -1
- package/dist/se/rules.js +8 -11
- package/package.json +1 -1
package/dist/se/metric-rules.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { decomposedFuncs } from './rules.js';
|
|
2
2
|
import { indexOf } from './graph-index.js';
|
|
3
|
-
import { moduleCrossings } from './module-crossings.js';
|
|
3
|
+
import { moduleCrossings, subtreeFuncs, funcSubtree, boxContracts } from './module-crossings.js';
|
|
4
4
|
/** Geteilte leere Menge — spart je Modul zwei Allokationen in `measureModulesUncached`. */
|
|
5
5
|
const EMPTY_SET = new Set();
|
|
6
6
|
/**
|
|
@@ -53,16 +53,13 @@ export function mt02Lcom4(graph, policy) {
|
|
|
53
53
|
return violations;
|
|
54
54
|
// CR-SM-232: die Union-Find-Rechnung steht in `measureModules`; hier nur die Stufen.
|
|
55
55
|
for (const m of measureModules(graph)) {
|
|
56
|
+
// CR-SM-326: `null` deckt jetzt beide Faelle, in denen nichts zu urteilen ist — unter zwei
|
|
57
|
+
// Boxen, und ohne einen einzigen Vertrag im ganzen Modul. Die Ausnahme aus CR-SM-263
|
|
58
|
+
// (reiner Container-MOD) ist damit erledigt: seit die Box ihren Teilbaum erbt, traegt auch
|
|
59
|
+
// ein Rollup Vertraege, und die Regel ist dort erfuellbar statt still.
|
|
56
60
|
if (m.lcom4 === null)
|
|
57
61
|
continue;
|
|
58
|
-
|
|
59
|
-
// LCOM4 verbindet ueber geteilte io-ZIELE, und CR-SM-256 hat festgehalten, dass
|
|
60
|
-
// ein Rollup genau die nicht traegt. Die Regel war dort nicht streng, sondern
|
|
61
|
-
// unerfuellbar: gruen wird sie erst mit der Kante, die CR-SM-256 fuer falsch erklaert.
|
|
62
|
-
// Die MESSUNG bleibt (`moduleMetrics` liefert `lcom4` unveraendert) — nur das Urteil faellt.
|
|
63
|
-
if (m.rollupContainer)
|
|
64
|
-
continue;
|
|
65
|
-
const message = `${m.moduleName} has LCOM4=${m.lcom4} (${m.allocatedFuncs} FUNCs in ${m.lcom4} disconnected groups)`;
|
|
62
|
+
const message = `${m.moduleName} has LCOM4=${m.lcom4} (${m.lcom4Boxes} boxes in ${m.lcom4} disconnected groups)`;
|
|
66
63
|
// CR-SM-288: EIN Budget fuer beide Stufen — `steps.warning - 1` ist der groesste LCOM4,
|
|
67
64
|
// der noch drin liegt. Die `info`-Stufe meldet frueher, aber sie meldet keine Ueberschreitung;
|
|
68
65
|
// ihre Masse ist 0.
|
|
@@ -76,6 +73,65 @@ export function mt02Lcom4(graph, policy) {
|
|
|
76
73
|
}
|
|
77
74
|
return violations;
|
|
78
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* MT-04: LCOM4 der FUNC-WHITEBOX (CR-SM-327).
|
|
78
|
+
*
|
|
79
|
+
* Dasselbe Prinzip wie MT-02, im anderen Baum: die Kinder eines zerlegten FUNC sind die Boxen
|
|
80
|
+
* seiner Ebene, jede mit den Vertraegen ihres Teilbaums, verbunden ueber den geteilten Vertrag.
|
|
81
|
+
* BW-02 misst am selben Element den RAND (CR-SM-283) — wie breit die Whitebox nach aussen ist;
|
|
82
|
+
* hier steht, ob sie innen zusammenhaengt. Zwei Aussagen, zwei Fixes: BW-02 heisst „schneide
|
|
83
|
+
* die Schnittstelle", MT-04 heisst „das sind zwei Bloecke".
|
|
84
|
+
*
|
|
85
|
+
* EIGENE REGEL statt `domain: ['MOD','FUNC']` an MT-02, und das ist keine Geschmacksfrage: die
|
|
86
|
+
* Grundgesamtheit einer Regel geht in den NENNER ihrer Readiness-Dimension (CR-SM-235/239).
|
|
87
|
+
* MT-02 liegt in `alloc` (Kern MOD); haengte man FUNC an sie, waechse `alloc.applicable` um
|
|
88
|
+
* jede FUNC des Graphen — gemessen ueber sieben Familiengraphen steigt der alloc-Score dadurch
|
|
89
|
+
* um bis zu 0,027 (graphcode 0,932 -> 0,955), ohne dass sich am Graphen etwas verbessert haette.
|
|
90
|
+
* Genau der Nenner-Defekt der Klasse CR-SM-235/270. Als eigene Regel in `arch` (Kern FUNC) ist
|
|
91
|
+
* die Bewegung <= 0,008, und die Befunde landen dort, wo FUNC der Gegenstand ist.
|
|
92
|
+
* Praezedenz fuer den Schnitt: R-20/R-26/R-27 — dieselbe Aussage („braucht einen realRef"),
|
|
93
|
+
* drei Populationen, drei IDs.
|
|
94
|
+
*
|
|
95
|
+
* Die Schwellen sind dieselben (`policy.lcom4`): dieselbe Masszahl, dieselbe Skala. Ein eigenes
|
|
96
|
+
* Policy-Feld waere eine zweite Schwelle fuer dieselbe Frage.
|
|
97
|
+
*/
|
|
98
|
+
export function mt04WhiteboxLcom4(graph, policy) {
|
|
99
|
+
const violations = [];
|
|
100
|
+
const steps = policy.lcom4;
|
|
101
|
+
if (steps === null)
|
|
102
|
+
return violations;
|
|
103
|
+
const idx = indexOf(graph);
|
|
104
|
+
const decomposed = decomposedFuncs(graph);
|
|
105
|
+
// In GRAPH-Reihenfolge, nicht in der Reihenfolge der `compose`-Kanten: der Anker eines
|
|
106
|
+
// Befundes darf nicht an der Kantenreihenfolge haengen (Gate 5).
|
|
107
|
+
for (const fn of idx.elementsOfType('FUNC')) {
|
|
108
|
+
if (!decomposed.has(fn.id))
|
|
109
|
+
continue;
|
|
110
|
+
const kids = idx.out(fn.id, 'compose')
|
|
111
|
+
.filter(t => idx.typeOf(t.target) === 'FUNC')
|
|
112
|
+
.map(t => t.target);
|
|
113
|
+
const boxes = kids.map(kid => boxContracts(graph, funcSubtree(graph, kid)));
|
|
114
|
+
const lcom4 = lcom4OfBoxes(boxes);
|
|
115
|
+
if (lcom4 === null || lcom4 < steps.info)
|
|
116
|
+
continue;
|
|
117
|
+
const message = `${fn.name} has LCOM4=${lcom4} (${boxes.length} boxes in ${lcom4} disconnected groups)`;
|
|
118
|
+
const context = {
|
|
119
|
+
element_type: fn.type,
|
|
120
|
+
element_name: fn.name,
|
|
121
|
+
value: lcom4,
|
|
122
|
+
threshold: steps.warning - 1,
|
|
123
|
+
};
|
|
124
|
+
violations.push({
|
|
125
|
+
rule_id: 'MT-04',
|
|
126
|
+
severity: lcom4 >= steps.warning ? 'warning' : 'info',
|
|
127
|
+
element_id: fn.id,
|
|
128
|
+
message,
|
|
129
|
+
fix_hint: 'Split the whitebox along its groups, or wire the groups together where they really share data — a block whose children share no contract is two blocks',
|
|
130
|
+
context,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
return violations;
|
|
134
|
+
}
|
|
79
135
|
/**
|
|
80
136
|
* CR-SM-221: connection pairs between non-FLOW elements, FLOW-transitive.
|
|
81
137
|
*
|
|
@@ -196,6 +252,7 @@ function measureModulesUncached(graph) {
|
|
|
196
252
|
for (const mod of mods) {
|
|
197
253
|
const allocated = idx.in(mod.id, 'allocate').map(t => t.source);
|
|
198
254
|
const modFuncIds = new Set(allocated);
|
|
255
|
+
const boxes = boxesOfModule(graph, mod.id, allocated);
|
|
199
256
|
// --- MT-01: fan-in / fan-out als querende VERTRAEGE je Richtung (CR-SM-293) ---
|
|
200
257
|
const fanIn = (crossings.afferentContracts.get(mod.id) ?? EMPTY_SET).size;
|
|
201
258
|
const fanOut = (crossings.efferentContracts.get(mod.id) ?? EMPTY_SET).size;
|
|
@@ -207,7 +264,8 @@ function measureModulesUncached(graph) {
|
|
|
207
264
|
fanIn,
|
|
208
265
|
fanOut,
|
|
209
266
|
instability,
|
|
210
|
-
lcom4:
|
|
267
|
+
lcom4: lcom4OfBoxes(boxes),
|
|
268
|
+
lcom4Boxes: boxes.length,
|
|
211
269
|
rollupContainer: allocated.length >= 2 && allocated.every(f => decomposed.has(f)),
|
|
212
270
|
cohesion: cohesionOf(pairs, modFuncIds),
|
|
213
271
|
// CR-SM-301: braucht die Instabilitaet ALLER Module, also zweiter Durchgang unten.
|
|
@@ -256,88 +314,82 @@ function measureModulesUncached(graph) {
|
|
|
256
314
|
}
|
|
257
315
|
return rows;
|
|
258
316
|
}
|
|
259
|
-
/**
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
// RD-02 sagt bereits das Verwandte (ein Parent-REQ mit direktem FUNC-satisfy; der satisfy
|
|
274
|
-
// gehoert an die Kinder).
|
|
275
|
-
//
|
|
276
|
-
// CR-SM-264: die ausgehenden Kanten kommen aus dem Index. Vorher lief je alloziertem FUNC
|
|
277
|
-
// ein Vollscan ueber alle Traces — MOD x FUNC x T.
|
|
317
|
+
/**
|
|
318
|
+
* CR-SM-326 — die BOXEN einer Modul-Whitebox: ihre Sub-MODs plus ihre direkt allozierten FUNC.
|
|
319
|
+
*
|
|
320
|
+
* Die Blackbox-Sicht verlangt genau das: ein zerlegtes Element steht nicht NEBEN seinen Kindern,
|
|
321
|
+
* es IST sie, von aussen gesehen — und es traegt selbst keine Kante (CR-SM-256). Wer es als
|
|
322
|
+
* gleichrangigen Nachbarn mit leerer Kantenmenge zaehlt, misst die Zerlegungstiefe statt der
|
|
323
|
+
* Kohaesion: je besser zerlegt, desto schlechter die Zahl. Dieselbe Besitzkette benutzen R-04
|
|
324
|
+
* (CR-SM-282), BW-02 (CR-SM-283) und R-23 (CR-SM-321).
|
|
325
|
+
*
|
|
326
|
+
* Ein Sub-MOD bringt die FUNCs seines ganzen Modul-Teilbaums mit (`subtreeFuncs`), jede davon
|
|
327
|
+
* mit ihrem eigenen compose-Teilbaum — sonst fehlten die Blaetter, die gar nicht eigens
|
|
328
|
+
* alloziert sind.
|
|
329
|
+
*/
|
|
330
|
+
function boxesOfModule(graph, modId, allocated) {
|
|
278
331
|
const idx = indexOf(graph);
|
|
279
|
-
const
|
|
280
|
-
for (const
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
332
|
+
const boxes = [];
|
|
333
|
+
for (const t of idx.out(modId, 'compose')) {
|
|
334
|
+
if (idx.typeOf(t.target) !== 'MOD')
|
|
335
|
+
continue;
|
|
336
|
+
const funcs = new Set();
|
|
337
|
+
for (const f of subtreeFuncs(graph, t.target))
|
|
338
|
+
for (const x of funcSubtree(graph, f))
|
|
339
|
+
funcs.add(x);
|
|
340
|
+
boxes.push(boxContracts(graph, funcs));
|
|
285
341
|
}
|
|
286
|
-
const
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
342
|
+
for (const func of allocated)
|
|
343
|
+
boxes.push(boxContracts(graph, funcSubtree(graph, func)));
|
|
344
|
+
return boxes;
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* MT-02 core: die Zahl der Komponenten ueber den Boxen. Verbunden sind zwei Boxen, wenn sie
|
|
348
|
+
* einen VERTRAG teilen — dieselbe Zaehlbasis, mit der CR-01, R-04, BW-02 und MT-01 seit
|
|
349
|
+
* CR-SM-274/276/293 Kopplung zaehlen. Vorher lief die Verbindung ueber FLOW-Identitaet, also
|
|
350
|
+
* ueber einen zweiten Kopplungsbegriff im selben Katalog.
|
|
351
|
+
*
|
|
352
|
+
* CR-SM-297 bleibt in Kraft: `satisfy` verbindet nicht. LCOM4 misst Datenkopplung; teilen sich
|
|
353
|
+
* zwei Funktionen ein Requirement, gehoert das REQUIREMENT zerlegt.
|
|
354
|
+
*
|
|
355
|
+
* `null` heisst messen, nicht urteilen (CR-SM-233) — hier zweimal: unter zwei Boxen gibt es
|
|
356
|
+
* keine Zerfallenheit, und ohne einen einzigen Vertrag gibt es keine Kopplung, ueber die man
|
|
357
|
+
* urteilen koennte. Der zweite Fall gehoert R-31 (`FUNC must be wired`), das ihn am Element
|
|
358
|
+
* meldet; eine Zahl hier waere dieselbe Aussage ein zweites Mal.
|
|
359
|
+
*/
|
|
360
|
+
function lcom4OfBoxes(boxes) {
|
|
361
|
+
if (boxes.length < 2)
|
|
362
|
+
return null;
|
|
363
|
+
if (boxes.every(b => b.size === 0))
|
|
364
|
+
return null;
|
|
365
|
+
const parent = boxes.map((_, i) => i);
|
|
366
|
+
const find = (x) => {
|
|
367
|
+
let cur = x;
|
|
368
|
+
while (parent[cur] !== cur) {
|
|
369
|
+
parent[cur] = parent[parent[cur]];
|
|
370
|
+
cur = parent[cur];
|
|
292
371
|
}
|
|
293
|
-
return
|
|
294
|
-
}
|
|
295
|
-
|
|
372
|
+
return cur;
|
|
373
|
+
};
|
|
374
|
+
const union = (a, b) => {
|
|
296
375
|
const ra = find(a), rb = find(b);
|
|
297
376
|
if (ra !== rb)
|
|
298
|
-
parent
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
}
|
|
312
|
-
// CR-165/CR-171: FUNCs sharing the same FLOW (via io in either direction) are connected.
|
|
313
|
-
const funcIdSet = new Set(funcIds);
|
|
314
|
-
const flowToFuncs = new Map();
|
|
315
|
-
for (const t of graph.traces) {
|
|
316
|
-
if (t.type !== 'io')
|
|
317
|
-
continue;
|
|
318
|
-
if (funcIdSet.has(t.target)) {
|
|
319
|
-
const srcEl = graph.elements.find(e => e.id === t.source);
|
|
320
|
-
if (srcEl?.type === 'FLOW') {
|
|
321
|
-
if (!flowToFuncs.has(t.source))
|
|
322
|
-
flowToFuncs.set(t.source, new Set());
|
|
323
|
-
flowToFuncs.get(t.source).add(t.target);
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
if (funcIdSet.has(t.source)) {
|
|
327
|
-
const tgtEl = graph.elements.find(e => e.id === t.target);
|
|
328
|
-
if (tgtEl?.type === 'FLOW') {
|
|
329
|
-
if (!flowToFuncs.has(t.target))
|
|
330
|
-
flowToFuncs.set(t.target, new Set());
|
|
331
|
-
flowToFuncs.get(t.target).add(t.source);
|
|
332
|
-
}
|
|
377
|
+
parent[ra] = rb;
|
|
378
|
+
};
|
|
379
|
+
// Ein Durchgang ueber alle Vertraege statt des Paarvergleichs: der erste Traeger eines
|
|
380
|
+
// Vertrags zieht jeden weiteren an sich. Linear in der Summe der Boxgroessen — und das
|
|
381
|
+
// Ergebnis haengt nicht an der Reihenfolge, weil die Komponentenzahl es nicht tut.
|
|
382
|
+
const firstHolder = new Map();
|
|
383
|
+
for (let i = 0; i < boxes.length; i++) {
|
|
384
|
+
for (const contract of boxes[i]) {
|
|
385
|
+
const first = firstHolder.get(contract);
|
|
386
|
+
if (first === undefined)
|
|
387
|
+
firstHolder.set(contract, i);
|
|
388
|
+
else
|
|
389
|
+
union(first, i);
|
|
333
390
|
}
|
|
334
391
|
}
|
|
335
|
-
|
|
336
|
-
const arr = [...funcsInFlow];
|
|
337
|
-
for (let i = 1; i < arr.length; i++)
|
|
338
|
-
union(arr[0], arr[i]);
|
|
339
|
-
}
|
|
340
|
-
return new Set(funcIds.map(find)).size;
|
|
392
|
+
return new Set(boxes.map((_, i) => find(i))).size;
|
|
341
393
|
}
|
|
342
394
|
/** CR-SM-223 core: internal vs. external connection pairs. null where there is no signal. */
|
|
343
395
|
function cohesionOf(pairs, funcIds) {
|
|
@@ -400,6 +452,9 @@ export function allocationCohesion(graph) {
|
|
|
400
452
|
export const MT_RULES = [
|
|
401
453
|
{ id: 'MT-01', name: 'Module instability', severity: 'warning', evaluate: mt01Instability, domain: ['MOD'] },
|
|
402
454
|
{ id: 'MT-02', name: 'Module cohesion (LCOM4)', severity: 'info', evaluate: mt02Lcom4, domain: ['MOD'] },
|
|
455
|
+
// CR-SM-327: dieselbe Masszahl an der FUNC-Whitebox. Eigene ID, weil die Grundgesamtheit
|
|
456
|
+
// die Readiness-Dimension bestimmt (Begruendung an `mt04WhiteboxLcom4`).
|
|
457
|
+
{ id: 'MT-04', name: 'Whitebox cohesion (LCOM4)', severity: 'info', evaluate: mt04WhiteboxLcom4, domain: ['FUNC'] },
|
|
403
458
|
// MT-03 retired as a rule (CR-SM-223) — see `allocationCohesion` above.
|
|
404
459
|
];
|
|
405
460
|
export function evaluateMTRules(graph, policy) {
|
|
@@ -100,3 +100,20 @@ export declare function crossingContractCount(graph: OntologyGraph, modId: strin
|
|
|
100
100
|
export declare function whiteboxContractCount(graph: OntologyGraph, funcId: string): number;
|
|
101
101
|
/** Die FUNCs im Teilbaum dieses Moduls (direkt alloziert + die seiner Sub-MODs) — CR-SM-282. */
|
|
102
102
|
export declare function subtreeFuncs(graph: OntologyGraph, modId: string): ReadonlySet<string>;
|
|
103
|
+
/**
|
|
104
|
+
* CR-SM-326: `funcId` plus seine `compose`-Nachfahren im FUNC-Baum — der TEILBAUM, an dessen
|
|
105
|
+
* Blaettern die io-Kanten liegen (CR-SM-256). Fuer ein Blatt die Einermenge, also derselbe
|
|
106
|
+
* Aufruf fuer Box und Blatt statt einer Fallunterscheidung beim Aufrufer.
|
|
107
|
+
*/
|
|
108
|
+
export declare function funcSubtree(graph: OntologyGraph, funcId: string): ReadonlySet<string>;
|
|
109
|
+
/**
|
|
110
|
+
* CR-SM-326: die Vertraege, die eine FUNC-Menge BERUEHRT — dieselbe Zaehlbasis wie
|
|
111
|
+
* `byModule`/`byFunc` (SCHEMA, sonst `UNBOUND:<flow>`), nur ohne die Rand-Frage.
|
|
112
|
+
*
|
|
113
|
+
* Warum ohne Rand: MT-02 fragt nicht, was die Box verlaesst, sondern ob zwei Boxen einen
|
|
114
|
+
* Vertrag TEILEN. Fuer diese Frage sind Rand- und Beruehrmenge dasselbe — ein Vertrag, den
|
|
115
|
+
* nur eine Box beruehrt, liegt in keiner zweiten und verbindet niemanden. Die Beruehrmenge
|
|
116
|
+
* ist die billigere von beiden und braucht die Whitebox-Bedingung aus `byFunc` nicht, die
|
|
117
|
+
* ein Blatt ausschliesst.
|
|
118
|
+
*/
|
|
119
|
+
export declare function boxContracts(graph: OntologyGraph, funcIds: Iterable<string>): ReadonlySet<string>;
|
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
import { indexOf } from './graph-index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Die Vertraege EINES Flusses: seine SCHEMAs, sonst er selbst als untypisierter Vertrag.
|
|
4
|
+
*
|
|
5
|
+
* Eine Stelle fuer die Zaehlbasis aus CR-SM-274 — `build()` und `boxContracts()` lesen
|
|
6
|
+
* dieselbe Funktion, damit es nicht zwei Vorstellungen davon gibt, was ein Vertrag ist.
|
|
7
|
+
*/
|
|
8
|
+
function contractsOfFlow(idx, flowId) {
|
|
9
|
+
const schemas = idx.out(flowId, 'relation')
|
|
10
|
+
.filter(t => idx.typeOf(t.target) === 'SCHEMA')
|
|
11
|
+
.map(t => t.target);
|
|
12
|
+
return schemas.length > 0 ? schemas : [`UNBOUND:${flowId}`];
|
|
13
|
+
}
|
|
2
14
|
const CACHE = new WeakMap();
|
|
3
15
|
function build(graph) {
|
|
4
16
|
const idx = indexOf(graph);
|
|
@@ -85,10 +97,7 @@ function build(graph) {
|
|
|
85
97
|
.filter(t => idx.typeOf(t.target) === 'FUNC')
|
|
86
98
|
.map(t => modOfFunc.get(t.target))
|
|
87
99
|
.filter((m) => !!m));
|
|
88
|
-
const
|
|
89
|
-
.filter(t => idx.typeOf(t.target) === 'SCHEMA')
|
|
90
|
-
.map(t => t.target);
|
|
91
|
-
const contracts = schemas.length > 0 ? schemas : [`UNBOUND:${flow.id}`];
|
|
100
|
+
const contracts = contractsOfFlow(idx, flow.id);
|
|
92
101
|
// CR-SM-283: derselbe Test auf dem compose-Baum. `allEndpoints` statt `endpoints` —
|
|
93
102
|
// die FUNC-Whitebox kennt keine Modul-Zugehoerigkeit, eine unallozierte FUNC liegt
|
|
94
103
|
// trotzdem drinnen oder draussen.
|
|
@@ -194,3 +203,48 @@ export function whiteboxContractCount(graph, funcId) {
|
|
|
194
203
|
export function subtreeFuncs(graph, modId) {
|
|
195
204
|
return moduleCrossings(graph).funcsByModule.get(modId) ?? new Set();
|
|
196
205
|
}
|
|
206
|
+
/**
|
|
207
|
+
* CR-SM-326: `funcId` plus seine `compose`-Nachfahren im FUNC-Baum — der TEILBAUM, an dessen
|
|
208
|
+
* Blaettern die io-Kanten liegen (CR-SM-256). Fuer ein Blatt die Einermenge, also derselbe
|
|
209
|
+
* Aufruf fuer Box und Blatt statt einer Fallunterscheidung beim Aufrufer.
|
|
210
|
+
*/
|
|
211
|
+
export function funcSubtree(graph, funcId) {
|
|
212
|
+
const idx = indexOf(graph);
|
|
213
|
+
const out = new Set();
|
|
214
|
+
const stack = [funcId];
|
|
215
|
+
while (stack.length > 0) {
|
|
216
|
+
const cur = stack.pop();
|
|
217
|
+
if (out.has(cur))
|
|
218
|
+
continue;
|
|
219
|
+
out.add(cur);
|
|
220
|
+
for (const t of idx.out(cur, 'compose')) {
|
|
221
|
+
if (idx.typeOf(t.target) === 'FUNC')
|
|
222
|
+
stack.push(t.target);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return out;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* CR-SM-326: die Vertraege, die eine FUNC-Menge BERUEHRT — dieselbe Zaehlbasis wie
|
|
229
|
+
* `byModule`/`byFunc` (SCHEMA, sonst `UNBOUND:<flow>`), nur ohne die Rand-Frage.
|
|
230
|
+
*
|
|
231
|
+
* Warum ohne Rand: MT-02 fragt nicht, was die Box verlaesst, sondern ob zwei Boxen einen
|
|
232
|
+
* Vertrag TEILEN. Fuer diese Frage sind Rand- und Beruehrmenge dasselbe — ein Vertrag, den
|
|
233
|
+
* nur eine Box beruehrt, liegt in keiner zweiten und verbindet niemanden. Die Beruehrmenge
|
|
234
|
+
* ist die billigere von beiden und braucht die Whitebox-Bedingung aus `byFunc` nicht, die
|
|
235
|
+
* ein Blatt ausschliesst.
|
|
236
|
+
*/
|
|
237
|
+
export function boxContracts(graph, funcIds) {
|
|
238
|
+
const idx = indexOf(graph);
|
|
239
|
+
const out = new Set();
|
|
240
|
+
for (const func of funcIds) {
|
|
241
|
+
const flows = [
|
|
242
|
+
...idx.out(func, 'io').filter(t => idx.typeOf(t.target) === 'FLOW').map(t => t.target),
|
|
243
|
+
...idx.in(func, 'io').filter(t => idx.typeOf(t.source) === 'FLOW').map(t => t.source),
|
|
244
|
+
];
|
|
245
|
+
for (const flow of flows)
|
|
246
|
+
for (const c of contractsOfFlow(idx, flow))
|
|
247
|
+
out.add(c);
|
|
248
|
+
}
|
|
249
|
+
return out;
|
|
250
|
+
}
|
package/dist/se/readiness.d.ts
CHANGED
|
@@ -45,6 +45,51 @@ export type ReadinessScoreType = z.infer<typeof ReadinessScore>;
|
|
|
45
45
|
* Ein interpretierbarer Ersatz (`1 − Σviolations / Σapplicable`) kommt, wenn er einen
|
|
46
46
|
* Konsumenten hat — nicht auf Vorrat.
|
|
47
47
|
*/
|
|
48
|
+
/**
|
|
49
|
+
* CR-SM-337 (ITEM-2026-059) — der STEUERUNGSRAUM im Bericht, Stufe 1: der Vertrag.
|
|
50
|
+
*
|
|
51
|
+
* Readiness misst ABDECKUNG ("wie viele Stellen sind erledigt"), der Steuer-Score AUSPRAEGUNG
|
|
52
|
+
* ("wie schlimm ist die schlimmste offene"). Beide lesen denselben Regelstrom, und beide
|
|
53
|
+
* gehoeren in denselben Bericht — sonst rechnet der naechste Leser die zweite Haelfte selbst
|
|
54
|
+
* nach. Genau das drohte: das GVE-Dashboard (ITEM-2026-018) haette `steerScore` nachbauen
|
|
55
|
+
* muessen, weil die Zahlen bisher nur als `verdict.steer.improvement` je Suggestion sichtbar
|
|
56
|
+
* waren. Eine zweite Rechnung ist eine zweite Wahrheit.
|
|
57
|
+
*
|
|
58
|
+
* Die Form ist die von `SteerScore` in @sigloch/se-engine (CR-SM-292), ZEICHENGLEICH
|
|
59
|
+
* uebernommen — contracts darf se-engine nicht importieren (es ist die Basis), also steht hier
|
|
60
|
+
* der Vertrag und dort die Rechnung. Wer die Form aendert, aendert sie an beiden Stellen; der
|
|
61
|
+
* Vertragstest daneben haelt die Felder fest.
|
|
62
|
+
*
|
|
63
|
+
* `worst` = der schlimmste normierte Ueberschuss `(wert - budget) / budget`. Normiert wird
|
|
64
|
+
* gegen die REGELSCHWELLE selbst, nicht gegen eine eigene Zahl — deshalb gibt es hier keine
|
|
65
|
+
* freien Parameter (CR-SM-292: daran ist der Vorgaenger CR-SM-281 gestorben).
|
|
66
|
+
*/
|
|
67
|
+
export declare const SteerTerm: z.ZodObject<{
|
|
68
|
+
ruleId: z.ZodString;
|
|
69
|
+
elementId: z.ZodString;
|
|
70
|
+
value: z.ZodNumber;
|
|
71
|
+
threshold: z.ZodNumber;
|
|
72
|
+
overshoot: z.ZodNumber;
|
|
73
|
+
}, z.core.$strip>;
|
|
74
|
+
export type SteerTermType = z.infer<typeof SteerTerm>;
|
|
75
|
+
export declare const SteerSpace: z.ZodObject<{
|
|
76
|
+
worst: z.ZodNumber;
|
|
77
|
+
worstAt: z.ZodNullable<z.ZodObject<{
|
|
78
|
+
ruleId: z.ZodString;
|
|
79
|
+
elementId: z.ZodString;
|
|
80
|
+
}, z.core.$strip>>;
|
|
81
|
+
mean: z.ZodNumber;
|
|
82
|
+
score: z.ZodNumber;
|
|
83
|
+
measured: z.ZodNumber;
|
|
84
|
+
terms: z.ZodArray<z.ZodObject<{
|
|
85
|
+
ruleId: z.ZodString;
|
|
86
|
+
elementId: z.ZodString;
|
|
87
|
+
value: z.ZodNumber;
|
|
88
|
+
threshold: z.ZodNumber;
|
|
89
|
+
overshoot: z.ZodNumber;
|
|
90
|
+
}, z.core.$strip>>;
|
|
91
|
+
}, z.core.$strip>;
|
|
92
|
+
export type SteerSpaceType = z.infer<typeof SteerSpace>;
|
|
48
93
|
export declare const ReadinessReport: z.ZodObject<{
|
|
49
94
|
scores: z.ZodArray<z.ZodObject<{
|
|
50
95
|
dimension: z.ZodEnum<{
|
|
@@ -63,6 +108,23 @@ export declare const ReadinessReport: z.ZodObject<{
|
|
|
63
108
|
coreApplicable: z.ZodNumber;
|
|
64
109
|
}, z.core.$strip>>;
|
|
65
110
|
timestamp: z.ZodISODateTime;
|
|
111
|
+
steer: z.ZodOptional<z.ZodObject<{
|
|
112
|
+
worst: z.ZodNumber;
|
|
113
|
+
worstAt: z.ZodNullable<z.ZodObject<{
|
|
114
|
+
ruleId: z.ZodString;
|
|
115
|
+
elementId: z.ZodString;
|
|
116
|
+
}, z.core.$strip>>;
|
|
117
|
+
mean: z.ZodNumber;
|
|
118
|
+
score: z.ZodNumber;
|
|
119
|
+
measured: z.ZodNumber;
|
|
120
|
+
terms: z.ZodArray<z.ZodObject<{
|
|
121
|
+
ruleId: z.ZodString;
|
|
122
|
+
elementId: z.ZodString;
|
|
123
|
+
value: z.ZodNumber;
|
|
124
|
+
threshold: z.ZodNumber;
|
|
125
|
+
overshoot: z.ZodNumber;
|
|
126
|
+
}, z.core.$strip>>;
|
|
127
|
+
}, z.core.$strip>>;
|
|
66
128
|
}, z.core.$strip>;
|
|
67
129
|
export type ReadinessReportType = z.infer<typeof ReadinessReport>;
|
|
68
130
|
/**
|
package/dist/se/readiness.js
CHANGED
|
@@ -57,9 +57,68 @@ export const ReadinessScore = z.object({
|
|
|
57
57
|
* Ein interpretierbarer Ersatz (`1 − Σviolations / Σapplicable`) kommt, wenn er einen
|
|
58
58
|
* Konsumenten hat — nicht auf Vorrat.
|
|
59
59
|
*/
|
|
60
|
+
/**
|
|
61
|
+
* CR-SM-337 (ITEM-2026-059) — der STEUERUNGSRAUM im Bericht, Stufe 1: der Vertrag.
|
|
62
|
+
*
|
|
63
|
+
* Readiness misst ABDECKUNG ("wie viele Stellen sind erledigt"), der Steuer-Score AUSPRAEGUNG
|
|
64
|
+
* ("wie schlimm ist die schlimmste offene"). Beide lesen denselben Regelstrom, und beide
|
|
65
|
+
* gehoeren in denselben Bericht — sonst rechnet der naechste Leser die zweite Haelfte selbst
|
|
66
|
+
* nach. Genau das drohte: das GVE-Dashboard (ITEM-2026-018) haette `steerScore` nachbauen
|
|
67
|
+
* muessen, weil die Zahlen bisher nur als `verdict.steer.improvement` je Suggestion sichtbar
|
|
68
|
+
* waren. Eine zweite Rechnung ist eine zweite Wahrheit.
|
|
69
|
+
*
|
|
70
|
+
* Die Form ist die von `SteerScore` in @sigloch/se-engine (CR-SM-292), ZEICHENGLEICH
|
|
71
|
+
* uebernommen — contracts darf se-engine nicht importieren (es ist die Basis), also steht hier
|
|
72
|
+
* der Vertrag und dort die Rechnung. Wer die Form aendert, aendert sie an beiden Stellen; der
|
|
73
|
+
* Vertragstest daneben haelt die Felder fest.
|
|
74
|
+
*
|
|
75
|
+
* `worst` = der schlimmste normierte Ueberschuss `(wert - budget) / budget`. Normiert wird
|
|
76
|
+
* gegen die REGELSCHWELLE selbst, nicht gegen eine eigene Zahl — deshalb gibt es hier keine
|
|
77
|
+
* freien Parameter (CR-SM-292: daran ist der Vorgaenger CR-SM-281 gestorben).
|
|
78
|
+
*/
|
|
79
|
+
export const SteerTerm = z.object({
|
|
80
|
+
/** Eine der STEER_RULES — die messenden Regeln (se-engine `STEER_RULES`). */
|
|
81
|
+
ruleId: z.string(),
|
|
82
|
+
/** Die Blackbox, an der der Term haengt. */
|
|
83
|
+
elementId: z.string(),
|
|
84
|
+
/** Der gemessene Wert (`violation.context.value`). */
|
|
85
|
+
value: z.number(),
|
|
86
|
+
/** Das Budget, gegen das normiert wird (`violation.context.threshold`). */
|
|
87
|
+
threshold: z.number(),
|
|
88
|
+
/** `max(0, (value - threshold) / threshold)` — dimensionslos, damit vergleichbar. */
|
|
89
|
+
overshoot: z.number().min(0),
|
|
90
|
+
});
|
|
91
|
+
export const SteerSpace = z.object({
|
|
92
|
+
worst: z.number().min(0),
|
|
93
|
+
/**
|
|
94
|
+
* Wo der schlimmste Ueberschuss sitzt — die Begruendung, nicht nur die Zahl.
|
|
95
|
+
* `null`, wenn nichts ueberschreitet.
|
|
96
|
+
*/
|
|
97
|
+
worstAt: z.object({ ruleId: z.string(), elementId: z.string() }).nullable(),
|
|
98
|
+
mean: z.number().min(0),
|
|
99
|
+
/** `worst + EPS_AUGMENT * mean`. **Kleiner ist besser**, 0 = alles im Budget. */
|
|
100
|
+
score: z.number().min(0),
|
|
101
|
+
/**
|
|
102
|
+
* Zahl der Blackboxes, die in die Rechnung eingegangen sind.
|
|
103
|
+
*
|
|
104
|
+
* DIE WICHTIGSTE ZAHL DES OBJEKTS, und zwar wegen `score: 0`: der steht sowohl fuer "alles
|
|
105
|
+
* innerhalb seiner Budgets" als auch fuer "es wurde nichts gemessen". `measured: 0`
|
|
106
|
+
* unterscheidet die beiden. Ohne sie waere eine stille Null nicht von einem guten Zustand zu
|
|
107
|
+
* trennen — dieselbe Konvention wie `policy.X = null` und `moduleMetrics.instability = null`.
|
|
108
|
+
*/
|
|
109
|
+
measured: z.number().int().min(0),
|
|
110
|
+
/** Die Terme je Element, aus denen `worst` und `mean` entstehen. */
|
|
111
|
+
terms: z.array(SteerTerm),
|
|
112
|
+
});
|
|
60
113
|
export const ReadinessReport = z.object({
|
|
61
114
|
scores: z.array(ReadinessScore),
|
|
62
115
|
timestamp: z.iso.datetime(),
|
|
116
|
+
/**
|
|
117
|
+
* CR-SM-337: OPTIONAL, damit ein aelterer Produzent gueltig bleibt — der Vertrag wandert vor
|
|
118
|
+
* dem Fueller (Stufe 2, graphcode CR-GC-537). Fehlt das Feld, heisst das "dieser Produzent
|
|
119
|
+
* kennt den Steuerungsraum noch nicht", und das ist etwas anderes als `measured: 0`.
|
|
120
|
+
*/
|
|
121
|
+
steer: SteerSpace.optional(),
|
|
63
122
|
});
|
|
64
123
|
/**
|
|
65
124
|
* CR-SM-305 — welche Profile in die readiness-Zahlen eingehen, und warum `conformance` nicht.
|
|
@@ -131,6 +190,10 @@ export const RULE_TO_DIMENSION = {
|
|
|
131
190
|
// MT-03 is no longer here: it became a measurement (`allocationCohesion`), not a
|
|
132
191
|
// rule (CR-SM-223) — a per-module advisory would depress this score permanently.
|
|
133
192
|
'MT-01': 'alloc', 'MT-02': 'alloc',
|
|
193
|
+
// CR-SM-327: MT-04 misst die FUNC-Whitebox, nicht das Modul — Grundgesamtheit FUNC,
|
|
194
|
+
// also `arch` (Kern FUNC). In `alloc` waere sie eine Fremdtyp-Regel und wuerde deren
|
|
195
|
+
// Nenner um jede FUNC des Graphen aufblaehen (Klasse CR-SM-235/270, gemessen im CR).
|
|
196
|
+
'MT-04': 'arch',
|
|
134
197
|
// CR traceability
|
|
135
198
|
'CR-R01': 'cr', 'CR-R02': 'cr', 'CR-R03': 'cr', // architecture optimization
|
|
136
199
|
// CR-SM-283: BW-02 (Whitebox-Randbreite) ersetzt AO-D03 an dieser Stelle — dieselbe
|
|
@@ -199,7 +262,7 @@ export const RULE_TO_PHASE = {
|
|
|
199
262
|
// CR-GC-366: Anschluss des Funktionsbaus — gehoert an dasselbe Gate wie R-15/IO-01,
|
|
200
263
|
// die auf derselben Kette aufsetzen.
|
|
201
264
|
'R-30': 'PDR', 'R-31': 'PDR',
|
|
202
|
-
'MT-01': 'PDR', 'MT-02': 'PDR',
|
|
265
|
+
'MT-01': 'PDR', 'MT-02': 'PDR', 'MT-04': 'PDR', // CR-SM-327: dieselbe Reifephase wie MT-02
|
|
203
266
|
'ND-01': 'PDR',
|
|
204
267
|
'BW-02': 'PDR', 'CR-01': 'PDR', 'IO-01': 'PDR', 'IO-02': 'PDR', // CR-SM-226: extended to all FCHAIN FUNC-pairs, added to the phase axis.
|
|
205
268
|
// CDR — critical design/schema completeness.
|
package/dist/se/rule-help.js
CHANGED
|
@@ -170,7 +170,11 @@ export const RULE_HELP = {
|
|
|
170
170
|
},
|
|
171
171
|
'MT-02': {
|
|
172
172
|
plain: "The parts inside this module never talk to each other → it is really several modules in one.",
|
|
173
|
-
se: "LCOM4
|
|
173
|
+
se: "LCOM4 over the BOXES of this module whitebox: its sub-MODs (`MOD -compose-> MOD`) plus its directly allocated `FUNC`s. A box owns its whole subtree, so a decomposed block carries the contracts of its leaves — the same ownership chain R-04 (CR-SM-282), BW-02 (CR-SM-283) and R-23 (CR-SM-321) use; without it a container counted as an empty group of its own and the number grew with decomposition DEPTH (CR-SM-326). Connected means a shared CONTRACT (`FLOW -relation-> SCHEMA`, else the untyped flow) — the same counting base as CR-01/R-04/MT-01 since CR-SM-274/276, not FLOW identity. CR-SM-297 dropped `satisfy`: two FUNCs meeting the same requirement can be fully decoupled at runtime, and if they share one, the REQUIREMENT is what needs decomposing. `null` (no judgement) below two boxes and where no box carries a contract at all — an unwired FUNC is R-31's statement, at the element. `info` from `metricPolicy.lcom4.info`, `warning` from `.warning` (CR-GC-329).",
|
|
174
|
+
},
|
|
175
|
+
'MT-04': {
|
|
176
|
+
plain: "This block's parts never talk to each other → it is really two blocks that happen to sit under one name.",
|
|
177
|
+
se: "LCOM4 of a decomposed FUNC: its `compose` children are the boxes of that level, each owning its whole subtree, connected when they share a CONTRACT (`FLOW -relation-> SCHEMA`, else the untyped flow) — the same computation MT-02 runs on the module whitebox (CR-SM-326). BW-02 measures the OUTSIDE of the same element (how wide the interface is, CR-SM-283); this one says whether the inside hangs together. `null` (no judgement) below two boxes and where no box carries a contract. Thresholds shared with MT-02 (`metricPolicy.lcom4`) — same measure, same scale.",
|
|
174
178
|
},
|
|
175
179
|
'ND-01': {
|
|
176
180
|
plain: "Two functions look like the same function written twice → merge them, or make clear what each one does differently.",
|
|
@@ -294,6 +298,10 @@ export const RULE_HELP = {
|
|
|
294
298
|
plain: "This element points at code in a package your project does not actually install → either add the package, or point at the one that owns the symbol now.",
|
|
295
299
|
se: "An `external: true` `realRef` names a package that is in neither `dependencies` nor `devDependencies` of the consumer. Absent dependency data is SILENCE, not a violation — the extractor never looked, and treating that as \"declares nothing\" would report every external binding at once (CR-SM-262).",
|
|
296
300
|
},
|
|
301
|
+
'RC-07': {
|
|
302
|
+
plain: "A change request is marked done in the model but still open in the folder (or the other way round), or an open change request has no entry in the model → the folder decides; fix the model.",
|
|
303
|
+
se: "`CR` node whose `status` (closed = done/dropped/rejected) contradicts `docs/cr/open|done/`, or an OPEN CR file with no `CR` node (anchored at the first `SYS`). Nodes without a file are not reported — history. Absent `crFiles` is SILENCE (CR-SM-329).",
|
|
304
|
+
},
|
|
297
305
|
'RD-01': {
|
|
298
306
|
plain: "A smallest-piece feature has nothing built to fulfil it → add what implements it.",
|
|
299
307
|
se: "Leaf `REQ` (no `compose`→`REQ` children) with no `satisfy` from a `FUNC`/`FCHAIN`/`MOD`/`SYS`.",
|
package/dist/se/rules.js
CHANGED
|
@@ -8,7 +8,7 @@ import { ElementType, TraceType, TestRefsSchema, RealRefSchema } from './ontolog
|
|
|
8
8
|
import { traceRejection, BOUNDED_PATTERNS, REQUIRED_PATTERNS, maxOccurs } from './meta-model.js';
|
|
9
9
|
import { indexOf } from './graph-index.js';
|
|
10
10
|
import { moduleCrossings, subtreeFuncs } from './module-crossings.js';
|
|
11
|
-
import { functionCriticality } from './function-criticality.js';
|
|
11
|
+
import { functionCriticality, chainsByFunc } from './function-criticality.js';
|
|
12
12
|
export const RuleSeverity = z.enum(['error', 'warning', 'info']);
|
|
13
13
|
/** Candidate target for resolving a violation (e.g. a REQ to satisfy, a TEST to link). */
|
|
14
14
|
export const ViolationCandidate = z.object({
|
|
@@ -1260,14 +1260,11 @@ function fchainMustHaveIntegrationTest(graph, policy) {
|
|
|
1260
1260
|
}
|
|
1261
1261
|
if (connections.length === 0)
|
|
1262
1262
|
return [];
|
|
1263
|
-
// FUNC → set of FCHAINs composing it.
|
|
1264
|
-
//
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
(chainsOfFunc.get(t.target) ?? chainsOfFunc.set(t.target, new Set()).get(t.target)).add(t.source);
|
|
1269
|
-
}
|
|
1270
|
-
}
|
|
1263
|
+
// FUNC → set of FCHAINs composing it. CR-SM-335: DIESELBE Rechnung, die auch die Kennzahl
|
|
1264
|
+
// speist — vorher stand hier ein zweiter, lokaler Index ueber dieselbe Kantenmenge. "Teilen
|
|
1265
|
+
// diese beiden EINE Kette" braucht die Mengen, die Infrastruktur-Ausnahme die Zahl; beides
|
|
1266
|
+
// kommt jetzt aus `chainsByFunc` (CR-SM-313 hatte den Helfer zugesagt, CR-SM-314 die Zahl).
|
|
1267
|
+
const chainsOf = chainsByFunc(graph);
|
|
1271
1268
|
// FCHAINs whose satisfy-REQ is verified by a TEST = chains with an integration test.
|
|
1272
1269
|
const verifiedReqs = new Set(idx.tracesOfType('verify').map(t => t.target));
|
|
1273
1270
|
const testedChains = new Set();
|
|
@@ -1285,8 +1282,8 @@ function fchainMustHaveIntegrationTest(graph, policy) {
|
|
|
1285
1282
|
const violations = [];
|
|
1286
1283
|
const seen = new Set();
|
|
1287
1284
|
for (const [p, c] of connections) {
|
|
1288
|
-
const pChains =
|
|
1289
|
-
const cChains =
|
|
1285
|
+
const pChains = chainsOf.get(p) ?? new Set();
|
|
1286
|
+
const cChains = chainsOf.get(c) ?? new Set();
|
|
1290
1287
|
// ZWEIG 1 — ein Ende in GAR KEINER Kette: still. Das ist R-30s Aussage, nicht R-21s.
|
|
1291
1288
|
// Ohne diese Klausel feuert die Regel an einem code-importierten Graphen wie moneyflow
|
|
1292
1289
|
// 219 von 219 Mal und meldet in Wahrheit "dieses Repo hat keine Wirkketten", einmal je Kante.
|