@sigloch/contracts 6.0.0 → 6.3.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 +1 -0
- package/dist/se/conformance-rules.js +67 -0
- package/dist/se/cr-quality-rules.js +24 -3
- 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/grammar-snapshot.d.ts +2 -1
- package/dist/se/grammar-snapshot.js +9 -1
- package/dist/se/graph-index.d.ts +56 -0
- package/dist/se/graph-index.js +60 -0
- package/dist/se/index.d.ts +3 -1
- package/dist/se/index.js +3 -1
- package/dist/se/metric-rules.d.ts +8 -0
- package/dist/se/metric-rules.js +95 -23
- package/dist/se/rules.d.ts +12 -0
- package/dist/se/rules.js +181 -95
- package/dist/se/uc-quality-rules.js +46 -23
- package/package.json +1 -1
package/dist/se/rules.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { z } from 'zod/v4';
|
|
7
7
|
import { ElementType, TraceType, TestRefsSchema, RealRefSchema } from './ontology.js';
|
|
8
8
|
import { isValidTrace } from './meta-model.js';
|
|
9
|
+
import { indexOf } from './graph-index.js';
|
|
9
10
|
export const RuleSeverity = z.enum(['error', 'warning', 'info']);
|
|
10
11
|
/** Candidate target for resolving a violation (e.g. a REQ to satisfy, a TEST to link). */
|
|
11
12
|
export const ViolationCandidate = z.object({
|
|
@@ -37,13 +38,32 @@ export const RuleViolation = z.object({
|
|
|
37
38
|
// Graph lookup helpers (shared across rules)
|
|
38
39
|
// ---------------------------------------------------------------------------
|
|
39
40
|
/** Find the MOD a FUNC/SCHEMA is allocated to. */
|
|
41
|
+
/**
|
|
42
|
+
* CR-SM-256: die EINE Definition von "zerlegt". Ein FUNC mit FUNC-Kindern ist ein Rollup
|
|
43
|
+
* seiner Kinder — ein Blackbox-Block, durch den nichts fliesst und der keine Anforderung
|
|
44
|
+
* erfuellt, die nicht schon ein Blatt erfuellt. Die Pflichten liegen vollstaendig auf den
|
|
45
|
+
* Blaettern (so schon R-30 seit CR-SM-249, so R-20 seit CR-210).
|
|
46
|
+
*
|
|
47
|
+
* Bewusst EIN Helper statt drei Kopien: eine zweite Definition von "Blatt" waere ein
|
|
48
|
+
* zweiter Weg zu derselben Aussage, und der billigere gewinnt immer. CR-SM-263 haengt als
|
|
49
|
+
* vierter Nutzer daran (MT-02, `metric-rules.ts`) — deshalb exportiert.
|
|
50
|
+
*/
|
|
51
|
+
export function decomposedFuncs(graph) {
|
|
52
|
+
const idx = indexOf(graph);
|
|
53
|
+
const funcIds = new Set(idx.elementsOfType('FUNC').map(f => f.id));
|
|
54
|
+
return new Set(graph.traces
|
|
55
|
+
.filter(t => t.type === 'compose' && funcIds.has(t.source) && funcIds.has(t.target))
|
|
56
|
+
.map(t => t.source));
|
|
57
|
+
}
|
|
40
58
|
function findParentModule(graph, elementId) {
|
|
41
|
-
const
|
|
59
|
+
const idx = indexOf(graph);
|
|
60
|
+
const trace = idx.out(elementId, 'allocate')[0];
|
|
42
61
|
return trace?.target;
|
|
43
62
|
}
|
|
44
63
|
/** Get all outgoing traces of a given type from an element. */
|
|
45
64
|
function outgoingTraces(graph, elementId, traceType) {
|
|
46
|
-
|
|
65
|
+
const idx = indexOf(graph);
|
|
66
|
+
return traceType === undefined ? idx.outAll(elementId) : idx.out(elementId, traceType);
|
|
47
67
|
}
|
|
48
68
|
/** Tokenize id/name/description into a lowercase token set (drops tokens < 3 chars). */
|
|
49
69
|
function overlapTokens(...parts) {
|
|
@@ -54,35 +74,69 @@ function overlapTokens(...parts) {
|
|
|
54
74
|
.split(/[^a-z0-9]+/)
|
|
55
75
|
.filter(t => t.length >= 3));
|
|
56
76
|
}
|
|
77
|
+
/**
|
|
78
|
+
* CR-SM-261: Tokenmenge je Element, einmal je Prozess statt einmal je Vergleich.
|
|
79
|
+
*
|
|
80
|
+
* `toCandidates` rief `overlapTokens` frueher INNERHALB des Sort-Komparators auf. Ein Sort
|
|
81
|
+
* ueber k Kandidaten macht rund k·log k Vergleiche, jeder Vergleich hat beide Seiten neu
|
|
82
|
+
* tokenisiert (join + toLowerCase + Regex-Split) — und das je meldendem Element. Bei R-31 mit
|
|
83
|
+
* 33 Befunden gegen 46 FLOWs sind das ueber 11 000 Tokenisierungen fuer 46 verschiedene
|
|
84
|
+
* Elemente. Das Profil aus CR-SM-260 hat R-31 deshalb mit 25,9 % und n^2,10 gemessen, obwohl
|
|
85
|
+
* die Regel selbst nur Mengen aufbaut: die Kosten lagen vollstaendig in der Kandidatenliste.
|
|
86
|
+
*
|
|
87
|
+
* Der Schluessel ist das Element-OBJEKT, nicht die id — zwei Graphen im selben Prozess (der
|
|
88
|
+
* Probegraph aus `applyRule` neben dem Original) teilen sich ids, aber nicht die Objekte.
|
|
89
|
+
* `WeakMap` gibt die Eintraege frei, sobald der Graph es tut; es entsteht kein Cache, den
|
|
90
|
+
* jemand invalidieren muesste, und damit auch keine zweite Wahrheit ueber ein Element.
|
|
91
|
+
*/
|
|
92
|
+
const TOKEN_CACHE = new WeakMap();
|
|
93
|
+
function tokensOf(e) {
|
|
94
|
+
let t = TOKEN_CACHE.get(e);
|
|
95
|
+
if (t === undefined) {
|
|
96
|
+
t = overlapTokens(e.id, e.name, e.description);
|
|
97
|
+
TOKEN_CACHE.set(e, t);
|
|
98
|
+
}
|
|
99
|
+
return t;
|
|
100
|
+
}
|
|
57
101
|
/**
|
|
58
102
|
* Map elements to ViolationCandidate format. When `ref` is given, rank candidates
|
|
59
103
|
* by id/name/description token overlap with `ref` (descending) so the most relevant
|
|
60
104
|
* target is first — e.g. REQ-bootstrap → TEST-bootstrap (CR-GC-203 item 3). Ranking
|
|
61
105
|
* is a hint, not an auto-link; semantic confirmation stays with the caller.
|
|
62
106
|
*/
|
|
63
|
-
function toCandidates(
|
|
107
|
+
function toCandidates(
|
|
108
|
+
// CR-SM-264: `readonly`, weil die Kandidaten jetzt aus `idx.elementsOfType()` kommen. Seit
|
|
109
|
+
// CR-SM-261 sortiert diese Funktion ohnehin eine eigene Liste (decorate-sort-undecorate) und
|
|
110
|
+
// fasst die Eingabe nicht mehr an — der mutable Typ war schon vorher zu weit.
|
|
111
|
+
elements, ref) {
|
|
64
112
|
if (!ref)
|
|
65
113
|
return elements.map(e => ({ id: e.id, type: e.type, name: e.name }));
|
|
66
|
-
const refTokens =
|
|
114
|
+
const refTokens = tokensOf(ref);
|
|
67
115
|
const score = (e) => {
|
|
68
116
|
let n = 0;
|
|
69
|
-
for (const tok of
|
|
117
|
+
for (const tok of tokensOf(e))
|
|
70
118
|
if (refTokens.has(tok))
|
|
71
119
|
n++;
|
|
72
120
|
return n;
|
|
73
121
|
};
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
122
|
+
// CR-SM-261: EINMAL bewerten, dann sortieren. Vorher stand `score` im Komparator und lief
|
|
123
|
+
// damit rund 2·k·log k mal statt k mal. Der Index ist der Gleichstands-Tiebreak und haelt die
|
|
124
|
+
// Ausgabe byte-identisch: `Array.prototype.sort` ist seit ES2019 stabil, die alte Fassung hat
|
|
125
|
+
// bei gleichem Score also die Eingangsreihenfolge behalten — diese tut es explizit.
|
|
126
|
+
return elements
|
|
127
|
+
.map((e, i) => ({ e, i, s: score(e) }))
|
|
128
|
+
.sort((a, b) => b.s - a.s || a.i - b.i)
|
|
129
|
+
.map(({ e }) => ({ id: e.id, type: e.type, name: e.name }));
|
|
77
130
|
}
|
|
78
131
|
// ---------------------------------------------------------------------------
|
|
79
132
|
// R-01: Every REQ must have at least one verify trace
|
|
80
133
|
// ---------------------------------------------------------------------------
|
|
81
134
|
function reqMustHaveVerification(graph) {
|
|
82
|
-
const
|
|
83
|
-
const
|
|
135
|
+
const idx = indexOf(graph);
|
|
136
|
+
const reqs = idx.elementsOfType('REQ');
|
|
137
|
+
const tests = idx.elementsOfType('TEST');
|
|
84
138
|
return reqs
|
|
85
|
-
.filter(req =>
|
|
139
|
+
.filter(req => idx.in(req.id, 'verify').length === 0)
|
|
86
140
|
.map(req => ({
|
|
87
141
|
rule_id: 'R-01',
|
|
88
142
|
severity: 'error',
|
|
@@ -101,8 +155,15 @@ function reqMustHaveVerification(graph) {
|
|
|
101
155
|
// R-02: Every FUNC must satisfy at least one REQ
|
|
102
156
|
// ---------------------------------------------------------------------------
|
|
103
157
|
function funcMustSatisfyReq(graph) {
|
|
104
|
-
const
|
|
105
|
-
|
|
158
|
+
const idx = indexOf(graph);
|
|
159
|
+
// CR-SM-256: Grundgesamtheit sind die BLAETTER. Ein zerlegter FUNC erfuellt keine REQ,
|
|
160
|
+
// die nicht schon eine seiner Blatt-Funktionen erfuellt — wer ihn befriedigen will,
|
|
161
|
+
// muss eine satisfy-Kante erfinden, die dieselbe REQ ein zweites Mal beansprucht.
|
|
162
|
+
// Am graphcode-Selbstmodell (graphVersion 171) melden 12 der 13 Bloecke R-02 UND R-31,
|
|
163
|
+
// also dieselbe Ursache zweimal gezaehlt — die Klasse CR-SM-235/-242.
|
|
164
|
+
const decomposed = decomposedFuncs(graph);
|
|
165
|
+
const funcs = idx.elementsOfType('FUNC').filter(e => !decomposed.has(e.id));
|
|
166
|
+
const allReqs = idx.elementsOfType('REQ');
|
|
106
167
|
// CR-GC-366: der Filter prueft den ZIELTYP. Vorher war er zieltyp-blind (`type === 'satisfy'`),
|
|
107
168
|
// womit ein `FUNC -satisfy-> UC` die Regel stumm schaltete, obwohl kein Requirement erfuellt war —
|
|
108
169
|
// der Regelname versprach mehr als der Code pruefte. Am graphcode-Selbstmodell verdeckte das 6 von
|
|
@@ -111,15 +172,15 @@ function funcMustSatisfyReq(graph) {
|
|
|
111
172
|
// Ziele das Meta-Modell gerade zulaesst.
|
|
112
173
|
const reqIds = new Set(allReqs.map(r => r.id));
|
|
113
174
|
return funcs
|
|
114
|
-
.filter(fn => !
|
|
175
|
+
.filter(fn => !idx.out(fn.id, 'satisfy').some(t => reqIds.has(t.target)))
|
|
115
176
|
.map(fn => {
|
|
116
177
|
const modId = findParentModule(graph, fn.id);
|
|
117
178
|
// Candidate REQs: same module's other FUNCs satisfy these, or constraints referencing this func
|
|
118
179
|
const siblingReqs = modId
|
|
119
180
|
? graph.traces
|
|
120
181
|
.filter(t => t.type === 'allocate' && t.target === modId && t.source !== fn.id)
|
|
121
|
-
.flatMap(t =>
|
|
122
|
-
.map(st =>
|
|
182
|
+
.flatMap(t => idx.out(t.source, 'satisfy'))
|
|
183
|
+
.map(st => idx.byId.get(st.target))
|
|
123
184
|
.filter((e) => !!e)
|
|
124
185
|
: [];
|
|
125
186
|
// Also include constraints whose description mentions this func's name
|
|
@@ -146,12 +207,13 @@ function funcMustSatisfyReq(graph) {
|
|
|
146
207
|
// R-03: ASIL isolation
|
|
147
208
|
// ---------------------------------------------------------------------------
|
|
148
209
|
function asilIsolation(graph) {
|
|
210
|
+
const idx = indexOf(graph);
|
|
149
211
|
const violations = [];
|
|
150
|
-
const modules =
|
|
212
|
+
const modules = idx.elementsOfType('MOD');
|
|
151
213
|
for (const mod of modules) {
|
|
152
214
|
const allocated = graph.traces
|
|
153
215
|
.filter(t => t.target === mod.id && t.type === 'allocate')
|
|
154
|
-
.map(t =>
|
|
216
|
+
.map(t => idx.byId.get(t.source))
|
|
155
217
|
.filter((e) => !!e);
|
|
156
218
|
const hasD = allocated.some(e => e.asil === 'D');
|
|
157
219
|
const hasQM = allocated.some(e => e.asil === 'QM');
|
|
@@ -185,22 +247,22 @@ function asilIsolation(graph) {
|
|
|
185
247
|
// `null` → messen, nicht urteilen: die Regel schweigt.
|
|
186
248
|
// ---------------------------------------------------------------------------
|
|
187
249
|
function maxModuleSize(graph, policy) {
|
|
250
|
+
const idx = indexOf(graph);
|
|
188
251
|
const violations = [];
|
|
189
252
|
const steps = policy.moduleSize;
|
|
190
253
|
if (steps === null)
|
|
191
254
|
return violations;
|
|
192
|
-
const modules =
|
|
255
|
+
const modules = idx.elementsOfType('MOD');
|
|
193
256
|
for (const mod of modules) {
|
|
194
|
-
const allocatedIds =
|
|
195
|
-
const allocated = allocatedIds.map(id =>
|
|
257
|
+
const allocatedIds = idx.in(mod.id, 'allocate').map(t => t.source);
|
|
258
|
+
const allocated = allocatedIds.map(id => idx.byId.get(id)).filter((e) => !!e);
|
|
196
259
|
const funcCount = allocated.length;
|
|
197
260
|
if (funcCount <= steps.coupled)
|
|
198
261
|
continue;
|
|
199
262
|
// Count crossing flows: io paths from FUNCs in this module to FUNCs in other modules
|
|
200
263
|
const funcIds = new Set(allocatedIds);
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
return false;
|
|
264
|
+
// CR-SM-264: nur die io-Kanten, nicht alle Traces je MOD.
|
|
265
|
+
const crossings = idx.tracesOfType('io').filter(t => {
|
|
204
266
|
const srcIn = funcIds.has(t.source);
|
|
205
267
|
const tgtIn = funcIds.has(t.target);
|
|
206
268
|
return (srcIn && !tgtIn) || (!srcIn && tgtIn);
|
|
@@ -242,10 +304,11 @@ function maxModuleSize(graph, policy) {
|
|
|
242
304
|
// R-05: Every TEST must verify at least one REQ
|
|
243
305
|
// ---------------------------------------------------------------------------
|
|
244
306
|
function testMustVerifyReq(graph) {
|
|
245
|
-
const
|
|
246
|
-
const
|
|
307
|
+
const idx = indexOf(graph);
|
|
308
|
+
const tests = idx.elementsOfType('TEST');
|
|
309
|
+
const allReqs = idx.elementsOfType('REQ');
|
|
247
310
|
return tests
|
|
248
|
-
.filter(test =>
|
|
311
|
+
.filter(test => idx.out(test.id, 'verify').length === 0)
|
|
249
312
|
.map(test => ({
|
|
250
313
|
rule_id: 'R-05',
|
|
251
314
|
severity: 'warning',
|
|
@@ -269,11 +332,12 @@ function testMustVerifyReq(graph) {
|
|
|
269
332
|
// R-08: Bidirectional trace consistency
|
|
270
333
|
// ---------------------------------------------------------------------------
|
|
271
334
|
function traceConsistency(graph) {
|
|
335
|
+
const idx = indexOf(graph);
|
|
272
336
|
return graph.traces
|
|
273
337
|
.filter(t => t.category !== 'audit')
|
|
274
338
|
.filter(t => {
|
|
275
|
-
const sourceExists =
|
|
276
|
-
const targetExists =
|
|
339
|
+
const sourceExists = idx.byId.has(t.source);
|
|
340
|
+
const targetExists = idx.byId.has(t.target);
|
|
277
341
|
return !sourceExists || !targetExists;
|
|
278
342
|
})
|
|
279
343
|
.map(t => ({
|
|
@@ -294,11 +358,12 @@ function traceConsistency(graph) {
|
|
|
294
358
|
// concern, stricter check — no parallel path); up to two warnings per FLOW.
|
|
295
359
|
// ---------------------------------------------------------------------------
|
|
296
360
|
function flowCompleteness(graph) {
|
|
297
|
-
const
|
|
361
|
+
const idx = indexOf(graph);
|
|
362
|
+
const flows = idx.elementsOfType('FLOW');
|
|
298
363
|
// Producers/consumers of a FLOW are FUNCs or ACTORs (io endpoints on the
|
|
299
364
|
// upstream/downstream side); UC may consume too. Offer both as candidates.
|
|
300
|
-
const sources = graph.elements.filter(e => e.type === 'FUNC' || e.type === 'ACTOR');
|
|
301
|
-
const io =
|
|
365
|
+
const sources = graph.elements.filter(e => e.type === 'FUNC' || e.type === 'ACTOR'); // Mehrtyp: ein Durchlauf ist hier billiger als vier Index-Listen zu mischen (Reihenfolge!)
|
|
366
|
+
const io = idx.tracesOfType('io');
|
|
302
367
|
const violations = [];
|
|
303
368
|
for (const f of flows) {
|
|
304
369
|
const ctx = { element_type: f.type, element_name: f.name, candidate_targets: toCandidates(sources) };
|
|
@@ -340,6 +405,7 @@ function flowCompleteness(graph) {
|
|
|
340
405
|
/** Trace types on which a direct 2-cycle is a real finding (CR-GC-315). */
|
|
341
406
|
const CIRCULAR_TRACE_TYPES = new Set(['compose', 'allocate', 'relation']);
|
|
342
407
|
function noDirectCircular(graph) {
|
|
408
|
+
const idx = indexOf(graph);
|
|
343
409
|
const violations = [];
|
|
344
410
|
// Direction-independent key: A↔B is ONE cycle, reported once. Keying on the
|
|
345
411
|
// message (CR-GC-315 predecessor) never collapsed anything — the two
|
|
@@ -348,7 +414,7 @@ function noDirectCircular(graph) {
|
|
|
348
414
|
for (const t of graph.traces) {
|
|
349
415
|
if (!CIRCULAR_TRACE_TYPES.has(t.type))
|
|
350
416
|
continue;
|
|
351
|
-
if (!
|
|
417
|
+
if (!idx.out(t.target, t.type).some(other => other.target === t.source))
|
|
352
418
|
continue;
|
|
353
419
|
const key = `${t.type}|${[t.source, t.target].sort().join('|')}`;
|
|
354
420
|
if (seen.has(key))
|
|
@@ -370,12 +436,12 @@ function noDirectCircular(graph) {
|
|
|
370
436
|
// RD-01: Unresolved requirement (leaf REQ without satisfy)
|
|
371
437
|
// ---------------------------------------------------------------------------
|
|
372
438
|
function unresolvedRequirement(graph) {
|
|
373
|
-
const
|
|
439
|
+
const idx = indexOf(graph);
|
|
440
|
+
const reqs = idx.elementsOfType('REQ');
|
|
374
441
|
// Leaf REQs: no compose→REQ children
|
|
375
|
-
const leafReqs = reqs.filter(req => !
|
|
376
|
-
graph.elements.some(e => e.id === t.target && e.type === 'REQ')));
|
|
442
|
+
const leafReqs = reqs.filter(req => !idx.out(req.id, 'compose').some(t => idx.typeOf(t.target) === 'REQ'));
|
|
377
443
|
return leafReqs
|
|
378
|
-
.filter(req =>
|
|
444
|
+
.filter(req => idx.in(req.id, 'satisfy').length === 0)
|
|
379
445
|
.map(req => ({
|
|
380
446
|
rule_id: 'RD-01',
|
|
381
447
|
severity: 'warning',
|
|
@@ -394,12 +460,11 @@ function unresolvedRequirement(graph) {
|
|
|
394
460
|
// RD-02: Decomposition consistency (parent REQ should not have FUNC satisfy)
|
|
395
461
|
// ---------------------------------------------------------------------------
|
|
396
462
|
function decompositionConsistency(graph) {
|
|
397
|
-
const
|
|
398
|
-
const
|
|
399
|
-
|
|
463
|
+
const idx = indexOf(graph);
|
|
464
|
+
const reqs = idx.elementsOfType('REQ');
|
|
465
|
+
const parentReqs = reqs.filter(req => idx.out(req.id, 'compose').some(t => idx.typeOf(t.target) === 'REQ'));
|
|
400
466
|
return parentReqs
|
|
401
|
-
.filter(req =>
|
|
402
|
-
graph.elements.some(e => e.id === t.source && e.type === 'FUNC')))
|
|
467
|
+
.filter(req => idx.in(req.id, 'satisfy').some(t => idx.typeOf(t.source) === 'FUNC'))
|
|
403
468
|
.map(req => ({
|
|
404
469
|
rule_id: 'RD-02',
|
|
405
470
|
severity: 'warning',
|
|
@@ -413,18 +478,18 @@ function decompositionConsistency(graph) {
|
|
|
413
478
|
// RD-03: No premature decomposition (all children same satisfy target)
|
|
414
479
|
// ---------------------------------------------------------------------------
|
|
415
480
|
function noPrematureDecomposition(graph) {
|
|
416
|
-
const
|
|
417
|
-
const
|
|
418
|
-
|
|
481
|
+
const idx = indexOf(graph);
|
|
482
|
+
const reqs = idx.elementsOfType('REQ');
|
|
483
|
+
const parentReqs = reqs.filter(req => idx.out(req.id, 'compose').some(t => idx.typeOf(t.target) === 'REQ'));
|
|
419
484
|
return parentReqs
|
|
420
485
|
.filter(parent => {
|
|
421
486
|
const childIds = graph.traces
|
|
422
487
|
.filter(t => t.source === parent.id && t.type === 'compose' &&
|
|
423
|
-
|
|
488
|
+
idx.typeOf(t.target) === 'REQ')
|
|
424
489
|
.map(t => t.target);
|
|
425
490
|
if (childIds.length < 2)
|
|
426
491
|
return false;
|
|
427
|
-
const satisfyTargets = childIds.map(cid =>
|
|
492
|
+
const satisfyTargets = childIds.map(cid => idx.in(cid, 'satisfy').filter(t => t.source !== cid).map(t => t.source));
|
|
428
493
|
// All children satisfied by the same single source
|
|
429
494
|
const allSame = satisfyTargets.every(targets => targets.length === 1 && targets[0] === satisfyTargets[0]?.[0]);
|
|
430
495
|
return allSame && satisfyTargets[0]?.length === 1;
|
|
@@ -452,8 +517,10 @@ function noPrematureDecomposition(graph) {
|
|
|
452
517
|
*/
|
|
453
518
|
const DECOMPOSITION_BREADTH_MAX = 11;
|
|
454
519
|
function decompositionBreadth(graph) {
|
|
455
|
-
const
|
|
456
|
-
|
|
520
|
+
const idx = indexOf(graph);
|
|
521
|
+
// CR-SM-264: `typeOf` und `byId` baute diese Regel je Aufruf selbst — der Index hat beide.
|
|
522
|
+
const typeOf = idx;
|
|
523
|
+
const byId = idx.byId;
|
|
457
524
|
const counts = new Map();
|
|
458
525
|
const bump = (parentId, kind) => {
|
|
459
526
|
const key = `${parentId}\u0000${kind}`;
|
|
@@ -464,8 +531,8 @@ function decompositionBreadth(graph) {
|
|
|
464
531
|
counts.set(key, { parentId, kind, n: 1 });
|
|
465
532
|
};
|
|
466
533
|
for (const t of graph.traces) {
|
|
467
|
-
const src = typeOf.
|
|
468
|
-
const tgt = typeOf.
|
|
534
|
+
const src = typeOf.typeOf(t.source);
|
|
535
|
+
const tgt = typeOf.typeOf(t.target);
|
|
469
536
|
if (t.type === 'compose' && src === 'FUNC' && tgt === 'FUNC') {
|
|
470
537
|
bump(t.source, 'sub-FUNC');
|
|
471
538
|
}
|
|
@@ -494,9 +561,10 @@ function decompositionBreadth(graph) {
|
|
|
494
561
|
// R-14: UC must have at least 1 compose trace (→ FCHAIN or REQ) (CR-117)
|
|
495
562
|
// ---------------------------------------------------------------------------
|
|
496
563
|
function ucMustHaveCompose(graph) {
|
|
497
|
-
const
|
|
564
|
+
const idx = indexOf(graph);
|
|
565
|
+
const ucs = idx.elementsOfType('UC');
|
|
498
566
|
return ucs
|
|
499
|
-
.filter(uc =>
|
|
567
|
+
.filter(uc => idx.out(uc.id, 'compose').length === 0)
|
|
500
568
|
.map(uc => ({
|
|
501
569
|
rule_id: 'R-14',
|
|
502
570
|
severity: 'warning',
|
|
@@ -528,14 +596,15 @@ function ucMustHaveCompose(graph) {
|
|
|
528
596
|
// einen einzigen Befund ueber beide Selbstmodelle.
|
|
529
597
|
// ---------------------------------------------------------------------------
|
|
530
598
|
function fchainCompleteness(graph) {
|
|
531
|
-
const
|
|
532
|
-
const
|
|
533
|
-
const
|
|
599
|
+
const idx = indexOf(graph);
|
|
600
|
+
const fchains = idx.elementsOfType('FCHAIN');
|
|
601
|
+
const funcs = idx.elementsOfType('FUNC');
|
|
602
|
+
const ucs = idx.elementsOfType('UC');
|
|
534
603
|
const ucIds = new Set(ucs.map(u => u.id));
|
|
535
604
|
const violations = [];
|
|
536
605
|
for (const fc of fchains) {
|
|
537
606
|
const ctx = { element_type: fc.type, element_name: fc.name };
|
|
538
|
-
if (
|
|
607
|
+
if (idx.out(fc.id, 'compose').length === 0) {
|
|
539
608
|
violations.push({
|
|
540
609
|
rule_id: 'R-15',
|
|
541
610
|
severity: 'warning',
|
|
@@ -545,7 +614,7 @@ function fchainCompleteness(graph) {
|
|
|
545
614
|
context: { ...ctx, candidate_targets: toCandidates(funcs) },
|
|
546
615
|
});
|
|
547
616
|
}
|
|
548
|
-
if (!
|
|
617
|
+
if (!idx.in(fc.id, 'compose').some(t => ucIds.has(t.source))) {
|
|
549
618
|
violations.push({
|
|
550
619
|
rule_id: 'R-15',
|
|
551
620
|
severity: 'warning',
|
|
@@ -562,9 +631,10 @@ function fchainCompleteness(graph) {
|
|
|
562
631
|
// R-16: ACTOR must have at least 1 io trace (CR-117)
|
|
563
632
|
// ---------------------------------------------------------------------------
|
|
564
633
|
function actorMustHaveTrace(graph) {
|
|
565
|
-
const
|
|
634
|
+
const idx = indexOf(graph);
|
|
635
|
+
const actors = idx.elementsOfType('ACTOR');
|
|
566
636
|
return actors
|
|
567
|
-
.filter(a =>
|
|
637
|
+
.filter(a => idx.out(a.id, 'io').length === 0 && idx.in(a.id, 'io').length === 0)
|
|
568
638
|
.map(a => ({
|
|
569
639
|
rule_id: 'R-16',
|
|
570
640
|
severity: 'warning',
|
|
@@ -581,9 +651,10 @@ function actorMustHaveTrace(graph) {
|
|
|
581
651
|
// R-17: SYS must have at least 1 compose trace (CR-117)
|
|
582
652
|
// ---------------------------------------------------------------------------
|
|
583
653
|
function sysMustHaveCompose(graph) {
|
|
584
|
-
const
|
|
654
|
+
const idx = indexOf(graph);
|
|
655
|
+
const systems = idx.elementsOfType('SYS');
|
|
585
656
|
return systems
|
|
586
|
-
.filter(sys =>
|
|
657
|
+
.filter(sys => idx.out(sys.id, 'compose').length === 0)
|
|
587
658
|
.map(sys => ({
|
|
588
659
|
rule_id: 'R-17',
|
|
589
660
|
severity: 'warning',
|
|
@@ -608,10 +679,10 @@ function sysMustHaveCompose(graph) {
|
|
|
608
679
|
// zusaetzlich compose) — genau deshalb ist jetzt der Zeitpunkt.
|
|
609
680
|
// ---------------------------------------------------------------------------
|
|
610
681
|
function msEmptyScope(graph) {
|
|
611
|
-
const
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
.filter(ms => !
|
|
682
|
+
const idx = indexOf(graph);
|
|
683
|
+
const crIds = new Set(idx.elementsOfType('CR').map(e => e.id));
|
|
684
|
+
return idx.elementsOfType('MS')
|
|
685
|
+
.filter(ms => !idx.in(ms.id, 'relation').some(t => crIds.has(t.source)))
|
|
615
686
|
.map(ms => ({
|
|
616
687
|
rule_id: 'MS-01',
|
|
617
688
|
severity: 'warning',
|
|
@@ -625,7 +696,8 @@ function msEmptyScope(graph) {
|
|
|
625
696
|
// MS-02: Milestone depends-on dangling target
|
|
626
697
|
// ---------------------------------------------------------------------------
|
|
627
698
|
function msDanglingDependency(graph) {
|
|
628
|
-
const
|
|
699
|
+
const idx = indexOf(graph);
|
|
700
|
+
const msIds = new Set(idx.elementsOfType('MS').map(e => e.id));
|
|
629
701
|
return graph.traces
|
|
630
702
|
.filter(t => t.type === 'relation' && t.label === 'depends-on' && msIds.has(t.source))
|
|
631
703
|
.filter(t => !msIds.has(t.target))
|
|
@@ -646,7 +718,9 @@ function msDanglingDependency(graph) {
|
|
|
646
718
|
// here we only judge pairs whose endpoints both resolve. Audit traces are exempt.
|
|
647
719
|
// ---------------------------------------------------------------------------
|
|
648
720
|
function validTracePattern(graph) {
|
|
649
|
-
const
|
|
721
|
+
const idx = indexOf(graph);
|
|
722
|
+
// CR-SM-264: die Typ-Map kommt aus dem Index statt je Aufruf neu gebaut zu werden.
|
|
723
|
+
const typeById = { get: (id) => idx.typeOf(id) };
|
|
650
724
|
return graph.traces
|
|
651
725
|
.filter(t => t.category !== 'audit')
|
|
652
726
|
.filter(t => {
|
|
@@ -676,8 +750,8 @@ function validTracePattern(graph) {
|
|
|
676
750
|
// only and surfaces an unbound runnable TEST in rules_evaluate / readiness.
|
|
677
751
|
// ---------------------------------------------------------------------------
|
|
678
752
|
function testMustHaveRunnableBinding(graph) {
|
|
679
|
-
|
|
680
|
-
|
|
753
|
+
const idx = indexOf(graph);
|
|
754
|
+
return idx.elementsOfType('TEST')
|
|
681
755
|
.filter(e => e.attributes?.concept !== true) // concept-only TESTs are exempt
|
|
682
756
|
// CR-SM-231: `testRefs` (Array, min 1) statt `testRef` (Objekt). Semantik unveraendert —
|
|
683
757
|
// Praesenz und Form, keine Datei-Existenz; die pruefst RC-02.
|
|
@@ -708,6 +782,7 @@ function testMustHaveRunnableBinding(graph) {
|
|
|
708
782
|
// zu sein.
|
|
709
783
|
// ---------------------------------------------------------------------------
|
|
710
784
|
function testFileExclusivity(graph) {
|
|
785
|
+
const idx = indexOf(graph);
|
|
711
786
|
const claimedBy = new Map();
|
|
712
787
|
for (const el of graph.elements) {
|
|
713
788
|
if (el.type !== 'TEST')
|
|
@@ -737,7 +812,7 @@ function testFileExclusivity(graph) {
|
|
|
737
812
|
element_id: owner,
|
|
738
813
|
message: `${owner} claims test file '${file}', which is also claimed by ${sorted.filter(o => o !== owner).join(', ')}`,
|
|
739
814
|
fix_hint: 'A test file belongs to at most one TEST. Split the file, or drop the entry from all but the one acceptance it really evidences',
|
|
740
|
-
context: { element_type: 'TEST', element_name:
|
|
815
|
+
context: { element_type: 'TEST', element_name: idx.byId.get(owner)?.name },
|
|
741
816
|
});
|
|
742
817
|
}
|
|
743
818
|
}
|
|
@@ -757,7 +832,8 @@ function testFileExclusivity(graph) {
|
|
|
757
832
|
// RC-01's job); this guards presence/shape only. Cycle-safe.
|
|
758
833
|
// ---------------------------------------------------------------------------
|
|
759
834
|
function funcMustHaveCodeBinding(graph) {
|
|
760
|
-
const
|
|
835
|
+
const idx = indexOf(graph);
|
|
836
|
+
const funcById = new Map(idx.elementsOfType('FUNC').map(e => [e.id, e]));
|
|
761
837
|
const composeFuncChildren = (id) => graph.traces
|
|
762
838
|
.filter(t => t.source === id && t.type === 'compose' && funcById.has(t.target))
|
|
763
839
|
.map(t => t.target);
|
|
@@ -848,9 +924,10 @@ function funcMustHaveCodeBinding(graph) {
|
|
|
848
924
|
// choice. The FCHAIN is the modelled claim; the test is owed on the claim.
|
|
849
925
|
// ---------------------------------------------------------------------------
|
|
850
926
|
function fchainMustHaveIntegrationTest(graph) {
|
|
927
|
+
const idx = indexOf(graph);
|
|
851
928
|
const typeOf = new Map(graph.elements.map(e => [e.id, e.type]));
|
|
852
929
|
const isFunc = (id) => typeOf.get(id) === 'FUNC';
|
|
853
|
-
const io =
|
|
930
|
+
const io = idx.tracesOfType('io');
|
|
854
931
|
// FUNC↔FUNC connections via the FLOW hop: (producer FUNC) → FLOW → (consumer FUNC).
|
|
855
932
|
const connections = [];
|
|
856
933
|
for (const e of graph.elements) {
|
|
@@ -873,7 +950,7 @@ function fchainMustHaveIntegrationTest(graph) {
|
|
|
873
950
|
}
|
|
874
951
|
}
|
|
875
952
|
// FCHAINs whose satisfy-REQ is verified by a TEST = chains with an integration test.
|
|
876
|
-
const verifiedReqs = new Set(
|
|
953
|
+
const verifiedReqs = new Set(idx.tracesOfType('verify').map(t => t.target));
|
|
877
954
|
const testedChains = new Set();
|
|
878
955
|
for (const t of graph.traces) {
|
|
879
956
|
if (t.type === 'satisfy' && typeOf.get(t.source) === 'FCHAIN' && verifiedReqs.has(t.target)) {
|
|
@@ -897,7 +974,7 @@ function fchainMustHaveIntegrationTest(graph) {
|
|
|
897
974
|
if (seen.has(key))
|
|
898
975
|
continue;
|
|
899
976
|
seen.add(key);
|
|
900
|
-
const anchorEl =
|
|
977
|
+
const anchorEl = idx.byId.get(anchor);
|
|
901
978
|
violations.push({
|
|
902
979
|
rule_id: 'R-21',
|
|
903
980
|
severity: 'warning',
|
|
@@ -919,11 +996,12 @@ function fchainMustHaveIntegrationTest(graph) {
|
|
|
919
996
|
// deliberately not checked (CR-GVE-173/174).
|
|
920
997
|
// ---------------------------------------------------------------------------
|
|
921
998
|
function funcMustBeAllocated(graph) {
|
|
922
|
-
const
|
|
923
|
-
const
|
|
999
|
+
const idx = indexOf(graph);
|
|
1000
|
+
const funcs = idx.elementsOfType('FUNC');
|
|
1001
|
+
const mods = idx.elementsOfType('MOD');
|
|
924
1002
|
const modIds = new Set(mods.map(m => m.id));
|
|
925
1003
|
return funcs
|
|
926
|
-
.filter(fn => !
|
|
1004
|
+
.filter(fn => !idx.out(fn.id, 'allocate').some(t => modIds.has(t.target)))
|
|
927
1005
|
.map(fn => ({
|
|
928
1006
|
rule_id: 'R-22',
|
|
929
1007
|
severity: 'warning',
|
|
@@ -946,13 +1024,13 @@ function funcMustBeAllocated(graph) {
|
|
|
946
1024
|
// R-22: one rule per element perspective (like R-01 REQ-side beside R-02/R-05).
|
|
947
1025
|
// ---------------------------------------------------------------------------
|
|
948
1026
|
function modMustHaveAllocatedFunc(graph) {
|
|
949
|
-
const
|
|
950
|
-
const
|
|
1027
|
+
const idx = indexOf(graph);
|
|
1028
|
+
const mods = idx.elementsOfType('MOD');
|
|
1029
|
+
const funcs = idx.elementsOfType('FUNC');
|
|
951
1030
|
const modIds = new Set(mods.map(m => m.id));
|
|
952
|
-
const unallocatedFuncs = funcs.filter(fn => !
|
|
1031
|
+
const unallocatedFuncs = funcs.filter(fn => !idx.out(fn.id, 'allocate').some(t => modIds.has(t.target)));
|
|
953
1032
|
return mods
|
|
954
|
-
.filter(mod => !
|
|
955
|
-
graph.elements.some(e => e.id === t.source && e.type === 'FUNC')))
|
|
1033
|
+
.filter(mod => !idx.in(mod.id, 'allocate').some(t => idx.typeOf(t.source) === 'FUNC'))
|
|
956
1034
|
.map(mod => ({
|
|
957
1035
|
rule_id: 'R-23',
|
|
958
1036
|
severity: 'warning',
|
|
@@ -980,8 +1058,8 @@ function modMustHaveAllocatedFunc(graph) {
|
|
|
980
1058
|
// out of scope here (pure, no I/O — that is RC-03's job).
|
|
981
1059
|
// ---------------------------------------------------------------------------
|
|
982
1060
|
function schemaMustHaveSchemaRef(graph) {
|
|
983
|
-
|
|
984
|
-
|
|
1061
|
+
const idx = indexOf(graph);
|
|
1062
|
+
return idx.elementsOfType('SCHEMA')
|
|
985
1063
|
.filter(e => e.attributes?.concept !== true && e.attributes?.external !== true)
|
|
986
1064
|
.filter(e => !RealRefSchema.safeParse(e.attributes?.realRef).success)
|
|
987
1065
|
.map(sc => ({
|
|
@@ -1004,8 +1082,9 @@ function schemaMustHaveSchemaRef(graph) {
|
|
|
1004
1082
|
// realRef with a file. RESOLUTION (file on disk) is a consumer/RC concern.
|
|
1005
1083
|
// ---------------------------------------------------------------------------
|
|
1006
1084
|
function physicalModMustHaveRealRef(graph) {
|
|
1007
|
-
|
|
1008
|
-
|
|
1085
|
+
const idx = indexOf(graph);
|
|
1086
|
+
return idx.elementsOfType('MOD')
|
|
1087
|
+
.filter(e => e.attributes?.kind === 'physical')
|
|
1009
1088
|
.filter(e => e.attributes?.concept !== true && e.attributes?.external !== true)
|
|
1010
1089
|
.filter(e => !RealRefSchema.safeParse(e.attributes?.realRef).success)
|
|
1011
1090
|
.map(mod => ({
|
|
@@ -1056,13 +1135,13 @@ function physicalModMustHaveRealRef(graph) {
|
|
|
1056
1135
|
// waere Neubau-Zwang statt Steuerung.
|
|
1057
1136
|
// ---------------------------------------------------------------------------
|
|
1058
1137
|
function funcMustBeInEffectChain(graph) {
|
|
1059
|
-
const
|
|
1060
|
-
const
|
|
1138
|
+
const idx = indexOf(graph);
|
|
1139
|
+
const funcs = idx.elementsOfType('FUNC');
|
|
1140
|
+
const fchains = idx.elementsOfType('FCHAIN');
|
|
1061
1141
|
const funcIds = new Set(funcs.map(f => f.id));
|
|
1062
1142
|
const fchainIds = new Set(fchains.map(fc => fc.id));
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
.map(t => t.source));
|
|
1143
|
+
// CR-SM-256: dieselbe Definition wie R-02 und R-31, nicht mehr die lokale Kopie.
|
|
1144
|
+
const decomposed = decomposedFuncs(graph);
|
|
1066
1145
|
const inChain = new Set(graph.traces
|
|
1067
1146
|
.filter(t => t.type === 'compose' && fchainIds.has(t.source) && funcIds.has(t.target))
|
|
1068
1147
|
.map(t => t.target));
|
|
@@ -1092,9 +1171,16 @@ function funcMustBeInEffectChain(graph) {
|
|
|
1092
1171
|
// eigenen Nenner-Beitrag, was CR-SM-242 fuer IO-01 als Fehlmessung nachgewiesen hat.
|
|
1093
1172
|
// ---------------------------------------------------------------------------
|
|
1094
1173
|
function funcMustBeWired(graph) {
|
|
1095
|
-
const
|
|
1096
|
-
|
|
1097
|
-
|
|
1174
|
+
const idx = indexOf(graph);
|
|
1175
|
+
// CR-SM-256: Grundgesamtheit sind die BLAETTER, wie bei R-02 und R-30. Durch einen
|
|
1176
|
+
// Blackbox-Block fliessen keine Daten — die io-Kanten haengen an seinen Kindern.
|
|
1177
|
+
// CR-GC-375 hat das fuer R-31 ausdruecklich offengelassen ("eine eigene Frage, hier
|
|
1178
|
+
// bewusst nicht"); die Messung liegt jetzt vor: 12 von 13 Bloecken am
|
|
1179
|
+
// graphcode-Selbstmodell melden nur deshalb, und keiner aus einem anderen Grund.
|
|
1180
|
+
const decomposed = decomposedFuncs(graph);
|
|
1181
|
+
const funcs = idx.elementsOfType('FUNC').filter(e => !decomposed.has(e.id));
|
|
1182
|
+
const flows = idx.elementsOfType('FLOW');
|
|
1183
|
+
const io = idx.tracesOfType('io');
|
|
1098
1184
|
const hasInput = new Set(io.map(t => t.target)); // FLOW -io-> FUNC
|
|
1099
1185
|
const hasOutput = new Set(io.map(t => t.source)); // FUNC -io-> FLOW
|
|
1100
1186
|
return funcs
|