@sigloch/contracts 10.0.0 → 10.2.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 +12 -0
- package/dist/se/conformance-rules.js +6 -6
- package/dist/se/evaluate-all.d.ts +1 -1
- package/dist/se/evaluate-all.js +21 -0
- package/dist/se/function-criticality.d.ts +75 -0
- package/dist/se/function-criticality.js +50 -0
- package/dist/se/grammar-snapshot.d.ts +5 -4
- package/dist/se/grammar-snapshot.js +92 -4
- package/dist/se/index.d.ts +9 -1
- package/dist/se/index.js +17 -1
- package/dist/se/policy.d.ts +4 -5
- package/dist/se/policy.js +37 -26
- package/dist/se/readiness.d.ts +22 -2
- package/dist/se/readiness.js +30 -6
- package/dist/se/rule-help.d.ts +52 -0
- package/dist/se/rule-help.js +350 -0
- package/dist/se/rules.js +229 -125
- package/package.json +3 -2
package/dist/se/rules.js
CHANGED
|
@@ -7,7 +7,8 @@ import { z } from 'zod/v4';
|
|
|
7
7
|
import { ElementType, TraceType, TestRefsSchema, RealRefSchema } from './ontology.js';
|
|
8
8
|
import { traceRejection, BOUNDED_PATTERNS, REQUIRED_PATTERNS, maxOccurs } from './meta-model.js';
|
|
9
9
|
import { indexOf } from './graph-index.js';
|
|
10
|
-
import {
|
|
10
|
+
import { moduleCrossings } from './module-crossings.js';
|
|
11
|
+
import { functionCriticality } from './function-criticality.js';
|
|
11
12
|
export const RuleSeverity = z.enum(['error', 'warning', 'info']);
|
|
12
13
|
/** Candidate target for resolving a violation (e.g. a REQ to satisfy, a TEST to link). */
|
|
13
14
|
export const ViolationCandidate = z.object({
|
|
@@ -233,74 +234,45 @@ function funcMustSatisfyReq(graph) {
|
|
|
233
234
|
// R-03: ASIL isolation
|
|
234
235
|
// ---------------------------------------------------------------------------
|
|
235
236
|
// ---------------------------------------------------------------------------
|
|
236
|
-
// R-04:
|
|
237
|
+
// R-04: Randbreite des Moduls (CR-SM-312)
|
|
237
238
|
//
|
|
238
|
-
// CR-SM-
|
|
239
|
-
//
|
|
240
|
-
//
|
|
241
|
-
//
|
|
242
|
-
// ueberpruefbar. Jetzt sind es `policy.moduleSize.{coupled,large,crossings}`.
|
|
243
|
-
// `null` → messen, nicht urteilen: die Regel schweigt.
|
|
239
|
+
// Die Groessenfrage ist mit CR-SM-311 an RD-04 gegangen: eine Modul-Ebene zeigt Untermodule UND
|
|
240
|
+
// eigene Funktionen, das IST ihre Breite. Was bleibt, ist die Kopplung — und die misst diese Regel
|
|
241
|
+
// jetzt allein: die VERSCHIEDENEN Vertraege, die den Modulrand queren (`moduleCrossings.byModule`,
|
|
242
|
+
// dieselbe Zaehlung wie CR-01 und BW-02, CR-SM-276).
|
|
244
243
|
//
|
|
245
|
-
//
|
|
246
|
-
// `
|
|
247
|
-
//
|
|
248
|
-
//
|
|
249
|
-
//
|
|
250
|
-
//
|
|
251
|
-
//
|
|
252
|
-
|
|
244
|
+
// Dieselbe Frage wie BW-02 auf dem zweiten Baum, also dieselbe Schwelle: `policy.boundaryWidth`.
|
|
245
|
+
// Damit faellt `policy.moduleSize` ersatzlos — drei Zahlen fuer eine Frage, die es nicht mehr gibt.
|
|
246
|
+
//
|
|
247
|
+
// Das schliesst die Blindstelle, die `se-engine/steer.ts` seit CR-SM-292 benennt: ein KLEINES
|
|
248
|
+
// Modul mit breitem Rand war stumm, weil R-04 den Rand nur zusammen mit der Groesse ansah
|
|
249
|
+
// (gemessen: MOD-kernel-measure 9 Vertraege bei 7 Funktionen). Seit diesem CR steuert R-04 mit
|
|
250
|
+
// (`STEER_RULES`), denn der Befund ist lokal, gerichtet und budgetiert.
|
|
251
|
+
// ---------------------------------------------------------------------------
|
|
252
|
+
function moduleBoundaryWidth(graph, policy) {
|
|
253
|
+
const steps = policy.boundaryWidth;
|
|
254
|
+
if (steps === null)
|
|
255
|
+
return [];
|
|
253
256
|
const idx = indexOf(graph);
|
|
254
257
|
const violations = [];
|
|
255
|
-
const
|
|
256
|
-
|
|
257
|
-
return violations;
|
|
258
|
-
const modules = idx.elementsOfType('MOD');
|
|
259
|
-
for (const mod of modules) {
|
|
260
|
-
// CR-SM-282: die Groesse ist die des TEILBAUMS, nicht der direkten Allokation. Ein
|
|
261
|
-
// Eltern-MOD (`MOD -compose-> MOD`) hat keine direkt allozierten FUNCs; `funcCount`
|
|
262
|
-
// war dort 0 und die Regel schwieg an genau der Whitebox, in die man hineinklickt.
|
|
263
|
-
// Fuer ein Blatt-MOD ist die Menge zeichengleich mit der bisherigen (nur `FUNC
|
|
264
|
-
// -allocate-> MOD` existiert als Pattern) — Regressions-Invariante.
|
|
265
|
-
const allocatedIds = [...subtreeFuncs(graph, mod.id)];
|
|
266
|
-
const allocated = allocatedIds.map(id => idx.byId.get(id)).filter((e) => !!e);
|
|
267
|
-
const funcCount = allocated.length;
|
|
268
|
-
if (funcCount <= steps.coupled)
|
|
258
|
+
for (const [modId, contracts] of moduleCrossings(graph).byModule) {
|
|
259
|
+
if (contracts.size < steps.warning)
|
|
269
260
|
continue;
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
else if (funcCount > steps.large && crossings > 0) {
|
|
283
|
-
violations.push({
|
|
284
|
-
rule_id: 'R-04',
|
|
285
|
-
severity: 'warning',
|
|
286
|
-
element_id: mod.id,
|
|
287
|
-
message: `${mod.id} has ${funcCount} functions and ${crossings} distinct contract(s) crossing its module boundary (split recommended)`,
|
|
288
|
-
fix_hint: `Split module into smaller units to reduce coupling (> ${steps.large} functions AND crossing flows)`,
|
|
289
|
-
context: { element_type: mod.type, element_name: mod.name, candidate_targets: toCandidates(allocated) },
|
|
290
|
-
});
|
|
291
|
-
}
|
|
292
|
-
else if (funcCount > steps.coupled && crossings > steps.crossings) {
|
|
293
|
-
violations.push({
|
|
294
|
-
rule_id: 'R-04',
|
|
295
|
-
severity: 'warning',
|
|
296
|
-
element_id: mod.id,
|
|
297
|
-
message: `${mod.id} has ${funcCount} functions and ${crossings} distinct contract(s) crossing its module boundary (high coupling)`,
|
|
298
|
-
fix_hint: 'Reduce crossing flows or split module',
|
|
299
|
-
context: { element_type: mod.type, element_name: mod.name, candidate_targets: toCandidates(allocated) },
|
|
300
|
-
});
|
|
301
|
-
}
|
|
261
|
+
const mod = idx.byId.get(modId);
|
|
262
|
+
violations.push({
|
|
263
|
+
rule_id: 'R-04',
|
|
264
|
+
severity: 'warning',
|
|
265
|
+
element_id: modId,
|
|
266
|
+
message: `${modId} exposes ${contracts.size} distinct contract(s) at its module boundary (>= ${steps.warning})`,
|
|
267
|
+
fix_hint: 'Reduce the contracts crossing this module boundary — merge contracts, or move the functions that carry them',
|
|
268
|
+
// CR-SM-288: `>= steps.warning` ist inklusiv, also ist `steps.warning - 1` der groesste Wert
|
|
269
|
+
// im Budget; die Masse `value - threshold` ist damit fuer jeden Befund > 0. Gleiche Konvention
|
|
270
|
+
// wie BW-02, dieselbe Schwelle.
|
|
271
|
+
context: { element_type: mod?.type, element_name: mod?.name, value: contracts.size, threshold: steps.warning - 1 },
|
|
272
|
+
});
|
|
302
273
|
}
|
|
303
|
-
|
|
274
|
+
// Kanonische Reihenfolge wie bei BW-02 — sonst haengt die Befund-Sequenz an der Trace-Reihenfolge.
|
|
275
|
+
return violations.sort((a, b) => (a.element_id < b.element_id ? -1 : a.element_id > b.element_id ? 1 : 0));
|
|
304
276
|
}
|
|
305
277
|
// ---------------------------------------------------------------------------
|
|
306
278
|
// R-05: Every TEST must verify at least one REQ
|
|
@@ -396,6 +368,95 @@ function flowCompleteness(graph) {
|
|
|
396
368
|
return violations;
|
|
397
369
|
}
|
|
398
370
|
// ---------------------------------------------------------------------------
|
|
371
|
+
// IO-02: ein FLOW hat genau EINEN Produzenten (CR-SM-307).
|
|
372
|
+
//
|
|
373
|
+
// R-10 prueft die UNTERgrenze (mindestens ein Ende), IO-02 die OBERgrenze der
|
|
374
|
+
// Produzentenseite. Die beiden koennen am selben FLOW nie zugleich feuern — |P| = 0
|
|
375
|
+
// gegen |P| > 1 ist disjunkt — also keine Doppelzaehlung im Sinne von Gate 4. Getrennte
|
|
376
|
+
// IDs, weil es getrennte Fragen sind (CR-SM-296, "je Frage eine Regel"): R-10 fragt
|
|
377
|
+
// "hat dieser Fluss ueberhaupt Enden?", IO-02 fragt "kommt sein Inhalt von einer Stelle?".
|
|
378
|
+
//
|
|
379
|
+
// WARUM NUR DIE PRODUZENTENSEITE. Der Inhalt eines FLOW kommt von einer Stelle; haengen
|
|
380
|
+
// zwei Quellen dran, sind es zwei Fluesse unter einem Namen, und kein Leser kann wissen,
|
|
381
|
+
// welche Fassung er bekommt. Mehrere KONSUMENTEN sind dagegen normal — wer abholt, aendert
|
|
382
|
+
// am Fluss nichts. Das ist genau die zentrale Konfiguration: eine Quelle, viele Verbraucher.
|
|
383
|
+
// Eine beidseitige Fassung wurde gemessen und VERWORFEN: sie haette allein in graphcode
|
|
384
|
+
// 8 saubere 1:N-Muster bestraft (FLOW-dimension-readiness 1x7, FLOW-steering-snapshot 1x3,
|
|
385
|
+
// sechs weitere), ohne einen einzigen zusaetzlichen echten Fehler zu finden.
|
|
386
|
+
//
|
|
387
|
+
// BESTAND vor der Einfuehrung, fuenf Systeme (graphcode/rig/flow-cardinality):
|
|
388
|
+
// graphcode 20/42 · graph-view-edit 5/8 · bok 3/15 · moneyflow 0/220 · test_karp 0/21
|
|
389
|
+
// Die beiden Referenzen sind auch BEIDSEITIG sauber — sie erfuellen die strengere Fassung
|
|
390
|
+
// ohnehin, die Lockerung rettet sie nicht. graphcode wurde vor diesem CR auf 9 repariert
|
|
391
|
+
// (CR-GC-499/500); die 9 sind Busse mit 3 bis 19 Produzenten, un-modellierte Entwurfsarbeit.
|
|
392
|
+
//
|
|
393
|
+
// WARUM ES BIS HIERHER KEIN GATE PRUEFTE: die vier io-Zeilen in TRACE_PATTERNS tragen kein
|
|
394
|
+
// `cardinality`-Feld und fallen deshalb durch REQUIRED_PATTERNS
|
|
395
|
+
// (= TRACE_PATTERNS.filter(p => p.cardinality === '1')). Die SCHEMA-Haelfte IST Grammatik
|
|
396
|
+
// (`FLOW -relation-> SCHEMA [1..1]`, R-18 seit CR-SM-271 Teil 2) — die io-Haelfte war nie
|
|
397
|
+
// deklariert. Es ist kein Regel-Loch, das jemand geschlossen und wieder geoeffnet haette:
|
|
398
|
+
// die Frage wurde nie gestellt.
|
|
399
|
+
//
|
|
400
|
+
// EIN BEFUND JE FLOW, nicht je ueberzaehliger Kante (Klasse CR-SM-242): sonst uebertraefe
|
|
401
|
+
// der Zaehler seinen eigenen Nenner-Beitrag (`domain: ['FLOW']`). `value`/`threshold` nach
|
|
402
|
+
// CR-SM-288, damit der Befund budgetierbar ist.
|
|
403
|
+
//
|
|
404
|
+
// SEVERITY error, aber NICHT in STEER_RULES: das ist Hygiene, keine Steuerdimension
|
|
405
|
+
// (Entscheidung Auftraggeber 2026-09-10).
|
|
406
|
+
//
|
|
407
|
+
// KORREKTUR (CR-SM-308): Hier stand als Beleg, das Auftrennen eines Flusses hebe BW-02 ("ein
|
|
408
|
+
// aufgetrennter Fluss kreuzt zwei Grenzen statt einer"), eine rankende Regel zoege also gegen
|
|
409
|
+
// diese urteilende. Das stimmt nicht. moduleCrossings zaehlt je Rand VERSCHIEDENE SCHEMA; ein
|
|
410
|
+
// aufgetrennter FLOW behaelt sein SCHEMA und hat eine Teilmenge der Enden, er erzeugt keinen
|
|
411
|
+
// neuen Randdurchgang. Gemessen an graphcode v257: alle sechs Mehrfach-Produzenten-FLOW je
|
|
412
|
+
// Produzent getrennt, BW-02 15 -> 15, byFunc und byModule unveraendert, IO-02 6 -> 0. Der
|
|
413
|
+
// Anstieg bei CR-GC-499/500 (BW-02 13 -> 14, Chebyshev -0,25) kam von drei NEUEN SCHEMA und
|
|
414
|
+
// umgelegten Ketten, nicht vom Auftrennen. Die Entscheidung oben steht; ein gemessener
|
|
415
|
+
// Zielkonflikt mit BW-02 traegt sie nicht.
|
|
416
|
+
// ---------------------------------------------------------------------------
|
|
417
|
+
function flowSingleProducer(graph) {
|
|
418
|
+
const idx = indexOf(graph);
|
|
419
|
+
const flowIds = new Set(idx.idsOfType('FLOW'));
|
|
420
|
+
const isEndpoint = (id) => { const t = idx.typeOf(id); return t === 'FUNC' || t === 'ACTOR'; };
|
|
421
|
+
// Produzenten je FLOW als MENGE: zwei io-Kanten derselben Quelle auf denselben FLOW sind
|
|
422
|
+
// eine Quelle, kein Verstoss. Konsumenten werden bewusst nicht gezaehlt (s. Kopf).
|
|
423
|
+
const producers = new Map();
|
|
424
|
+
for (const t of idx.tracesOfType('io')) {
|
|
425
|
+
if (!flowIds.has(t.target) || !isEndpoint(t.source))
|
|
426
|
+
continue;
|
|
427
|
+
if (!producers.has(t.target))
|
|
428
|
+
producers.set(t.target, new Set());
|
|
429
|
+
producers.get(t.target).add(t.source);
|
|
430
|
+
}
|
|
431
|
+
const violations = [];
|
|
432
|
+
// Ueber die Elementliste laufen, nicht ueber die Map: die Reihenfolge eines Befundes darf
|
|
433
|
+
// nicht an der Trace-Reihenfolge haengen (CR-SM-244, Ordnung).
|
|
434
|
+
for (const f of idx.elementsOfType('FLOW')) {
|
|
435
|
+
const ps = producers.get(f.id);
|
|
436
|
+
if (!ps || ps.size <= 1)
|
|
437
|
+
continue;
|
|
438
|
+
const sorted = [...ps].sort();
|
|
439
|
+
violations.push({
|
|
440
|
+
rule_id: 'IO-02',
|
|
441
|
+
severity: 'error',
|
|
442
|
+
element_id: f.id,
|
|
443
|
+
message: `${f.id} has ${ps.size} producers (${sorted.join(', ')}) — the content of a FLOW comes from exactly one place`,
|
|
444
|
+
fix_hint: 'Give each source its own FLOW; the shared contract stays on the SCHEMA (n FLOW -> 1 SCHEMA is legal)',
|
|
445
|
+
context: {
|
|
446
|
+
element_type: f.type,
|
|
447
|
+
element_name: f.name,
|
|
448
|
+
value: ps.size,
|
|
449
|
+
threshold: 1,
|
|
450
|
+
candidate_targets: sorted.slice(0, 3).map(id => {
|
|
451
|
+
const e = idx.byId.get(id);
|
|
452
|
+
return { id, type: e?.type ?? 'FUNC', name: e?.name ?? id };
|
|
453
|
+
}),
|
|
454
|
+
},
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
return violations;
|
|
458
|
+
}
|
|
459
|
+
// ---------------------------------------------------------------------------
|
|
399
460
|
// R-11: REMOVED — superseded by SC-02 (identical check, better severity).
|
|
400
461
|
// ---------------------------------------------------------------------------
|
|
401
462
|
// ---------------------------------------------------------------------------
|
|
@@ -564,54 +625,10 @@ function noPrematureDecomposition(graph) {
|
|
|
564
625
|
context: { element_type: req.type, element_name: req.name },
|
|
565
626
|
}));
|
|
566
627
|
}
|
|
567
|
-
|
|
568
|
-
// RD-04: Decomposition breadth (CR-SM-221)
|
|
569
|
-
// ---------------------------------------------------------------------------
|
|
570
|
-
/**
|
|
571
|
-
* Max children on one decomposition level. The 7±2 convention existed only as
|
|
572
|
-
* prose ("darüber func-of-func"); below 7 is a guideline, not a violation, so only
|
|
573
|
-
* the upper bound is a rule.
|
|
574
|
-
*
|
|
575
|
-
* Counted per (parent, kind) — a MOD that both holds 12 FUNCs and composes 12
|
|
576
|
-
* sub-MODs has two breadth problems, not one. (The spike reference merged both into
|
|
577
|
-
* a single counter on the MOD id and would have reported 24 under one kind.)
|
|
578
|
-
*
|
|
579
|
-
* CR-SM-282, zwei Aenderungen:
|
|
580
|
-
*
|
|
581
|
-
* 1. **Die Schwelle steht in `policy.decompositionBreadth`**, nicht mehr inline. `null`
|
|
582
|
-
* schaltet die Regel ab (messen statt urteilen). Der Default war hier 11 (verhaltensgleich
|
|
583
|
-
* zum vorherigen Literal) und ist seit CR-SM-296 **9** — die Obergrenze von 7±2.
|
|
584
|
-
* 2. **Der FUNC-Wurzelwald zaehlt mit.** Die Regel zaehlt Kinder je *Parent*; die Wurzeln des
|
|
585
|
-
* `FUNC -compose-> FUNC`-Waldes haben keinen — die oberste Funktionsebene war damit fuer
|
|
586
|
-
* RD-04 unsichtbar. Gemessen: moneyflow hat 306 Wurzel-FUNCs und RD-04 meldete **0**.
|
|
587
|
-
* Die MOD-Seite hatte das Problem nie, weil `SYS -compose-> MOD` eine echte Kante ist und
|
|
588
|
-
* unten schon als `sub-MOD` gezaehlt wird; die FUNC-Seite hat keine solche Kante
|
|
589
|
-
* (`se:top-level`: „the top FUNC set is a PROJECTION, not an edge").
|
|
590
|
-
*
|
|
591
|
-
* Anker ist der SYS-Knoten — determiniert, weil genau einer existiert; gibt es nicht genau
|
|
592
|
-
* einen, schweigt dieser Zweig statt zu raten (R-17 deckt den Fall ab). Kein zweiter Weg zur
|
|
593
|
-
* Wurzelmenge: es ist dieselbe Definition wie in `se:top-level` (FUNC ohne compose-Elternteil).
|
|
594
|
-
*/
|
|
595
|
-
// CR-SM-296: das Bein `FUNC -allocate-> MOD` ist ENTFALLEN und an R-04 abgegeben.
|
|
596
|
-
//
|
|
597
|
-
// Es zaehlte die allozierten FUNCs je Modul — also die MODULGROESSE, und die misst R-04 auch.
|
|
598
|
-
// Beide feuerten am selben Modul im selben Batch, mit zwei Schwellen aus zwei Policy-Feldern:
|
|
599
|
-
// RD-04 > 11 allozierte FUNC-Kinder policy.decompositionBreadth
|
|
600
|
-
// R-04 > 12 FUNCs (bzw. 8-12 mit Kreuzungen) policy.moduleSize
|
|
601
|
-
// Ein Sachverhalt, zwei Regeln, zwei Zahlen — und welche recht hat, stand nirgends. Schlimmer:
|
|
602
|
-
// dieselbe Ursache ging doppelt in den readiness-Nenner. R-04 ist die reichere Aussage (Groesse
|
|
603
|
-
// GEGEN Kreuzungen, drei Urteile) und liest ueber `moduleCrossings` ohnehin dieselben Daten.
|
|
604
|
-
//
|
|
605
|
-
// RD-04 bleibt fuer ZERLEGUNGSBREITE zustaendig: `FUNC compose FUNC`, `SYS/MOD compose MOD` und
|
|
606
|
-
// der FUNC-Wurzelwald am SYS (CR-SM-282). Je Frage eine Regel, je Frage eine Schwelle.
|
|
607
|
-
function decompositionBreadth(graph, policy) {
|
|
608
|
-
const max = policy.decompositionBreadth;
|
|
609
|
-
if (max === null)
|
|
610
|
-
return [];
|
|
628
|
+
function breadthCounts(graph) {
|
|
611
629
|
const idx = indexOf(graph);
|
|
612
630
|
// CR-SM-264: `typeOf` und `byId` baute diese Regel je Aufruf selbst — der Index hat beide.
|
|
613
631
|
const typeOf = idx;
|
|
614
|
-
const byId = idx.byId;
|
|
615
632
|
const counts = new Map();
|
|
616
633
|
const bump = (parentId, kind) => {
|
|
617
634
|
const key = `${parentId}\u0000${kind}`;
|
|
@@ -627,9 +644,24 @@ function decompositionBreadth(graph, policy) {
|
|
|
627
644
|
if (t.type === 'compose' && src === 'FUNC' && tgt === 'FUNC') {
|
|
628
645
|
bump(t.source, 'sub-FUNC');
|
|
629
646
|
}
|
|
630
|
-
else if (t.type === 'compose' &&
|
|
647
|
+
else if (t.type === 'compose' && src === 'SYS' && tgt === 'MOD') {
|
|
631
648
|
bump(t.source, 'sub-MOD');
|
|
632
649
|
}
|
|
650
|
+
else if (t.type === 'compose' && src === 'MOD' && tgt === 'MOD') {
|
|
651
|
+
bump(t.source, 'module element');
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
// CR-SM-311: eine MOD-Ebene liest sich aus Untermodulen UND eigenen Funktionen. Gezaehlt werden
|
|
655
|
+
// die BLAETTER — ein zerlegter FUNC ist ein Wertbaum-Knoten; seine Blaetter tragen den Code und
|
|
656
|
+
// stehen selbst im Modul, ihn mitzuzaehlen hiesse dieselbe Funktion zweimal.
|
|
657
|
+
const decomposedFuncs = new Set(graph.traces
|
|
658
|
+
.filter(t => t.type === 'compose' && typeOf.typeOf(t.source) === 'FUNC' && typeOf.typeOf(t.target) === 'FUNC')
|
|
659
|
+
.map(t => t.source));
|
|
660
|
+
for (const t of graph.traces) {
|
|
661
|
+
if (t.type !== 'allocate' || typeOf.typeOf(t.source) !== 'FUNC' || typeOf.typeOf(t.target) !== 'MOD')
|
|
662
|
+
continue;
|
|
663
|
+
if (!decomposedFuncs.has(t.source))
|
|
664
|
+
bump(t.target, 'module element');
|
|
633
665
|
}
|
|
634
666
|
// CR-SM-282: der FUNC-Wurzelwald als eigene Blackbox, verankert am SYS-Knoten.
|
|
635
667
|
const composedFuncs = new Set(graph.traces
|
|
@@ -641,18 +673,56 @@ function decompositionBreadth(graph, policy) {
|
|
|
641
673
|
if (rootFuncs > 0)
|
|
642
674
|
counts.set(`${systems[0].id}\u0000root FUNC`, { parentId: systems[0].id, kind: 'root FUNC', n: rootFuncs });
|
|
643
675
|
}
|
|
644
|
-
return [...counts.values()]
|
|
645
|
-
|
|
676
|
+
return [...counts.values()];
|
|
677
|
+
}
|
|
678
|
+
function decompositionBreadth(graph, policy) {
|
|
679
|
+
const band = policy.decompositionBreadth;
|
|
680
|
+
if (band === null)
|
|
681
|
+
return [];
|
|
682
|
+
const byId = indexOf(graph).byId;
|
|
683
|
+
return breadthCounts(graph)
|
|
684
|
+
.filter(c => c.n > band.warning)
|
|
646
685
|
.map(c => {
|
|
647
686
|
const parent = byId.get(c.parentId);
|
|
648
687
|
return {
|
|
649
688
|
rule_id: 'RD-04',
|
|
650
689
|
severity: 'warning',
|
|
651
690
|
element_id: c.parentId,
|
|
652
|
-
message: `${c.parentId} has ${c.n} ${c.kind} children on one level (>${
|
|
691
|
+
message: `${c.parentId} has ${c.n} ${c.kind} children on one level (>${band.warning})`,
|
|
653
692
|
fix_hint: 'Introduce an intermediate level (func-of-func / sub-MOD)',
|
|
654
|
-
// CR-SM-288: `>
|
|
655
|
-
context: { element_type: parent?.type, element_name: parent?.name, value: c.n, threshold:
|
|
693
|
+
// CR-SM-288: `> band.warning` ist exklusiv, also ist `band.warning` selbst noch im Budget.
|
|
694
|
+
context: { element_type: parent?.type, element_name: parent?.name, value: c.n, threshold: band.warning },
|
|
695
|
+
};
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
// ---------------------------------------------------------------------------
|
|
699
|
+
// RD-05: Zerlegung zu SCHMAL — eine Ebene mit weniger als `decompositionBreadth.min` Elementen
|
|
700
|
+
// abstrahiert nichts (CR-SM-311, Z3 aus ITEM-2026-053). Dieselbe Zaehlung wie RD-04.
|
|
701
|
+
//
|
|
702
|
+
// Eine eigene Regel und nicht die untere Haelfte von RD-04: RD-04 ist eine der messenden Regeln
|
|
703
|
+
// (CR-SM-288) — jeder ihrer Befunde traegt `value` und `threshold`, und `value > threshold`
|
|
704
|
+
// heisst warning. Eine Unterschreitung hat keinen Ueberschuss; als RD-04 haette sie diese Zusage
|
|
705
|
+
// fuer alle Konsumenten gebrochen und waere in `steerScore` als Masse 0 mitgelaufen. RD-05 fuehrt
|
|
706
|
+
// deshalb keine Zahl im Kontext und steuert nicht — sie ist Lesbarkeit, nicht Randbreite.
|
|
707
|
+
// Eine Ebene ohne jedes Kind ist nicht ihr Fall: ein leeres MOD meldet R-23, eine FUNC ohne Kinder
|
|
708
|
+
// ist ein Blatt.
|
|
709
|
+
// ---------------------------------------------------------------------------
|
|
710
|
+
function decompositionNarrow(graph, policy) {
|
|
711
|
+
const band = policy.decompositionBreadth;
|
|
712
|
+
if (band === null)
|
|
713
|
+
return [];
|
|
714
|
+
const byId = indexOf(graph).byId;
|
|
715
|
+
return breadthCounts(graph)
|
|
716
|
+
.filter(c => c.n < band.min)
|
|
717
|
+
.map(c => {
|
|
718
|
+
const parent = byId.get(c.parentId);
|
|
719
|
+
return {
|
|
720
|
+
rule_id: 'RD-05',
|
|
721
|
+
severity: 'warning',
|
|
722
|
+
element_id: c.parentId,
|
|
723
|
+
message: `${c.parentId} has only ${c.n} ${c.kind} children on one level (<${band.min})`,
|
|
724
|
+
fix_hint: 'Dissolve the level into its parent, or gather related elements under it',
|
|
725
|
+
context: { element_type: parent?.type, element_name: parent?.name },
|
|
656
726
|
};
|
|
657
727
|
});
|
|
658
728
|
}
|
|
@@ -1171,12 +1241,12 @@ function funcMustHaveCodeBinding(graph) {
|
|
|
1171
1241
|
// as one produced P·C findings per hub FLOW and made reuse the expensive
|
|
1172
1242
|
// choice. The FCHAIN is the modelled claim; the test is owed on the claim.
|
|
1173
1243
|
// ---------------------------------------------------------------------------
|
|
1174
|
-
function fchainMustHaveIntegrationTest(graph) {
|
|
1244
|
+
function fchainMustHaveIntegrationTest(graph, policy) {
|
|
1175
1245
|
const idx = indexOf(graph);
|
|
1176
1246
|
const typeOf = new Map(graph.elements.map(e => [e.id, e.type]));
|
|
1177
1247
|
const isFunc = (id) => typeOf.get(id) === 'FUNC';
|
|
1178
1248
|
const io = idx.tracesOfType('io');
|
|
1179
|
-
// FUNC↔FUNC
|
|
1249
|
+
// FUNC↔FUNC handovers via the FLOW hop: (producer FUNC) → FLOW → (consumer FUNC).
|
|
1180
1250
|
const connections = [];
|
|
1181
1251
|
for (const e of graph.elements) {
|
|
1182
1252
|
if (e.type !== 'FLOW')
|
|
@@ -1190,7 +1260,8 @@ function fchainMustHaveIntegrationTest(graph) {
|
|
|
1190
1260
|
}
|
|
1191
1261
|
if (connections.length === 0)
|
|
1192
1262
|
return [];
|
|
1193
|
-
// FUNC → set of FCHAINs composing it.
|
|
1263
|
+
// FUNC → set of FCHAINs composing it. LOKALER Index fuer eine andere Frage als die Kennzahl:
|
|
1264
|
+
// "teilen diese beiden EINE Kette" braucht die Mengen, nicht die Zahl (CR-SM-313).
|
|
1194
1265
|
const chainsOfFunc = new Map();
|
|
1195
1266
|
for (const t of graph.traces) {
|
|
1196
1267
|
if (t.type === 'compose' && typeOf.get(t.source) === 'FCHAIN' && isFunc(t.target)) {
|
|
@@ -1205,16 +1276,46 @@ function fchainMustHaveIntegrationTest(graph) {
|
|
|
1205
1276
|
testedChains.add(t.source);
|
|
1206
1277
|
}
|
|
1207
1278
|
}
|
|
1279
|
+
// CR-SM-313: die Infrastruktur-Ausnahme. Die ZAHL kommt aus der Kennzahl (CR-SM-314), nicht
|
|
1280
|
+
// aus einer zweiten Rechnung — eine Uebergabe an eine Funktion, die quer durch alle Ketten
|
|
1281
|
+
// laeuft, ist kein fehlender Integrationstest, sondern ein Bus. `null` = keine Ausnahme.
|
|
1282
|
+
const infra = policy.criticality?.infrastructure ?? null;
|
|
1283
|
+
const chainCount = infra === null ? null : new Map(functionCriticality(graph).map(r => [r.funcId, r.chains]));
|
|
1284
|
+
const isInfrastructure = (id) => infra !== null && chainCount !== null && (chainCount.get(id) ?? 0) >= infra;
|
|
1208
1285
|
const violations = [];
|
|
1209
1286
|
const seen = new Set();
|
|
1210
1287
|
for (const [p, c] of connections) {
|
|
1211
|
-
const
|
|
1212
|
-
|
|
1213
|
-
//
|
|
1214
|
-
//
|
|
1215
|
-
//
|
|
1216
|
-
if (
|
|
1288
|
+
const pChains = chainsOfFunc.get(p) ?? new Set();
|
|
1289
|
+
const cChains = chainsOfFunc.get(c) ?? new Set();
|
|
1290
|
+
// ZWEIG 1 — ein Ende in GAR KEINER Kette: still. Das ist R-30s Aussage, nicht R-21s.
|
|
1291
|
+
// Ohne diese Klausel feuert die Regel an einem code-importierten Graphen wie moneyflow
|
|
1292
|
+
// 219 von 219 Mal und meldet in Wahrheit "dieses Repo hat keine Wirkketten", einmal je Kante.
|
|
1293
|
+
if (pChains.size === 0 || cChains.size === 0)
|
|
1294
|
+
continue;
|
|
1295
|
+
const shared = [...pChains].filter(ch => cChains.has(ch));
|
|
1296
|
+
// ZWEIG 3 — beide in Ketten, aber KEINE gemeinsame (CR-SM-313). CR-GC-315 hatte diesen Fall
|
|
1297
|
+
// stillgelegt, weil ein FLOW damals viele Produzenten haben durfte und die P·C-Ableitung
|
|
1298
|
+
// Wiederverwendung quadratisch besteuerte; seit IO-02 (CR-SM-307) hat er genau EINEN.
|
|
1299
|
+
// Befund am SENDER: ohne gemeinsame Kette gibt es keinen Kettenanker.
|
|
1300
|
+
if (shared.length === 0) {
|
|
1301
|
+
if (isInfrastructure(p) || isInfrastructure(c))
|
|
1302
|
+
continue;
|
|
1303
|
+
const key = `pair|${p}>${c}`;
|
|
1304
|
+
if (seen.has(key))
|
|
1305
|
+
continue;
|
|
1306
|
+
seen.add(key);
|
|
1307
|
+
const sender = idx.byId.get(p);
|
|
1308
|
+
violations.push({
|
|
1309
|
+
rule_id: 'R-21',
|
|
1310
|
+
severity: 'warning',
|
|
1311
|
+
element_id: p,
|
|
1312
|
+
message: `${p} hands over to ${c}, but the two share no FCHAIN — no integration scope is declared`,
|
|
1313
|
+
fix_hint: 'Put both functions into one FCHAIN, or route the handover through a function of an existing chain',
|
|
1314
|
+
context: { element_type: sender?.type, element_name: sender?.name },
|
|
1315
|
+
});
|
|
1217
1316
|
continue;
|
|
1317
|
+
}
|
|
1318
|
+
// ZWEIG 2 — gemeinsame Kette, aber keine davon geprueft: Befund an der Kette. Unveraendert.
|
|
1218
1319
|
if (shared.some(ch => testedChains.has(ch)))
|
|
1219
1320
|
continue;
|
|
1220
1321
|
const anchor = shared[0];
|
|
@@ -1444,7 +1545,7 @@ export const V3_RULES = [
|
|
|
1444
1545
|
{ id: 'R-02', name: 'FUNC must satisfy REQ', severity: 'warning', evaluate: funcMustSatisfyReq, domain: ['FUNC'] },
|
|
1445
1546
|
// CR-SM-243: domain ist MOD, nicht FUNC — die Regel iteriert Module und meldet am Modul,
|
|
1446
1547
|
// das ASIL-D und QM mischt. Die FUNCs sind der Anlass des Urteils, nicht seine Traeger.
|
|
1447
|
-
{ id: 'R-04', name: 'Module
|
|
1548
|
+
{ id: 'R-04', name: 'Module boundary width', severity: 'warning', evaluate: moduleBoundaryWidth, domain: ['MOD'] },
|
|
1448
1549
|
{ id: 'R-05', name: 'TEST must verify REQ', severity: 'warning', evaluate: testMustVerifyReq, domain: ['TEST'] },
|
|
1449
1550
|
{ id: 'R-15', name: 'FCHAIN completeness', severity: 'warning', evaluate: fchainCompleteness, domain: ['FCHAIN'] },
|
|
1450
1551
|
{ id: 'R-16', name: 'ACTOR must have io', severity: 'warning', evaluate: actorMustHaveTrace, domain: ['ACTOR'] },
|
|
@@ -1454,12 +1555,14 @@ export const V3_RULES = [
|
|
|
1454
1555
|
// Als ['FUNC'] deklariert erhoehte sie den Zaehler einer Grundgesamtheit, zu der ihre
|
|
1455
1556
|
// Verstoesse nicht gehoerten (bis zu 2 Legs je FLOW gegen einen FUNC-Nenner).
|
|
1456
1557
|
{ id: 'R-10', name: 'FLOW completeness', severity: 'warning', evaluate: flowCompleteness, domain: ['FLOW'] },
|
|
1558
|
+
// Obergrenze zu R-10s Untergrenze — disjunkt, deshalb zwei IDs (CR-SM-307).
|
|
1559
|
+
{ id: 'IO-02', name: 'FLOW single producer', severity: 'error', evaluate: flowSingleProducer, domain: ['FLOW'] },
|
|
1457
1560
|
{ id: 'R-12', name: 'No circular dependencies', severity: 'warning', evaluate: noDirectCircular, domain: ['FUNC'] },
|
|
1458
1561
|
{ id: 'R-18', name: 'Valid trace pattern', severity: 'error', evaluate: validTracePattern, domain: ['all'] },
|
|
1459
1562
|
{ id: 'R-19', name: 'Runnable TEST binding', severity: 'warning', evaluate: testMustHaveRunnableBinding, domain: ['TEST'] },
|
|
1460
1563
|
{ id: 'R-29', name: 'Test file exclusivity', severity: 'error', evaluate: testFileExclusivity, domain: ['TEST'] },
|
|
1461
1564
|
{ id: 'R-20', name: 'FUNC realRef binding', severity: 'warning', evaluate: funcMustHaveCodeBinding, domain: ['FUNC'] },
|
|
1462
|
-
{ id: 'R-21', name: 'FUNC↔FUNC
|
|
1565
|
+
{ id: 'R-21', name: 'FUNC↔FUNC handover needs a shared chain and an integration test', severity: 'warning', evaluate: fchainMustHaveIntegrationTest, domain: ['FCHAIN', 'FUNC'] },
|
|
1463
1566
|
{ id: 'R-22', name: 'FUNC must be allocated to MOD', severity: 'warning', evaluate: funcMustBeAllocated, domain: ['FUNC'] },
|
|
1464
1567
|
{ id: 'R-23', name: 'MOD must have allocated FUNC', severity: 'warning', evaluate: modMustHaveAllocatedFunc, domain: ['MOD'] },
|
|
1465
1568
|
{ id: 'R-26', name: 'SCHEMA must have realRef', severity: 'warning', evaluate: schemaMustHaveSchemaRef, domain: ['SCHEMA'] },
|
|
@@ -1467,6 +1570,7 @@ export const V3_RULES = [
|
|
|
1467
1570
|
{ id: 'RD-02', name: 'Decomposition consistency', severity: 'warning', evaluate: decompositionConsistency, domain: ['REQ'] },
|
|
1468
1571
|
{ id: 'RD-03', name: 'No premature decomposition', severity: 'info', evaluate: noPrematureDecomposition, domain: ['REQ'] },
|
|
1469
1572
|
{ id: 'RD-04', name: 'Decomposition breadth', severity: 'warning', evaluate: decompositionBreadth, domain: ['FUNC', 'MOD', 'SYS'] },
|
|
1573
|
+
{ id: 'RD-05', name: 'Decomposition too narrow', severity: 'warning', evaluate: decompositionNarrow, domain: ['FUNC', 'MOD', 'SYS'] },
|
|
1470
1574
|
{ id: 'MS-01', name: 'Milestone empty scope', severity: 'warning', evaluate: msEmptyScope, domain: ['MS'] },
|
|
1471
1575
|
{ id: 'MS-02', name: 'Milestone dangling dependency', severity: 'error', evaluate: msDanglingDependency, domain: ['MS'] },
|
|
1472
1576
|
{ id: 'R-30', name: 'FUNC leaf must belong to a function chain', severity: 'warning', evaluate: funcMustBeInEffectChain, domain: ['FUNC'] },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sigloch/contracts",
|
|
3
|
-
"version": "10.
|
|
3
|
+
"version": "10.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -30,7 +30,8 @@
|
|
|
30
30
|
"grammar:snapshot": "SE_WRITE_GRAMMAR_SNAPSHOT=1 vitest run tests/unit/se-grammar-invariant.test.ts",
|
|
31
31
|
"prepublishOnly": "npm run build && npm run test",
|
|
32
32
|
"grammar:measure": "SE_MEASURE=1 vitest run tests/unit/se-grammar-measure.test.ts",
|
|
33
|
-
"report:silence": "node scripts/rules-silence-report.mjs"
|
|
33
|
+
"report:silence": "node scripts/rules-silence-report.mjs",
|
|
34
|
+
"report:bestand": "node scripts/bestand-report.mjs"
|
|
34
35
|
},
|
|
35
36
|
"dependencies": {
|
|
36
37
|
"zod": "^4.3.6"
|