@sigloch/contracts 6.3.0 → 10.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/se/ao-rules.d.ts +12 -12
- package/dist/se/ao-rules.js +88 -225
- package/dist/se/conformance-rules.d.ts +28 -0
- package/dist/se/conformance-rules.js +40 -17
- package/dist/se/cr-quality-rules.js +75 -58
- package/dist/se/evaluate-all.d.ts +4 -2
- package/dist/se/evaluate-all.js +40 -24
- package/dist/se/fchain-quality-rules.d.ts +0 -1
- package/dist/se/fchain-quality-rules.js +0 -44
- package/dist/se/flat-graph.d.ts +9 -1
- package/dist/se/flat-graph.js +15 -6
- package/dist/se/format-e-parser.d.ts +14 -2
- package/dist/se/format-e-parser.js +33 -9
- package/dist/se/grammar-snapshot.d.ts +10 -7
- package/dist/se/grammar-snapshot.js +172 -27
- package/dist/se/index.d.ts +4 -3
- package/dist/se/index.js +4 -3
- package/dist/se/meta-model.d.ts +99 -7
- package/dist/se/meta-model.js +155 -14
- package/dist/se/metric-rules.d.ts +20 -1
- package/dist/se/metric-rules.js +97 -58
- package/dist/se/module-crossings.d.ts +102 -0
- package/dist/se/module-crossings.js +196 -0
- package/dist/se/near-duplicate-rules.d.ts +16 -24
- package/dist/se/near-duplicate-rules.js +21 -92
- package/dist/se/ontology.d.ts +32 -47
- package/dist/se/ontology.js +56 -13
- package/dist/se/policy.d.ts +6 -0
- package/dist/se/policy.js +53 -2
- package/dist/se/quality-rules.d.ts +18 -0
- package/dist/se/quality-rules.js +68 -12
- package/dist/se/readiness.d.ts +7 -5
- package/dist/se/readiness.js +40 -22
- package/dist/se/rules.d.ts +6 -7
- package/dist/se/rules.js +358 -128
- package/dist/se/schema-quality-rules.d.ts +0 -1
- package/dist/se/schema-quality-rules.js +7 -28
- package/dist/se/similarity.d.ts +61 -0
- package/dist/se/similarity.js +116 -0
- package/dist/se/uc-quality-rules.js +31 -12
- package/package.json +3 -2
package/dist/se/ao-rules.d.ts
CHANGED
|
@@ -6,20 +6,20 @@ import type { OntologyGraph } from './ontology.js';
|
|
|
6
6
|
import type { RuleDefinition, RuleViolation } from './rules.js';
|
|
7
7
|
import type { MetricPolicy } from './policy.js';
|
|
8
8
|
/**
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* CR-SM-283: **AO-D03 (`DuplicatePathDetection`) ist ersatzlos entfallen.**
|
|
10
|
+
*
|
|
11
|
+
* Die Regel suchte `FUNC -io-> FUNC` — ein Paar, das `TRACE_PATTERNS` nicht kennt und das R-18
|
|
12
|
+
* als `error` ablehnt. Sie konnte STRUKTURELL nicht feuern und hat es an keinem Familiengraphen
|
|
13
|
+
* je getan (Gate 7: 0 Befunde ueber fuenf Graphen). Eine Regel, die nie feuert, kostet trotzdem
|
|
14
|
+
* dauerhaft: einen Nenner-Anteil, eine Katalogzeile, eine readiness-Zuordnung, einen
|
|
15
|
+
* Golden-File-Eintrag und einen Leser, der sie von den anderen unterscheiden muss.
|
|
16
|
+
*
|
|
17
|
+
* **Nicht verwechseln:** die Aufgabe, die sie tragen sollte — zwei Wirkketten, die auf einer
|
|
18
|
+
* Ebene mitgliedsgleich werden — bleibt ungeloest (`se:top-level`: „Compare the member sets
|
|
19
|
+
* yourself — no rule does it for you"). BW-02 ersetzt sie nicht; das ist ein eigener CR.
|
|
12
20
|
*/
|
|
13
|
-
export declare function
|
|
14
|
-
/**
|
|
15
|
-
* AO-D03: Duplicate Path Detection.
|
|
16
|
-
* FUNC A sends io to both B and C, where B and C connect to the same SCHEMA elements.
|
|
17
|
-
*/
|
|
18
|
-
export declare function aoD03DuplicatePath(graph: OntologyGraph): RuleViolation[];
|
|
21
|
+
export declare function bw02WhiteboxWidth(graph: OntologyGraph, policy: MetricPolicy): RuleViolation[];
|
|
19
22
|
export declare function cr01CrossingFlowCount(graph: OntologyGraph, policy: MetricPolicy): RuleViolation[];
|
|
20
|
-
export declare function rt01PhysicalBoundaryIntegrity(graph: OntologyGraph): RuleViolation[];
|
|
21
|
-
export declare function ph01PhysicalModCompleteness(graph: OntologyGraph): RuleViolation[];
|
|
22
|
-
export declare function ca01CapabilityAllocation(graph: OntologyGraph): RuleViolation[];
|
|
23
23
|
export declare function io01CrossModuleCompleteness(graph: OntologyGraph): RuleViolation[];
|
|
24
24
|
export declare const AO_RULES: RuleDefinition[];
|
|
25
25
|
export declare function evaluateAORules(graph: OntologyGraph, policy: MetricPolicy): RuleViolation[];
|
package/dist/se/ao-rules.js
CHANGED
|
@@ -1,42 +1,6 @@
|
|
|
1
|
-
import { getND02SimilarityMatrix } from './near-duplicate-rules.js';
|
|
2
1
|
import { indexOf } from './graph-index.js';
|
|
2
|
+
import { moduleCrossings } from './module-crossings.js';
|
|
3
3
|
const SCHEMA_OVERLAP_THRESHOLD = 0.5;
|
|
4
|
-
/**
|
|
5
|
-
* AO-D01: Relay Node Detection.
|
|
6
|
-
* A FUNC is a relay if it (a) satisfies no REQ, (b) has >=2 outgoing io to FUNCs,
|
|
7
|
-
* and (c) target FUNCs share SCHEMA overlap >= 0.5 (via ND-02 matrix, skipped if unavailable).
|
|
8
|
-
*/
|
|
9
|
-
export function aoD01RelayNode(graph) {
|
|
10
|
-
const idx = indexOf(graph);
|
|
11
|
-
const violations = [];
|
|
12
|
-
const funcs = idx.elementsOfType('FUNC');
|
|
13
|
-
const funcIdSet = new Set(funcs.map(f => f.id));
|
|
14
|
-
for (const func of funcs) {
|
|
15
|
-
// (a) No satisfy traces (FUNC as source) connecting to any REQ
|
|
16
|
-
const hasSatisfy = graph.traces.some(t => t.type === 'satisfy' &&
|
|
17
|
-
t.source === func.id && idx.typeOf(t.target) === 'REQ');
|
|
18
|
-
if (hasSatisfy)
|
|
19
|
-
continue;
|
|
20
|
-
// (b) >=2 outgoing io traces to other FUNCs
|
|
21
|
-
const ioTargets = graph.traces
|
|
22
|
-
.filter(t => t.type === 'io' && t.source === func.id && funcIdSet.has(t.target))
|
|
23
|
-
.map(t => t.target);
|
|
24
|
-
if (ioTargets.length < 2)
|
|
25
|
-
continue;
|
|
26
|
-
// (c) Target FUNCs share SCHEMA overlap >= 0.5 (skip check if no matrix)
|
|
27
|
-
if (!passesSchemaOverlap(graph, ioTargets))
|
|
28
|
-
continue;
|
|
29
|
-
const el = idx.byId.get(func.id);
|
|
30
|
-
violations.push({
|
|
31
|
-
rule_id: 'AO-D01',
|
|
32
|
-
severity: 'info',
|
|
33
|
-
element_id: func.id,
|
|
34
|
-
message: `${func.id}${el?.name ? ' (' + el.name + ')' : ''} is a relay node: no REQ satisfied, ${ioTargets.length} io targets`,
|
|
35
|
-
fix_hint: 'Consider eliminating relay by connecting consumers directly to producer',
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
return violations;
|
|
39
|
-
}
|
|
40
4
|
/** CR-165: Resolve FUNC→SCHEMA via FUNC→FLOW(io) + FLOW→SCHEMA(relation). */
|
|
41
5
|
function funcToSchemas(graph, funcId) {
|
|
42
6
|
const idx = indexOf(graph);
|
|
@@ -44,7 +8,7 @@ function funcToSchemas(graph, funcId) {
|
|
|
44
8
|
// Find FLOWs connected to this FUNC via io (FUNC→FLOW or FLOW→FUNC)
|
|
45
9
|
//
|
|
46
10
|
// CR-SM-264: die io-Kanten dieser FUNC statt zweier Vollscans ueber ALLE Traces je FUNC
|
|
47
|
-
// (`F x T`) — AO-
|
|
11
|
+
// (`F x T`) — AO-D01 ruft diese Funktion einmal je FUNC. Die Reihenfolge von `flowIds` ist
|
|
48
12
|
// dabei egal, die Menge wird nur auf Mitgliedschaft geprueft.
|
|
49
13
|
const flowIds = new Set();
|
|
50
14
|
for (const t of idx.out(funcId, 'io'))
|
|
@@ -56,7 +20,7 @@ function funcToSchemas(graph, funcId) {
|
|
|
56
20
|
// Find SCHEMAs connected to those FLOWs via relation.
|
|
57
21
|
//
|
|
58
22
|
// Hier NICHT ueber `flowIds` iterieren, obwohl das billiger waere: die Einfuegereihenfolge
|
|
59
|
-
// dieser Menge landet ueber `shared.join(', ')` woertlich in der AO-
|
|
23
|
+
// dieser Menge landet ueber `shared.join(', ')` woertlich in der AO-D01-Meldung. Der Lauf
|
|
60
24
|
// ueber die relation-Kanten in Graph-Reihenfolge haelt sie identisch — und ist trotzdem
|
|
61
25
|
// deutlich kuerzer als der Vollscan, weil er nur einen Kantentyp sieht.
|
|
62
26
|
for (const t of idx.tracesOfType('relation')) {
|
|
@@ -65,124 +29,101 @@ function funcToSchemas(graph, funcId) {
|
|
|
65
29
|
}
|
|
66
30
|
return schemas;
|
|
67
31
|
}
|
|
68
|
-
/** Check if any pair of target FUNCs shares SCHEMA overlap via FLOW→SCHEMA(relation) + ND-02 matrix. */
|
|
69
|
-
function passesSchemaOverlap(graph, targetFuncIds) {
|
|
70
|
-
const idx = indexOf(graph);
|
|
71
|
-
const nd02 = getND02SimilarityMatrix();
|
|
72
|
-
if (!nd02)
|
|
73
|
-
return true; // no matrix → skip check, assume pass
|
|
74
|
-
// For each target FUNC, collect connected SCHEMA ids via FLOW→SCHEMA(relation)
|
|
75
|
-
const funcSchemasMap = new Map();
|
|
76
|
-
for (const fid of targetFuncIds) {
|
|
77
|
-
funcSchemasMap.set(fid, funcToSchemas(graph, fid));
|
|
78
|
-
}
|
|
79
|
-
// Check pairwise: any pair of target FUNCs with overlapping SCHEMAs (similarity >= threshold)
|
|
80
|
-
const { schemaIds, matrix } = nd02;
|
|
81
|
-
const idxOf = new Map(schemaIds.map((id, i) => [id, i]));
|
|
82
|
-
for (let i = 0; i < targetFuncIds.length; i++) {
|
|
83
|
-
for (let j = i + 1; j < targetFuncIds.length; j++) {
|
|
84
|
-
const si = funcSchemasMap.get(targetFuncIds[i]);
|
|
85
|
-
const sj = funcSchemasMap.get(targetFuncIds[j]);
|
|
86
|
-
for (const a of si) {
|
|
87
|
-
for (const b of sj) {
|
|
88
|
-
const ai = idxOf.get(a), bi = idxOf.get(b);
|
|
89
|
-
if (ai !== undefined && bi !== undefined && matrix[ai][bi] >= SCHEMA_OVERLAP_THRESHOLD)
|
|
90
|
-
return true;
|
|
91
|
-
if (a === b)
|
|
92
|
-
return true; // same SCHEMA = 100% overlap
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
return false;
|
|
98
|
-
}
|
|
99
32
|
/**
|
|
100
|
-
* AO-D03
|
|
101
|
-
*
|
|
33
|
+
* CR-SM-283: **AO-D03 (`DuplicatePathDetection`) ist ersatzlos entfallen.**
|
|
34
|
+
*
|
|
35
|
+
* Die Regel suchte `FUNC -io-> FUNC` — ein Paar, das `TRACE_PATTERNS` nicht kennt und das R-18
|
|
36
|
+
* als `error` ablehnt. Sie konnte STRUKTURELL nicht feuern und hat es an keinem Familiengraphen
|
|
37
|
+
* je getan (Gate 7: 0 Befunde ueber fuenf Graphen). Eine Regel, die nie feuert, kostet trotzdem
|
|
38
|
+
* dauerhaft: einen Nenner-Anteil, eine Katalogzeile, eine readiness-Zuordnung, einen
|
|
39
|
+
* Golden-File-Eintrag und einen Leser, der sie von den anderen unterscheiden muss.
|
|
40
|
+
*
|
|
41
|
+
* **Nicht verwechseln:** die Aufgabe, die sie tragen sollte — zwei Wirkketten, die auf einer
|
|
42
|
+
* Ebene mitgliedsgleich werden — bleibt ungeloest (`se:top-level`: „Compare the member sets
|
|
43
|
+
* yourself — no rule does it for you"). BW-02 ersetzt sie nicht; das ist ein eigener CR.
|
|
102
44
|
*/
|
|
103
|
-
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// BW-02: Randbreite der FUNC-Whitebox (CR-SM-283)
|
|
47
|
+
//
|
|
48
|
+
// Die Zahl der VERSCHIEDENEN Vertraege, die den Rand einer zerlegten FUNC queren — dieselbe
|
|
49
|
+
// Zaehlbasis wie CR-01/R-04 (SCHEMA, nicht rohe io-Kanten, CR-SM-274/276), nur ist die
|
|
50
|
+
// Innen-Menge der `compose`-Teilbaum statt des Modul-Teilbaums. Gerechnet im selben Durchlauf
|
|
51
|
+
// wie `byModule` (`module-crossings.ts`), damit es EINE Definition von „Rand" gibt.
|
|
52
|
+
//
|
|
53
|
+
// Warum es die Regel ueberhaupt braucht: eine zerlegte FUNC ist die Whitebox, in die der Leser
|
|
54
|
+
// hineinklickt. Ihre Randbreite entscheidet, ob er den Ueberblick behaelt — und bis hierher
|
|
55
|
+
// misst sie NIEMAND. AO-D01 misst Relay-Knoten, IO-01 die Kettenkohaerenz, R-31 die blosse
|
|
56
|
+
// Verdrahtung. Gemessen: 12 Befunde an graphcode, davon 0 in Ueberlappung mit irgendeiner
|
|
57
|
+
// FUNC-Regel (Gate 6).
|
|
58
|
+
//
|
|
59
|
+
// Schwelle aus der Verteilung, nicht gewaehlt: bok und graph-view-edit enden beide bei 3,
|
|
60
|
+
// graphcode geht bis 19, moneyflow bis 17. Kein `info`-Level — bei Schwelle 3 truege bok 4 von
|
|
61
|
+
// 9 Blackboxes, und `readiness` zaehlt `info` ungefiltert in den Nenner (MT-03-Praezedenz).
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
export function bw02WhiteboxWidth(graph, policy) {
|
|
64
|
+
const steps = policy.boundaryWidth;
|
|
65
|
+
if (steps === null)
|
|
66
|
+
return [];
|
|
104
67
|
const idx = indexOf(graph);
|
|
105
68
|
const violations = [];
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
const seen = new Set();
|
|
109
|
-
// Pre-compute: FUNC → set of SCHEMA targets via FLOW→SCHEMA(relation) (CR-165)
|
|
110
|
-
const funcToSchemasMap = new Map();
|
|
111
|
-
for (const f of funcs) {
|
|
112
|
-
funcToSchemasMap.set(f.id, funcToSchemas(graph, f.id));
|
|
113
|
-
}
|
|
114
|
-
for (const a of funcs) {
|
|
115
|
-
const ioTargets = graph.traces
|
|
116
|
-
.filter(t => t.type === 'io' && t.source === a.id && funcIdSet.has(t.target))
|
|
117
|
-
.map(t => t.target);
|
|
118
|
-
if (ioTargets.length < 2)
|
|
69
|
+
for (const [funcId, contracts] of moduleCrossings(graph).byFunc) {
|
|
70
|
+
if (contracts.size < steps.warning)
|
|
119
71
|
continue;
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
violations.push({
|
|
133
|
-
rule_id: 'AO-D03',
|
|
134
|
-
severity: 'info',
|
|
135
|
-
element_id: a.id,
|
|
136
|
-
message: `${a.id} sends io to ${b} and ${c} which share ${shared.length} SCHEMA target(s): ${shared.join(', ')}`,
|
|
137
|
-
fix_hint: 'Consider introducing a mediator or unifying the data paths',
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
}
|
|
72
|
+
const el = idx.byId.get(funcId);
|
|
73
|
+
violations.push({
|
|
74
|
+
rule_id: 'BW-02',
|
|
75
|
+
severity: 'warning',
|
|
76
|
+
element_id: funcId,
|
|
77
|
+
message: `${funcId} exposes ${contracts.size} distinct contract(s) at its whitebox boundary (>= ${steps.warning})`,
|
|
78
|
+
fix_hint: 'Consolidate the contracts crossing this boundary, or introduce a level so fewer of them are visible at once',
|
|
79
|
+
// CR-SM-288: `>= steps.warning` ist inklusiv, also ist `steps.warning - 1` der groesste
|
|
80
|
+
// Wert, der noch im Budget liegt — die Masse `value - threshold` ist damit > 0 fuer jeden
|
|
81
|
+
// Befund dieser Regel.
|
|
82
|
+
context: { element_type: el?.type, element_name: el?.name, value: contracts.size, threshold: steps.warning - 1 },
|
|
83
|
+
});
|
|
141
84
|
}
|
|
142
|
-
|
|
85
|
+
// Die Einfuegereihenfolge von `byFunc` folgt der FLOW-Reihenfolge des Graphen — hier wird sie
|
|
86
|
+
// kanonisch gemacht, sonst haengt die Befund-Sequenz an der Trace-Reihenfolge (Gate 5).
|
|
87
|
+
return violations.sort((a, b) => (a.element_id < b.element_id ? -1 : a.element_id > b.element_id ? 1 : 0));
|
|
143
88
|
}
|
|
144
89
|
// ---------------------------------------------------------------------------
|
|
145
90
|
// CR-01: Crossing Flow Count (CR-191)
|
|
146
|
-
//
|
|
91
|
+
// Je Modulpaar (A, B): die VERSCHIEDENEN Vertraege, die den Rand queren — der io-Pfad
|
|
92
|
+
// `FUNC_A -io-> FLOW -io-> FUNC_B` mit FUNC_A in A und FUNC_B in B, gezaehlt ueber das
|
|
93
|
+
// SCHEMA des FLOW.
|
|
147
94
|
//
|
|
148
95
|
// CR-SM-236: die Schwelle ist Eingabe. `policy.crossingFlows === null` heisst messen statt
|
|
149
96
|
// urteilen — die Regel schweigt dann vollstaendig, auch unterhalb der Schwelle. Vorher gab es
|
|
150
97
|
// diesen Aus-Zustand nicht: jedes Modulpaar mit >= 1 Kreuzung erzeugte eine info-Meldung.
|
|
98
|
+
//
|
|
99
|
+
// CR-SM-274 (Befund aus Spike CR-GC-438 §6): die Regel suchte direkte `FUNC -io-> FUNC`-Kanten.
|
|
100
|
+
// `TRACE_PATTERNS` kennt fuer `io` nur `ACTOR<->FLOW` und `FUNC<->FLOW` — R-18 lehnt eine
|
|
101
|
+
// FUNC->FUNC-io-Kante ab, die Regel konnte STRUKTURELL nicht feuern und tat es in keinem der
|
|
102
|
+
// 10 gemessenen Familie-Graphen (0 Befunde), waehrend ihre Schwelle in der MetricPolicy
|
|
103
|
+
// gepflegt wurde. Jetzt laeuft sie den Pfad, den ihr Kopf meint.
|
|
104
|
+
//
|
|
105
|
+
// Zaehlbasis = SCHEMA, nicht Einzelquerung. Begruendung (CR-SM-274):
|
|
106
|
+
// 1. Modul-Kopplung ist die Zahl der Vertraege, auf die sich zwei Module einigen muessen.
|
|
107
|
+
// Ein FLOW mit 3 Produzenten und 3 Konsumenten ist EIN Vertrag, nicht 9 Kopplungen —
|
|
108
|
+
// der Fix-Hint ("Mediator einziehen") greift je Vertrag, nicht je Konsument.
|
|
109
|
+
// 2. Die rohe Produzent-x-Konsument-Zaehlung ist hub-empfindlich und durch FLOW-Splitting
|
|
110
|
+
// manipulierbar (CR-436 Befund 3: X 172 -> 1539 durch einen Merge ohne Code-Kopplung).
|
|
111
|
+
// SCHEMA ist die Zaehlbasis, die der Spike als gaming-fest gemessen hat (Kill 2).
|
|
112
|
+
// 3. `FLOW -relation-> SCHEMA` ist seit CR-SM-271 Teil 2 `1..1` und per R-18 erzwungen —
|
|
113
|
+
// die Zaehlbasis ist damit garantiert vorhanden, kein optionales Feld.
|
|
114
|
+
// Ein FLOW ohne SCHEMA zaehlt als eigener, untypisierter Vertrag (R-18 meldet ihn separat);
|
|
115
|
+
// er darf nicht stillschweigend mit anderen verschmelzen.
|
|
116
|
+
//
|
|
117
|
+
// CR-SM-276: die Traversierung steht in `module-crossings.ts` — dieselbe Definition von
|
|
118
|
+
// „Kreuzung" fuer CR-01 (je Modulpaar) und R-04 (je Modul). Zwei Kopien waeren zwei Begriffe.
|
|
151
119
|
// ---------------------------------------------------------------------------
|
|
152
120
|
export function cr01CrossingFlowCount(graph, policy) {
|
|
153
|
-
const idx = indexOf(graph);
|
|
154
121
|
const violations = [];
|
|
155
122
|
const steps = policy.crossingFlows;
|
|
156
123
|
if (steps === null)
|
|
157
124
|
return violations;
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
const modFuncs = new Map();
|
|
161
|
-
for (const mod of mods) {
|
|
162
|
-
const funcIds = new Set(idx.in(mod.id, 'allocate').map(t => t.source));
|
|
163
|
-
modFuncs.set(mod.id, funcIds);
|
|
164
|
-
}
|
|
165
|
-
// Count crossings per module pair
|
|
166
|
-
const pairCounts = new Map();
|
|
167
|
-
for (const t of graph.traces) {
|
|
168
|
-
if (t.type !== 'io')
|
|
169
|
-
continue;
|
|
170
|
-
let srcMod;
|
|
171
|
-
let tgtMod;
|
|
172
|
-
for (const [mid, fids] of modFuncs) {
|
|
173
|
-
if (fids.has(t.source))
|
|
174
|
-
srcMod = mid;
|
|
175
|
-
if (fids.has(t.target))
|
|
176
|
-
tgtMod = mid;
|
|
177
|
-
}
|
|
178
|
-
if (!srcMod || !tgtMod || srcMod === tgtMod)
|
|
179
|
-
continue;
|
|
180
|
-
const key = [srcMod, tgtMod].sort().join('::');
|
|
181
|
-
if (!pairCounts.has(key))
|
|
182
|
-
pairCounts.set(key, { modA: srcMod, modB: tgtMod, count: 0 });
|
|
183
|
-
pairCounts.get(key).count++;
|
|
184
|
-
}
|
|
185
|
-
for (const { modA, modB, count } of pairCounts.values()) {
|
|
125
|
+
for (const { modA, modB, contracts } of moduleCrossings(graph).pairs.values()) {
|
|
126
|
+
const count = contracts.size;
|
|
186
127
|
if (count <= 0)
|
|
187
128
|
continue;
|
|
188
129
|
const over = count >= steps.warning;
|
|
@@ -190,93 +131,19 @@ export function cr01CrossingFlowCount(graph, policy) {
|
|
|
190
131
|
rule_id: 'CR-01',
|
|
191
132
|
severity: over ? 'warning' : 'info',
|
|
192
133
|
element_id: modA,
|
|
193
|
-
message: `${modA} ↔ ${modB}: ${count} crossing io
|
|
194
|
-
fix_hint: over
|
|
134
|
+
message: `${modA} ↔ ${modB}: ${count} distinct contract(s) crossing the module boundary (io path FUNC→FLOW→FUNC)`,
|
|
135
|
+
fix_hint: over
|
|
136
|
+
? `Reduce coupling between modules or introduce a mediator (>= ${steps.warning} distinct contracts at one boundary)`
|
|
137
|
+
: 'Consider if coupling is intentional',
|
|
138
|
+
// CR-SM-288: dasselbe Budget fuer beide Severities. Ein `info`-Paar liegt per Konstruktion
|
|
139
|
+
// INNERHALB des Budgets und traegt damit die Masse 0 — es ist kein kleiner Verstoss,
|
|
140
|
+
// sondern keiner.
|
|
141
|
+
context: { value: count, threshold: steps.warning - 1 },
|
|
195
142
|
});
|
|
196
143
|
}
|
|
197
144
|
return violations;
|
|
198
145
|
}
|
|
199
146
|
// ---------------------------------------------------------------------------
|
|
200
|
-
// RT-01: Physical boundary integrity (CR-191)
|
|
201
|
-
// FUNCs must not be allocated directly to physical MODs — use logical sub-modules.
|
|
202
|
-
// ---------------------------------------------------------------------------
|
|
203
|
-
export function rt01PhysicalBoundaryIntegrity(graph) {
|
|
204
|
-
const idx = indexOf(graph);
|
|
205
|
-
const physicalMods = graph.elements.filter(e => e.type === 'MOD' && e.attributes?.kind === 'physical');
|
|
206
|
-
return graph.traces
|
|
207
|
-
.filter(t => t.type === 'allocate' && physicalMods.some(m => m.id === t.target))
|
|
208
|
-
.filter(t => idx.typeOf(t.source) === 'FUNC')
|
|
209
|
-
.map(t => ({
|
|
210
|
-
rule_id: 'RT-01',
|
|
211
|
-
severity: 'error',
|
|
212
|
-
element_id: t.source,
|
|
213
|
-
message: `${t.source} allocated directly to physical module ${t.target} — must use logical sub-module`,
|
|
214
|
-
fix_hint: 'Create a logical MOD inside the physical MOD and allocate the FUNC there',
|
|
215
|
-
}));
|
|
216
|
-
}
|
|
217
|
-
// ---------------------------------------------------------------------------
|
|
218
|
-
// PH-01: Physical MOD completeness (CR-191)
|
|
219
|
-
// Physical MODs should have at least one logical sub-module via compose trace.
|
|
220
|
-
// ---------------------------------------------------------------------------
|
|
221
|
-
export function ph01PhysicalModCompleteness(graph) {
|
|
222
|
-
const idx = indexOf(graph);
|
|
223
|
-
const physicalMods = graph.elements.filter(e => e.type === 'MOD' && e.attributes?.kind === 'physical');
|
|
224
|
-
return physicalMods
|
|
225
|
-
.filter(pm => !graph.traces.some(t => t.source === pm.id && t.type === 'compose' &&
|
|
226
|
-
idx.typeOf(t.target) === 'MOD'))
|
|
227
|
-
.map(pm => ({
|
|
228
|
-
rule_id: 'PH-01',
|
|
229
|
-
severity: 'info',
|
|
230
|
-
element_id: pm.id,
|
|
231
|
-
message: `${pm.id} (physical) has no logical sub-modules`,
|
|
232
|
-
fix_hint: 'Add at least one logical MOD via compose trace',
|
|
233
|
-
context: { element_type: pm.type, element_name: pm.name },
|
|
234
|
-
}));
|
|
235
|
-
}
|
|
236
|
-
// ---------------------------------------------------------------------------
|
|
237
|
-
// CA-01: Capability allocation check (CR-191)
|
|
238
|
-
// FUNC @requires must be satisfied by the physical MOD's @capability.
|
|
239
|
-
// ---------------------------------------------------------------------------
|
|
240
|
-
export function ca01CapabilityAllocation(graph) {
|
|
241
|
-
const idx = indexOf(graph);
|
|
242
|
-
const violations = [];
|
|
243
|
-
const funcsWithRequires = graph.elements.filter(e => e.type === 'FUNC' && e.attributes?.requires);
|
|
244
|
-
for (const func of funcsWithRequires) {
|
|
245
|
-
const requires = (Array.isArray(func.attributes.requires)
|
|
246
|
-
? func.attributes.requires
|
|
247
|
-
: [func.attributes.requires]);
|
|
248
|
-
// Find logical MOD via allocate
|
|
249
|
-
const allocTrace = graph.traces.find(t => t.source === func.id && t.type === 'allocate');
|
|
250
|
-
if (!allocTrace)
|
|
251
|
-
continue;
|
|
252
|
-
const logicalModCandidate = idx.byId.get(allocTrace.target);
|
|
253
|
-
const logicalMod = logicalModCandidate?.type === 'MOD' ? logicalModCandidate : undefined;
|
|
254
|
-
if (!logicalMod)
|
|
255
|
-
continue;
|
|
256
|
-
// Find physical parent MOD (compose source → logical MOD target)
|
|
257
|
-
const physParentTrace = graph.traces.find(t => t.type === 'compose' && t.target === logicalMod.id &&
|
|
258
|
-
(idx.byId.get(t.source)?.type === 'MOD' && idx.byId.get(t.source)?.attributes?.kind === 'physical'));
|
|
259
|
-
if (!physParentTrace)
|
|
260
|
-
continue;
|
|
261
|
-
const physMod = idx.byId.get(physParentTrace.source);
|
|
262
|
-
const capabilities = (Array.isArray(physMod.attributes?.capability)
|
|
263
|
-
? physMod.attributes.capability
|
|
264
|
-
: physMod.attributes?.capability ? [physMod.attributes.capability] : []);
|
|
265
|
-
const missing = requires.filter(r => !capabilities.includes(r));
|
|
266
|
-
if (missing.length > 0) {
|
|
267
|
-
violations.push({
|
|
268
|
-
rule_id: 'CA-01',
|
|
269
|
-
severity: 'error',
|
|
270
|
-
element_id: func.id,
|
|
271
|
-
message: `${func.id} requires [${missing.join(', ')}] but ${physMod.id} only provides [${capabilities.join(', ')}]`,
|
|
272
|
-
fix_hint: `Add missing capabilities to ${physMod.id} or move ${func.id} to a capable module`,
|
|
273
|
-
context: { element_type: func.type, element_name: func.name, parent_module: physMod.id },
|
|
274
|
-
});
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
return violations;
|
|
278
|
-
}
|
|
279
|
-
// ---------------------------------------------------------------------------
|
|
280
147
|
// IO-01: die Wirkkette haengt zusammen (CR-192, CR-SM-226, CR-SM-250)
|
|
281
148
|
//
|
|
282
149
|
// Geprueft wird ERREICHBARKEIT im io-Teilgraphen der Kette, nicht Adjazenz. Bis CR-SM-250
|
|
@@ -393,12 +260,8 @@ export function io01CrossModuleCompleteness(graph) {
|
|
|
393
260
|
return violations;
|
|
394
261
|
}
|
|
395
262
|
export const AO_RULES = [
|
|
396
|
-
{ id: '
|
|
397
|
-
{ id: 'AO-D03', name: 'DuplicatePathDetection', severity: 'info', evaluate: aoD03DuplicatePath, domain: ['FUNC'] },
|
|
263
|
+
{ id: 'BW-02', name: 'Whitebox boundary width', severity: 'warning', evaluate: bw02WhiteboxWidth, domain: ['FUNC'] },
|
|
398
264
|
{ id: 'CR-01', name: 'CrossingFlowCount', severity: 'warning', evaluate: cr01CrossingFlowCount, domain: ['MOD'] },
|
|
399
|
-
{ id: 'RT-01', name: 'PhysicalBoundaryIntegrity', severity: 'error', evaluate: rt01PhysicalBoundaryIntegrity, domain: ['FUNC'] },
|
|
400
|
-
{ id: 'PH-01', name: 'PhysicalModCompleteness', severity: 'info', evaluate: ph01PhysicalModCompleteness, domain: ['MOD'] },
|
|
401
|
-
{ id: 'CA-01', name: 'CapabilityAllocation', severity: 'error', evaluate: ca01CapabilityAllocation, domain: ['FUNC'] },
|
|
402
265
|
// CR-SM-242: domain ist FUNC, nicht MOD — die Regel laeuft ueber FUNC-Paare in FCHAINs und
|
|
403
266
|
// meldet am FUNC. Mit ['MOD'] uebertraf ihr Zaehler (21) ihren eigenen Nenner-Beitrag (10);
|
|
404
267
|
// nur der Clamp verhinderte einen negativen Score.
|
|
@@ -59,6 +59,34 @@ export interface ConformanceRuleDefinition {
|
|
|
59
59
|
severity: RuleSeverity;
|
|
60
60
|
evaluate: (graph: OntologyGraph, facts: CodeFacts) => RuleViolation[];
|
|
61
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* Wie viel des Import-Graphen konnte RC-05 überhaupt ansehen? (CR-SM-268 Teil 2)
|
|
64
|
+
*
|
|
65
|
+
* Eine MESSUNG über den Lauf, kein Befund über das Modell — deshalb eine eigene Funktion neben
|
|
66
|
+
* den Regeln und keine `RuleViolation`. „12 von 178 Endpunkten nicht zugeordnet" sagt nichts
|
|
67
|
+
* über den Graphen aus; es sagt, wie belastbar die Zahl daneben ist.
|
|
68
|
+
*
|
|
69
|
+
* Vorher hing die Liste als Textanhang an der Meldung eines ANDEREN Befundes und verschwand mit
|
|
70
|
+
* ihm: CR-GC-423 hat die drei RC-05-Drift-Befunde von graphcode geschlossen, und mit ihnen war
|
|
71
|
+
* die Liste der nicht zugeordneten Dateien weg — sie musste in CR-GC-424/425 von Hand
|
|
72
|
+
* rekonstruiert werden. „RC-05: 0 Befunde" hieß damit wahlweise „alle 178 Endpunkte geprüft,
|
|
73
|
+
* keiner driftet" oder „166 geprüft, 12 gar nicht angesehen", und niemand konnte die beiden
|
|
74
|
+
* unterscheiden.
|
|
75
|
+
*
|
|
76
|
+
* Geschwister von `skipped` (CR-GC-398), nicht dasselbe: `skipped` heißt „diese Quelle wurde GAR
|
|
77
|
+
* NICHT ausgewertet" (kein lesbarer repoRoot). Hier ist sie ausgewertet worden, und trotzdem
|
|
78
|
+
* fällt ein Teil durch. Zwei Zustände, zwei Felder — zusammengelegt wären „nicht gelaufen" und
|
|
79
|
+
* „gelaufen, aber blind" wieder ununterscheidbar.
|
|
80
|
+
*/
|
|
81
|
+
export interface ImportCoverage {
|
|
82
|
+
/** Verschiedene Dateien, die überhaupt als Import-Endpunkt vorkommen. */
|
|
83
|
+
endpoints: number;
|
|
84
|
+
/** Davon einer MOD zugeordnet — nur diese konnte RC-05 beurteilen. */
|
|
85
|
+
assigned: number;
|
|
86
|
+
/** Der Rest, benannt statt gezählt: ohne die Namen ist die Lücke nicht schließbar. */
|
|
87
|
+
unassigned: string[];
|
|
88
|
+
}
|
|
89
|
+
export declare function importCoverage(graph: OntologyGraph, facts: CodeFacts): ImportCoverage;
|
|
62
90
|
/** All RC conformance rules — evaluated by executors that can supply CodeFacts. */
|
|
63
91
|
export declare const CODE_CONFORMANCE_RULES: ConformanceRuleDefinition[];
|
|
64
92
|
/** Run all RC rules against a graph + extracted code facts. */
|
|
@@ -267,15 +267,19 @@ function schemaRefMustBeUsed(graph, facts) {
|
|
|
267
267
|
// such adjacency ⇒ one RC-05 per (fromMod → toMod) pair, evidence = the
|
|
268
268
|
// crossing file imports.
|
|
269
269
|
// ---------------------------------------------------------------------------
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
270
|
+
/**
|
|
271
|
+
* Datei → MOD, die EINE Auflösung für RC-05 und `importCoverage` (CR-SM-268 Teil 2).
|
|
272
|
+
*
|
|
273
|
+
* Bewusst herausgezogen und nicht zweimal geschrieben: die Abdeckungszahl muss dieselbe Frage
|
|
274
|
+
* stellen wie die Regel, sonst misst sie etwas anderes als das, was geprüft wird — und eine
|
|
275
|
+
* Abdeckungszahl, die nicht zur Prüfung passt, ist schlimmer als keine.
|
|
276
|
+
*
|
|
277
|
+
* Zwei Wege, in dieser Reihenfolge: die `realRef` einer allozierten FUNC (eine gebundene Datei
|
|
278
|
+
* gehört zur MOD ihrer FUNC), danach das `path`-Präfix einer MOD, longest-prefix. Wer keinen
|
|
279
|
+
* von beiden trifft, ist `unassigned` — und wird nie still verworfen.
|
|
280
|
+
*/
|
|
281
|
+
function buildModResolver(graph) {
|
|
274
282
|
const typeOf = new Map(graph.elements.map(e => [e.id, e.type]));
|
|
275
|
-
const modIds = graph.elements.filter(e => e.type === 'MOD').map(e => e.id);
|
|
276
|
-
if (modIds.length === 0)
|
|
277
|
-
return [];
|
|
278
|
-
// 1. file → MOD. Direct realRef bindings first.
|
|
279
283
|
const fileToMod = new Map();
|
|
280
284
|
for (const el of graph.elements) {
|
|
281
285
|
if (el.type !== 'FUNC')
|
|
@@ -292,7 +296,7 @@ function importDriftConformance(graph, facts) {
|
|
|
292
296
|
.filter(e => e.type === 'MOD' && typeof e.attributes?.path === 'string')
|
|
293
297
|
.map(e => ({ id: e.id, path: e.attributes.path.replace(/[*].*$/, '').replace(/\/+$/, '') }))
|
|
294
298
|
.filter(m => m.path.length > 0);
|
|
295
|
-
|
|
299
|
+
return (file) => {
|
|
296
300
|
if (fileToMod.has(file))
|
|
297
301
|
return fileToMod.get(file);
|
|
298
302
|
let best;
|
|
@@ -303,6 +307,27 @@ function importDriftConformance(graph, facts) {
|
|
|
303
307
|
}
|
|
304
308
|
return best?.id;
|
|
305
309
|
};
|
|
310
|
+
}
|
|
311
|
+
export function importCoverage(graph, facts) {
|
|
312
|
+
const importEdges = facts.importEdges ?? [];
|
|
313
|
+
const resolveMod = buildModResolver(graph);
|
|
314
|
+
const endpoints = new Set();
|
|
315
|
+
for (const edge of importEdges) {
|
|
316
|
+
endpoints.add(edge.from);
|
|
317
|
+
endpoints.add(edge.to);
|
|
318
|
+
}
|
|
319
|
+
const unassigned = [...endpoints].filter(f => resolveMod(f) === undefined).sort();
|
|
320
|
+
return { endpoints: endpoints.size, assigned: endpoints.size - unassigned.length, unassigned };
|
|
321
|
+
}
|
|
322
|
+
function importDriftConformance(graph, facts) {
|
|
323
|
+
const importEdges = facts.importEdges ?? [];
|
|
324
|
+
if (importEdges.length === 0)
|
|
325
|
+
return [];
|
|
326
|
+
const modIds = graph.elements.filter(e => e.type === 'MOD').map(e => e.id);
|
|
327
|
+
if (modIds.length === 0)
|
|
328
|
+
return [];
|
|
329
|
+
const typeOf = new Map(graph.elements.map(e => [e.id, e.type]));
|
|
330
|
+
const resolveMod = buildModResolver(graph);
|
|
306
331
|
// 2. graph MOD-adjacency (undirected) from io crossings FUNC_A ─io→ FLOW ─io→ FUNC_B.
|
|
307
332
|
const funcMod = (funcId) => {
|
|
308
333
|
const t = graph.traces.find(tr => tr.source === funcId && tr.type === 'allocate' && typeOf.get(tr.target) === 'MOD');
|
|
@@ -332,16 +357,15 @@ function importDriftConformance(graph, facts) {
|
|
|
332
357
|
const [x, y] = [a, b].sort();
|
|
333
358
|
return adjacent.has(`${x}|${y}`);
|
|
334
359
|
};
|
|
335
|
-
// 3. drift per (fromMod → toMod), aggregating evidence
|
|
360
|
+
// 3. drift per (fromMod → toMod), aggregating evidence.
|
|
361
|
+
// CR-SM-268 Teil 2: die nicht zugeordneten Dateien werden hier NICHT mehr gesammelt. Sie
|
|
362
|
+
// hingen als Textanhang an der Meldung eines anderen Befundes und verschwanden mit ihm
|
|
363
|
+
// (CR-GC-423). Ihr Ort ist jetzt `importCoverage` — eine Messung, die auch dann etwas sagt,
|
|
364
|
+
// wenn diese Regel schweigt.
|
|
336
365
|
const drift = new Map();
|
|
337
|
-
const unassigned = new Set();
|
|
338
366
|
for (const edge of importEdges) {
|
|
339
367
|
const fromMod = resolveMod(edge.from);
|
|
340
368
|
const toMod = resolveMod(edge.to);
|
|
341
|
-
if (!fromMod)
|
|
342
|
-
unassigned.add(edge.from);
|
|
343
|
-
if (!toMod)
|
|
344
|
-
unassigned.add(edge.to);
|
|
345
369
|
if (!fromMod || !toMod || fromMod === toMod)
|
|
346
370
|
continue;
|
|
347
371
|
if (isAdjacent(fromMod, toMod))
|
|
@@ -352,7 +376,6 @@ function importDriftConformance(graph, facts) {
|
|
|
352
376
|
drift.set(key, entry);
|
|
353
377
|
}
|
|
354
378
|
const nameOf = new Map(graph.elements.map(e => [e.id, e.name]));
|
|
355
|
-
const unassignedNote = unassigned.size > 0 ? ` (unassigned files not mapped to a MOD: ${[...unassigned].sort().join(', ')})` : '';
|
|
356
379
|
return [...drift.values()].map(d => {
|
|
357
380
|
const shown = d.evidence.slice(0, 5);
|
|
358
381
|
const more = d.evidence.length > shown.length ? ` +${d.evidence.length - shown.length} more` : '';
|
|
@@ -360,7 +383,7 @@ function importDriftConformance(graph, facts) {
|
|
|
360
383
|
rule_id: 'RC-05',
|
|
361
384
|
severity: 'warning',
|
|
362
385
|
element_id: d.fromMod,
|
|
363
|
-
message: `${d.fromMod} imports ${d.toMod} across a module boundary the graph does not document — evidence: ${shown.join('; ')}${more}
|
|
386
|
+
message: `${d.fromMod} imports ${d.toMod} across a module boundary the graph does not document — evidence: ${shown.join('; ')}${more}`,
|
|
364
387
|
fix_hint: `Document the ${d.fromMod}→${d.toMod} dependency in the graph (an io/FLOW connection between their FUNCs), or remove the import`,
|
|
365
388
|
context: {
|
|
366
389
|
element_type: 'MOD',
|