@sigloch/contracts 6.1.0 → 9.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.js +44 -29
- package/dist/se/conformance-rules.d.ts +29 -0
- package/dist/se/conformance-rules.js +107 -17
- package/dist/se/fchain-quality-rules.js +57 -53
- package/dist/se/flat-graph.d.ts +3 -0
- package/dist/se/flat-graph.js +52 -0
- package/dist/se/format-e-parser.d.ts +14 -2
- package/dist/se/format-e-parser.js +33 -9
- package/dist/se/grammar-snapshot.d.ts +7 -6
- package/dist/se/grammar-snapshot.js +17 -16
- package/dist/se/graph-index.d.ts +56 -0
- package/dist/se/graph-index.js +60 -0
- package/dist/se/index.d.ts +5 -3
- package/dist/se/index.js +5 -3
- package/dist/se/meta-model.d.ts +52 -7
- package/dist/se/meta-model.js +108 -14
- package/dist/se/metric-rules.d.ts +8 -0
- package/dist/se/metric-rules.js +95 -23
- package/dist/se/ontology.d.ts +32 -25
- package/dist/se/ontology.js +56 -11
- package/dist/se/quality-rules.js +19 -5
- package/dist/se/readiness.d.ts +4 -2
- package/dist/se/readiness.js +23 -1
- package/dist/se/rules.d.ts +12 -7
- package/dist/se/rules.js +215 -103
- package/dist/se/uc-quality-rules.js +67 -25
- package/package.json +1 -1
package/dist/se/meta-model.d.ts
CHANGED
|
@@ -3,24 +3,69 @@
|
|
|
3
3
|
* Single source of truth for which ElementType pairs are valid for which TraceType.
|
|
4
4
|
* @sigloch/contracts/se
|
|
5
5
|
*/
|
|
6
|
-
import type { ElementType, TraceType } from './ontology.js';
|
|
6
|
+
import type { ElementType, ReqKind, TraceType } from './ontology.js';
|
|
7
|
+
/**
|
|
8
|
+
* Ein Attribut-Praedikat am Pattern (CR-SM-266 B, REQ-X03).
|
|
9
|
+
*
|
|
10
|
+
* Bis hierher entschied AUSSCHLIESSLICH das Typ-Paar ueber die Gueltigkeit einer Kante. Das
|
|
11
|
+
* liess vier satisfy-Patterns ununterscheidbar nebeneinander stehen: ein FUNKTIONALES REQ
|
|
12
|
+
* konnte von einem MOD oder SYS "erfuellt" werden, ohne dass je eine Wirkkette es trug —
|
|
13
|
+
* strukturell derselbe billige Zweitweg, den CR-GC-366 fuer `FUNC -satisfy-> UC` entfernt hat,
|
|
14
|
+
* und er umging dieselben Pruefungen (IO-01, R-21 sehen nur INNERHALB einer Kette).
|
|
15
|
+
*
|
|
16
|
+
* Bewusst eng gehalten: `kinds` ist das einzige mehrwertige Enum-Feld am Knoten, und ein
|
|
17
|
+
* Praedikat ueber freien Text oder abgeleitete Werte waere nicht deterministisch
|
|
18
|
+
* entscheidbar. Kein generischer Ausdruck, keine Funktion — eine Liste erlaubter Enum-Werte.
|
|
19
|
+
*/
|
|
20
|
+
export interface AttributePredicate {
|
|
21
|
+
/** An welchem Kantenende das Praedikat haengt. */
|
|
22
|
+
on: 'source' | 'target';
|
|
23
|
+
/** Das geprueft Feld — heute nur `kinds` (REQ). */
|
|
24
|
+
field: 'kinds';
|
|
25
|
+
/** Die erlaubten Werte. */
|
|
26
|
+
allowed: readonly ReqKind[];
|
|
27
|
+
}
|
|
7
28
|
export interface TracePattern {
|
|
8
29
|
source: ElementType | '*';
|
|
9
30
|
target: ElementType | '*';
|
|
10
31
|
type: TraceType;
|
|
11
|
-
category?: 'modeling' | 'audit';
|
|
12
32
|
label?: string;
|
|
13
|
-
cardinality?: '1' | '1..*' | '0..*';
|
|
33
|
+
cardinality?: '1' | '0..1' | '1..*' | '0..*';
|
|
34
|
+
/** Zusatzbedingung am Quell-/Zielknoten. Fehlt sie, entscheidet allein das Typ-Paar. */
|
|
35
|
+
where?: AttributePredicate;
|
|
14
36
|
description: string;
|
|
15
37
|
}
|
|
16
38
|
export declare const TRACE_PATTERNS: TracePattern[];
|
|
17
39
|
/**
|
|
18
|
-
*
|
|
19
|
-
*
|
|
40
|
+
* Die Kante, wie `isValidTrace` sie sieht: die beiden Typen, der Kantentyp — und, sobald ein
|
|
41
|
+
* Pattern ein `where` traegt, die deklarierten Kinds des betroffenen Endes.
|
|
42
|
+
*
|
|
43
|
+
* `sourceKinds`/`targetKinds` sind OPTIONAL, und ihr Fehlen bedeutet "nicht deklariert", nicht
|
|
44
|
+
* "beliebig": ein Pattern mit `where` lehnt dann ab (s. `satisfiesPredicate`). Wer nur das
|
|
45
|
+
* Typ-Paar kennt — die Vorschlags-Pfade in fix-templates/rule-apply, die eine Kante erst noch
|
|
46
|
+
* bauen — bekommt fuer die where-Patterns korrekterweise `false` und schlaegt sie nicht vor.
|
|
20
47
|
*/
|
|
21
|
-
export
|
|
48
|
+
export interface TraceShape {
|
|
22
49
|
source: ElementType;
|
|
23
50
|
target: ElementType;
|
|
24
51
|
type: TraceType;
|
|
25
52
|
label?: string;
|
|
26
|
-
|
|
53
|
+
sourceKinds?: readonly ReqKind[];
|
|
54
|
+
targetKinds?: readonly ReqKind[];
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Die Obergrenze eines Kardinalitaets-Ausdrucks — `Infinity`, wo keine gilt.
|
|
58
|
+
*
|
|
59
|
+
* Getrennt von `isValidTrace`, weil es eine andere ART von Bedingung ist: `isValidTrace`
|
|
60
|
+
* urteilt ueber EINE Kante und braucht nur deren Enden, eine Kardinalitaet urteilt ueber die
|
|
61
|
+
* MENGE der Kanten eines Knotens. Der urspruengliche CR-Entwurf wollte beides in
|
|
62
|
+
* `isValidTrace` erledigen; das geht nicht, und die Korrektur steht im Impact-Audit.
|
|
63
|
+
*/
|
|
64
|
+
export declare function maxOccurs(cardinality: TracePattern['cardinality']): number;
|
|
65
|
+
/** Die Patterns mit einer echten Obergrenze — die Arbeitsliste des Kardinalitaets-Beins. */
|
|
66
|
+
export declare const BOUNDED_PATTERNS: readonly TracePattern[];
|
|
67
|
+
/**
|
|
68
|
+
* Check if a trace matches a valid pattern in the meta-model.
|
|
69
|
+
* Uses element types (not IDs) plus — where a pattern declares one — an attribute predicate.
|
|
70
|
+
*/
|
|
71
|
+
export declare function isValidTrace(trace: TraceShape, patterns?: TracePattern[]): boolean;
|
package/dist/se/meta-model.js
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import { normalizeReqKinds } from './ontology.js';
|
|
2
|
+
/** Die Kinds, die eine Wirkkette bzw. ein Verhalten traegt. */
|
|
3
|
+
const BEHAVIOURAL_KINDS = ['functional', 'precondition', 'postcondition'];
|
|
4
|
+
/** Die Kinds, die eine Struktur (Modul/System) traegt. */
|
|
5
|
+
const STRUCTURAL_KINDS = ['non-functional', 'risk', 'mitigation'];
|
|
1
6
|
export const TRACE_PATTERNS = [
|
|
2
7
|
// ── compose (parent → child) ──
|
|
3
8
|
{ source: 'SYS', target: 'SYS', type: 'compose', cardinality: '0..*', description: 'System decomposes into subsystems' },
|
|
@@ -15,11 +20,25 @@ export const TRACE_PATTERNS = [
|
|
|
15
20
|
{ source: 'FLOW', target: 'ACTOR', type: 'io', description: 'Flow delivers to actor' },
|
|
16
21
|
{ source: 'FUNC', target: 'FLOW', type: 'io', description: 'Function outputs to flow' },
|
|
17
22
|
{ source: 'FLOW', target: 'FUNC', type: 'io', description: 'Flow feeds into function' },
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
23
|
+
// CR-SM-266 D1: `ACTOR -io-> UC` ENTFAELLT (BREAKING). Der tragende Pfad existiert und ist
|
|
24
|
+
// typisiert: `ACTOR -io-> FLOW -io-> FUNC`, FUNC in der FCHAIN des UC. Die Direktkante war
|
|
25
|
+
// die TYPLOSE Abkuerzung daneben — ein Actor-Beitrag ohne FLOW traegt kein SCHEMA und
|
|
26
|
+
// entzieht sich damit derselben Typgarantie, die fuer jeden anderen Fluss gilt. Migration
|
|
27
|
+
// je Bestandskante: FLOW einfuehren oder Kante loeschen (70 Kanten ueber 6 Graphen).
|
|
28
|
+
// CR-SM-266 D2: `MOD -io-> MOD` ENTFAELLT (BREAKING). Modul-Kopplung ist vollstaendig
|
|
29
|
+
// ABLEITBAR: `FUNC -allocate-> MOD` x `FUNC -io-> FLOW -io-> FUNC` ergibt die
|
|
30
|
+
// Modul-Ebenen-Projektion. Eine BEHAUPTETE Kopplungskante kann von der tatsaechlichen
|
|
31
|
+
// driften, eine berechnete nicht — und die behauptete gewann, weil sie billiger war.
|
|
32
|
+
// Soll-Architektur wird als REQ (non-functional) modelliert, nicht als Kante.
|
|
33
|
+
// CR-SM-266 D4: `FLOW -io-> UC` ENTFAELLT (BREAKING). Zweitweg neben `FLOW -io-> FUNC` mit
|
|
34
|
+
// FUNC in der FCHAIN des UC. Die UC-Grenze ERGIBT SICH aus den FLOWs der Kettenglieder; eine
|
|
35
|
+
// direkte FLOW-UC-Kante umgeht die Kettenzuordnung und damit IO-01 und R-21.
|
|
21
36
|
// ── satisfy (implementation) ──
|
|
22
|
-
{
|
|
37
|
+
{
|
|
38
|
+
source: 'FUNC', target: 'REQ', type: 'satisfy',
|
|
39
|
+
where: { on: 'target', field: 'kinds', allowed: BEHAVIOURAL_KINDS },
|
|
40
|
+
description: 'Function implements requirement — behavioural kinds only',
|
|
41
|
+
},
|
|
23
42
|
// CR-GC-366: `FUNC -satisfy-> UC` ENTFAELLT (BREAKING). Es war ein zweiter, redundanter Weg
|
|
24
43
|
// vom Verhalten zum Use Case neben dem einzig tragenden `UC -compose-> FCHAIN -compose-> FUNC`.
|
|
25
44
|
// Zwei Wege = zwei Wahrheiten: eine FUNC konnte einen UC bedienen, ohne in dessen Wirkkette zu
|
|
@@ -29,14 +48,38 @@ export const TRACE_PATTERNS = [
|
|
|
29
48
|
// stand ohnehin in einer FCHAIN); die restlichen 13 sind der Migrationsanlass, nicht ein
|
|
30
49
|
// Argument fuer das Pattern. Ersatz: die FUNC in die FCHAIN des UC aufnehmen (R-30).
|
|
31
50
|
// CR-154: NFR satisfy — non-functional REQs can be satisfied by chains, modules, or the system
|
|
51
|
+
//
|
|
52
|
+
// CR-SM-266 B: die drei strukturellen satisfy-Patterns tragen jetzt ein `where`. Ohne es
|
|
53
|
+
// konnte ein MOD oder SYS ein FUNKTIONALES REQ erfuellen, ohne dass eine Wirkkette es traegt
|
|
54
|
+
// — der Zweitweg aus CR-GC-366, eine Ebene hoeher. Die FCHAIN behaelt bewusst ALLE Kinds:
|
|
55
|
+
// sie IST die Wirkkette, an ihr haengen IO-01 und R-21, sie ist also nie die Abkuerzung.
|
|
32
56
|
{ source: 'FCHAIN', target: 'REQ', type: 'satisfy', description: 'Function chain satisfies end-to-end NFR (e.g. latency)' },
|
|
33
|
-
{
|
|
34
|
-
|
|
57
|
+
{
|
|
58
|
+
source: 'MOD', target: 'REQ', type: 'satisfy',
|
|
59
|
+
where: { on: 'target', field: 'kinds', allowed: STRUCTURAL_KINDS },
|
|
60
|
+
description: 'Module satisfies budget NFR (e.g. uptime, memory) — structural kinds only',
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
source: 'SYS', target: 'REQ', type: 'satisfy',
|
|
64
|
+
where: { on: 'target', field: 'kinds', allowed: STRUCTURAL_KINDS },
|
|
65
|
+
description: 'System satisfies system-level NFR (e.g. availability) — structural kinds only',
|
|
66
|
+
},
|
|
35
67
|
// ── verify (INCOSE: test verifies requirement) ──
|
|
36
68
|
{ source: 'TEST', target: 'REQ', type: 'verify', description: 'Test verifies requirement' },
|
|
37
69
|
// ── allocate (deployment/assignment) ──
|
|
38
|
-
|
|
39
|
-
|
|
70
|
+
// CR-SM-266b A/C: OBERGRENZEN als Grammatik. Beide Kanten sind Besitz-Aussagen, und Besitz
|
|
71
|
+
// ist nicht mehrfach vergebbar — eine FUNC wohnt in EINEM Modul, ein FLOW traegt EINEN
|
|
72
|
+
// Datenvertrag. Zwei Allokationen sind kein "staerkeres" Modell, sondern eine Frage ohne
|
|
73
|
+
// Antwort ("in welchem Modul liegt der Code?"), und zwei SCHEMA an einem FLOW heben genau
|
|
74
|
+
// die Typgarantie auf, fuer die der geteilte SCHEMA-Knoten existiert (CR-SM-266 C): zwei
|
|
75
|
+
// Vertraege koennen divergieren, einer kann es nicht.
|
|
76
|
+
//
|
|
77
|
+
// Nur die OBERE Grenze steht hier, bewusst als `0..1` und nicht `1..1`. Die untere haben
|
|
78
|
+
// R-22 (FUNC muss alloziert sein) und SC-04 (FLOW hat SCHEMA) laengst — sie hier zu
|
|
79
|
+
// wiederholen waere die Doppelzaehlung aus Gate 4: eine Ursache, zwei Befunde, ein Nenner
|
|
80
|
+
// doppelt belastet. Die beiden Haelften ergeben zusammen das `[1..1]` des CR.
|
|
81
|
+
{ source: 'FUNC', target: 'MOD', type: 'allocate', cardinality: '0..1', description: 'Function deployed in exactly one module' },
|
|
82
|
+
{ source: 'FLOW', target: 'SCHEMA', type: 'relation', cardinality: '0..1', description: 'Flow data format defined by exactly one schema' },
|
|
40
83
|
// CR-228: REQ→MOD allocate removed — a MOD does not own a REQ. Physical NFRs use
|
|
41
84
|
// MOD→satisfy→REQ, behavioral NFRs FCHAIN→satisfy→REQ; structural constraints are
|
|
42
85
|
// rules, not REQs. R-18 now flags any REQ→MOD allocate edge as invalid.
|
|
@@ -44,24 +87,75 @@ export const TRACE_PATTERNS = [
|
|
|
44
87
|
{ source: 'MS', target: 'FUNC', type: 'compose', description: 'Milestone includes function' },
|
|
45
88
|
{ source: 'MS', target: 'REQ', type: 'compose', description: 'Milestone includes requirement' },
|
|
46
89
|
{ source: 'MS', target: 'UC', type: 'compose', description: 'Milestone includes use case' },
|
|
47
|
-
|
|
48
|
-
|
|
90
|
+
// CR-SM-266 D3: `MS -compose-> MS` ENTFAELLT (BREAKING). Zwei Kantenarten zwischen denselben
|
|
91
|
+
// zwei Meilensteinen sagten dasselbe auf zwei Arten — Schachtelung und Abfolge sind an einem
|
|
92
|
+
// Meilenstein nicht unterscheidbar, MS-01 zaehlte den Scope einmal ueber `CR -relation-> MS`
|
|
93
|
+
// (seit CR-SM-245 der einzige Weg) und die compose-Kante trug nichts mehr bei. `relation`
|
|
94
|
+
// bleibt als EINZIGE MS-MS-Kante, Semantik: Ordnung/Abfolge (Vorgaenger → Nachfolger).
|
|
95
|
+
// Migration: keine — 0 solche Kanten in allen 9 aktiven Familie-Graphen (gemessen 2026-08-25).
|
|
96
|
+
{ source: 'MS', target: 'MS', type: 'relation', label: 'depends-on', description: 'Milestone order: predecessor → successor' },
|
|
49
97
|
{ source: 'CR', target: 'MS', type: 'relation', description: 'CR assigned to milestone' },
|
|
50
98
|
// ── CR traceability (CR-155: CR tracks mutated elements) ──
|
|
51
99
|
{ source: 'CR', target: 'UC', type: 'relation', description: 'CR affects use case' },
|
|
52
100
|
{ source: 'CR', target: 'REQ', type: 'relation', description: 'CR affects requirement' },
|
|
53
101
|
{ source: 'CR', target: 'FUNC', type: 'relation', description: 'CR affects function' },
|
|
54
102
|
{ source: 'CR', target: 'MOD', type: 'relation', description: 'CR affects module' },
|
|
55
|
-
//
|
|
56
|
-
|
|
103
|
+
// CR-SM-266 D5: `SESSION -produces-> *` ENTFAELLT ersatzlos, mit ihm der TraceType `produces`
|
|
104
|
+
// und der ElementType `SESSION`. Der urspruengliche Entwurf hielt das Pattern fuer die
|
|
105
|
+
// Provenance-Kante und wollte es behalten; die MESSUNG hat das widerlegt: 0 produces-Kanten
|
|
106
|
+
// und 0 SESSION-Knoten in ALLEN 9 aktiven Familie-Graphen, auch im graphcode-Selbstmodell
|
|
107
|
+
// nach 195 gegateten Versionen. Die reale Provenance lebt seit CR-GC-347 in
|
|
108
|
+
// `.graphcode/audit.jsonl` — audit_trail/audit_stats lesen die DATEI, nie den Graphen.
|
|
109
|
+
// Einziger Schreiber war ein toter Pfad (CR-195d in graph-api-core), Leser mit Wirkung: keiner.
|
|
110
|
+
// Nicht auf `relation` umgebogen: eine Kante ohne Schreiber UND ohne Leser wird gestrichen,
|
|
111
|
+
// nicht umbenannt.
|
|
57
112
|
];
|
|
113
|
+
/**
|
|
114
|
+
* Erfuellt das betroffene Kantenende das Praedikat?
|
|
115
|
+
*
|
|
116
|
+
* TEILMENGEN-Semantik, nicht Schnittmenge: JEDER deklarierte Kind muss in der erlaubten Liste
|
|
117
|
+
* liegen. Die schwaechere Lesart ("mindestens einer passt") waere das Loch, das der ganze
|
|
118
|
+
* Abschnitt schliessen will — ein funktionales REQ zusaetzlich als `non-functional` zu
|
|
119
|
+
* deklarieren haette gereicht, damit ein MOD es wieder erfuellen darf, und der billige
|
|
120
|
+
* Zweitweg waere ueber ein Attribut zurueck.
|
|
121
|
+
*
|
|
122
|
+
* Der leere Fall LEHNT AB (`kinds.length === 0`): ohne Deklaration ist nicht entscheidbar, ob
|
|
123
|
+
* die Kante zulaessig ist, und "unentscheidbar" darf nicht "erlaubt" heissen — sonst wird das
|
|
124
|
+
* WEGLASSEN des Attributs zum Umgehungsweg. Das ist die durchsetzende Haelfte von REQ-X06
|
|
125
|
+
* (`kinds` ist Pflicht am REQ); die meldende Haelfte haengt an BQ-07.
|
|
126
|
+
*/
|
|
127
|
+
function satisfiesPredicate(where, trace) {
|
|
128
|
+
// `normalizeReqKinds` statt direktem Zugriff: drei Familie-Graphen tragen `kinds` als String
|
|
129
|
+
// statt als Liste, und ein `.every()` darauf WIRFT — mitten im Regellauf, statt einen Befund
|
|
130
|
+
// zu melden. Die Normalisierung heilt die Drift nicht, sie macht sie nur urteilsfaehig.
|
|
131
|
+
const kinds = normalizeReqKinds(where.on === 'source' ? trace.sourceKinds : trace.targetKinds);
|
|
132
|
+
if (kinds.length === 0)
|
|
133
|
+
return false;
|
|
134
|
+
return kinds.every(k => where.allowed.includes(k));
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Die Obergrenze eines Kardinalitaets-Ausdrucks — `Infinity`, wo keine gilt.
|
|
138
|
+
*
|
|
139
|
+
* Getrennt von `isValidTrace`, weil es eine andere ART von Bedingung ist: `isValidTrace`
|
|
140
|
+
* urteilt ueber EINE Kante und braucht nur deren Enden, eine Kardinalitaet urteilt ueber die
|
|
141
|
+
* MENGE der Kanten eines Knotens. Der urspruengliche CR-Entwurf wollte beides in
|
|
142
|
+
* `isValidTrace` erledigen; das geht nicht, und die Korrektur steht im Impact-Audit.
|
|
143
|
+
*/
|
|
144
|
+
export function maxOccurs(cardinality) {
|
|
145
|
+
if (cardinality === undefined)
|
|
146
|
+
return Infinity;
|
|
147
|
+
return cardinality === '1' || cardinality === '0..1' ? 1 : Infinity;
|
|
148
|
+
}
|
|
149
|
+
/** Die Patterns mit einer echten Obergrenze — die Arbeitsliste des Kardinalitaets-Beins. */
|
|
150
|
+
export const BOUNDED_PATTERNS = TRACE_PATTERNS.filter(p => maxOccurs(p.cardinality) < Infinity);
|
|
58
151
|
/**
|
|
59
152
|
* Check if a trace matches a valid pattern in the meta-model.
|
|
60
|
-
* Uses element types (not IDs)
|
|
153
|
+
* Uses element types (not IDs) plus — where a pattern declares one — an attribute predicate.
|
|
61
154
|
*/
|
|
62
155
|
export function isValidTrace(trace, patterns = TRACE_PATTERNS) {
|
|
63
156
|
return patterns.some(p => (p.source === '*' || p.source === trace.source) &&
|
|
64
157
|
(p.target === '*' || p.target === trace.target) &&
|
|
65
158
|
p.type === trace.type &&
|
|
66
|
-
(!p.label || p.label === trace.label)
|
|
159
|
+
(!p.label || p.label === trace.label) &&
|
|
160
|
+
(!p.where || satisfiesPredicate(p.where, trace)));
|
|
67
161
|
}
|
|
@@ -71,6 +71,14 @@ export interface ModuleMetrics {
|
|
|
71
71
|
instability: number | null;
|
|
72
72
|
/** MT-02 core: connected-component count. null below 2 allocated FUNCs (not measurable). */
|
|
73
73
|
lcom4: number | null;
|
|
74
|
+
/**
|
|
75
|
+
* CR-SM-263: JEDE allozierte FUNC ist ein zerlegter Block (Rollup), das MOD also ein
|
|
76
|
+
* Container statt eines Implementierungsmoduls. Dann ist `lcom4` = Zahl der Bloecke,
|
|
77
|
+
* garantiert durch CR-SM-256 — eine wahre Zahl ohne Aussage ueber Kohaesion. MT-02
|
|
78
|
+
* urteilt hier nicht; der Wert steht trotzdem da, damit der Leser ihn deuten kann.
|
|
79
|
+
* `false` unterhalb von 2 allozierten FUNCs (kein Container, nur zu klein zum Messen).
|
|
80
|
+
*/
|
|
81
|
+
rollupContainer: boolean;
|
|
74
82
|
/** The CR-SM-223 measurement, deliberately threshold-free. null where contracts omits it. */
|
|
75
83
|
cohesion: {
|
|
76
84
|
internal: number;
|
package/dist/se/metric-rules.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { decomposedFuncs } from './rules.js';
|
|
2
|
+
import { indexOf } from './graph-index.js';
|
|
1
3
|
/**
|
|
2
4
|
* MT-01: Module Instability (CR-165: indirect via allocate-path).
|
|
3
5
|
* I = fan_out / (fan_in + fan_out) > `policy.instability` → warning.
|
|
@@ -46,6 +48,13 @@ export function mt02Lcom4(graph, policy) {
|
|
|
46
48
|
for (const m of measureModules(graph)) {
|
|
47
49
|
if (m.lcom4 === null)
|
|
48
50
|
continue;
|
|
51
|
+
// CR-SM-263: ein MOD aus lauter zerlegten Bloecken ist per Konstruktion zerfallen —
|
|
52
|
+
// LCOM4 verbindet ueber geteilte io/satisfy-ZIELE, und CR-SM-256 hat festgehalten, dass
|
|
53
|
+
// ein Rollup genau die nicht traegt. Die Regel war dort nicht streng, sondern
|
|
54
|
+
// unerfuellbar: gruen wird sie erst mit der Kante, die CR-SM-256 fuer falsch erklaert.
|
|
55
|
+
// Die MESSUNG bleibt (`moduleMetrics` liefert `lcom4` unveraendert) — nur das Urteil faellt.
|
|
56
|
+
if (m.rollupContainer)
|
|
57
|
+
continue;
|
|
49
58
|
const message = `${m.moduleName} has LCOM4=${m.lcom4} (${m.allocatedFuncs} FUNCs in ${m.lcom4} disconnected groups)`;
|
|
50
59
|
if (m.lcom4 >= steps.info && m.lcom4 < steps.warning) {
|
|
51
60
|
violations.push({ rule_id: 'MT-02', severity: 'info', element_id: m.moduleId, message });
|
|
@@ -120,30 +129,86 @@ function connectionPairs(graph) {
|
|
|
120
129
|
* Every value that is not measurable is `null`, never 0: a module with one allocated
|
|
121
130
|
* FUNC has no LCOM4, and a 1 there would be an invented statement about cohesion.
|
|
122
131
|
*/
|
|
132
|
+
const MEASURE_CACHE = new WeakMap();
|
|
133
|
+
/**
|
|
134
|
+
* CR-SM-264: die Rechnung EINMAL je Graph.
|
|
135
|
+
*
|
|
136
|
+
* `mt01Instability` und `mt02Lcom4` riefen `measureModules` beide auf, also lief die
|
|
137
|
+
* vollstaendige Modulmessung je Auswertungslauf zweimal — im Profil aus CR-SM-260 stehen sie
|
|
138
|
+
* mit 7,1 % und 7,0 % nebeneinander, und die zweite Zahl ist reine Wiederholung der ersten.
|
|
139
|
+
* Schluessel ist das Graph-OBJEKT (dieselbe Begruendung wie in `graph-index.ts`).
|
|
140
|
+
*
|
|
141
|
+
* Der Cache gibt seine EIGENE Liste heraus. Wer sie sortieren will, kopiert vorher —
|
|
142
|
+
* `moduleMetrics` tut das; ein `.sort()` auf dem Cache-Eintrag wuerde ihn dauerhaft umordnen.
|
|
143
|
+
*/
|
|
123
144
|
function measureModules(graph) {
|
|
124
|
-
const
|
|
145
|
+
const cached = MEASURE_CACHE.get(graph);
|
|
146
|
+
if (cached !== undefined)
|
|
147
|
+
return cached;
|
|
148
|
+
const rowsForCache = measureModulesUncached(graph);
|
|
149
|
+
MEASURE_CACHE.set(graph, rowsForCache);
|
|
150
|
+
return rowsForCache;
|
|
151
|
+
}
|
|
152
|
+
function measureModulesUncached(graph) {
|
|
153
|
+
const idx = indexOf(graph);
|
|
154
|
+
const mods = idx.elementsOfType('MOD');
|
|
125
155
|
const pairs = [...connectionPairs(graph)].map(p => p.split('|'));
|
|
156
|
+
// CR-SM-263: EIN Helper fuer "Blatt" — derselbe, den R-02/R-30/R-31 seit CR-SM-256 lesen.
|
|
157
|
+
// Eine zweite Definition hier waere ein zweiter Weg zu derselben Aussage (Gate 2).
|
|
158
|
+
const decomposed = decomposedFuncs(graph);
|
|
126
159
|
const rows = [];
|
|
160
|
+
// --- CR-SM-264: die indirekte Kopplung UMGEDREHT ---
|
|
161
|
+
//
|
|
162
|
+
// Vorher lief je MOD eine Schleife ueber ALLE Traces (`MOD x T`) — nach Teil c die teuerste
|
|
163
|
+
// verbliebene Form im Katalog, und sie traegt MT-01 UND MT-02, weil beide dieselbe Rechnung
|
|
164
|
+
// lesen. Der Index loest sie nicht auf: die Frage ist nicht "welche Kanten haengen an diesem
|
|
165
|
+
// Knoten", sondern "welche Kanten kreuzen diese Modulgrenze".
|
|
166
|
+
//
|
|
167
|
+
// Also einmal ueber die Traces statt einmal je Modul. Je Kante steht ueber die
|
|
168
|
+
// allocate-Zuordnung fest, welche Module ihre Enden besitzen; nur die zaehlen mit. Ein
|
|
169
|
+
// Element allokiert praktisch immer auf hoechstens ein MOD, die innere Schleife ist damit
|
|
170
|
+
// kurz — und das Ergebnis ist Zaehler fuer Zaehler dasselbe.
|
|
171
|
+
const modIds = idx.idsOfType('MOD');
|
|
172
|
+
const ownersOf = new Map(); // alloziertes Element -> seine MODs
|
|
173
|
+
const membersOf = new Map(); // MOD -> seine allozierten Elemente
|
|
174
|
+
for (const t of idx.tracesOfType('allocate')) {
|
|
175
|
+
if (!modIds.has(t.target))
|
|
176
|
+
continue;
|
|
177
|
+
const owners = ownersOf.get(t.source);
|
|
178
|
+
if (owners)
|
|
179
|
+
owners.push(t.target);
|
|
180
|
+
else
|
|
181
|
+
ownersOf.set(t.source, [t.target]);
|
|
182
|
+
const members = membersOf.get(t.target);
|
|
183
|
+
if (members)
|
|
184
|
+
members.add(t.source);
|
|
185
|
+
else
|
|
186
|
+
membersOf.set(t.target, new Set([t.source]));
|
|
187
|
+
}
|
|
188
|
+
const indirectOutOf = new Map();
|
|
189
|
+
const indirectInOf = new Map();
|
|
190
|
+
const bump = (m, key) => { m.set(key, (m.get(key) ?? 0) + 1); };
|
|
191
|
+
for (const t of graph.traces) {
|
|
192
|
+
if (t.type === 'allocate')
|
|
193
|
+
continue; // allocate itself doesn't count as coupling
|
|
194
|
+
for (const owner of ownersOf.get(t.source) ?? []) {
|
|
195
|
+
if (!membersOf.get(owner).has(t.target) && t.target !== owner)
|
|
196
|
+
bump(indirectOutOf, owner);
|
|
197
|
+
}
|
|
198
|
+
for (const owner of ownersOf.get(t.target) ?? []) {
|
|
199
|
+
if (!membersOf.get(owner).has(t.source) && t.source !== owner)
|
|
200
|
+
bump(indirectInOf, owner);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
127
203
|
for (const mod of mods) {
|
|
128
|
-
const allocated =
|
|
129
|
-
.filter(t => t.type === 'allocate' && t.target === mod.id)
|
|
130
|
-
.map(t => t.source);
|
|
204
|
+
const allocated = idx.in(mod.id, 'allocate').map(t => t.source);
|
|
131
205
|
const modFuncIds = new Set(allocated);
|
|
132
206
|
// --- MT-01: fan-in / fan-out (CR-165, indirect via the allocate path) ---
|
|
133
|
-
const directOut =
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
if (t.type === 'allocate')
|
|
139
|
-
continue; // allocate itself doesn't count as coupling
|
|
140
|
-
if (modFuncIds.has(t.source) && !modFuncIds.has(t.target) && t.target !== mod.id)
|
|
141
|
-
indirectOut++;
|
|
142
|
-
if (modFuncIds.has(t.target) && !modFuncIds.has(t.source) && t.source !== mod.id)
|
|
143
|
-
indirectIn++;
|
|
144
|
-
}
|
|
145
|
-
const fanOut = directOut + indirectOut;
|
|
146
|
-
const fanIn = directIn + indirectIn;
|
|
207
|
+
const directOut = idx.out(mod.id, 'io').filter(t => t.target !== mod.id).length +
|
|
208
|
+
idx.out(mod.id, 'compose').filter(t => t.target !== mod.id).length;
|
|
209
|
+
const directIn = idx.in(mod.id, 'io').length + idx.in(mod.id, 'compose').length + idx.in(mod.id, 'allocate').length;
|
|
210
|
+
const fanOut = directOut + (indirectOutOf.get(mod.id) ?? 0);
|
|
211
|
+
const fanIn = directIn + (indirectInOf.get(mod.id) ?? 0);
|
|
147
212
|
const instability = fanIn + fanOut === 0 ? null : fanOut / (fanIn + fanOut);
|
|
148
213
|
rows.push({
|
|
149
214
|
moduleId: mod.id,
|
|
@@ -153,6 +218,7 @@ function measureModules(graph) {
|
|
|
153
218
|
fanOut,
|
|
154
219
|
instability,
|
|
155
220
|
lcom4: lcom4Of(graph, allocated),
|
|
221
|
+
rollupContainer: allocated.length >= 2 && allocated.every(f => decomposed.has(f)),
|
|
156
222
|
cohesion: cohesionOf(pairs, modFuncIds),
|
|
157
223
|
});
|
|
158
224
|
}
|
|
@@ -163,13 +229,19 @@ function lcom4Of(graph, funcIds) {
|
|
|
163
229
|
if (funcIds.length < 2)
|
|
164
230
|
return null;
|
|
165
231
|
// Two FUNCs are connected if they share a common io/satisfy target.
|
|
232
|
+
// CR-SM-264: die ausgehenden Kanten kommen aus dem Index. Vorher lief je alloziertem FUNC
|
|
233
|
+
// ein Vollscan ueber alle Traces — MOD x FUNC x T. Die Einfuegereihenfolge in `targets`
|
|
234
|
+
// bleibt identisch (io vor satisfy war sie nicht; der Index liefert beide Listen in
|
|
235
|
+
// Graph-Reihenfolge, und `targets` ist ein Set, dessen Inhalt hier nur auf Mitgliedschaft
|
|
236
|
+
// geprueft wird).
|
|
237
|
+
const idx = indexOf(graph);
|
|
166
238
|
const funcTargets = new Map();
|
|
167
239
|
for (const fid of funcIds) {
|
|
168
240
|
const targets = new Set();
|
|
169
|
-
for (const t of
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
241
|
+
for (const t of idx.out(fid, 'io'))
|
|
242
|
+
targets.add(t.target);
|
|
243
|
+
for (const t of idx.out(fid, 'satisfy'))
|
|
244
|
+
targets.add(t.target);
|
|
173
245
|
funcTargets.set(fid, targets);
|
|
174
246
|
}
|
|
175
247
|
const parent = new Map();
|
|
@@ -262,7 +334,7 @@ function cohesionOf(pairs, funcIds) {
|
|
|
262
334
|
*/
|
|
263
335
|
export function moduleMetrics(graph) {
|
|
264
336
|
const byId = (a, b) => a.moduleId < b.moduleId ? -1 : a.moduleId > b.moduleId ? 1 : 0;
|
|
265
|
-
return measureModules(graph).sort((a, b) => {
|
|
337
|
+
return [...measureModules(graph)].sort((a, b) => {
|
|
266
338
|
if (a.cohesion && b.cohesion)
|
|
267
339
|
return a.cohesion.ratio - b.cohesion.ratio || byId(a, b);
|
|
268
340
|
if (a.cohesion)
|
package/dist/se/ontology.d.ts
CHANGED
|
@@ -21,7 +21,6 @@ export declare const ElementType: z.ZodEnum<{
|
|
|
21
21
|
TEST: "TEST";
|
|
22
22
|
MOD: "MOD";
|
|
23
23
|
SCHEMA: "SCHEMA";
|
|
24
|
-
SESSION: "SESSION";
|
|
25
24
|
CR: "CR";
|
|
26
25
|
MS: "MS";
|
|
27
26
|
}>;
|
|
@@ -38,20 +37,48 @@ export declare const TraceType: z.ZodEnum<{
|
|
|
38
37
|
verify: "verify";
|
|
39
38
|
allocate: "allocate";
|
|
40
39
|
relation: "relation";
|
|
41
|
-
produces: "produces";
|
|
42
40
|
}>;
|
|
43
41
|
export type TraceType = z.infer<typeof TraceType>;
|
|
44
|
-
/**
|
|
42
|
+
/**
|
|
43
|
+
* REQ kind — 6 values aligned with SysML 2.0 + FMEA (CR-180).
|
|
44
|
+
*
|
|
45
|
+
* CR-SM-266 B: `negative` ENTFAELLT. Ein Verbots-REQ ("das System tut X nie") ist auch eine
|
|
46
|
+
* Anforderung — die Implementierung muss Regeln abfragen oder Sicherungen einbauen, und das
|
|
47
|
+
* ist `functional`. Der Wert hatte NULL Leser (kein Regel-, Gate- oder View-Konsument; anders
|
|
48
|
+
* als risk/mitigation → FM-01..03, pre/postcondition → UC-05/06, non-functional → NFR-01) und
|
|
49
|
+
* haette durch die where-Praedikate am satisfy-Pattern per AUSLASSUNG erstmals Wirkung
|
|
50
|
+
* bekommen: er stand in keiner der vier Listen und waere damit nur noch per FCHAIN erfuellbar
|
|
51
|
+
* gewesen. Die sechs verbleibenden Werte partitionieren die where-Listen VOLLSTAENDIG — kein
|
|
52
|
+
* Wert ohne Zuordnung, keine Kante, die durch ein Loch in der Aufzaehlung faellt.
|
|
53
|
+
*/
|
|
45
54
|
export declare const ReqKind: z.ZodEnum<{
|
|
46
55
|
functional: "functional";
|
|
47
56
|
"non-functional": "non-functional";
|
|
48
57
|
risk: "risk";
|
|
49
|
-
negative: "negative";
|
|
50
58
|
mitigation: "mitigation";
|
|
51
59
|
precondition: "precondition";
|
|
52
60
|
postcondition: "postcondition";
|
|
53
61
|
}>;
|
|
54
62
|
export type ReqKind = z.infer<typeof ReqKind>;
|
|
63
|
+
/**
|
|
64
|
+
* `kinds` in der Form, die WIRKLICH auf Platte liegt — als Liste.
|
|
65
|
+
*
|
|
66
|
+
* Gefunden beim Migrations-Audit zu CR-SM-266 (2026-08-25), nicht von Hand: drei Graphen der
|
|
67
|
+
* Familie tragen `kinds` als blossen STRING statt als Liste (`graph-view-edit`
|
|
68
|
+
* `REQ-edit-genesis: 'functional'`, dazu gc_test-graphview und graphify), einer davon mit dem
|
|
69
|
+
* Wert `'security'`, den das Enum gar nicht kennt. Ursache ist der Format-E-Weg: dort reist
|
|
70
|
+
* das Feld als `@kinds a,b` und wird erst vom Konsumenten gehoben (graph-api-core, CR-195d).
|
|
71
|
+
*
|
|
72
|
+
* Seit die where-Praedikate `kinds` LESEN (CR-SM-266 B), ist diese Drift tragend: ein
|
|
73
|
+
* `kinds.every(...)` auf einem String wirft, und eine geworfene Exception in `isValidTrace`
|
|
74
|
+
* reisst den ganzen Regellauf statt einen Befund zu melden. Die SSOT muss auch bei kaputten
|
|
75
|
+
* Daten ein URTEIL liefern, keinen Absturz.
|
|
76
|
+
*
|
|
77
|
+
* Normalisieren heisst hier ausdruecklich NICHT raten: `'a,b'` wird zu `['a','b']`, weil genau
|
|
78
|
+
* das die Schreibform ist — ein unbekannter Wert wie `'security'` ueberlebt die Normalisierung
|
|
79
|
+
* und faellt danach am Enum-Vergleich durch. Die Drift wird sichtbar, nicht geheilt.
|
|
80
|
+
*/
|
|
81
|
+
export declare function normalizeReqKinds(raw: unknown): readonly string[];
|
|
55
82
|
export declare const AsilLevel: z.ZodEnum<{
|
|
56
83
|
QM: "QM";
|
|
57
84
|
A: "A";
|
|
@@ -222,7 +249,6 @@ export declare const OntologyElement: z.ZodObject<{
|
|
|
222
249
|
TEST: "TEST";
|
|
223
250
|
MOD: "MOD";
|
|
224
251
|
SCHEMA: "SCHEMA";
|
|
225
|
-
SESSION: "SESSION";
|
|
226
252
|
CR: "CR";
|
|
227
253
|
MS: "MS";
|
|
228
254
|
}>;
|
|
@@ -245,7 +271,6 @@ export declare const OntologyElement: z.ZodObject<{
|
|
|
245
271
|
functional: "functional";
|
|
246
272
|
"non-functional": "non-functional";
|
|
247
273
|
risk: "risk";
|
|
248
|
-
negative: "negative";
|
|
249
274
|
mitigation: "mitigation";
|
|
250
275
|
precondition: "precondition";
|
|
251
276
|
postcondition: "postcondition";
|
|
@@ -265,12 +290,6 @@ export type OntologyElement = z.infer<typeof OntologyElement>;
|
|
|
265
290
|
* A trace (edge) in the SE ontology graph.
|
|
266
291
|
* `label` provides semantic context for 'relation' edges (e.g. 'derives', 'depends-on').
|
|
267
292
|
*/
|
|
268
|
-
/** Trace category: modeling traces are user-visible, audit traces are internal. */
|
|
269
|
-
export declare const TraceCategory: z.ZodEnum<{
|
|
270
|
-
modeling: "modeling";
|
|
271
|
-
audit: "audit";
|
|
272
|
-
}>;
|
|
273
|
-
export type TraceCategory = z.infer<typeof TraceCategory>;
|
|
274
293
|
export declare const Trace: z.ZodObject<{
|
|
275
294
|
source: z.ZodString;
|
|
276
295
|
target: z.ZodString;
|
|
@@ -281,12 +300,7 @@ export declare const Trace: z.ZodObject<{
|
|
|
281
300
|
verify: "verify";
|
|
282
301
|
allocate: "allocate";
|
|
283
302
|
relation: "relation";
|
|
284
|
-
produces: "produces";
|
|
285
303
|
}>;
|
|
286
|
-
category: z.ZodOptional<z.ZodEnum<{
|
|
287
|
-
modeling: "modeling";
|
|
288
|
-
audit: "audit";
|
|
289
|
-
}>>;
|
|
290
304
|
label: z.ZodOptional<z.ZodString>;
|
|
291
305
|
weight: z.ZodDefault<z.ZodNumber>;
|
|
292
306
|
attributes: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
@@ -308,7 +322,6 @@ export declare const OntologyGraph: z.ZodObject<{
|
|
|
308
322
|
TEST: "TEST";
|
|
309
323
|
MOD: "MOD";
|
|
310
324
|
SCHEMA: "SCHEMA";
|
|
311
|
-
SESSION: "SESSION";
|
|
312
325
|
CR: "CR";
|
|
313
326
|
MS: "MS";
|
|
314
327
|
}>;
|
|
@@ -331,7 +344,6 @@ export declare const OntologyGraph: z.ZodObject<{
|
|
|
331
344
|
functional: "functional";
|
|
332
345
|
"non-functional": "non-functional";
|
|
333
346
|
risk: "risk";
|
|
334
|
-
negative: "negative";
|
|
335
347
|
mitigation: "mitigation";
|
|
336
348
|
precondition: "precondition";
|
|
337
349
|
postcondition: "postcondition";
|
|
@@ -356,12 +368,7 @@ export declare const OntologyGraph: z.ZodObject<{
|
|
|
356
368
|
verify: "verify";
|
|
357
369
|
allocate: "allocate";
|
|
358
370
|
relation: "relation";
|
|
359
|
-
produces: "produces";
|
|
360
371
|
}>;
|
|
361
|
-
category: z.ZodOptional<z.ZodEnum<{
|
|
362
|
-
modeling: "modeling";
|
|
363
|
-
audit: "audit";
|
|
364
|
-
}>>;
|
|
365
372
|
label: z.ZodOptional<z.ZodString>;
|
|
366
373
|
weight: z.ZodDefault<z.ZodNumber>;
|
|
367
374
|
attributes: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
@@ -372,7 +379,7 @@ export declare const OntologyGraph: z.ZodObject<{
|
|
|
372
379
|
export type OntologyGraph = z.infer<typeof OntologyGraph>;
|
|
373
380
|
/** Human-readable descriptions for each ElementType. */
|
|
374
381
|
export declare const ELEMENT_DESCRIPTIONS: Record<ElementType, string>;
|
|
375
|
-
/** Element types used in modeling (user-visible). Excludes
|
|
382
|
+
/** Element types used in modeling (user-visible). Excludes the planning type CR. */
|
|
376
383
|
export declare const MODELING_ELEMENT_TYPES: ElementType[];
|
|
377
384
|
export interface AttributeSpec {
|
|
378
385
|
key: string;
|