@sigloch/contracts 6.1.0 → 9.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/se/ao-rules.js +44 -29
- package/dist/se/conformance-rules.d.ts +29 -0
- package/dist/se/conformance-rules.js +107 -17
- package/dist/se/fchain-quality-rules.js +57 -53
- package/dist/se/flat-graph.d.ts +3 -0
- package/dist/se/flat-graph.js +52 -0
- package/dist/se/format-e-parser.d.ts +14 -2
- package/dist/se/format-e-parser.js +33 -9
- package/dist/se/grammar-snapshot.d.ts +7 -6
- package/dist/se/grammar-snapshot.js +17 -16
- package/dist/se/graph-index.d.ts +56 -0
- package/dist/se/graph-index.js +60 -0
- package/dist/se/index.d.ts +5 -3
- package/dist/se/index.js +5 -3
- package/dist/se/meta-model.d.ts +52 -7
- package/dist/se/meta-model.js +108 -14
- package/dist/se/metric-rules.d.ts +8 -0
- package/dist/se/metric-rules.js +95 -23
- package/dist/se/ontology.d.ts +32 -25
- package/dist/se/ontology.js +56 -11
- package/dist/se/quality-rules.js +19 -5
- package/dist/se/readiness.d.ts +4 -2
- package/dist/se/readiness.js +23 -1
- package/dist/se/rules.d.ts +12 -7
- package/dist/se/rules.js +215 -103
- package/dist/se/uc-quality-rules.js +67 -25
- package/package.json +1 -1
package/dist/se/ao-rules.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getND02SimilarityMatrix } from './near-duplicate-rules.js';
|
|
2
|
+
import { indexOf } from './graph-index.js';
|
|
2
3
|
const SCHEMA_OVERLAP_THRESHOLD = 0.5;
|
|
3
4
|
/**
|
|
4
5
|
* AO-D01: Relay Node Detection.
|
|
@@ -6,13 +7,14 @@ const SCHEMA_OVERLAP_THRESHOLD = 0.5;
|
|
|
6
7
|
* and (c) target FUNCs share SCHEMA overlap >= 0.5 (via ND-02 matrix, skipped if unavailable).
|
|
7
8
|
*/
|
|
8
9
|
export function aoD01RelayNode(graph) {
|
|
10
|
+
const idx = indexOf(graph);
|
|
9
11
|
const violations = [];
|
|
10
|
-
const funcs =
|
|
12
|
+
const funcs = idx.elementsOfType('FUNC');
|
|
11
13
|
const funcIdSet = new Set(funcs.map(f => f.id));
|
|
12
14
|
for (const func of funcs) {
|
|
13
15
|
// (a) No satisfy traces (FUNC as source) connecting to any REQ
|
|
14
16
|
const hasSatisfy = graph.traces.some(t => t.type === 'satisfy' &&
|
|
15
|
-
t.source === func.id &&
|
|
17
|
+
t.source === func.id && idx.typeOf(t.target) === 'REQ');
|
|
16
18
|
if (hasSatisfy)
|
|
17
19
|
continue;
|
|
18
20
|
// (b) >=2 outgoing io traces to other FUNCs
|
|
@@ -24,7 +26,7 @@ export function aoD01RelayNode(graph) {
|
|
|
24
26
|
// (c) Target FUNCs share SCHEMA overlap >= 0.5 (skip check if no matrix)
|
|
25
27
|
if (!passesSchemaOverlap(graph, ioTargets))
|
|
26
28
|
continue;
|
|
27
|
-
const el =
|
|
29
|
+
const el = idx.byId.get(func.id);
|
|
28
30
|
violations.push({
|
|
29
31
|
rule_id: 'AO-D01',
|
|
30
32
|
severity: 'info',
|
|
@@ -37,29 +39,35 @@ export function aoD01RelayNode(graph) {
|
|
|
37
39
|
}
|
|
38
40
|
/** CR-165: Resolve FUNC→SCHEMA via FUNC→FLOW(io) + FLOW→SCHEMA(relation). */
|
|
39
41
|
function funcToSchemas(graph, funcId) {
|
|
42
|
+
const idx = indexOf(graph);
|
|
40
43
|
const schemas = new Set();
|
|
41
44
|
// Find FLOWs connected to this FUNC via io (FUNC→FLOW or FLOW→FUNC)
|
|
45
|
+
//
|
|
46
|
+
// CR-SM-264: die io-Kanten dieser FUNC statt zweier Vollscans ueber ALLE Traces je FUNC
|
|
47
|
+
// (`F x T`) — AO-D03 ruft diese Funktion einmal je FUNC. Die Reihenfolge von `flowIds` ist
|
|
48
|
+
// dabei egal, die Menge wird nur auf Mitgliedschaft geprueft.
|
|
42
49
|
const flowIds = new Set();
|
|
43
|
-
for (const t of
|
|
44
|
-
if (t.
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
//
|
|
54
|
-
|
|
55
|
-
|
|
50
|
+
for (const t of idx.out(funcId, 'io'))
|
|
51
|
+
if (idx.typeOf(t.target) === 'FLOW')
|
|
52
|
+
flowIds.add(t.target);
|
|
53
|
+
for (const t of idx.in(funcId, 'io'))
|
|
54
|
+
if (idx.typeOf(t.source) === 'FLOW')
|
|
55
|
+
flowIds.add(t.source);
|
|
56
|
+
// Find SCHEMAs connected to those FLOWs via relation.
|
|
57
|
+
//
|
|
58
|
+
// Hier NICHT ueber `flowIds` iterieren, obwohl das billiger waere: die Einfuegereihenfolge
|
|
59
|
+
// dieser Menge landet ueber `shared.join(', ')` woertlich in der AO-D03-Meldung. Der Lauf
|
|
60
|
+
// ueber die relation-Kanten in Graph-Reihenfolge haelt sie identisch — und ist trotzdem
|
|
61
|
+
// deutlich kuerzer als der Vollscan, weil er nur einen Kantentyp sieht.
|
|
62
|
+
for (const t of idx.tracesOfType('relation')) {
|
|
63
|
+
if (flowIds.has(t.source) && idx.typeOf(t.target) === 'SCHEMA')
|
|
56
64
|
schemas.add(t.target);
|
|
57
|
-
}
|
|
58
65
|
}
|
|
59
66
|
return schemas;
|
|
60
67
|
}
|
|
61
68
|
/** Check if any pair of target FUNCs shares SCHEMA overlap via FLOW→SCHEMA(relation) + ND-02 matrix. */
|
|
62
69
|
function passesSchemaOverlap(graph, targetFuncIds) {
|
|
70
|
+
const idx = indexOf(graph);
|
|
63
71
|
const nd02 = getND02SimilarityMatrix();
|
|
64
72
|
if (!nd02)
|
|
65
73
|
return true; // no matrix → skip check, assume pass
|
|
@@ -93,8 +101,9 @@ function passesSchemaOverlap(graph, targetFuncIds) {
|
|
|
93
101
|
* FUNC A sends io to both B and C, where B and C connect to the same SCHEMA elements.
|
|
94
102
|
*/
|
|
95
103
|
export function aoD03DuplicatePath(graph) {
|
|
104
|
+
const idx = indexOf(graph);
|
|
96
105
|
const violations = [];
|
|
97
|
-
const funcs =
|
|
106
|
+
const funcs = idx.elementsOfType('FUNC');
|
|
98
107
|
const funcIdSet = new Set(funcs.map(f => f.id));
|
|
99
108
|
const seen = new Set();
|
|
100
109
|
// Pre-compute: FUNC → set of SCHEMA targets via FLOW→SCHEMA(relation) (CR-165)
|
|
@@ -141,15 +150,16 @@ export function aoD03DuplicatePath(graph) {
|
|
|
141
150
|
// diesen Aus-Zustand nicht: jedes Modulpaar mit >= 1 Kreuzung erzeugte eine info-Meldung.
|
|
142
151
|
// ---------------------------------------------------------------------------
|
|
143
152
|
export function cr01CrossingFlowCount(graph, policy) {
|
|
153
|
+
const idx = indexOf(graph);
|
|
144
154
|
const violations = [];
|
|
145
155
|
const steps = policy.crossingFlows;
|
|
146
156
|
if (steps === null)
|
|
147
157
|
return violations;
|
|
148
|
-
const mods =
|
|
158
|
+
const mods = idx.elementsOfType('MOD');
|
|
149
159
|
// Build MOD → Set<FUNC-id> mapping
|
|
150
160
|
const modFuncs = new Map();
|
|
151
161
|
for (const mod of mods) {
|
|
152
|
-
const funcIds = new Set(
|
|
162
|
+
const funcIds = new Set(idx.in(mod.id, 'allocate').map(t => t.source));
|
|
153
163
|
modFuncs.set(mod.id, funcIds);
|
|
154
164
|
}
|
|
155
165
|
// Count crossings per module pair
|
|
@@ -191,10 +201,11 @@ export function cr01CrossingFlowCount(graph, policy) {
|
|
|
191
201
|
// FUNCs must not be allocated directly to physical MODs — use logical sub-modules.
|
|
192
202
|
// ---------------------------------------------------------------------------
|
|
193
203
|
export function rt01PhysicalBoundaryIntegrity(graph) {
|
|
204
|
+
const idx = indexOf(graph);
|
|
194
205
|
const physicalMods = graph.elements.filter(e => e.type === 'MOD' && e.attributes?.kind === 'physical');
|
|
195
206
|
return graph.traces
|
|
196
207
|
.filter(t => t.type === 'allocate' && physicalMods.some(m => m.id === t.target))
|
|
197
|
-
.filter(t =>
|
|
208
|
+
.filter(t => idx.typeOf(t.source) === 'FUNC')
|
|
198
209
|
.map(t => ({
|
|
199
210
|
rule_id: 'RT-01',
|
|
200
211
|
severity: 'error',
|
|
@@ -208,10 +219,11 @@ export function rt01PhysicalBoundaryIntegrity(graph) {
|
|
|
208
219
|
// Physical MODs should have at least one logical sub-module via compose trace.
|
|
209
220
|
// ---------------------------------------------------------------------------
|
|
210
221
|
export function ph01PhysicalModCompleteness(graph) {
|
|
222
|
+
const idx = indexOf(graph);
|
|
211
223
|
const physicalMods = graph.elements.filter(e => e.type === 'MOD' && e.attributes?.kind === 'physical');
|
|
212
224
|
return physicalMods
|
|
213
225
|
.filter(pm => !graph.traces.some(t => t.source === pm.id && t.type === 'compose' &&
|
|
214
|
-
|
|
226
|
+
idx.typeOf(t.target) === 'MOD'))
|
|
215
227
|
.map(pm => ({
|
|
216
228
|
rule_id: 'PH-01',
|
|
217
229
|
severity: 'info',
|
|
@@ -226,6 +238,7 @@ export function ph01PhysicalModCompleteness(graph) {
|
|
|
226
238
|
// FUNC @requires must be satisfied by the physical MOD's @capability.
|
|
227
239
|
// ---------------------------------------------------------------------------
|
|
228
240
|
export function ca01CapabilityAllocation(graph) {
|
|
241
|
+
const idx = indexOf(graph);
|
|
229
242
|
const violations = [];
|
|
230
243
|
const funcsWithRequires = graph.elements.filter(e => e.type === 'FUNC' && e.attributes?.requires);
|
|
231
244
|
for (const func of funcsWithRequires) {
|
|
@@ -236,15 +249,16 @@ export function ca01CapabilityAllocation(graph) {
|
|
|
236
249
|
const allocTrace = graph.traces.find(t => t.source === func.id && t.type === 'allocate');
|
|
237
250
|
if (!allocTrace)
|
|
238
251
|
continue;
|
|
239
|
-
const
|
|
252
|
+
const logicalModCandidate = idx.byId.get(allocTrace.target);
|
|
253
|
+
const logicalMod = logicalModCandidate?.type === 'MOD' ? logicalModCandidate : undefined;
|
|
240
254
|
if (!logicalMod)
|
|
241
255
|
continue;
|
|
242
256
|
// Find physical parent MOD (compose source → logical MOD target)
|
|
243
257
|
const physParentTrace = graph.traces.find(t => t.type === 'compose' && t.target === logicalMod.id &&
|
|
244
|
-
|
|
258
|
+
(idx.byId.get(t.source)?.type === 'MOD' && idx.byId.get(t.source)?.attributes?.kind === 'physical'));
|
|
245
259
|
if (!physParentTrace)
|
|
246
260
|
continue;
|
|
247
|
-
const physMod =
|
|
261
|
+
const physMod = idx.byId.get(physParentTrace.source);
|
|
248
262
|
const capabilities = (Array.isArray(physMod.attributes?.capability)
|
|
249
263
|
? physMod.attributes.capability
|
|
250
264
|
: physMod.attributes?.capability ? [physMod.attributes.capability] : []);
|
|
@@ -289,9 +303,10 @@ export function ca01CapabilityAllocation(graph) {
|
|
|
289
303
|
// andere Regel trifft.
|
|
290
304
|
// ---------------------------------------------------------------------------
|
|
291
305
|
export function io01CrossModuleCompleteness(graph) {
|
|
306
|
+
const idx = indexOf(graph);
|
|
292
307
|
const violations = [];
|
|
293
308
|
const flowIds = new Set(graph.elements.filter(e => e.type === 'FLOW').map(e => e.id));
|
|
294
|
-
const funcIdSet =
|
|
309
|
+
const funcIdSet = idx.idsOfType('FUNC');
|
|
295
310
|
/** io-Nachbarschaft FUNC <-> FLOW, richtungslos: die Frage ist "haengt das zusammen?". */
|
|
296
311
|
const flowsOfFunc = new Map();
|
|
297
312
|
for (const t of graph.traces) {
|
|
@@ -306,7 +321,7 @@ export function io01CrossModuleCompleteness(graph) {
|
|
|
306
321
|
flowsOfFunc.set(f, new Set());
|
|
307
322
|
flowsOfFunc.get(f).add(fl);
|
|
308
323
|
}
|
|
309
|
-
for (const fc of
|
|
324
|
+
for (const fc of idx.elementsOfType('FCHAIN')) {
|
|
310
325
|
const members = graph.traces
|
|
311
326
|
.filter(t => t.source === fc.id && t.type === 'compose')
|
|
312
327
|
.map(t => t.target)
|
|
@@ -356,7 +371,7 @@ export function io01CrossModuleCompleteness(graph) {
|
|
|
356
371
|
for (const id of wired) {
|
|
357
372
|
if (largestSet.has(id))
|
|
358
373
|
continue;
|
|
359
|
-
const el =
|
|
374
|
+
const el = idx.byId.get(id);
|
|
360
375
|
const mod = graph.traces.find(t => t.source === id && t.type === 'allocate')?.target ?? 'unallocated';
|
|
361
376
|
violations.push({
|
|
362
377
|
rule_id: 'IO-01',
|
|
@@ -368,7 +383,7 @@ export function io01CrossModuleCompleteness(graph) {
|
|
|
368
383
|
element_type: 'FUNC',
|
|
369
384
|
element_name: el?.name ?? id,
|
|
370
385
|
candidate_targets: largest.slice(0, 3).map(t => {
|
|
371
|
-
const te =
|
|
386
|
+
const te = idx.byId.get(t);
|
|
372
387
|
return { id: t, type: te?.type ?? 'FUNC', name: te?.name ?? t };
|
|
373
388
|
}),
|
|
374
389
|
},
|
|
@@ -49,6 +49,7 @@ export declare const CodeFactsSchema: z.ZodObject<{
|
|
|
49
49
|
from: z.ZodString;
|
|
50
50
|
to: z.ZodString;
|
|
51
51
|
}, z.core.$strip>>>;
|
|
52
|
+
declaredDependencies: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
52
53
|
}, z.core.$strip>;
|
|
53
54
|
export type CodeFacts = z.infer<typeof CodeFactsSchema>;
|
|
54
55
|
/** A conformance rule: pure over (graph, facts) — never touches I/O itself. */
|
|
@@ -58,6 +59,34 @@ export interface ConformanceRuleDefinition {
|
|
|
58
59
|
severity: RuleSeverity;
|
|
59
60
|
evaluate: (graph: OntologyGraph, facts: CodeFacts) => RuleViolation[];
|
|
60
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;
|
|
61
90
|
/** All RC conformance rules — evaluated by executors that can supply CodeFacts. */
|
|
62
91
|
export declare const CODE_CONFORMANCE_RULES: ConformanceRuleDefinition[];
|
|
63
92
|
/** Run all RC rules against a graph + extracted code facts. */
|
|
@@ -48,6 +48,17 @@ export const CodeFactsSchema = z.object({
|
|
|
48
48
|
* given), so pre-CR-212 CodeFacts stay valid.
|
|
49
49
|
*/
|
|
50
50
|
importEdges: z.array(ImportEdgeSchema).optional(),
|
|
51
|
+
/**
|
|
52
|
+
* Package names the consumer declares in `dependencies` + `devDependencies`, for
|
|
53
|
+
* RC-06 (CR-SM-262). The extractor reads them from the repo's package.json.
|
|
54
|
+
*
|
|
55
|
+
* Optional, and the ABSENT case is silence, not a violation — the opposite of `files`,
|
|
56
|
+
* where a missing key must surface loudly. The asymmetry is deliberate: a missing file
|
|
57
|
+
* entry means the extractor looked and found nothing, while an absent dependency list
|
|
58
|
+
* means it never looked. Treating "never looked" as "declares nothing" would report
|
|
59
|
+
* every external binding in the graph at once, which is noise, not a finding.
|
|
60
|
+
*/
|
|
61
|
+
declaredDependencies: z.array(z.string()).optional(),
|
|
51
62
|
});
|
|
52
63
|
const missingFile = (facts, file) => facts.files[file]?.exists !== true;
|
|
53
64
|
// RC-01: every valid FUNC realRef must resolve — file on disk, symbol declared in
|
|
@@ -256,15 +267,19 @@ function schemaRefMustBeUsed(graph, facts) {
|
|
|
256
267
|
// such adjacency ⇒ one RC-05 per (fromMod → toMod) pair, evidence = the
|
|
257
268
|
// crossing file imports.
|
|
258
269
|
// ---------------------------------------------------------------------------
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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) {
|
|
263
282
|
const typeOf = new Map(graph.elements.map(e => [e.id, e.type]));
|
|
264
|
-
const modIds = graph.elements.filter(e => e.type === 'MOD').map(e => e.id);
|
|
265
|
-
if (modIds.length === 0)
|
|
266
|
-
return [];
|
|
267
|
-
// 1. file → MOD. Direct realRef bindings first.
|
|
268
283
|
const fileToMod = new Map();
|
|
269
284
|
for (const el of graph.elements) {
|
|
270
285
|
if (el.type !== 'FUNC')
|
|
@@ -281,7 +296,7 @@ function importDriftConformance(graph, facts) {
|
|
|
281
296
|
.filter(e => e.type === 'MOD' && typeof e.attributes?.path === 'string')
|
|
282
297
|
.map(e => ({ id: e.id, path: e.attributes.path.replace(/[*].*$/, '').replace(/\/+$/, '') }))
|
|
283
298
|
.filter(m => m.path.length > 0);
|
|
284
|
-
|
|
299
|
+
return (file) => {
|
|
285
300
|
if (fileToMod.has(file))
|
|
286
301
|
return fileToMod.get(file);
|
|
287
302
|
let best;
|
|
@@ -292,6 +307,27 @@ function importDriftConformance(graph, facts) {
|
|
|
292
307
|
}
|
|
293
308
|
return best?.id;
|
|
294
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);
|
|
295
331
|
// 2. graph MOD-adjacency (undirected) from io crossings FUNC_A ─io→ FLOW ─io→ FUNC_B.
|
|
296
332
|
const funcMod = (funcId) => {
|
|
297
333
|
const t = graph.traces.find(tr => tr.source === funcId && tr.type === 'allocate' && typeOf.get(tr.target) === 'MOD');
|
|
@@ -321,16 +357,15 @@ function importDriftConformance(graph, facts) {
|
|
|
321
357
|
const [x, y] = [a, b].sort();
|
|
322
358
|
return adjacent.has(`${x}|${y}`);
|
|
323
359
|
};
|
|
324
|
-
// 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.
|
|
325
365
|
const drift = new Map();
|
|
326
|
-
const unassigned = new Set();
|
|
327
366
|
for (const edge of importEdges) {
|
|
328
367
|
const fromMod = resolveMod(edge.from);
|
|
329
368
|
const toMod = resolveMod(edge.to);
|
|
330
|
-
if (!fromMod)
|
|
331
|
-
unassigned.add(edge.from);
|
|
332
|
-
if (!toMod)
|
|
333
|
-
unassigned.add(edge.to);
|
|
334
369
|
if (!fromMod || !toMod || fromMod === toMod)
|
|
335
370
|
continue;
|
|
336
371
|
if (isAdjacent(fromMod, toMod))
|
|
@@ -341,7 +376,6 @@ function importDriftConformance(graph, facts) {
|
|
|
341
376
|
drift.set(key, entry);
|
|
342
377
|
}
|
|
343
378
|
const nameOf = new Map(graph.elements.map(e => [e.id, e.name]));
|
|
344
|
-
const unassignedNote = unassigned.size > 0 ? ` (unassigned files not mapped to a MOD: ${[...unassigned].sort().join(', ')})` : '';
|
|
345
379
|
return [...drift.values()].map(d => {
|
|
346
380
|
const shown = d.evidence.slice(0, 5);
|
|
347
381
|
const more = d.evidence.length > shown.length ? ` +${d.evidence.length - shown.length} more` : '';
|
|
@@ -349,7 +383,7 @@ function importDriftConformance(graph, facts) {
|
|
|
349
383
|
rule_id: 'RC-05',
|
|
350
384
|
severity: 'warning',
|
|
351
385
|
element_id: d.fromMod,
|
|
352
|
-
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}`,
|
|
353
387
|
fix_hint: `Document the ${d.fromMod}→${d.toMod} dependency in the graph (an io/FLOW connection between their FUNCs), or remove the import`,
|
|
354
388
|
context: {
|
|
355
389
|
element_type: 'MOD',
|
|
@@ -359,6 +393,61 @@ function importDriftConformance(graph, facts) {
|
|
|
359
393
|
};
|
|
360
394
|
});
|
|
361
395
|
}
|
|
396
|
+
// ---------------------------------------------------------------------------
|
|
397
|
+
// RC-06: an `external` realRef names a package the consumer actually depends on
|
|
398
|
+
// (CR-SM-262).
|
|
399
|
+
//
|
|
400
|
+
// RC-01..03 skip `external === true` — and rightly so: the path of a foreign package is
|
|
401
|
+
// not in this repo and cannot be resolved here. The consequence was that external bindings
|
|
402
|
+
// were never checked at all and rotted in silence. Found by hand at the graphcode
|
|
403
|
+
// self-model on 2026-08-22, not by a rule: `SCHEMA-metric-vector` carried
|
|
404
|
+
// `realRef.file = "packages/se-optimizer/src/metrics.ts"`, and `@sigloch/se-optimizer` had
|
|
405
|
+
// ceased to exist with CR-SM-248 — `MetricVector` moved to `se-engine`. The node pointed at
|
|
406
|
+
// a package that is not there, and RC-01 said nothing because `external: true` was set.
|
|
407
|
+
//
|
|
408
|
+
// The PATH is unverifiable here; the PACKAGE NAME is not. A path of the shape
|
|
409
|
+
// `packages/<name>/…` claims a workspace package, so `@sigloch/<name>` must appear in the
|
|
410
|
+
// consumer's dependencies. Any other path shape stays unchecked rather than guessed.
|
|
411
|
+
//
|
|
412
|
+
// Severity `warning`, not `error`: the path may deviate from the convention for honest
|
|
413
|
+
// reasons (monorepo restructuring, a linked working copy), and an `error` would block every
|
|
414
|
+
// further mutation through the delta gate. A rotted binding is a completeness signal, not a
|
|
415
|
+
// mismeasurement — unlike R-29, where the number itself becomes wrong.
|
|
416
|
+
// ---------------------------------------------------------------------------
|
|
417
|
+
/** `packages/<name>/…` — the workspace-package path shape RC-06 can decide. */
|
|
418
|
+
const WORKSPACE_PATH = /^packages\/([^/]+)\//;
|
|
419
|
+
/** Package scope of this family. A path claims `<SCOPE>/<name>`. */
|
|
420
|
+
const WORKSPACE_SCOPE = '@sigloch';
|
|
421
|
+
function externalRefMustNameDependency(graph, facts) {
|
|
422
|
+
const declared = facts.declaredDependencies;
|
|
423
|
+
if (declared === undefined)
|
|
424
|
+
return []; // extractor supplied none — silence, s. CodeFactsSchema
|
|
425
|
+
const known = new Set(declared);
|
|
426
|
+
const violations = [];
|
|
427
|
+
for (const el of graph.elements) {
|
|
428
|
+
// `concept: true` has no binding that could rot — a concept node claims nothing about code.
|
|
429
|
+
if (el.attributes?.external !== true || el.attributes?.concept === true)
|
|
430
|
+
continue;
|
|
431
|
+
const parsed = RealRefSchema.safeParse(el.attributes?.realRef);
|
|
432
|
+
if (!parsed.success)
|
|
433
|
+
continue;
|
|
434
|
+
const match = WORKSPACE_PATH.exec(parsed.data.file);
|
|
435
|
+
if (!match)
|
|
436
|
+
continue; // not the shape this rule can decide
|
|
437
|
+
const pkg = `${WORKSPACE_SCOPE}/${match[1]}`;
|
|
438
|
+
if (known.has(pkg))
|
|
439
|
+
continue;
|
|
440
|
+
violations.push({
|
|
441
|
+
rule_id: 'RC-06',
|
|
442
|
+
severity: 'warning',
|
|
443
|
+
element_id: el.id,
|
|
444
|
+
message: `${el.id} binds to '${parsed.data.file}', but '${pkg}' is not a declared dependency`,
|
|
445
|
+
fix_hint: `Add '${pkg}' to dependencies, or re-point the realRef at the package that now owns the symbol`,
|
|
446
|
+
context: { element_type: el.type, element_name: el.name },
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
return violations;
|
|
450
|
+
}
|
|
362
451
|
/** All RC conformance rules — evaluated by executors that can supply CodeFacts. */
|
|
363
452
|
export const CODE_CONFORMANCE_RULES = [
|
|
364
453
|
{ id: 'RC-01', name: 'FUNC realRef resolves to a declared symbol', severity: 'error', evaluate: codeRefMustResolve },
|
|
@@ -366,6 +455,7 @@ export const CODE_CONFORMANCE_RULES = [
|
|
|
366
455
|
{ id: 'RC-03', name: 'SCHEMA realRef resolves to a declared export', severity: 'error', evaluate: schemaRefMustResolve },
|
|
367
456
|
{ id: 'RC-04', name: 'SCHEMA realRef is parsed at its interface', severity: 'warning', evaluate: schemaRefMustBeUsed },
|
|
368
457
|
{ id: 'RC-05', name: 'cross-module import drift', severity: 'warning', evaluate: importDriftConformance },
|
|
458
|
+
{ id: 'RC-06', name: 'external realRef names a declared dependency', severity: 'warning', evaluate: externalRefMustNameDependency },
|
|
369
459
|
];
|
|
370
460
|
/** Run all RC rules against a graph + extracted code facts. */
|
|
371
461
|
export function evaluateConformanceRules(graph, facts) {
|
|
@@ -1,33 +1,36 @@
|
|
|
1
|
+
import { indexOf } from './graph-index.js';
|
|
1
2
|
// ---------------------------------------------------------------------------
|
|
2
3
|
// FC-01: FCHAIN must have Actor boundary (input or output via FLOW→ACTOR io)
|
|
3
4
|
// ---------------------------------------------------------------------------
|
|
4
5
|
export function fc01ActorBoundary(graph) {
|
|
5
|
-
|
|
6
|
-
|
|
6
|
+
// CR-SM-264: dieselbe Frage, einmal statt je Kette neu.
|
|
7
|
+
//
|
|
8
|
+
// Die alte Fassung war die verschachtelte Form, die UC-02 kubisch gemacht hat (CR-SM-261):
|
|
9
|
+
// je FCHAIN alle Traces, darin je io-Kante noch einmal alle Traces, darin je Treffer alle
|
|
10
|
+
// Elemente — `FCHAIN x FUNC x T x T x E`. Im Profil nach Teil a war FC-01 mit 6,3 % die
|
|
11
|
+
// teuerste FCHAIN-Regel.
|
|
12
|
+
//
|
|
13
|
+
// Beide Zweige fragen in Wahrheit DASSELBE: "beruehrt dieses Element eine io-Kante, deren
|
|
14
|
+
// anderes Ende ein ACTOR ist?" Einmal fuer den Zwischenknoten der Kettenglieder, einmal
|
|
15
|
+
// direkt fuer den Eltern-UC. Eine Menge beantwortet beide.
|
|
16
|
+
const idx = indexOf(graph);
|
|
17
|
+
const actorAdjacent = new Set();
|
|
18
|
+
for (const t of idx.tracesOfType('io')) {
|
|
19
|
+
if (idx.typeOf(t.target) === 'ACTOR')
|
|
20
|
+
actorAdjacent.add(t.source);
|
|
21
|
+
if (idx.typeOf(t.source) === 'ACTOR')
|
|
22
|
+
actorAdjacent.add(t.target);
|
|
23
|
+
}
|
|
24
|
+
return idx.elementsOfType('FCHAIN')
|
|
7
25
|
.filter(fc => {
|
|
8
|
-
//
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
// FUNC→FLOW or FLOW→FUNC
|
|
17
|
-
const flowId = t.source === fid ? t.target : (t.target === fid ? t.source : null);
|
|
18
|
-
if (!flowId)
|
|
19
|
-
return false;
|
|
20
|
-
// FLOW→ACTOR or ACTOR→FLOW
|
|
21
|
-
return graph.traces.some(ft => ft.type === 'io' &&
|
|
22
|
-
((ft.source === flowId && graph.elements.some(e => e.id === ft.target && e.type === 'ACTOR')) ||
|
|
23
|
-
(ft.target === flowId && graph.elements.some(e => e.id === ft.source && e.type === 'ACTOR'))));
|
|
24
|
-
}));
|
|
25
|
-
// Also check direct ACTOR→UC io on the parent UC
|
|
26
|
-
const parentUC = graph.traces.find(t => t.type === 'compose' && t.target === fc.id &&
|
|
27
|
-
graph.elements.some(e => e.id === t.source && e.type === 'UC'));
|
|
28
|
-
const hasDirectActorIO = parentUC && graph.traces.some(t => t.type === 'io' &&
|
|
29
|
-
((t.target === parentUC.source && graph.elements.some(e => e.id === t.source && e.type === 'ACTOR')) ||
|
|
30
|
-
(t.source === parentUC.source && graph.elements.some(e => e.id === t.target && e.type === 'ACTOR'))));
|
|
26
|
+
// Mitglieder der Kette — bewusst OHNE Typfilter, wie in der alten Fassung: FC-01 fragt
|
|
27
|
+
// nach dem, was komponiert ist, nicht nach dem, was eine FUNC ist (FC-03 tut das).
|
|
28
|
+
const memberIds = idx.out(fc.id, 'compose').map(t => t.target);
|
|
29
|
+
const hasActorIO = memberIds.some(fid => idx.out(fid, 'io').some(t => actorAdjacent.has(t.target)) ||
|
|
30
|
+
idx.in(fid, 'io').some(t => actorAdjacent.has(t.source)));
|
|
31
|
+
// Der Umweg ueber den Eltern-UC: dessen eigene ACTOR-io zaehlt auch.
|
|
32
|
+
const parentUC = idx.in(fc.id, 'compose').find(t => idx.typeOf(t.source) === 'UC');
|
|
33
|
+
const hasDirectActorIO = parentUC !== undefined && actorAdjacent.has(parentUC.source);
|
|
31
34
|
return !hasActorIO && !hasDirectActorIO;
|
|
32
35
|
})
|
|
33
36
|
.map(fc => ({
|
|
@@ -43,17 +46,15 @@ export function fc01ActorBoundary(graph) {
|
|
|
43
46
|
// FC-02: Leaf UC (no UC→compose→UC) must have at least one FCHAIN
|
|
44
47
|
// ---------------------------------------------------------------------------
|
|
45
48
|
export function fc02LeafUcHasFchain(graph) {
|
|
46
|
-
|
|
47
|
-
|
|
49
|
+
const idx = indexOf(graph);
|
|
50
|
+
return idx.elementsOfType('UC')
|
|
48
51
|
.filter(uc => {
|
|
52
|
+
const composed = idx.out(uc.id, 'compose');
|
|
49
53
|
// Leaf = no compose→UC children
|
|
50
|
-
|
|
51
|
-
graph.elements.some(e => e.id === t.target && e.type === 'UC'));
|
|
52
|
-
if (hasUCChild)
|
|
54
|
+
if (composed.some(t => idx.typeOf(t.target) === 'UC'))
|
|
53
55
|
return false;
|
|
54
56
|
// Must have compose→FCHAIN
|
|
55
|
-
return !
|
|
56
|
-
graph.elements.some(e => e.id === t.target && e.type === 'FCHAIN'));
|
|
57
|
+
return !composed.some(t => idx.typeOf(t.target) === 'FCHAIN');
|
|
57
58
|
})
|
|
58
59
|
.map(uc => ({
|
|
59
60
|
rule_id: 'FC-02',
|
|
@@ -69,15 +70,13 @@ export function fc02LeafUcHasFchain(graph) {
|
|
|
69
70
|
// ---------------------------------------------------------------------------
|
|
70
71
|
export function fc03FchainFlat(graph) {
|
|
71
72
|
const violations = [];
|
|
72
|
-
const
|
|
73
|
-
for (const fc of
|
|
74
|
-
const funcIds =
|
|
75
|
-
.filter(t => t.source === fc.id && t.type === 'compose')
|
|
73
|
+
const idx = indexOf(graph);
|
|
74
|
+
for (const fc of idx.elementsOfType('FCHAIN')) {
|
|
75
|
+
const funcIds = idx.out(fc.id, 'compose')
|
|
76
76
|
.map(t => t.target)
|
|
77
|
-
.filter(id =>
|
|
77
|
+
.filter(id => idx.typeOf(id) === 'FUNC');
|
|
78
78
|
for (const fid of funcIds) {
|
|
79
|
-
const nestedCompose =
|
|
80
|
-
graph.elements.some(e => e.id === t.target && e.type === 'FUNC'));
|
|
79
|
+
const nestedCompose = idx.out(fid, 'compose').filter(t => idx.typeOf(t.target) === 'FUNC');
|
|
81
80
|
if (nestedCompose.length > 0) {
|
|
82
81
|
violations.push({
|
|
83
82
|
rule_id: 'FC-03',
|
|
@@ -85,7 +84,7 @@ export function fc03FchainFlat(graph) {
|
|
|
85
84
|
element_id: fid,
|
|
86
85
|
message: `${fid} in ${fc.id} has nested FUNC compose (should be flat)`,
|
|
87
86
|
fix_hint: 'Move nested functions to FCHAIN level (flat composition)',
|
|
88
|
-
context: { element_type: 'FUNC', element_name:
|
|
87
|
+
context: { element_type: 'FUNC', element_name: idx.byId.get(fid)?.name ?? fid },
|
|
89
88
|
});
|
|
90
89
|
}
|
|
91
90
|
}
|
|
@@ -105,24 +104,29 @@ export function fc03FchainFlat(graph) {
|
|
|
105
104
|
// to have unrelated ACTOR io.
|
|
106
105
|
// ---------------------------------------------------------------------------
|
|
107
106
|
export function fc04ActorBounded(graph) {
|
|
108
|
-
|
|
109
|
-
|
|
107
|
+
// CR-SM-264: die beiden Zwischenmengen einmal je Graph statt |io|^2 je Kette.
|
|
108
|
+
// `actorFed` sind die Knoten, in die ein ACTOR hineinschickt, `actorConsumed` die, aus denen
|
|
109
|
+
// ein ACTOR liest. Beide Richtungen bleiben getrennt — anders als bei FC-01, das nur fragt,
|
|
110
|
+
// OB ein ACTOR anliegt; FC-04 verlangt Eingang UND Ausgang.
|
|
111
|
+
const idx = indexOf(graph);
|
|
112
|
+
const io = idx.tracesOfType('io');
|
|
113
|
+
const actorFed = new Set();
|
|
114
|
+
const actorConsumed = new Set();
|
|
115
|
+
for (const a of io) {
|
|
116
|
+
if (idx.typeOf(a.source) === 'ACTOR')
|
|
117
|
+
actorFed.add(a.target);
|
|
118
|
+
if (idx.typeOf(a.target) === 'ACTOR')
|
|
119
|
+
actorConsumed.add(a.source);
|
|
120
|
+
}
|
|
121
|
+
return idx.elementsOfType('FCHAIN')
|
|
110
122
|
.filter(fc => {
|
|
111
|
-
const funcIds = new Set(
|
|
112
|
-
.filter(t => t.source === fc.id && t.type === 'compose')
|
|
113
|
-
.map(t => t.target)
|
|
114
|
-
.filter(id => graph.elements.some(e => e.id === id && e.type === 'FUNC')));
|
|
123
|
+
const funcIds = new Set(idx.out(fc.id, 'compose').map(t => t.target).filter(id => idx.typeOf(id) === 'FUNC'));
|
|
115
124
|
if (funcIds.size === 0)
|
|
116
125
|
return false; // R-15 already flags the empty-chain case
|
|
117
|
-
const io = graph.traces.filter(t => t.type === 'io');
|
|
118
|
-
const isActor = (id) => graph.elements.some(e => e.id === id && e.type === 'ACTOR');
|
|
119
|
-
const isFlow = (id) => graph.elements.some(e => e.id === id && e.type === 'FLOW');
|
|
120
126
|
// Entry: ACTOR→FLOW→FUNC∈chain (something triggers the chain).
|
|
121
|
-
const entry = io.some(e => funcIds.has(e.target) &&
|
|
122
|
-
io.some(a => a.target === e.source && isActor(a.source)));
|
|
127
|
+
const entry = io.some(e => funcIds.has(e.target) && idx.typeOf(e.source) === 'FLOW' && actorFed.has(e.source));
|
|
123
128
|
// Exit: FUNC∈chain→FLOW→ACTOR (the chain produces something back to an actor).
|
|
124
|
-
const exit = io.some(e => funcIds.has(e.source) &&
|
|
125
|
-
io.some(a => a.source === e.target && isActor(a.target)));
|
|
129
|
+
const exit = io.some(e => funcIds.has(e.source) && idx.typeOf(e.target) === 'FLOW' && actorConsumed.has(e.target));
|
|
126
130
|
return !(entry && exit);
|
|
127
131
|
})
|
|
128
132
|
.map(fc => ({
|