@sigloch/contracts 9.1.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/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/grammar-snapshot.d.ts +8 -5
- package/dist/se/grammar-snapshot.js +167 -15
- package/dist/se/index.d.ts +4 -3
- package/dist/se/index.js +4 -3
- package/dist/se/meta-model.d.ts +47 -0
- package/dist/se/meta-model.js +52 -5
- package/dist/se/metric-rules.d.ts +20 -1
- package/dist/se/metric-rules.js +97 -58
- package/dist/se/module-crossings.d.ts +102 -0
- package/dist/se/module-crossings.js +196 -0
- package/dist/se/near-duplicate-rules.d.ts +16 -24
- package/dist/se/near-duplicate-rules.js +21 -92
- package/dist/se/ontology.d.ts +0 -22
- package/dist/se/ontology.js +0 -2
- package/dist/se/policy.d.ts +6 -0
- package/dist/se/policy.js +53 -2
- package/dist/se/quality-rules.d.ts +18 -0
- package/dist/se/quality-rules.js +51 -9
- package/dist/se/readiness.d.ts +3 -3
- package/dist/se/readiness.js +17 -21
- package/dist/se/rules.d.ts +6 -0
- package/dist/se/rules.js +303 -125
- package/dist/se/schema-quality-rules.d.ts +0 -1
- package/dist/se/schema-quality-rules.js +7 -28
- package/dist/se/similarity.d.ts +61 -0
- package/dist/se/similarity.js +116 -0
- package/package.json +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.
|
|
@@ -1,16 +1,55 @@
|
|
|
1
1
|
// ---------------------------------------------------------------------------
|
|
2
|
-
// CR-R01:
|
|
2
|
+
// CR-R01: ein offener CR nennt, WAS er aendert (CR-SM-295)
|
|
3
|
+
//
|
|
4
|
+
// Bis hierher waren es zwei Regeln, und beide zielten daneben. CR-R01 verlangte
|
|
5
|
+
// IRGENDEINE relation-Kante — und akzeptierte damit einen CR, der nur an einem
|
|
6
|
+
// Meilenstein haengt: das ist ein Termin, kein Umfang. Ueber 514 CR-Knoten in 19
|
|
7
|
+
// Familiengraphen feuerte sie 5-mal, waehrend 49 CRs ausschliesslich auf ein MS zeigten
|
|
8
|
+
// und unbeanstandet blieben. CR-R04 verlangte umgekehrt einen FUNC — zu eng: 21 CRs
|
|
9
|
+
// meldeten, und ein grosser Teil davon aendert Daten, Doku oder Konfiguration und
|
|
10
|
+
// beruehrt zu Recht keine Funktion.
|
|
11
|
+
//
|
|
12
|
+
// Die tragende Frage ist keine von beiden, sondern: nennt dieser CR ein Element, das er
|
|
13
|
+
// AENDERT? Umfang sind FUNC, MOD, SCHEMA, REQ und UC. MS ist Terminplanung und zaehlt
|
|
14
|
+
// nicht — MS-03 stellt diese Frage getrennt und bleibt unberuehrt.
|
|
15
|
+
//
|
|
16
|
+
// Grundgesamtheit wie bei CR-R04 (CR-SM-255) — nur was noch steuerbar ist. An einem
|
|
17
|
+
// geschlossenen CR ist die Frage Archaeologie: was er beruehrt hat, steht im Commit.
|
|
18
|
+
//
|
|
19
|
+
// Die Menge ist aber UMGEDREHT formuliert: nicht "open oder in-progress", sondern "nicht
|
|
20
|
+
// abgeschlossen". Der Grund ist gemessen — im Bestand tragen CRs die Status `done` (463),
|
|
21
|
+
// `open` (34), `planned` (12), `rejected` (2), `dropped` (1) und keinen (2). `planned`
|
|
22
|
+
// steht in keinem Enum der Ontologie, wird aber real geschrieben; eine Positivliste haette
|
|
23
|
+
// diese zwoelf CRs stillschweigend uebersprungen. Eine Regel, die durch einen ihr
|
|
24
|
+
// unbekannten Statuswert stumm wird, ist von einer bestandenen Pruefung nicht zu
|
|
25
|
+
// unterscheiden — dieselbe Fail-open-Klasse wie ND-01/ND-02 vor CR-SM-286.
|
|
26
|
+
//
|
|
27
|
+
// Ein CR ganz OHNE status ist erst recht nicht abgeschlossen.
|
|
28
|
+
//
|
|
29
|
+
// Gemessen vor der Umstellung: von 514 CRs tragen 54 keinen Umfangsbezug — davon sind
|
|
30
|
+
// **0 offen**, 52 `done` und 2 ohne Status. Die neue Fassung feuert am Bestand also auf
|
|
31
|
+
// hoechstens 2 und laesst die Historie unangetastet; ihre Wirkung beginnt beim naechsten
|
|
32
|
+
// CR, der geschrieben wird.
|
|
3
33
|
// ---------------------------------------------------------------------------
|
|
34
|
+
/** Elementtypen, die einen Aenderungsumfang darstellen — MS gehoert bewusst nicht dazu. */
|
|
35
|
+
const SCOPE_TYPES = new Set(['FUNC', 'MOD', 'SCHEMA', 'REQ', 'UC']);
|
|
36
|
+
/** Abgeschlossen heisst: nicht mehr steuerbar. Alles andere ist Grundgesamtheit. */
|
|
37
|
+
const CLOSED_STATUS = new Set(['done', 'dropped', 'rejected']);
|
|
4
38
|
function crMustTrack(graph) {
|
|
5
|
-
const
|
|
39
|
+
const typeOf = new Map(graph.elements.map(e => [e.id, e.type]));
|
|
40
|
+
const crs = graph.elements.filter(e => {
|
|
41
|
+
if (e.type !== 'CR')
|
|
42
|
+
return false;
|
|
43
|
+
return !CLOSED_STATUS.has(String(e.attributes?.status ?? ''));
|
|
44
|
+
});
|
|
6
45
|
return crs
|
|
7
|
-
.filter(cr => !graph.traces.some(t => t.source === cr.id && t.type === 'relation'))
|
|
46
|
+
.filter(cr => !graph.traces.some(t => t.source === cr.id && t.type === 'relation' && SCOPE_TYPES.has(typeOf.get(t.target) ?? '')))
|
|
8
47
|
.map(cr => ({
|
|
9
48
|
rule_id: 'CR-R01',
|
|
10
49
|
severity: 'error',
|
|
11
50
|
element_id: cr.id,
|
|
12
|
-
message: `${cr.id}
|
|
13
|
-
fix_hint: 'Add relation
|
|
51
|
+
message: `${cr.id} names no scope (no relation to FUNC/MOD/SCHEMA/REQ/UC)`,
|
|
52
|
+
fix_hint: 'Add a relation trace to the FUNC, MOD, SCHEMA, REQ or UC this CR changes — a milestone alone is a date, not a scope',
|
|
14
53
|
context: {
|
|
15
54
|
element_type: cr.type,
|
|
16
55
|
element_name: cr.name,
|
|
@@ -63,71 +102,50 @@ function noConcurrentMutation(graph) {
|
|
|
63
102
|
targetToCrs.set(tgt, list);
|
|
64
103
|
}
|
|
65
104
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
105
|
+
// CR-SM-304 — EIN Befund je Konflikt-MENGE, aber an einem BERECHENBAREN Element.
|
|
106
|
+
//
|
|
107
|
+
// Die Ein-Befund-Regel bleibt: dieselben zwei offenen CRs, die sich an drei Knoten begegnen,
|
|
108
|
+
// sind EIN Koordinationsproblem und nicht drei (Gate 4, Doppelzaehlung). Falsch war, woher der
|
|
109
|
+
// Anker kam: `targetToCrs` folgt der TRACE-Reihenfolge, also entschied die Array-Ordnung im
|
|
110
|
+
// Graphen, welches Ziel meldet — "kein erster Treffer" ist Gate 5 woertlich, und dieselbe
|
|
111
|
+
// Klasse wie der offene R-28-Fall (CR-SM-245). Gemessen am graphcode-Selbstmodell wechselte
|
|
112
|
+
// der Befund bei umgedrehter Reihenfolge von FUNC-graph-suggest auf MS-9-generation; ein
|
|
113
|
+
// Reseed aus dem Kuzu-Store reichte, damit eine Vorher/Nachher-Bilanz bei NULL Zuegen
|
|
114
|
+
// "einen geschlossen, einen neu" meldete.
|
|
115
|
+
//
|
|
116
|
+
// Zweiter Teil desselben Defekts: das unterdrueckte Ziel wurde nicht nur nicht angekert, es
|
|
117
|
+
// wurde gar nicht genannt. Wer koordinieren soll, braucht die vollstaendige Liste — sie steht
|
|
118
|
+
// jetzt in der Meldung und als `also_affects` im Kontext.
|
|
119
|
+
const bySet = new Map();
|
|
120
|
+
for (const target of [...targetToCrs.keys()].sort()) {
|
|
121
|
+
const crIds = [...targetToCrs.get(target)].sort();
|
|
69
122
|
if (crIds.length < 2)
|
|
70
123
|
continue;
|
|
71
|
-
const key = crIds.
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
124
|
+
const key = crIds.join(',');
|
|
125
|
+
bySet.set(key, [...(bySet.get(key) ?? []), target]);
|
|
126
|
+
}
|
|
127
|
+
const violations = [];
|
|
128
|
+
for (const [key, targets] of bySet) {
|
|
129
|
+
const crIds = key.split(',');
|
|
130
|
+
const [anchor, ...also] = targets;
|
|
131
|
+
const el = graph.elements.find(e => e.id === anchor);
|
|
75
132
|
violations.push({
|
|
76
133
|
rule_id: 'CR-R03',
|
|
77
134
|
severity: 'warning',
|
|
78
|
-
element_id:
|
|
79
|
-
message: `${
|
|
135
|
+
element_id: anchor,
|
|
136
|
+
message: `${anchor} is tracked by ${crIds.length} open CRs: ${crIds.join(', ')}` +
|
|
137
|
+
(also.length > 0 ? ` — the same CRs also track ${also.join(', ')}` : ''),
|
|
80
138
|
fix_hint: 'Coordinate changes to avoid merge conflicts',
|
|
81
139
|
context: {
|
|
82
|
-
element_type:
|
|
83
|
-
element_name:
|
|
140
|
+
element_type: el?.type,
|
|
141
|
+
element_name: el?.name,
|
|
142
|
+
also_affects: also,
|
|
84
143
|
},
|
|
85
144
|
});
|
|
86
145
|
}
|
|
87
146
|
return violations;
|
|
88
147
|
}
|
|
89
148
|
// ---------------------------------------------------------------------------
|
|
90
|
-
// CR-R04: an OPEN CR must have at least one relation→FUNC trace (CR-207,
|
|
91
|
-
// Grundgesamtheit verengt in CR-SM-255)
|
|
92
|
-
//
|
|
93
|
-
// "Welche Funktionen fasst dieser CR an" ist eine PLANUNGSfrage — sie steuert, bevor
|
|
94
|
-
// gebaut wird. An einem geschlossenen CR ist sie Archaeologie: was er beruehrt hat,
|
|
95
|
-
// steht im Commit und im Diff, nicht im Graphen. Wer die Kanten nachtraeglich zieht,
|
|
96
|
-
// raet, und erfundene Praezision ist teurer als eine fehlende Kante.
|
|
97
|
-
//
|
|
98
|
-
// Gemessen am graphcode-Selbstmodell (graphVersion 171): 41 meldende CRs, davon 40
|
|
99
|
-
// `done` und 1 `dropped` — kein einziger offener. Die Regel feuerte ausschliesslich
|
|
100
|
-
// dort, wo sie nichts mehr steuern kann, und trug damit 41 unbearbeitbare Befunde.
|
|
101
|
-
//
|
|
102
|
-
// Die Statusmenge ist woertlich die von CR-R03 (s. o.): dieselbe Frage nach "noch in
|
|
103
|
-
// Arbeit", dieselbe Antwort. CR-R01 ("ein CR trackt ueberhaupt etwas") bleibt fuer
|
|
104
|
-
// ALLE CRs gueltig — nur die Verschaerfung "und zwar einen FUNC" ist Planung.
|
|
105
|
-
// ---------------------------------------------------------------------------
|
|
106
|
-
function crMustHaveFunc(graph) {
|
|
107
|
-
// Ein CR ganz OHNE status ist kein geschlossener CR — die Regel darf nicht durch ein
|
|
108
|
-
// fehlendes Attribut stumm werden.
|
|
109
|
-
const crs = graph.elements.filter(e => {
|
|
110
|
-
if (e.type !== 'CR')
|
|
111
|
-
return false;
|
|
112
|
-
const status = e.attributes?.status;
|
|
113
|
-
return status === undefined || status === 'open' || status === 'in-progress';
|
|
114
|
-
});
|
|
115
|
-
return crs
|
|
116
|
-
.filter(cr => !graph.traces.some(t => t.source === cr.id && t.type === 'relation' &&
|
|
117
|
-
graph.elements.some(e => e.id === t.target && e.type === 'FUNC')))
|
|
118
|
-
.map(cr => ({
|
|
119
|
-
rule_id: 'CR-R04',
|
|
120
|
-
severity: 'warning',
|
|
121
|
-
element_id: cr.id,
|
|
122
|
-
message: `${cr.id} has no relation→FUNC trace (no implementation scope)`,
|
|
123
|
-
fix_hint: 'Add CR→FUNC [relation] traces to define which functions this CR changes',
|
|
124
|
-
context: {
|
|
125
|
-
element_type: cr.type,
|
|
126
|
-
element_name: cr.name,
|
|
127
|
-
},
|
|
128
|
-
}));
|
|
129
|
-
}
|
|
130
|
-
// ---------------------------------------------------------------------------
|
|
131
149
|
// MS-03: CR should have a CR→MS [relation] trace (CR-207)
|
|
132
150
|
// ---------------------------------------------------------------------------
|
|
133
151
|
function crShouldHaveMilestone(graph) {
|
|
@@ -159,7 +177,6 @@ export const CR_RULES = [
|
|
|
159
177
|
// (= alle Elemente) ist die einzige Menge, aus der der Zielknoten garantiert stammt.
|
|
160
178
|
// Damit ist sie nach R-08/R-18 die dritte 'all'-Regel — bewusst, nicht vergessen.
|
|
161
179
|
{ id: 'CR-R03', name: 'No concurrent mutation', severity: 'warning', evaluate: noConcurrentMutation, domain: ['all'] },
|
|
162
|
-
{ id: 'CR-R04', name: 'Open CR must have FUNC', severity: 'warning', evaluate: crMustHaveFunc, domain: ['CR'] },
|
|
163
180
|
{ id: 'MS-03', name: 'CR without milestone', severity: 'info', evaluate: crShouldHaveMilestone, domain: ['CR'] },
|
|
164
181
|
];
|
|
165
182
|
// CR-SM-236: `policy` wird durchgereicht, auch wo diese Familie heute keine Schwelle hat —
|
|
@@ -13,14 +13,16 @@ import type { MetricPolicy } from './policy.js';
|
|
|
13
13
|
* hier statt eine eigene Tabelle zu fuehren. Eine zweite Tabelle kann nicht hinterherhinken,
|
|
14
14
|
* wenn es keine zweite gibt.
|
|
15
15
|
*/
|
|
16
|
+
/** Profile groupings — see CATALOGS. */
|
|
17
|
+
export type ProfileId = 'default' | 'se' | 'coding';
|
|
16
18
|
export declare const ALL_RULE_DEFS: ReadonlyArray<{
|
|
17
19
|
id: string;
|
|
18
20
|
name: string;
|
|
19
21
|
severity: 'error' | 'warning' | 'info';
|
|
20
22
|
domain: readonly string[];
|
|
23
|
+
/** CR-SM-285: das Profil reist mit der Regel, statt aus ihrer ID geraten zu werden. */
|
|
24
|
+
profile: Exclude<ProfileId, 'default'>;
|
|
21
25
|
}>;
|
|
22
|
-
/** Profile groupings derived from rule prefixes. */
|
|
23
|
-
export type ProfileId = 'default' | 'se' | 'coding';
|
|
24
26
|
export declare function getRuleDefsForProfile(profile: ProfileId): typeof ALL_RULE_DEFS;
|
|
25
27
|
/**
|
|
26
28
|
* Evaluate all rules against a graph. Single call replaces the individual evaluator calls.
|