@sigloch/contracts 9.1.0 → 10.1.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/ao-rules.d.ts +12 -12
- package/dist/se/ao-rules.js +88 -225
- package/dist/se/conformance-rules.d.ts +12 -0
- package/dist/se/conformance-rules.js +6 -6
- package/dist/se/cr-quality-rules.js +75 -58
- package/dist/se/evaluate-all.d.ts +4 -2
- package/dist/se/evaluate-all.js +61 -24
- package/dist/se/fchain-quality-rules.d.ts +0 -1
- package/dist/se/fchain-quality-rules.js +0 -44
- package/dist/se/flat-graph.d.ts +9 -1
- package/dist/se/flat-graph.js +15 -6
- package/dist/se/grammar-snapshot.d.ts +9 -5
- package/dist/se/grammar-snapshot.js +250 -15
- package/dist/se/index.d.ts +6 -3
- package/dist/se/index.js +14 -3
- package/dist/se/meta-model.d.ts +47 -0
- package/dist/se/meta-model.js +52 -5
- package/dist/se/metric-rules.d.ts +20 -1
- package/dist/se/metric-rules.js +97 -58
- package/dist/se/module-crossings.d.ts +102 -0
- package/dist/se/module-crossings.js +196 -0
- package/dist/se/near-duplicate-rules.d.ts +16 -24
- package/dist/se/near-duplicate-rules.js +21 -92
- package/dist/se/ontology.d.ts +0 -22
- package/dist/se/ontology.js +0 -2
- package/dist/se/policy.d.ts +6 -0
- package/dist/se/policy.js +53 -2
- package/dist/se/quality-rules.d.ts +18 -0
- package/dist/se/quality-rules.js +51 -9
- package/dist/se/readiness.d.ts +25 -3
- package/dist/se/readiness.js +39 -21
- package/dist/se/rule-help.d.ts +52 -0
- package/dist/se/rule-help.js +342 -0
- package/dist/se/rules.d.ts +6 -0
- package/dist/se/rules.js +303 -125
- package/dist/se/schema-quality-rules.d.ts +0 -1
- package/dist/se/schema-quality-rules.js +7 -28
- package/dist/se/similarity.d.ts +61 -0
- package/dist/se/similarity.js +116 -0
- package/package.json +4 -2
package/dist/se/rules.js
CHANGED
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { z } from 'zod/v4';
|
|
7
7
|
import { ElementType, TraceType, TestRefsSchema, RealRefSchema } from './ontology.js';
|
|
8
|
-
import {
|
|
8
|
+
import { traceRejection, BOUNDED_PATTERNS, REQUIRED_PATTERNS, maxOccurs } from './meta-model.js';
|
|
9
9
|
import { indexOf } from './graph-index.js';
|
|
10
|
+
import { crossingContractCount, subtreeFuncs } from './module-crossings.js';
|
|
10
11
|
export const RuleSeverity = z.enum(['error', 'warning', 'info']);
|
|
11
12
|
/** Candidate target for resolving a violation (e.g. a REQ to satisfy, a TEST to link). */
|
|
12
13
|
export const ViolationCandidate = z.object({
|
|
@@ -25,6 +26,31 @@ export const ViolationContext = z.object({
|
|
|
25
26
|
})).optional(),
|
|
26
27
|
parent_module: z.string().optional(),
|
|
27
28
|
current_description: z.string().optional(),
|
|
29
|
+
/**
|
|
30
|
+
* CR-SM-304: die WEITEREN Elemente, die derselbe Befund betrifft — heute gesetzt von CR-R03,
|
|
31
|
+
* das einen Befund je Konflikt-MENGE meldet und ihn am kanonisch ersten Ziel verankert. Ohne
|
|
32
|
+
* dieses Feld verschwanden die uebrigen Ziele lautlos: der Leser erfuhr von genau einem der
|
|
33
|
+
* Knoten, an denen sich zwei offene CRs begegnen. Ein Befund darf zusammenfassen, aber nichts
|
|
34
|
+
* unterschlagen.
|
|
35
|
+
*/
|
|
36
|
+
also_affects: z.array(z.string()).optional(),
|
|
37
|
+
/**
|
|
38
|
+
* CR-SM-288: die gemessene Zahl der Regel an diesem Element — und das Budget, gegen das sie
|
|
39
|
+
* geschwellt wurde. Zusammen ergeben sie die **Verstoßmasse** `d = max(0, value - threshold)`,
|
|
40
|
+
* die CR-SM-287 lexikographisch rankt. Ohne diese zwei Felder muesste ein Ranker den
|
|
41
|
+
* Meldungstext parsen; CR-SM-280 hat genau diese Texte umgeschrieben, ohne dass sich ein
|
|
42
|
+
* Urteil aenderte.
|
|
43
|
+
*
|
|
44
|
+
* `threshold` ist der **groesste Wert, der noch INNERHALB des Budgets liegt** — nicht der
|
|
45
|
+
* Vergleichswert der Regel. Nur so gilt ueber alle Regeln einheitlich `d > 0 <=> warning`,
|
|
46
|
+
* obwohl die einen bei `>` und die anderen bei `>=` melden (RD-04/MT-01 exklusiv,
|
|
47
|
+
* BW-02/CR-01/MT-02 inklusiv). Ein `info`-Befund innerhalb des Budgets hat damit `d = 0`.
|
|
48
|
+
*
|
|
49
|
+
* Nur die fuenf messenden Regeln setzen sie (RD-04, BW-02, CR-01, MT-01, MT-02); eine Regel
|
|
50
|
+
* ohne Zahl laesst sie weg, statt eine zu erfinden.
|
|
51
|
+
*/
|
|
52
|
+
value: z.number().optional(),
|
|
53
|
+
threshold: z.number().optional(),
|
|
28
54
|
});
|
|
29
55
|
export const RuleViolation = z.object({
|
|
30
56
|
rule_id: z.string(),
|
|
@@ -206,36 +232,6 @@ function funcMustSatisfyReq(graph) {
|
|
|
206
232
|
// ---------------------------------------------------------------------------
|
|
207
233
|
// R-03: ASIL isolation
|
|
208
234
|
// ---------------------------------------------------------------------------
|
|
209
|
-
function asilIsolation(graph) {
|
|
210
|
-
const idx = indexOf(graph);
|
|
211
|
-
const violations = [];
|
|
212
|
-
const modules = idx.elementsOfType('MOD');
|
|
213
|
-
for (const mod of modules) {
|
|
214
|
-
const allocated = graph.traces
|
|
215
|
-
.filter(t => t.target === mod.id && t.type === 'allocate')
|
|
216
|
-
.map(t => idx.byId.get(t.source))
|
|
217
|
-
.filter((e) => !!e);
|
|
218
|
-
const hasD = allocated.some(e => e.asil === 'D');
|
|
219
|
-
const hasQM = allocated.some(e => e.asil === 'QM');
|
|
220
|
-
if (hasD && hasQM) {
|
|
221
|
-
const dFuncs = allocated.filter(e => e.asil === 'D');
|
|
222
|
-
const qmFuncs = allocated.filter(e => e.asil === 'QM');
|
|
223
|
-
violations.push({
|
|
224
|
-
rule_id: 'R-03',
|
|
225
|
-
severity: 'error',
|
|
226
|
-
element_id: mod.id,
|
|
227
|
-
message: `${mod.id} mixes ASIL-D and ASIL-QM functions`,
|
|
228
|
-
fix_hint: 'Separate ASIL-D functions into isolated module',
|
|
229
|
-
context: {
|
|
230
|
-
element_type: mod.type,
|
|
231
|
-
element_name: mod.name,
|
|
232
|
-
candidate_targets: toCandidates([...dFuncs, ...qmFuncs]),
|
|
233
|
-
},
|
|
234
|
-
});
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
return violations;
|
|
238
|
-
}
|
|
239
235
|
// ---------------------------------------------------------------------------
|
|
240
236
|
// R-04: Modulgroesse GEGEN Kreuzungen (nicht „max module size")
|
|
241
237
|
//
|
|
@@ -245,6 +241,13 @@ function asilIsolation(graph) {
|
|
|
245
241
|
// Urteilsschwelle, die sich weder per grep noch aus dem Regelnamen ablesen laesst, ist nicht
|
|
246
242
|
// ueberpruefbar. Jetzt sind es `policy.moduleSize.{coupled,large,crossings}`.
|
|
247
243
|
// `null` → messen, nicht urteilen: die Regel schweigt.
|
|
244
|
+
//
|
|
245
|
+
// CR-SM-276: „Kreuzung" heisst hier dasselbe wie bei CR-01 — ein Vertrag auf dem io-Pfad
|
|
246
|
+
// `FUNC -io-> FLOW -io-> FUNC` UEBER die Modulgrenze (`module-crossings.ts`). Vorher zaehlte
|
|
247
|
+
// die Regel jede io-Kante einer Modul-FUNC „mit einem Endpunkt ausserhalb der FUNC-Menge" —
|
|
248
|
+
// FLOWs liegen nie in dieser Menge, also zaehlte auch jeder modulINTERNE Fluss mit. Das war der
|
|
249
|
+
// io-Grad des Moduls und fuer jedes Modul mit Datenfluss trivial > `crossings`; damit war R-04
|
|
250
|
+
// faktisch wieder „max module size", der Name, den CR-SM-236 gerade verworfen hatte.
|
|
248
251
|
// ---------------------------------------------------------------------------
|
|
249
252
|
function maxModuleSize(graph, policy) {
|
|
250
253
|
const idx = indexOf(graph);
|
|
@@ -254,25 +257,24 @@ function maxModuleSize(graph, policy) {
|
|
|
254
257
|
return violations;
|
|
255
258
|
const modules = idx.elementsOfType('MOD');
|
|
256
259
|
for (const mod of modules) {
|
|
257
|
-
|
|
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)];
|
|
258
266
|
const allocated = allocatedIds.map(id => idx.byId.get(id)).filter((e) => !!e);
|
|
259
267
|
const funcCount = allocated.length;
|
|
260
268
|
if (funcCount <= steps.coupled)
|
|
261
269
|
continue;
|
|
262
|
-
//
|
|
263
|
-
const
|
|
264
|
-
// CR-SM-264: nur die io-Kanten, nicht alle Traces je MOD.
|
|
265
|
-
const crossings = idx.tracesOfType('io').filter(t => {
|
|
266
|
-
const srcIn = funcIds.has(t.source);
|
|
267
|
-
const tgtIn = funcIds.has(t.target);
|
|
268
|
-
return (srcIn && !tgtIn) || (!srcIn && tgtIn);
|
|
269
|
-
}).length;
|
|
270
|
+
// Verschiedene Vertraege (SCHEMA) auf io-Pfaden ueber DIESEN Modulrand — CR-SM-276.
|
|
271
|
+
const crossings = crossingContractCount(graph, mod.id);
|
|
270
272
|
if (funcCount > steps.large && crossings === 0) {
|
|
271
273
|
violations.push({
|
|
272
274
|
rule_id: 'R-04',
|
|
273
275
|
severity: 'info',
|
|
274
276
|
element_id: mod.id,
|
|
275
|
-
message: `${mod.id} has ${funcCount} functions but 0
|
|
277
|
+
message: `${mod.id} has ${funcCount} functions but 0 contracts crossing its module boundary (cohesive, just large)`,
|
|
276
278
|
fix_hint: `Size alone is not the finding: > ${steps.large} functions without crossing flows is cohesive. Split only if crossings appear`,
|
|
277
279
|
context: { element_type: mod.type, element_name: mod.name, candidate_targets: toCandidates(allocated) },
|
|
278
280
|
});
|
|
@@ -282,7 +284,7 @@ function maxModuleSize(graph, policy) {
|
|
|
282
284
|
rule_id: 'R-04',
|
|
283
285
|
severity: 'warning',
|
|
284
286
|
element_id: mod.id,
|
|
285
|
-
message: `${mod.id} has ${funcCount} functions and ${crossings} crossing
|
|
287
|
+
message: `${mod.id} has ${funcCount} functions and ${crossings} distinct contract(s) crossing its module boundary (split recommended)`,
|
|
286
288
|
fix_hint: `Split module into smaller units to reduce coupling (> ${steps.large} functions AND crossing flows)`,
|
|
287
289
|
context: { element_type: mod.type, element_name: mod.name, candidate_targets: toCandidates(allocated) },
|
|
288
290
|
});
|
|
@@ -292,7 +294,7 @@ function maxModuleSize(graph, policy) {
|
|
|
292
294
|
rule_id: 'R-04',
|
|
293
295
|
severity: 'warning',
|
|
294
296
|
element_id: mod.id,
|
|
295
|
-
message: `${mod.id} has ${funcCount} functions and ${crossings} crossing
|
|
297
|
+
message: `${mod.id} has ${funcCount} functions and ${crossings} distinct contract(s) crossing its module boundary (high coupling)`,
|
|
296
298
|
fix_hint: 'Reduce crossing flows or split module',
|
|
297
299
|
context: { element_type: mod.type, element_name: mod.name, candidate_targets: toCandidates(allocated) },
|
|
298
300
|
});
|
|
@@ -410,25 +412,81 @@ const CIRCULAR_TRACE_TYPES = new Set(['compose', 'allocate', 'relation']);
|
|
|
410
412
|
function noDirectCircular(graph) {
|
|
411
413
|
const idx = indexOf(graph);
|
|
412
414
|
const violations = [];
|
|
413
|
-
//
|
|
414
|
-
//
|
|
415
|
-
//
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
415
|
+
// CR-SM-285: ECHTE Zyklenerkennung statt Rueckkanten-Test.
|
|
416
|
+
//
|
|
417
|
+
// Bis hierher pruefte die Regel nur, ob eine Kante DIREKT zurueckläuft (A->B und B->A). Ein
|
|
418
|
+
// Dreierzyklus A->B->C->A ging vollstaendig durch das Gate, mit null Fehlern — bei einem
|
|
419
|
+
// Regelnamen, der Azyklizitaet verspricht. Das ist die gefaehrlichere Haelfte: ein Absturz
|
|
420
|
+
// meldet sich, eine Pruefung, die ihren Gegenstand nicht erreicht, BESTAETIGT.
|
|
421
|
+
//
|
|
422
|
+
// Warum es zaehlt: jeder Rollup der Familie setzt Azyklizitaet voraus — `chainOf`/`funcChainOf`
|
|
423
|
+
// in `module-crossings.ts` (CR-SM-282/-283) tragen `seen`-Guards gegen genau diese Endlosschleife,
|
|
424
|
+
// `moduleMetrics`, die RD-04-Breitenzaehlung und gves Container-Sicht ebenso. Unter einem Zyklus
|
|
425
|
+
// ist ein Rollup nicht falsch, sondern UNDEFINIERT. Das R-18-Bein aus CR-SM-283 deckt es NICHT:
|
|
426
|
+
// bei A->B->C->A hat jeder Knoten genau EINEN compose-Elternteil, die Baum-Eigenschaft ist formal
|
|
427
|
+
// erfuellt und die Struktur trotzdem kein Baum.
|
|
428
|
+
//
|
|
429
|
+
// Gemessen ueber neun Familiengraphen: 0 Zyklen jeder Laenge, also 0 -> 0 Befunde. Die Schaerfung
|
|
430
|
+
// laesst keinen bestehenden Graphen fallen; sie schliesst eine Luecke, bevor sie jemand findet.
|
|
431
|
+
//
|
|
432
|
+
// EIN Befund je Zyklus, nicht je Kante (Klasse CR-SM-242): bei einem Ring der Laenge n sind nicht
|
|
433
|
+
// n Kanten falsch, es ist EINE Struktur. Verankert am kleinsten Knoten des Rings — determiniert,
|
|
434
|
+
// unabhaengig davon, wo die DFS eingestiegen ist (Gate 5).
|
|
435
|
+
for (const type of [...CIRCULAR_TRACE_TYPES].sort()) {
|
|
436
|
+
const adj = new Map();
|
|
437
|
+
for (const t of graph.traces) {
|
|
438
|
+
if (t.type !== type)
|
|
439
|
+
continue;
|
|
440
|
+
const list = adj.get(t.source);
|
|
441
|
+
if (list)
|
|
442
|
+
list.push(t.target);
|
|
443
|
+
else
|
|
444
|
+
adj.set(t.source, [t.target]);
|
|
445
|
+
}
|
|
446
|
+
for (const list of adj.values())
|
|
447
|
+
list.sort();
|
|
448
|
+
const WHITE = 0, GREY = 1, BLACK = 2;
|
|
449
|
+
const color = new Map();
|
|
450
|
+
const stack = [];
|
|
451
|
+
const found = new Map(); // kanonischer Schluessel -> Ring in Reihenfolge
|
|
452
|
+
const visit = (u) => {
|
|
453
|
+
color.set(u, GREY);
|
|
454
|
+
stack.push(u);
|
|
455
|
+
for (const v of adj.get(u) ?? []) {
|
|
456
|
+
if (color.get(v) === GREY) {
|
|
457
|
+
const ring = stack.slice(stack.indexOf(v));
|
|
458
|
+
// Kanonisch: am kleinsten Knoten rotiert, damit derselbe Ring aus jedem Einstieg
|
|
459
|
+
// denselben Schluessel und dieselbe Meldung ergibt.
|
|
460
|
+
let min = 0;
|
|
461
|
+
for (let i = 1; i < ring.length; i++)
|
|
462
|
+
if (ring[i] < ring[min])
|
|
463
|
+
min = i;
|
|
464
|
+
const rotated = [...ring.slice(min), ...ring.slice(0, min)];
|
|
465
|
+
found.set(rotated.join('|'), rotated);
|
|
466
|
+
}
|
|
467
|
+
else if (color.get(v) === undefined) {
|
|
468
|
+
visit(v);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
color.set(u, BLACK);
|
|
472
|
+
stack.pop();
|
|
473
|
+
};
|
|
474
|
+
for (const u of [...adj.keys()].sort())
|
|
475
|
+
if (color.get(u) === undefined)
|
|
476
|
+
visit(u);
|
|
477
|
+
for (const ring of [...found.keys()].sort().map(k => found.get(k))) {
|
|
478
|
+
const el = idx.byId.get(ring[0]);
|
|
479
|
+
violations.push({
|
|
480
|
+
rule_id: 'R-12',
|
|
481
|
+
severity: 'warning',
|
|
482
|
+
element_id: ring[0],
|
|
483
|
+
message: ring.length === 2
|
|
484
|
+
? `Circular ${type} between ${ring[0]} and ${ring[1]}`
|
|
485
|
+
: `Circular ${type} over ${ring.length} elements: ${[...ring, ring[0]].join(' -> ')}`,
|
|
486
|
+
fix_hint: `Break the ${type} ring — every rollup (module boundary, decomposition breadth, container view) is undefined while it exists`,
|
|
487
|
+
context: { element_type: el?.type, element_name: el?.name },
|
|
488
|
+
});
|
|
489
|
+
}
|
|
432
490
|
}
|
|
433
491
|
return violations;
|
|
434
492
|
}
|
|
@@ -510,16 +568,46 @@ function noPrematureDecomposition(graph) {
|
|
|
510
568
|
// RD-04: Decomposition breadth (CR-SM-221)
|
|
511
569
|
// ---------------------------------------------------------------------------
|
|
512
570
|
/**
|
|
513
|
-
* Max children on one decomposition level. The 7
|
|
571
|
+
* Max children on one decomposition level. The 7±2 convention existed only as
|
|
514
572
|
* prose ("darüber func-of-func"); below 7 is a guideline, not a violation, so only
|
|
515
573
|
* the upper bound is a rule.
|
|
516
574
|
*
|
|
517
575
|
* Counted per (parent, kind) — a MOD that both holds 12 FUNCs and composes 12
|
|
518
576
|
* sub-MODs has two breadth problems, not one. (The spike reference merged both into
|
|
519
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).
|
|
520
594
|
*/
|
|
521
|
-
|
|
522
|
-
|
|
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 [];
|
|
523
611
|
const idx = indexOf(graph);
|
|
524
612
|
// CR-SM-264: `typeOf` und `byId` baute diese Regel je Aufruf selbst — der Index hat beide.
|
|
525
613
|
const typeOf = idx;
|
|
@@ -539,48 +627,36 @@ function decompositionBreadth(graph) {
|
|
|
539
627
|
if (t.type === 'compose' && src === 'FUNC' && tgt === 'FUNC') {
|
|
540
628
|
bump(t.source, 'sub-FUNC');
|
|
541
629
|
}
|
|
542
|
-
else if (t.type === 'allocate' && tgt === 'MOD' && src === 'FUNC') {
|
|
543
|
-
bump(t.target, 'allocated FUNC');
|
|
544
|
-
}
|
|
545
630
|
else if (t.type === 'compose' && (src === 'SYS' || src === 'MOD') && tgt === 'MOD') {
|
|
546
631
|
bump(t.source, 'sub-MOD');
|
|
547
632
|
}
|
|
548
633
|
}
|
|
634
|
+
// CR-SM-282: der FUNC-Wurzelwald als eigene Blackbox, verankert am SYS-Knoten.
|
|
635
|
+
const composedFuncs = new Set(graph.traces
|
|
636
|
+
.filter(t => t.type === 'compose' && typeOf.typeOf(t.source) === 'FUNC' && typeOf.typeOf(t.target) === 'FUNC')
|
|
637
|
+
.map(t => t.target));
|
|
638
|
+
const systems = idx.elementsOfType('SYS');
|
|
639
|
+
if (systems.length === 1) {
|
|
640
|
+
const rootFuncs = idx.elementsOfType('FUNC').filter(f => !composedFuncs.has(f.id)).length;
|
|
641
|
+
if (rootFuncs > 0)
|
|
642
|
+
counts.set(`${systems[0].id}\u0000root FUNC`, { parentId: systems[0].id, kind: 'root FUNC', n: rootFuncs });
|
|
643
|
+
}
|
|
549
644
|
return [...counts.values()]
|
|
550
|
-
.filter(c => c.n >
|
|
645
|
+
.filter(c => c.n > max.warning)
|
|
551
646
|
.map(c => {
|
|
552
647
|
const parent = byId.get(c.parentId);
|
|
553
648
|
return {
|
|
554
649
|
rule_id: 'RD-04',
|
|
555
650
|
severity: 'warning',
|
|
556
651
|
element_id: c.parentId,
|
|
557
|
-
message: `${c.parentId} has ${c.n} ${c.kind} children on one level (>${
|
|
652
|
+
message: `${c.parentId} has ${c.n} ${c.kind} children on one level (>${max.warning})`,
|
|
558
653
|
fix_hint: 'Introduce an intermediate level (func-of-func / sub-MOD)',
|
|
559
|
-
|
|
654
|
+
// CR-SM-288: `> max.warning` ist exklusiv, also ist `max.warning` selbst noch im Budget.
|
|
655
|
+
context: { element_type: parent?.type, element_name: parent?.name, value: c.n, threshold: max.warning },
|
|
560
656
|
};
|
|
561
657
|
});
|
|
562
658
|
}
|
|
563
659
|
// ---------------------------------------------------------------------------
|
|
564
|
-
// R-14: UC must have at least 1 compose trace (→ FCHAIN or REQ) (CR-117)
|
|
565
|
-
// ---------------------------------------------------------------------------
|
|
566
|
-
function ucMustHaveCompose(graph) {
|
|
567
|
-
const idx = indexOf(graph);
|
|
568
|
-
const ucs = idx.elementsOfType('UC');
|
|
569
|
-
return ucs
|
|
570
|
-
.filter(uc => idx.out(uc.id, 'compose').length === 0)
|
|
571
|
-
.map(uc => ({
|
|
572
|
-
rule_id: 'R-14',
|
|
573
|
-
severity: 'warning',
|
|
574
|
-
element_id: uc.id,
|
|
575
|
-
message: `${uc.id} has no compose traces (no scenarios or requirements)`,
|
|
576
|
-
fix_hint: 'Add FCHAIN or REQ via compose trace',
|
|
577
|
-
context: {
|
|
578
|
-
element_type: uc.type,
|
|
579
|
-
element_name: uc.name,
|
|
580
|
-
},
|
|
581
|
-
}));
|
|
582
|
-
}
|
|
583
|
-
// ---------------------------------------------------------------------------
|
|
584
660
|
// R-15: FCHAIN completeness — a chain needs functions BELOW it and a use case
|
|
585
661
|
// ABOVE it (CR-117, second leg CR-SM-249).
|
|
586
662
|
//
|
|
@@ -663,7 +739,7 @@ function sysMustHaveCompose(graph) {
|
|
|
663
739
|
severity: 'warning',
|
|
664
740
|
element_id: sys.id,
|
|
665
741
|
message: `${sys.id} has no compose traces (empty system)`,
|
|
666
|
-
fix_hint: 'Add
|
|
742
|
+
fix_hint: 'Add at least one compose trace — the system decomposes into nothing',
|
|
667
743
|
context: {
|
|
668
744
|
element_type: sys.type,
|
|
669
745
|
element_name: sys.name,
|
|
@@ -729,30 +805,125 @@ function validTracePattern(graph) {
|
|
|
729
805
|
// Schlupfloch geworden — eine Kante mit `category: 'audit'` haette die Matrix komplett
|
|
730
806
|
// umgangen. Das Attribut existiert nicht mehr (ontology.ts), hier faellt der Leser mit.
|
|
731
807
|
return graph.traces
|
|
732
|
-
.
|
|
808
|
+
.flatMap((t) => {
|
|
733
809
|
const src = typeById.get(t.source);
|
|
734
810
|
const tgt = typeById.get(t.target);
|
|
735
811
|
if (!src || !tgt)
|
|
736
|
-
return
|
|
812
|
+
return []; // dangling endpoint → R-08, not R-18
|
|
737
813
|
// CR-SM-266 B: die where-Patterns lesen die Kinds des betroffenen Endes mit.
|
|
738
|
-
|
|
814
|
+
// CR-SM-280: und der GRUND der Ablehnung kommt mit heraus, statt zu `false` zu kollabieren.
|
|
815
|
+
const why = traceRejection({
|
|
739
816
|
source: src, target: tgt, type: t.type, label: t.label,
|
|
740
817
|
sourceKinds: idx.byId.get(t.source)?.kinds,
|
|
741
818
|
targetKinds: idx.byId.get(t.target)?.kinds,
|
|
742
819
|
});
|
|
820
|
+
if (!why)
|
|
821
|
+
return [];
|
|
822
|
+
const { detail, hint } = explainRejection(why, t, src, tgt);
|
|
823
|
+
return [{
|
|
824
|
+
rule_id: 'R-18',
|
|
825
|
+
severity: 'error',
|
|
826
|
+
element_id: t.source,
|
|
827
|
+
message: `Invalid trace ${t.source} -${t.type}-> ${t.target}: ${detail}`,
|
|
828
|
+
fix_hint: hint,
|
|
829
|
+
}];
|
|
743
830
|
})
|
|
744
|
-
.
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
831
|
+
.concat(cardinalityViolations(graph))
|
|
832
|
+
.concat(singleComposeParent(graph));
|
|
833
|
+
}
|
|
834
|
+
/**
|
|
835
|
+
* CR-SM-280: aus dem Ablehnungsgrund den Satz bauen, den der Leser braucht.
|
|
836
|
+
*
|
|
837
|
+
* Der `no-pattern`-Zweig traegt den Wortlaut von vorher UNVERAENDERT — dort war die alte
|
|
838
|
+
* Meldung richtig. Nur die beiden `where`-Zweige sind neu, und sie unterscheiden zwei
|
|
839
|
+
* Ursachen, die vorher zu einer verschmolzen waren: `kinds` fehlt (die BQ-07-Migration) vs.
|
|
840
|
+
* `kinds` steht da und passt nicht (Kante heben oder Kind korrigieren). Wer das nicht
|
|
841
|
+
* unterscheiden kann, sucht am falschen Ende.
|
|
842
|
+
*/
|
|
843
|
+
function explainRejection(why, t, src, tgt) {
|
|
844
|
+
if (why.reason === 'no-pattern') {
|
|
845
|
+
return {
|
|
846
|
+
detail: `${src} → ${tgt} is not a valid ${t.type} pattern`,
|
|
847
|
+
hint: 'Use a trace type whose TRACE_PATTERNS allows this source/target element-type pair',
|
|
848
|
+
};
|
|
849
|
+
}
|
|
850
|
+
const { where } = why.pattern;
|
|
851
|
+
// Der Knoten wird NUR ueber seine Id benannt, nicht zusaetzlich ueber seinen Typ: der
|
|
852
|
+
// ElementUid-Kanon (`<TYPE>-<slug>`, ONTOLOGY 8.0.0) traegt den Typ bereits im Namen, und
|
|
853
|
+
// "REQ REQ-f2-riegel" liest sich wie ein Fehler im Werkzeug.
|
|
854
|
+
const end = where.on === 'source' ? t.source : t.target;
|
|
855
|
+
const allowed = [...where.allowed].sort().join(', ');
|
|
856
|
+
const admits = `the ${src} → ${tgt} ${t.type} pattern admits only ${where.field} {${allowed}}`;
|
|
857
|
+
return why.reason === 'kinds-undeclared'
|
|
858
|
+
? {
|
|
859
|
+
detail: `${admits}, and ${end} declares no ${where.field}`,
|
|
860
|
+
hint: `Declare '${where.field}' on ${end} — undeclared is not "any" (CR-SM-266 B); it must be a subset of {${allowed}}`,
|
|
861
|
+
}
|
|
862
|
+
: {
|
|
863
|
+
detail: `${admits}, but ${end} declares {${[...why.declared].sort().join(', ')}}`,
|
|
864
|
+
hint: `Correct '${where.field}' on ${end}, or use a source type whose ${t.type} pattern admits them (FCHAIN carries every kind)`,
|
|
865
|
+
};
|
|
866
|
+
}
|
|
867
|
+
/**
|
|
868
|
+
* CR-SM-283 — das VIERTE BEIN von R-18: die `compose`-Baeume sind Baeume.
|
|
869
|
+
*
|
|
870
|
+
* `TRACE_PATTERNS` begrenzt heute nur die KINDER einer Kante, nie die ELTERN
|
|
871
|
+
* (`se:top-level`: „the cardinalities cap children, not parents"). Ein Element kann damit zwei
|
|
872
|
+
* `compose`-Elternteile bekommen, und `graph_mutate` legt so eine Kante anstandslos an.
|
|
873
|
+
*
|
|
874
|
+
* Warum das hart sein muss, obwohl es NIE vorgekommen ist (0 Verstoesse ueber fuenf
|
|
875
|
+
* Familiengraphen): jeder Rollup dieser Familie ist unter Verletzung nicht falsch, sondern
|
|
876
|
+
* UNDEFINIERT — `moduleCrossings.byModule`/`byFunc` (CR-SM-282/-283), die Breiten-Zaehlung von
|
|
877
|
+
* RD-04, die Container-Sicht von gve. Ein mehrdeutiges Rollup liefert keine schlechtere Zahl,
|
|
878
|
+
* sondern keine. Das ist der Unterschied zu einer Regel, die „etwas prueft, das nie vorkommt".
|
|
879
|
+
*
|
|
880
|
+
* Kein eigener Regel-Eintrag, aus demselben Grund wie beim zweiten und dritten Bein: die
|
|
881
|
+
* Aussage ist dieselbe — „diese Kante ist nach dem Meta-Modell nicht zulaessig" —, und der
|
|
882
|
+
* naechste Schritt ist derselbe: eine der beiden Kanten weg.
|
|
883
|
+
*
|
|
884
|
+
* EIN Befund je KIND, nicht je ueberzaehliger Kante: bei n Eltern sind nicht n-1 Kanten
|
|
885
|
+
* „falsch", es ist EINE Entscheidung offen (welcher Elternteil), und n-1 Befunde trieben den
|
|
886
|
+
* Zaehler ueber den Nenner-Beitrag (die Fehlmessung aus CR-SM-242).
|
|
887
|
+
*/
|
|
888
|
+
function singleComposeParent(graph) {
|
|
889
|
+
const idx = indexOf(graph);
|
|
890
|
+
const parents = new Map();
|
|
891
|
+
for (const t of graph.traces) {
|
|
892
|
+
if (t.type !== 'compose')
|
|
893
|
+
continue;
|
|
894
|
+
const src = idx.typeOf(t.source);
|
|
895
|
+
const tgt = idx.typeOf(t.target);
|
|
896
|
+
if (!((src === 'FUNC' && tgt === 'FUNC') || (src === 'MOD' && tgt === 'MOD')))
|
|
897
|
+
continue;
|
|
898
|
+
let set = parents.get(t.target);
|
|
899
|
+
if (!set) {
|
|
900
|
+
set = new Set();
|
|
901
|
+
parents.set(t.target, set);
|
|
902
|
+
}
|
|
903
|
+
set.add(t.source);
|
|
904
|
+
}
|
|
905
|
+
return [...parents.entries()]
|
|
906
|
+
.filter(([, ps]) => ps.size > 1)
|
|
907
|
+
// Kanonisch statt in Trace-Reihenfolge — sonst haengt die Befund-Sequenz an der
|
|
908
|
+
// Eingabereihenfolge (Reihenfolgen-Invariante, se-grammar-invariant.test.ts).
|
|
909
|
+
.sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0))
|
|
910
|
+
.map(([child, ps]) => {
|
|
911
|
+
const el = idx.byId.get(child);
|
|
912
|
+
const sorted = [...ps].sort();
|
|
913
|
+
return {
|
|
914
|
+
rule_id: 'R-18',
|
|
915
|
+
severity: 'error',
|
|
916
|
+
element_id: child,
|
|
917
|
+
message: `${child} has ${sorted.length} compose parents (${sorted.join(', ')}): the compose trees must stay trees`,
|
|
918
|
+
fix_hint: `Keep exactly one compose parent for ${child} and delete the other edge(s) — every rollup (module boundary, decomposition breadth, container view) is undefined while there are two`,
|
|
919
|
+
context: { element_type: el?.type, element_name: el?.name },
|
|
920
|
+
};
|
|
921
|
+
});
|
|
753
922
|
}
|
|
754
923
|
/**
|
|
755
924
|
* CR-SM-266b — das ZWEITE BEIN von R-18: die Kardinalitaets-Obergrenzen.
|
|
925
|
+
* CR-SM-271 Teil 2 — das DRITTE: die durchgesetzten Untergrenzen (heute genau
|
|
926
|
+
* `FLOW -relation-> SCHEMA [1..1]`; SC-04 ist damit ersatzlos entfallen).
|
|
756
927
|
*
|
|
757
928
|
* Bewusst dieselbe `rule_id` und keine eigene, nach dem Muster von R-10 (Produzent/Konsument)
|
|
758
929
|
* und R-15 seit CR-SM-249: die Aussage ist dieselbe — "diese Kante ist nach dem Meta-Modell
|
|
@@ -789,6 +960,31 @@ function cardinalityViolations(graph) {
|
|
|
789
960
|
});
|
|
790
961
|
}
|
|
791
962
|
}
|
|
963
|
+
// Untergrenzen (CR-SM-271 Teil 2): ein FLOW ohne SCHEMA ist untypisiert, nicht unfertig —
|
|
964
|
+
// der frueher von SC-04 (warning) gemeldete Fall ist jetzt Grammatik und meldet als error.
|
|
965
|
+
// EIN Befund je Quellknoten, mit candidate_targets im Kontext (wie SC-04 sie trug), damit
|
|
966
|
+
// ein Agent die Kante aus dem Payload heraus ergaenzen kann.
|
|
967
|
+
for (const p of REQUIRED_PATTERNS) {
|
|
968
|
+
const candidates = idx.elementsOfType(p.target);
|
|
969
|
+
for (const el of idx.elementsOfType(p.source)) {
|
|
970
|
+
const matching = idx.out(el.id, p.type).filter(t => idx.typeOf(t.target) === p.target);
|
|
971
|
+
if (matching.length > 0)
|
|
972
|
+
continue;
|
|
973
|
+
violations.push({
|
|
974
|
+
rule_id: 'R-18',
|
|
975
|
+
severity: 'error',
|
|
976
|
+
element_id: el.id,
|
|
977
|
+
message: `${el.id} has no ${p.type} trace to a ${p.target} — the meta-model requires exactly one`,
|
|
978
|
+
fix_hint: `Add a ${p.type} trace from ${el.id} to the ${p.target} that defines its contract` +
|
|
979
|
+
` (create the ${p.target} in the same batch if it does not exist yet)`,
|
|
980
|
+
context: {
|
|
981
|
+
element_type: el.type,
|
|
982
|
+
element_name: el.name,
|
|
983
|
+
candidate_targets: candidates.map(c => ({ id: c.id, type: c.type, name: c.name })),
|
|
984
|
+
},
|
|
985
|
+
});
|
|
986
|
+
}
|
|
987
|
+
}
|
|
792
988
|
return violations;
|
|
793
989
|
}
|
|
794
990
|
// ---------------------------------------------------------------------------
|
|
@@ -1133,30 +1329,15 @@ function schemaMustHaveSchemaRef(graph) {
|
|
|
1133
1329
|
// Symbol is optional on a realRef (a geometry file has none) — presence = a valid
|
|
1134
1330
|
// realRef with a file. RESOLUTION (file on disk) is a consumer/RC concern.
|
|
1135
1331
|
// ---------------------------------------------------------------------------
|
|
1136
|
-
function physicalModMustHaveRealRef(graph) {
|
|
1137
|
-
const idx = indexOf(graph);
|
|
1138
|
-
return idx.elementsOfType('MOD')
|
|
1139
|
-
.filter(e => e.attributes?.kind === 'physical')
|
|
1140
|
-
.filter(e => e.attributes?.concept !== true && e.attributes?.external !== true)
|
|
1141
|
-
.filter(e => !RealRefSchema.safeParse(e.attributes?.realRef).success)
|
|
1142
|
-
.map(mod => ({
|
|
1143
|
-
rule_id: 'R-27',
|
|
1144
|
-
severity: 'warning',
|
|
1145
|
-
element_id: mod.id,
|
|
1146
|
-
message: `${mod.id} is a physical MOD without a valid realRef (CAD/geometry) binding`,
|
|
1147
|
-
fix_hint: 'Add attributes.realRef {file, symbol?, lang?} pointing at the CAD/geometry artefact, or set attributes.concept:true / attributes.external:true',
|
|
1148
|
-
context: { element_type: mod.type, element_name: mod.name },
|
|
1149
|
-
}));
|
|
1150
|
-
}
|
|
1151
1332
|
// ---------------------------------------------------------------------------
|
|
1152
1333
|
// R-28 (Ebenen-Präsenz) ist mit CR-SM-247 ersatzlos entfallen — nicht weil sie falsch war,
|
|
1153
1334
|
// sondern weil sie seit CR-GC-366 doppelt ist. Sie schloss das Vacuous-Complete-Loch (mit 0/1
|
|
1154
1335
|
// FUNC feuert keine Pro-Element-Regel, alles liest "fertig"); beide Hälften trägt heute je eine
|
|
1155
1336
|
// präzisere Regel: kein FLOW → R-31 (io-Verdrahtung, je FUNC statt einmal je Graph) + IO-01,
|
|
1156
|
-
// kein SCHEMA → SC-04 (je FLOW)
|
|
1337
|
+
// kein SCHEMA → damals SC-04 (je FLOW), seit CR-SM-271 das R-18-Untergrenzen-Bein. Probe
|
|
1338
|
+
// (SYS+UC+FCHAIN+3 FUNC, ohne FLOW/SCHEMA): 28 Befunde aus
|
|
1157
1339
|
// 18 Regeln, ohne R-28 bleiben 17 — das Loch bleibt zu. Gemessen feuerte sie 1× / 0× an den
|
|
1158
|
-
// Selbstmodellen, dort neben 14× IO-01 und 10× R-31.
|
|
1159
|
-
// SCHEMA-Absenz rutscht von PDR nach CDR, weil SC-04 erst feuert, wenn FLOWs existieren.
|
|
1340
|
+
// Selbstmodellen, dort neben 14× IO-01 und 10× R-31.
|
|
1160
1341
|
// ---------------------------------------------------------------------------
|
|
1161
1342
|
// ---------------------------------------------------------------------------
|
|
1162
1343
|
// R-30: jedes BLATT gehoert in eine Wirkkette (CR-GC-366, Grundgesamtheit
|
|
@@ -1263,10 +1444,8 @@ export const V3_RULES = [
|
|
|
1263
1444
|
{ id: 'R-02', name: 'FUNC must satisfy REQ', severity: 'warning', evaluate: funcMustSatisfyReq, domain: ['FUNC'] },
|
|
1264
1445
|
// CR-SM-243: domain ist MOD, nicht FUNC — die Regel iteriert Module und meldet am Modul,
|
|
1265
1446
|
// das ASIL-D und QM mischt. Die FUNCs sind der Anlass des Urteils, nicht seine Traeger.
|
|
1266
|
-
{ id: 'R-03', name: 'ASIL isolation', severity: 'error', evaluate: asilIsolation, domain: ['MOD'] },
|
|
1267
1447
|
{ id: 'R-04', name: 'Module size relative to crossing flows', severity: 'warning', evaluate: maxModuleSize, domain: ['MOD'] },
|
|
1268
1448
|
{ id: 'R-05', name: 'TEST must verify REQ', severity: 'warning', evaluate: testMustVerifyReq, domain: ['TEST'] },
|
|
1269
|
-
{ id: 'R-14', name: 'UC must have compose', severity: 'warning', evaluate: ucMustHaveCompose, domain: ['UC'] },
|
|
1270
1449
|
{ id: 'R-15', name: 'FCHAIN completeness', severity: 'warning', evaluate: fchainCompleteness, domain: ['FCHAIN'] },
|
|
1271
1450
|
{ id: 'R-16', name: 'ACTOR must have io', severity: 'warning', evaluate: actorMustHaveTrace, domain: ['ACTOR'] },
|
|
1272
1451
|
{ id: 'R-17', name: 'SYS must have compose', severity: 'warning', evaluate: sysMustHaveCompose, domain: ['SYS'] },
|
|
@@ -1284,7 +1463,6 @@ export const V3_RULES = [
|
|
|
1284
1463
|
{ id: 'R-22', name: 'FUNC must be allocated to MOD', severity: 'warning', evaluate: funcMustBeAllocated, domain: ['FUNC'] },
|
|
1285
1464
|
{ id: 'R-23', name: 'MOD must have allocated FUNC', severity: 'warning', evaluate: modMustHaveAllocatedFunc, domain: ['MOD'] },
|
|
1286
1465
|
{ id: 'R-26', name: 'SCHEMA must have realRef', severity: 'warning', evaluate: schemaMustHaveSchemaRef, domain: ['SCHEMA'] },
|
|
1287
|
-
{ id: 'R-27', name: 'Physical MOD must have realRef', severity: 'warning', evaluate: physicalModMustHaveRealRef, domain: ['MOD'] },
|
|
1288
1466
|
{ id: 'RD-01', name: 'Unresolved requirement', severity: 'warning', evaluate: unresolvedRequirement, domain: ['REQ'] },
|
|
1289
1467
|
{ id: 'RD-02', name: 'Decomposition consistency', severity: 'warning', evaluate: decompositionConsistency, domain: ['REQ'] },
|
|
1290
1468
|
{ id: 'RD-03', name: 'No premature decomposition', severity: 'info', evaluate: noPrematureDecomposition, domain: ['REQ'] },
|
|
@@ -16,6 +16,5 @@ import type { OntologyGraph } from './ontology.js';
|
|
|
16
16
|
import type { RuleDefinition, RuleViolation } from './rules.js';
|
|
17
17
|
import type { MetricPolicy } from './policy.js';
|
|
18
18
|
export declare function sc02IsReferenced(graph: OntologyGraph): RuleViolation[];
|
|
19
|
-
export declare function sc04FlowHasSchema(graph: OntologyGraph): RuleViolation[];
|
|
20
19
|
export declare const SC_RULES: RuleDefinition[];
|
|
21
20
|
export declare function evaluateSCRules(graph: OntologyGraph, policy: MetricPolicy): RuleViolation[];
|
|
@@ -20,40 +20,19 @@ export function sc02IsReferenced(graph) {
|
|
|
20
20
|
}));
|
|
21
21
|
}
|
|
22
22
|
// ---------------------------------------------------------------------------
|
|
23
|
-
// SC-04
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
// rule.
|
|
23
|
+
// SC-04 (FLOW must reference a SCHEMA, CR-SM-226) ist mit CR-SM-271 Teil 2 ersatzlos
|
|
24
|
+
// entfallen: die Untergrenze ist Grammatik geworden (`FLOW -relation-> SCHEMA [1..1]`,
|
|
25
|
+
// meta-model.ts REQUIRED_PATTERNS) und meldet als error aus dem R-18-Kardinalitaets-Bein
|
|
26
|
+
// (rules.ts cardinalityViolations) — Grammatik ersetzt Regel, kein Regel-Loch.
|
|
28
27
|
// ---------------------------------------------------------------------------
|
|
29
|
-
export function sc04FlowHasSchema(graph) {
|
|
30
|
-
const schemas = graph.elements.filter(e => e.type === 'SCHEMA');
|
|
31
|
-
return graph.elements
|
|
32
|
-
.filter(e => e.type === 'FLOW')
|
|
33
|
-
.filter(f => !graph.traces.some(t => t.source === f.id && t.type === 'relation' &&
|
|
34
|
-
graph.elements.some(e => e.id === t.target && e.type === 'SCHEMA')))
|
|
35
|
-
.map(f => ({
|
|
36
|
-
rule_id: 'SC-04',
|
|
37
|
-
severity: 'warning',
|
|
38
|
-
element_id: f.id,
|
|
39
|
-
message: `${f.id} has no SCHEMA binding`,
|
|
40
|
-
fix_hint: 'Link a SCHEMA via relation trace to define this FLOW\'s data contract',
|
|
41
|
-
context: {
|
|
42
|
-
element_type: f.type,
|
|
43
|
-
element_name: f.name,
|
|
44
|
-
candidate_targets: schemas.map(s => ({ id: s.id, type: s.type, name: s.name })),
|
|
45
|
-
},
|
|
46
|
-
}));
|
|
47
|
-
}
|
|
48
28
|
// ---------------------------------------------------------------------------
|
|
49
29
|
// Aggregated array & convenience runner
|
|
50
30
|
// ---------------------------------------------------------------------------
|
|
51
31
|
export const SC_RULES = [
|
|
52
|
-
// Die Asymmetrie ist gewollt (CR-SM-242): SC-02 iteriert ueber SCHEMA
|
|
53
|
-
// benutzt")
|
|
54
|
-
//
|
|
32
|
+
// Die Asymmetrie zur FLOW-Seite ist gewollt (CR-SM-242): SC-02 iteriert ueber SCHEMA
|
|
33
|
+
// („wird dieses SCHEMA benutzt"); die Gegenrichtung je FLOW („hat dieser FLOW einen
|
|
34
|
+
// Datenvertrag") traegt seit CR-SM-271 das R-18-Untergrenzen-Bein.
|
|
55
35
|
{ id: 'SC-02', name: 'Schema referenced by FLOW', severity: 'warning', evaluate: sc02IsReferenced, domain: ['SCHEMA'] },
|
|
56
|
-
{ id: 'SC-04', name: 'FLOW has SCHEMA binding', severity: 'warning', evaluate: sc04FlowHasSchema, domain: ['FLOW'] },
|
|
57
36
|
];
|
|
58
37
|
// CR-SM-236: `policy` wird durchgereicht, auch wo diese Familie heute keine Schwelle hat —
|
|
59
38
|
// ein Sonderweg je Familie waere genau der zweite Pfad, den der Regelsatz verbietet.
|