@sigloch/contracts 6.1.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/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 +155 -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({
|
|
@@ -44,21 +45,25 @@ export const RuleViolation = z.object({
|
|
|
44
45
|
* Blaettern (so schon R-30 seit CR-SM-249, so R-20 seit CR-210).
|
|
45
46
|
*
|
|
46
47
|
* Bewusst EIN Helper statt drei Kopien: eine zweite Definition von "Blatt" waere ein
|
|
47
|
-
* zweiter Weg zu derselben Aussage, und der billigere gewinnt immer.
|
|
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.
|
|
48
50
|
*/
|
|
49
|
-
function decomposedFuncs(graph) {
|
|
50
|
-
const
|
|
51
|
+
export function decomposedFuncs(graph) {
|
|
52
|
+
const idx = indexOf(graph);
|
|
53
|
+
const funcIds = new Set(idx.elementsOfType('FUNC').map(f => f.id));
|
|
51
54
|
return new Set(graph.traces
|
|
52
55
|
.filter(t => t.type === 'compose' && funcIds.has(t.source) && funcIds.has(t.target))
|
|
53
56
|
.map(t => t.source));
|
|
54
57
|
}
|
|
55
58
|
function findParentModule(graph, elementId) {
|
|
56
|
-
const
|
|
59
|
+
const idx = indexOf(graph);
|
|
60
|
+
const trace = idx.out(elementId, 'allocate')[0];
|
|
57
61
|
return trace?.target;
|
|
58
62
|
}
|
|
59
63
|
/** Get all outgoing traces of a given type from an element. */
|
|
60
64
|
function outgoingTraces(graph, elementId, traceType) {
|
|
61
|
-
|
|
65
|
+
const idx = indexOf(graph);
|
|
66
|
+
return traceType === undefined ? idx.outAll(elementId) : idx.out(elementId, traceType);
|
|
62
67
|
}
|
|
63
68
|
/** Tokenize id/name/description into a lowercase token set (drops tokens < 3 chars). */
|
|
64
69
|
function overlapTokens(...parts) {
|
|
@@ -69,35 +74,69 @@ function overlapTokens(...parts) {
|
|
|
69
74
|
.split(/[^a-z0-9]+/)
|
|
70
75
|
.filter(t => t.length >= 3));
|
|
71
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
|
+
}
|
|
72
101
|
/**
|
|
73
102
|
* Map elements to ViolationCandidate format. When `ref` is given, rank candidates
|
|
74
103
|
* by id/name/description token overlap with `ref` (descending) so the most relevant
|
|
75
104
|
* target is first — e.g. REQ-bootstrap → TEST-bootstrap (CR-GC-203 item 3). Ranking
|
|
76
105
|
* is a hint, not an auto-link; semantic confirmation stays with the caller.
|
|
77
106
|
*/
|
|
78
|
-
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) {
|
|
79
112
|
if (!ref)
|
|
80
113
|
return elements.map(e => ({ id: e.id, type: e.type, name: e.name }));
|
|
81
|
-
const refTokens =
|
|
114
|
+
const refTokens = tokensOf(ref);
|
|
82
115
|
const score = (e) => {
|
|
83
116
|
let n = 0;
|
|
84
|
-
for (const tok of
|
|
117
|
+
for (const tok of tokensOf(e))
|
|
85
118
|
if (refTokens.has(tok))
|
|
86
119
|
n++;
|
|
87
120
|
return n;
|
|
88
121
|
};
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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 }));
|
|
92
130
|
}
|
|
93
131
|
// ---------------------------------------------------------------------------
|
|
94
132
|
// R-01: Every REQ must have at least one verify trace
|
|
95
133
|
// ---------------------------------------------------------------------------
|
|
96
134
|
function reqMustHaveVerification(graph) {
|
|
97
|
-
const
|
|
98
|
-
const
|
|
135
|
+
const idx = indexOf(graph);
|
|
136
|
+
const reqs = idx.elementsOfType('REQ');
|
|
137
|
+
const tests = idx.elementsOfType('TEST');
|
|
99
138
|
return reqs
|
|
100
|
-
.filter(req =>
|
|
139
|
+
.filter(req => idx.in(req.id, 'verify').length === 0)
|
|
101
140
|
.map(req => ({
|
|
102
141
|
rule_id: 'R-01',
|
|
103
142
|
severity: 'error',
|
|
@@ -116,14 +155,15 @@ function reqMustHaveVerification(graph) {
|
|
|
116
155
|
// R-02: Every FUNC must satisfy at least one REQ
|
|
117
156
|
// ---------------------------------------------------------------------------
|
|
118
157
|
function funcMustSatisfyReq(graph) {
|
|
158
|
+
const idx = indexOf(graph);
|
|
119
159
|
// CR-SM-256: Grundgesamtheit sind die BLAETTER. Ein zerlegter FUNC erfuellt keine REQ,
|
|
120
160
|
// die nicht schon eine seiner Blatt-Funktionen erfuellt — wer ihn befriedigen will,
|
|
121
161
|
// muss eine satisfy-Kante erfinden, die dieselbe REQ ein zweites Mal beansprucht.
|
|
122
162
|
// Am graphcode-Selbstmodell (graphVersion 171) melden 12 der 13 Bloecke R-02 UND R-31,
|
|
123
163
|
// also dieselbe Ursache zweimal gezaehlt — die Klasse CR-SM-235/-242.
|
|
124
164
|
const decomposed = decomposedFuncs(graph);
|
|
125
|
-
const funcs =
|
|
126
|
-
const allReqs =
|
|
165
|
+
const funcs = idx.elementsOfType('FUNC').filter(e => !decomposed.has(e.id));
|
|
166
|
+
const allReqs = idx.elementsOfType('REQ');
|
|
127
167
|
// CR-GC-366: der Filter prueft den ZIELTYP. Vorher war er zieltyp-blind (`type === 'satisfy'`),
|
|
128
168
|
// womit ein `FUNC -satisfy-> UC` die Regel stumm schaltete, obwohl kein Requirement erfuellt war —
|
|
129
169
|
// der Regelname versprach mehr als der Code pruefte. Am graphcode-Selbstmodell verdeckte das 6 von
|
|
@@ -132,15 +172,15 @@ function funcMustSatisfyReq(graph) {
|
|
|
132
172
|
// Ziele das Meta-Modell gerade zulaesst.
|
|
133
173
|
const reqIds = new Set(allReqs.map(r => r.id));
|
|
134
174
|
return funcs
|
|
135
|
-
.filter(fn => !
|
|
175
|
+
.filter(fn => !idx.out(fn.id, 'satisfy').some(t => reqIds.has(t.target)))
|
|
136
176
|
.map(fn => {
|
|
137
177
|
const modId = findParentModule(graph, fn.id);
|
|
138
178
|
// Candidate REQs: same module's other FUNCs satisfy these, or constraints referencing this func
|
|
139
179
|
const siblingReqs = modId
|
|
140
180
|
? graph.traces
|
|
141
181
|
.filter(t => t.type === 'allocate' && t.target === modId && t.source !== fn.id)
|
|
142
|
-
.flatMap(t =>
|
|
143
|
-
.map(st =>
|
|
182
|
+
.flatMap(t => idx.out(t.source, 'satisfy'))
|
|
183
|
+
.map(st => idx.byId.get(st.target))
|
|
144
184
|
.filter((e) => !!e)
|
|
145
185
|
: [];
|
|
146
186
|
// Also include constraints whose description mentions this func's name
|
|
@@ -167,12 +207,13 @@ function funcMustSatisfyReq(graph) {
|
|
|
167
207
|
// R-03: ASIL isolation
|
|
168
208
|
// ---------------------------------------------------------------------------
|
|
169
209
|
function asilIsolation(graph) {
|
|
210
|
+
const idx = indexOf(graph);
|
|
170
211
|
const violations = [];
|
|
171
|
-
const modules =
|
|
212
|
+
const modules = idx.elementsOfType('MOD');
|
|
172
213
|
for (const mod of modules) {
|
|
173
214
|
const allocated = graph.traces
|
|
174
215
|
.filter(t => t.target === mod.id && t.type === 'allocate')
|
|
175
|
-
.map(t =>
|
|
216
|
+
.map(t => idx.byId.get(t.source))
|
|
176
217
|
.filter((e) => !!e);
|
|
177
218
|
const hasD = allocated.some(e => e.asil === 'D');
|
|
178
219
|
const hasQM = allocated.some(e => e.asil === 'QM');
|
|
@@ -206,22 +247,22 @@ function asilIsolation(graph) {
|
|
|
206
247
|
// `null` → messen, nicht urteilen: die Regel schweigt.
|
|
207
248
|
// ---------------------------------------------------------------------------
|
|
208
249
|
function maxModuleSize(graph, policy) {
|
|
250
|
+
const idx = indexOf(graph);
|
|
209
251
|
const violations = [];
|
|
210
252
|
const steps = policy.moduleSize;
|
|
211
253
|
if (steps === null)
|
|
212
254
|
return violations;
|
|
213
|
-
const modules =
|
|
255
|
+
const modules = idx.elementsOfType('MOD');
|
|
214
256
|
for (const mod of modules) {
|
|
215
|
-
const allocatedIds =
|
|
216
|
-
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);
|
|
217
259
|
const funcCount = allocated.length;
|
|
218
260
|
if (funcCount <= steps.coupled)
|
|
219
261
|
continue;
|
|
220
262
|
// Count crossing flows: io paths from FUNCs in this module to FUNCs in other modules
|
|
221
263
|
const funcIds = new Set(allocatedIds);
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
return false;
|
|
264
|
+
// CR-SM-264: nur die io-Kanten, nicht alle Traces je MOD.
|
|
265
|
+
const crossings = idx.tracesOfType('io').filter(t => {
|
|
225
266
|
const srcIn = funcIds.has(t.source);
|
|
226
267
|
const tgtIn = funcIds.has(t.target);
|
|
227
268
|
return (srcIn && !tgtIn) || (!srcIn && tgtIn);
|
|
@@ -263,10 +304,11 @@ function maxModuleSize(graph, policy) {
|
|
|
263
304
|
// R-05: Every TEST must verify at least one REQ
|
|
264
305
|
// ---------------------------------------------------------------------------
|
|
265
306
|
function testMustVerifyReq(graph) {
|
|
266
|
-
const
|
|
267
|
-
const
|
|
307
|
+
const idx = indexOf(graph);
|
|
308
|
+
const tests = idx.elementsOfType('TEST');
|
|
309
|
+
const allReqs = idx.elementsOfType('REQ');
|
|
268
310
|
return tests
|
|
269
|
-
.filter(test =>
|
|
311
|
+
.filter(test => idx.out(test.id, 'verify').length === 0)
|
|
270
312
|
.map(test => ({
|
|
271
313
|
rule_id: 'R-05',
|
|
272
314
|
severity: 'warning',
|
|
@@ -290,11 +332,12 @@ function testMustVerifyReq(graph) {
|
|
|
290
332
|
// R-08: Bidirectional trace consistency
|
|
291
333
|
// ---------------------------------------------------------------------------
|
|
292
334
|
function traceConsistency(graph) {
|
|
335
|
+
const idx = indexOf(graph);
|
|
293
336
|
return graph.traces
|
|
294
337
|
.filter(t => t.category !== 'audit')
|
|
295
338
|
.filter(t => {
|
|
296
|
-
const sourceExists =
|
|
297
|
-
const targetExists =
|
|
339
|
+
const sourceExists = idx.byId.has(t.source);
|
|
340
|
+
const targetExists = idx.byId.has(t.target);
|
|
298
341
|
return !sourceExists || !targetExists;
|
|
299
342
|
})
|
|
300
343
|
.map(t => ({
|
|
@@ -315,11 +358,12 @@ function traceConsistency(graph) {
|
|
|
315
358
|
// concern, stricter check — no parallel path); up to two warnings per FLOW.
|
|
316
359
|
// ---------------------------------------------------------------------------
|
|
317
360
|
function flowCompleteness(graph) {
|
|
318
|
-
const
|
|
361
|
+
const idx = indexOf(graph);
|
|
362
|
+
const flows = idx.elementsOfType('FLOW');
|
|
319
363
|
// Producers/consumers of a FLOW are FUNCs or ACTORs (io endpoints on the
|
|
320
364
|
// upstream/downstream side); UC may consume too. Offer both as candidates.
|
|
321
|
-
const sources = graph.elements.filter(e => e.type === 'FUNC' || e.type === 'ACTOR');
|
|
322
|
-
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');
|
|
323
367
|
const violations = [];
|
|
324
368
|
for (const f of flows) {
|
|
325
369
|
const ctx = { element_type: f.type, element_name: f.name, candidate_targets: toCandidates(sources) };
|
|
@@ -361,6 +405,7 @@ function flowCompleteness(graph) {
|
|
|
361
405
|
/** Trace types on which a direct 2-cycle is a real finding (CR-GC-315). */
|
|
362
406
|
const CIRCULAR_TRACE_TYPES = new Set(['compose', 'allocate', 'relation']);
|
|
363
407
|
function noDirectCircular(graph) {
|
|
408
|
+
const idx = indexOf(graph);
|
|
364
409
|
const violations = [];
|
|
365
410
|
// Direction-independent key: A↔B is ONE cycle, reported once. Keying on the
|
|
366
411
|
// message (CR-GC-315 predecessor) never collapsed anything — the two
|
|
@@ -369,7 +414,7 @@ function noDirectCircular(graph) {
|
|
|
369
414
|
for (const t of graph.traces) {
|
|
370
415
|
if (!CIRCULAR_TRACE_TYPES.has(t.type))
|
|
371
416
|
continue;
|
|
372
|
-
if (!
|
|
417
|
+
if (!idx.out(t.target, t.type).some(other => other.target === t.source))
|
|
373
418
|
continue;
|
|
374
419
|
const key = `${t.type}|${[t.source, t.target].sort().join('|')}`;
|
|
375
420
|
if (seen.has(key))
|
|
@@ -391,12 +436,12 @@ function noDirectCircular(graph) {
|
|
|
391
436
|
// RD-01: Unresolved requirement (leaf REQ without satisfy)
|
|
392
437
|
// ---------------------------------------------------------------------------
|
|
393
438
|
function unresolvedRequirement(graph) {
|
|
394
|
-
const
|
|
439
|
+
const idx = indexOf(graph);
|
|
440
|
+
const reqs = idx.elementsOfType('REQ');
|
|
395
441
|
// Leaf REQs: no compose→REQ children
|
|
396
|
-
const leafReqs = reqs.filter(req => !
|
|
397
|
-
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'));
|
|
398
443
|
return leafReqs
|
|
399
|
-
.filter(req =>
|
|
444
|
+
.filter(req => idx.in(req.id, 'satisfy').length === 0)
|
|
400
445
|
.map(req => ({
|
|
401
446
|
rule_id: 'RD-01',
|
|
402
447
|
severity: 'warning',
|
|
@@ -415,12 +460,11 @@ function unresolvedRequirement(graph) {
|
|
|
415
460
|
// RD-02: Decomposition consistency (parent REQ should not have FUNC satisfy)
|
|
416
461
|
// ---------------------------------------------------------------------------
|
|
417
462
|
function decompositionConsistency(graph) {
|
|
418
|
-
const
|
|
419
|
-
const
|
|
420
|
-
|
|
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'));
|
|
421
466
|
return parentReqs
|
|
422
|
-
.filter(req =>
|
|
423
|
-
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'))
|
|
424
468
|
.map(req => ({
|
|
425
469
|
rule_id: 'RD-02',
|
|
426
470
|
severity: 'warning',
|
|
@@ -434,18 +478,18 @@ function decompositionConsistency(graph) {
|
|
|
434
478
|
// RD-03: No premature decomposition (all children same satisfy target)
|
|
435
479
|
// ---------------------------------------------------------------------------
|
|
436
480
|
function noPrematureDecomposition(graph) {
|
|
437
|
-
const
|
|
438
|
-
const
|
|
439
|
-
|
|
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'));
|
|
440
484
|
return parentReqs
|
|
441
485
|
.filter(parent => {
|
|
442
486
|
const childIds = graph.traces
|
|
443
487
|
.filter(t => t.source === parent.id && t.type === 'compose' &&
|
|
444
|
-
|
|
488
|
+
idx.typeOf(t.target) === 'REQ')
|
|
445
489
|
.map(t => t.target);
|
|
446
490
|
if (childIds.length < 2)
|
|
447
491
|
return false;
|
|
448
|
-
const satisfyTargets = childIds.map(cid =>
|
|
492
|
+
const satisfyTargets = childIds.map(cid => idx.in(cid, 'satisfy').filter(t => t.source !== cid).map(t => t.source));
|
|
449
493
|
// All children satisfied by the same single source
|
|
450
494
|
const allSame = satisfyTargets.every(targets => targets.length === 1 && targets[0] === satisfyTargets[0]?.[0]);
|
|
451
495
|
return allSame && satisfyTargets[0]?.length === 1;
|
|
@@ -473,8 +517,10 @@ function noPrematureDecomposition(graph) {
|
|
|
473
517
|
*/
|
|
474
518
|
const DECOMPOSITION_BREADTH_MAX = 11;
|
|
475
519
|
function decompositionBreadth(graph) {
|
|
476
|
-
const
|
|
477
|
-
|
|
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;
|
|
478
524
|
const counts = new Map();
|
|
479
525
|
const bump = (parentId, kind) => {
|
|
480
526
|
const key = `${parentId}\u0000${kind}`;
|
|
@@ -485,8 +531,8 @@ function decompositionBreadth(graph) {
|
|
|
485
531
|
counts.set(key, { parentId, kind, n: 1 });
|
|
486
532
|
};
|
|
487
533
|
for (const t of graph.traces) {
|
|
488
|
-
const src = typeOf.
|
|
489
|
-
const tgt = typeOf.
|
|
534
|
+
const src = typeOf.typeOf(t.source);
|
|
535
|
+
const tgt = typeOf.typeOf(t.target);
|
|
490
536
|
if (t.type === 'compose' && src === 'FUNC' && tgt === 'FUNC') {
|
|
491
537
|
bump(t.source, 'sub-FUNC');
|
|
492
538
|
}
|
|
@@ -515,9 +561,10 @@ function decompositionBreadth(graph) {
|
|
|
515
561
|
// R-14: UC must have at least 1 compose trace (→ FCHAIN or REQ) (CR-117)
|
|
516
562
|
// ---------------------------------------------------------------------------
|
|
517
563
|
function ucMustHaveCompose(graph) {
|
|
518
|
-
const
|
|
564
|
+
const idx = indexOf(graph);
|
|
565
|
+
const ucs = idx.elementsOfType('UC');
|
|
519
566
|
return ucs
|
|
520
|
-
.filter(uc =>
|
|
567
|
+
.filter(uc => idx.out(uc.id, 'compose').length === 0)
|
|
521
568
|
.map(uc => ({
|
|
522
569
|
rule_id: 'R-14',
|
|
523
570
|
severity: 'warning',
|
|
@@ -549,14 +596,15 @@ function ucMustHaveCompose(graph) {
|
|
|
549
596
|
// einen einzigen Befund ueber beide Selbstmodelle.
|
|
550
597
|
// ---------------------------------------------------------------------------
|
|
551
598
|
function fchainCompleteness(graph) {
|
|
552
|
-
const
|
|
553
|
-
const
|
|
554
|
-
const
|
|
599
|
+
const idx = indexOf(graph);
|
|
600
|
+
const fchains = idx.elementsOfType('FCHAIN');
|
|
601
|
+
const funcs = idx.elementsOfType('FUNC');
|
|
602
|
+
const ucs = idx.elementsOfType('UC');
|
|
555
603
|
const ucIds = new Set(ucs.map(u => u.id));
|
|
556
604
|
const violations = [];
|
|
557
605
|
for (const fc of fchains) {
|
|
558
606
|
const ctx = { element_type: fc.type, element_name: fc.name };
|
|
559
|
-
if (
|
|
607
|
+
if (idx.out(fc.id, 'compose').length === 0) {
|
|
560
608
|
violations.push({
|
|
561
609
|
rule_id: 'R-15',
|
|
562
610
|
severity: 'warning',
|
|
@@ -566,7 +614,7 @@ function fchainCompleteness(graph) {
|
|
|
566
614
|
context: { ...ctx, candidate_targets: toCandidates(funcs) },
|
|
567
615
|
});
|
|
568
616
|
}
|
|
569
|
-
if (!
|
|
617
|
+
if (!idx.in(fc.id, 'compose').some(t => ucIds.has(t.source))) {
|
|
570
618
|
violations.push({
|
|
571
619
|
rule_id: 'R-15',
|
|
572
620
|
severity: 'warning',
|
|
@@ -583,9 +631,10 @@ function fchainCompleteness(graph) {
|
|
|
583
631
|
// R-16: ACTOR must have at least 1 io trace (CR-117)
|
|
584
632
|
// ---------------------------------------------------------------------------
|
|
585
633
|
function actorMustHaveTrace(graph) {
|
|
586
|
-
const
|
|
634
|
+
const idx = indexOf(graph);
|
|
635
|
+
const actors = idx.elementsOfType('ACTOR');
|
|
587
636
|
return actors
|
|
588
|
-
.filter(a =>
|
|
637
|
+
.filter(a => idx.out(a.id, 'io').length === 0 && idx.in(a.id, 'io').length === 0)
|
|
589
638
|
.map(a => ({
|
|
590
639
|
rule_id: 'R-16',
|
|
591
640
|
severity: 'warning',
|
|
@@ -602,9 +651,10 @@ function actorMustHaveTrace(graph) {
|
|
|
602
651
|
// R-17: SYS must have at least 1 compose trace (CR-117)
|
|
603
652
|
// ---------------------------------------------------------------------------
|
|
604
653
|
function sysMustHaveCompose(graph) {
|
|
605
|
-
const
|
|
654
|
+
const idx = indexOf(graph);
|
|
655
|
+
const systems = idx.elementsOfType('SYS');
|
|
606
656
|
return systems
|
|
607
|
-
.filter(sys =>
|
|
657
|
+
.filter(sys => idx.out(sys.id, 'compose').length === 0)
|
|
608
658
|
.map(sys => ({
|
|
609
659
|
rule_id: 'R-17',
|
|
610
660
|
severity: 'warning',
|
|
@@ -629,10 +679,10 @@ function sysMustHaveCompose(graph) {
|
|
|
629
679
|
// zusaetzlich compose) — genau deshalb ist jetzt der Zeitpunkt.
|
|
630
680
|
// ---------------------------------------------------------------------------
|
|
631
681
|
function msEmptyScope(graph) {
|
|
632
|
-
const
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
.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)))
|
|
636
686
|
.map(ms => ({
|
|
637
687
|
rule_id: 'MS-01',
|
|
638
688
|
severity: 'warning',
|
|
@@ -646,7 +696,8 @@ function msEmptyScope(graph) {
|
|
|
646
696
|
// MS-02: Milestone depends-on dangling target
|
|
647
697
|
// ---------------------------------------------------------------------------
|
|
648
698
|
function msDanglingDependency(graph) {
|
|
649
|
-
const
|
|
699
|
+
const idx = indexOf(graph);
|
|
700
|
+
const msIds = new Set(idx.elementsOfType('MS').map(e => e.id));
|
|
650
701
|
return graph.traces
|
|
651
702
|
.filter(t => t.type === 'relation' && t.label === 'depends-on' && msIds.has(t.source))
|
|
652
703
|
.filter(t => !msIds.has(t.target))
|
|
@@ -667,7 +718,9 @@ function msDanglingDependency(graph) {
|
|
|
667
718
|
// here we only judge pairs whose endpoints both resolve. Audit traces are exempt.
|
|
668
719
|
// ---------------------------------------------------------------------------
|
|
669
720
|
function validTracePattern(graph) {
|
|
670
|
-
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) };
|
|
671
724
|
return graph.traces
|
|
672
725
|
.filter(t => t.category !== 'audit')
|
|
673
726
|
.filter(t => {
|
|
@@ -697,8 +750,8 @@ function validTracePattern(graph) {
|
|
|
697
750
|
// only and surfaces an unbound runnable TEST in rules_evaluate / readiness.
|
|
698
751
|
// ---------------------------------------------------------------------------
|
|
699
752
|
function testMustHaveRunnableBinding(graph) {
|
|
700
|
-
|
|
701
|
-
|
|
753
|
+
const idx = indexOf(graph);
|
|
754
|
+
return idx.elementsOfType('TEST')
|
|
702
755
|
.filter(e => e.attributes?.concept !== true) // concept-only TESTs are exempt
|
|
703
756
|
// CR-SM-231: `testRefs` (Array, min 1) statt `testRef` (Objekt). Semantik unveraendert —
|
|
704
757
|
// Praesenz und Form, keine Datei-Existenz; die pruefst RC-02.
|
|
@@ -729,6 +782,7 @@ function testMustHaveRunnableBinding(graph) {
|
|
|
729
782
|
// zu sein.
|
|
730
783
|
// ---------------------------------------------------------------------------
|
|
731
784
|
function testFileExclusivity(graph) {
|
|
785
|
+
const idx = indexOf(graph);
|
|
732
786
|
const claimedBy = new Map();
|
|
733
787
|
for (const el of graph.elements) {
|
|
734
788
|
if (el.type !== 'TEST')
|
|
@@ -758,7 +812,7 @@ function testFileExclusivity(graph) {
|
|
|
758
812
|
element_id: owner,
|
|
759
813
|
message: `${owner} claims test file '${file}', which is also claimed by ${sorted.filter(o => o !== owner).join(', ')}`,
|
|
760
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',
|
|
761
|
-
context: { element_type: 'TEST', element_name:
|
|
815
|
+
context: { element_type: 'TEST', element_name: idx.byId.get(owner)?.name },
|
|
762
816
|
});
|
|
763
817
|
}
|
|
764
818
|
}
|
|
@@ -778,7 +832,8 @@ function testFileExclusivity(graph) {
|
|
|
778
832
|
// RC-01's job); this guards presence/shape only. Cycle-safe.
|
|
779
833
|
// ---------------------------------------------------------------------------
|
|
780
834
|
function funcMustHaveCodeBinding(graph) {
|
|
781
|
-
const
|
|
835
|
+
const idx = indexOf(graph);
|
|
836
|
+
const funcById = new Map(idx.elementsOfType('FUNC').map(e => [e.id, e]));
|
|
782
837
|
const composeFuncChildren = (id) => graph.traces
|
|
783
838
|
.filter(t => t.source === id && t.type === 'compose' && funcById.has(t.target))
|
|
784
839
|
.map(t => t.target);
|
|
@@ -869,9 +924,10 @@ function funcMustHaveCodeBinding(graph) {
|
|
|
869
924
|
// choice. The FCHAIN is the modelled claim; the test is owed on the claim.
|
|
870
925
|
// ---------------------------------------------------------------------------
|
|
871
926
|
function fchainMustHaveIntegrationTest(graph) {
|
|
927
|
+
const idx = indexOf(graph);
|
|
872
928
|
const typeOf = new Map(graph.elements.map(e => [e.id, e.type]));
|
|
873
929
|
const isFunc = (id) => typeOf.get(id) === 'FUNC';
|
|
874
|
-
const io =
|
|
930
|
+
const io = idx.tracesOfType('io');
|
|
875
931
|
// FUNC↔FUNC connections via the FLOW hop: (producer FUNC) → FLOW → (consumer FUNC).
|
|
876
932
|
const connections = [];
|
|
877
933
|
for (const e of graph.elements) {
|
|
@@ -894,7 +950,7 @@ function fchainMustHaveIntegrationTest(graph) {
|
|
|
894
950
|
}
|
|
895
951
|
}
|
|
896
952
|
// FCHAINs whose satisfy-REQ is verified by a TEST = chains with an integration test.
|
|
897
|
-
const verifiedReqs = new Set(
|
|
953
|
+
const verifiedReqs = new Set(idx.tracesOfType('verify').map(t => t.target));
|
|
898
954
|
const testedChains = new Set();
|
|
899
955
|
for (const t of graph.traces) {
|
|
900
956
|
if (t.type === 'satisfy' && typeOf.get(t.source) === 'FCHAIN' && verifiedReqs.has(t.target)) {
|
|
@@ -918,7 +974,7 @@ function fchainMustHaveIntegrationTest(graph) {
|
|
|
918
974
|
if (seen.has(key))
|
|
919
975
|
continue;
|
|
920
976
|
seen.add(key);
|
|
921
|
-
const anchorEl =
|
|
977
|
+
const anchorEl = idx.byId.get(anchor);
|
|
922
978
|
violations.push({
|
|
923
979
|
rule_id: 'R-21',
|
|
924
980
|
severity: 'warning',
|
|
@@ -940,11 +996,12 @@ function fchainMustHaveIntegrationTest(graph) {
|
|
|
940
996
|
// deliberately not checked (CR-GVE-173/174).
|
|
941
997
|
// ---------------------------------------------------------------------------
|
|
942
998
|
function funcMustBeAllocated(graph) {
|
|
943
|
-
const
|
|
944
|
-
const
|
|
999
|
+
const idx = indexOf(graph);
|
|
1000
|
+
const funcs = idx.elementsOfType('FUNC');
|
|
1001
|
+
const mods = idx.elementsOfType('MOD');
|
|
945
1002
|
const modIds = new Set(mods.map(m => m.id));
|
|
946
1003
|
return funcs
|
|
947
|
-
.filter(fn => !
|
|
1004
|
+
.filter(fn => !idx.out(fn.id, 'allocate').some(t => modIds.has(t.target)))
|
|
948
1005
|
.map(fn => ({
|
|
949
1006
|
rule_id: 'R-22',
|
|
950
1007
|
severity: 'warning',
|
|
@@ -967,13 +1024,13 @@ function funcMustBeAllocated(graph) {
|
|
|
967
1024
|
// R-22: one rule per element perspective (like R-01 REQ-side beside R-02/R-05).
|
|
968
1025
|
// ---------------------------------------------------------------------------
|
|
969
1026
|
function modMustHaveAllocatedFunc(graph) {
|
|
970
|
-
const
|
|
971
|
-
const
|
|
1027
|
+
const idx = indexOf(graph);
|
|
1028
|
+
const mods = idx.elementsOfType('MOD');
|
|
1029
|
+
const funcs = idx.elementsOfType('FUNC');
|
|
972
1030
|
const modIds = new Set(mods.map(m => m.id));
|
|
973
|
-
const unallocatedFuncs = funcs.filter(fn => !
|
|
1031
|
+
const unallocatedFuncs = funcs.filter(fn => !idx.out(fn.id, 'allocate').some(t => modIds.has(t.target)));
|
|
974
1032
|
return mods
|
|
975
|
-
.filter(mod => !
|
|
976
|
-
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'))
|
|
977
1034
|
.map(mod => ({
|
|
978
1035
|
rule_id: 'R-23',
|
|
979
1036
|
severity: 'warning',
|
|
@@ -1001,8 +1058,8 @@ function modMustHaveAllocatedFunc(graph) {
|
|
|
1001
1058
|
// out of scope here (pure, no I/O — that is RC-03's job).
|
|
1002
1059
|
// ---------------------------------------------------------------------------
|
|
1003
1060
|
function schemaMustHaveSchemaRef(graph) {
|
|
1004
|
-
|
|
1005
|
-
|
|
1061
|
+
const idx = indexOf(graph);
|
|
1062
|
+
return idx.elementsOfType('SCHEMA')
|
|
1006
1063
|
.filter(e => e.attributes?.concept !== true && e.attributes?.external !== true)
|
|
1007
1064
|
.filter(e => !RealRefSchema.safeParse(e.attributes?.realRef).success)
|
|
1008
1065
|
.map(sc => ({
|
|
@@ -1025,8 +1082,9 @@ function schemaMustHaveSchemaRef(graph) {
|
|
|
1025
1082
|
// realRef with a file. RESOLUTION (file on disk) is a consumer/RC concern.
|
|
1026
1083
|
// ---------------------------------------------------------------------------
|
|
1027
1084
|
function physicalModMustHaveRealRef(graph) {
|
|
1028
|
-
|
|
1029
|
-
|
|
1085
|
+
const idx = indexOf(graph);
|
|
1086
|
+
return idx.elementsOfType('MOD')
|
|
1087
|
+
.filter(e => e.attributes?.kind === 'physical')
|
|
1030
1088
|
.filter(e => e.attributes?.concept !== true && e.attributes?.external !== true)
|
|
1031
1089
|
.filter(e => !RealRefSchema.safeParse(e.attributes?.realRef).success)
|
|
1032
1090
|
.map(mod => ({
|
|
@@ -1077,8 +1135,9 @@ function physicalModMustHaveRealRef(graph) {
|
|
|
1077
1135
|
// waere Neubau-Zwang statt Steuerung.
|
|
1078
1136
|
// ---------------------------------------------------------------------------
|
|
1079
1137
|
function funcMustBeInEffectChain(graph) {
|
|
1080
|
-
const
|
|
1081
|
-
const
|
|
1138
|
+
const idx = indexOf(graph);
|
|
1139
|
+
const funcs = idx.elementsOfType('FUNC');
|
|
1140
|
+
const fchains = idx.elementsOfType('FCHAIN');
|
|
1082
1141
|
const funcIds = new Set(funcs.map(f => f.id));
|
|
1083
1142
|
const fchainIds = new Set(fchains.map(fc => fc.id));
|
|
1084
1143
|
// CR-SM-256: dieselbe Definition wie R-02 und R-31, nicht mehr die lokale Kopie.
|
|
@@ -1112,15 +1171,16 @@ function funcMustBeInEffectChain(graph) {
|
|
|
1112
1171
|
// eigenen Nenner-Beitrag, was CR-SM-242 fuer IO-01 als Fehlmessung nachgewiesen hat.
|
|
1113
1172
|
// ---------------------------------------------------------------------------
|
|
1114
1173
|
function funcMustBeWired(graph) {
|
|
1174
|
+
const idx = indexOf(graph);
|
|
1115
1175
|
// CR-SM-256: Grundgesamtheit sind die BLAETTER, wie bei R-02 und R-30. Durch einen
|
|
1116
1176
|
// Blackbox-Block fliessen keine Daten — die io-Kanten haengen an seinen Kindern.
|
|
1117
1177
|
// CR-GC-375 hat das fuer R-31 ausdruecklich offengelassen ("eine eigene Frage, hier
|
|
1118
1178
|
// bewusst nicht"); die Messung liegt jetzt vor: 12 von 13 Bloecken am
|
|
1119
1179
|
// graphcode-Selbstmodell melden nur deshalb, und keiner aus einem anderen Grund.
|
|
1120
1180
|
const decomposed = decomposedFuncs(graph);
|
|
1121
|
-
const funcs =
|
|
1122
|
-
const flows =
|
|
1123
|
-
const io =
|
|
1181
|
+
const funcs = idx.elementsOfType('FUNC').filter(e => !decomposed.has(e.id));
|
|
1182
|
+
const flows = idx.elementsOfType('FLOW');
|
|
1183
|
+
const io = idx.tracesOfType('io');
|
|
1124
1184
|
const hasInput = new Set(io.map(t => t.target)); // FLOW -io-> FUNC
|
|
1125
1185
|
const hasOutput = new Set(io.map(t => t.source)); // FUNC -io-> FLOW
|
|
1126
1186
|
return funcs
|