@sigloch/contracts 0.7.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/LICENSE +21 -0
- package/dist/harness/index.d.ts +185 -0
- package/dist/harness/index.js +185 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +14 -0
- package/dist/se/ao-rules.d.ts +59 -0
- package/dist/se/ao-rules.js +341 -0
- package/dist/se/conformance-rules.d.ts +64 -0
- package/dist/se/conformance-rules.js +364 -0
- package/dist/se/cr-quality-rules.d.ts +8 -0
- package/dist/se/cr-quality-rules.js +141 -0
- package/dist/se/evaluate-all.d.ts +17 -0
- package/dist/se/evaluate-all.js +50 -0
- package/dist/se/fchain-quality-rules.d.ts +10 -0
- package/dist/se/fchain-quality-rules.js +105 -0
- package/dist/se/fmea-rules.d.ts +17 -0
- package/dist/se/fmea-rules.js +137 -0
- package/dist/se/format-e-parser.d.ts +28 -0
- package/dist/se/format-e-parser.js +217 -0
- package/dist/se/index.d.ts +28 -0
- package/dist/se/index.js +28 -0
- package/dist/se/meta-model.d.ts +26 -0
- package/dist/se/meta-model.js +60 -0
- package/dist/se/metric-rules.d.ts +45 -0
- package/dist/se/metric-rules.js +208 -0
- package/dist/se/near-duplicate-rules.d.ts +44 -0
- package/dist/se/near-duplicate-rules.js +106 -0
- package/dist/se/ontology.d.ts +327 -0
- package/dist/se/ontology.js +216 -0
- package/dist/se/quality-rules.d.ts +28 -0
- package/dist/se/quality-rules.js +206 -0
- package/dist/se/readiness.d.ts +62 -0
- package/dist/se/readiness.js +79 -0
- package/dist/se/rules.d.ts +159 -0
- package/dist/se/rules.js +854 -0
- package/dist/se/schema-quality-rules.d.ts +11 -0
- package/dist/se/schema-quality-rules.js +73 -0
- package/dist/se/semantic-id.d.ts +30 -0
- package/dist/se/semantic-id.js +90 -0
- package/dist/se/uc-quality-rules.d.ts +13 -0
- package/dist/se/uc-quality-rules.js +123 -0
- package/dist/se/view-rules.d.ts +11 -0
- package/dist/se/view-rules.js +56 -0
- package/package.json +51 -0
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RC conformance rules (CR-GC-253) — graph↔code resolution, the RESOLUTION
|
|
3
|
+
* twins of the presence rules R-20 (codeRef) / R-19 (testRef).
|
|
4
|
+
*
|
|
5
|
+
* Rules stay in THIS library (one rule base per onto set — no rule definitions
|
|
6
|
+
* in executor codebases). They are pure functions over (graph, facts): all
|
|
7
|
+
* filesystem/parser work happens in the executor (graphcode extracts CodeFacts
|
|
8
|
+
* from the repo), so this module stays I/O-free and browser-bundlable.
|
|
9
|
+
*
|
|
10
|
+
* CodeFacts semantics: `files` is keyed by repo-relative path and MUST contain
|
|
11
|
+
* an entry for every file referenced by a codeRef/testRef the extractor saw.
|
|
12
|
+
* A MISSING key is treated like `exists:false` — an extractor gap must surface
|
|
13
|
+
* loudly as a violation, never as a silent pass.
|
|
14
|
+
*
|
|
15
|
+
* @sigloch/contracts/se — single source of truth for SE validation rules.
|
|
16
|
+
*/
|
|
17
|
+
import { z } from 'zod/v4';
|
|
18
|
+
import { CodeRefSchema, TestRefSchema, SchemaRefSchema } from './ontology.js';
|
|
19
|
+
/** Parser facts about one source file (extracted by the executor). */
|
|
20
|
+
export const FileFactsSchema = z.object({
|
|
21
|
+
/** File exists on disk (repo-relative path). */
|
|
22
|
+
exists: z.boolean(),
|
|
23
|
+
/** Every declared symbol name (functions, classes, consts, methods, …). */
|
|
24
|
+
declaredSymbols: z.array(z.string()).default([]),
|
|
25
|
+
/** Every `it`/`test`/`describe` case name (string first-arg). */
|
|
26
|
+
testCases: z.array(z.string()).default([]),
|
|
27
|
+
/** Every imported symbol name (import specifiers) — for RC-04 usage checks (CR-211). */
|
|
28
|
+
importedSymbols: z.array(z.string()).default([]),
|
|
29
|
+
/** Symbols X invoked as `X.parse(` / `X.safeParse(` — the schema is actually used (CR-211). */
|
|
30
|
+
parsedSymbols: z.array(z.string()).default([]),
|
|
31
|
+
});
|
|
32
|
+
/** One file→file import edge (repo-relative paths) — the module-drift evidence (CR-212). */
|
|
33
|
+
export const ImportEdgeSchema = z.object({
|
|
34
|
+
from: z.string(),
|
|
35
|
+
to: z.string(),
|
|
36
|
+
});
|
|
37
|
+
/**
|
|
38
|
+
* Deterministic, serializable code facts — the data contract between the
|
|
39
|
+
* facts extractor (graphcode: TS parser today, ast-grep later — CR-GC-254)
|
|
40
|
+
* and the RC rules. Testable without any filesystem.
|
|
41
|
+
*/
|
|
42
|
+
export const CodeFactsSchema = z.object({
|
|
43
|
+
files: z.record(z.string(), FileFactsSchema),
|
|
44
|
+
/**
|
|
45
|
+
* File-level import graph of the repo (JS/TS), for the cross-module drift rule
|
|
46
|
+
* RC-05 (CR-212). Optional: an extractor that does not supply it simply means
|
|
47
|
+
* RC-05 has nothing to check (never a silent gap — RC-05 only judges edges it is
|
|
48
|
+
* given), so pre-CR-212 CodeFacts stay valid.
|
|
49
|
+
*/
|
|
50
|
+
importEdges: z.array(ImportEdgeSchema).optional(),
|
|
51
|
+
});
|
|
52
|
+
const missingFile = (facts, file) => facts.files[file]?.exists !== true;
|
|
53
|
+
// RC-01: every valid codeRef must resolve — file on disk, symbol declared in it.
|
|
54
|
+
// Presence is R-20's concern (incl. concept/external/decomposition-parent
|
|
55
|
+
// exemptions, CR-GC-244); RC-01 only judges bindings that exist. `lang:'prompt'`
|
|
56
|
+
// is realized by a skill file — file-exists is the whole binding.
|
|
57
|
+
function codeRefMustResolve(graph, facts) {
|
|
58
|
+
const violations = [];
|
|
59
|
+
for (const el of graph.elements) {
|
|
60
|
+
if (el.type !== 'FUNC')
|
|
61
|
+
continue;
|
|
62
|
+
if (el.attributes?.concept === true || el.attributes?.external === true)
|
|
63
|
+
continue;
|
|
64
|
+
const parsed = CodeRefSchema.safeParse(el.attributes?.codeRef);
|
|
65
|
+
if (!parsed.success)
|
|
66
|
+
continue; // no/invalid binding → R-20 territory
|
|
67
|
+
const ref = parsed.data;
|
|
68
|
+
if (missingFile(facts, ref.file)) {
|
|
69
|
+
violations.push({
|
|
70
|
+
rule_id: 'RC-01',
|
|
71
|
+
severity: 'error',
|
|
72
|
+
element_id: el.id,
|
|
73
|
+
message: `${el.id} codeRef.file '${ref.file}' does not exist on disk`,
|
|
74
|
+
fix_hint: 'Re-realize the FUNC (graph_realize) against the current source tree, or fix the moved/renamed file path',
|
|
75
|
+
context: { element_type: el.type, element_name: el.name },
|
|
76
|
+
});
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (ref.lang === 'prompt')
|
|
80
|
+
continue; // skill file existing IS the binding
|
|
81
|
+
if (!facts.files[ref.file].declaredSymbols.includes(ref.symbol)) {
|
|
82
|
+
violations.push({
|
|
83
|
+
rule_id: 'RC-01',
|
|
84
|
+
severity: 'error',
|
|
85
|
+
element_id: el.id,
|
|
86
|
+
message: `${el.id} codeRef.symbol '${ref.symbol}' is not declared in '${ref.file}'`,
|
|
87
|
+
fix_hint: 'The symbol was renamed or removed — re-realize the FUNC against the current code',
|
|
88
|
+
context: { element_type: el.type, element_name: el.name },
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return violations;
|
|
93
|
+
}
|
|
94
|
+
// RC-02: every valid testRef must resolve — file on disk and, when `case` is
|
|
95
|
+
// set, the case name declared as an it/test/describe in that file.
|
|
96
|
+
function testRefMustResolve(graph, facts) {
|
|
97
|
+
const violations = [];
|
|
98
|
+
for (const el of graph.elements) {
|
|
99
|
+
if (el.type !== 'TEST')
|
|
100
|
+
continue;
|
|
101
|
+
if (el.attributes?.concept === true)
|
|
102
|
+
continue;
|
|
103
|
+
const parsed = TestRefSchema.safeParse(el.attributes?.testRef);
|
|
104
|
+
if (!parsed.success)
|
|
105
|
+
continue; // no/invalid binding → R-19 territory
|
|
106
|
+
const ref = parsed.data;
|
|
107
|
+
if (missingFile(facts, ref.file)) {
|
|
108
|
+
violations.push({
|
|
109
|
+
rule_id: 'RC-02',
|
|
110
|
+
severity: 'error',
|
|
111
|
+
element_id: el.id,
|
|
112
|
+
message: `${el.id} testRef.file '${ref.file}' does not exist on disk`,
|
|
113
|
+
fix_hint: 'The test file was moved or deleted — rebind the TEST (graph_realize) to the current file',
|
|
114
|
+
context: { element_type: el.type, element_name: el.name },
|
|
115
|
+
});
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
// `case` is a name FILTER (vitest -t semantics): it matches by substring,
|
|
119
|
+
// so it must occur within some declared it/test/describe name — not equal it.
|
|
120
|
+
const caseName = ref.case;
|
|
121
|
+
if (caseName !== undefined && !facts.files[ref.file].testCases.some((name) => name.includes(caseName))) {
|
|
122
|
+
violations.push({
|
|
123
|
+
rule_id: 'RC-02',
|
|
124
|
+
severity: 'error',
|
|
125
|
+
element_id: el.id,
|
|
126
|
+
message: `${el.id} testRef.case '${ref.case}' is not declared in '${ref.file}'`,
|
|
127
|
+
fix_hint: 'The test case was renamed or removed — rebind the TEST case name',
|
|
128
|
+
context: { element_type: el.type, element_name: el.name },
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return violations;
|
|
133
|
+
}
|
|
134
|
+
// RC-03: every valid SCHEMA schemaRef must resolve — file on disk, symbol declared
|
|
135
|
+
// in it (CR-211). Presence (a SCHEMA with NO schemaRef) is the concern of the
|
|
136
|
+
// R-26 presence rule, not RC-03; concept/external SCHEMAs are exempt there and
|
|
137
|
+
// here. Severity error, like RC-01: a bound-but-broken schema IS a defect. Fires
|
|
138
|
+
// 0× until schemaRefs exist (it only judges SCHEMAs that carry one).
|
|
139
|
+
function schemaRefMustResolve(graph, facts) {
|
|
140
|
+
const violations = [];
|
|
141
|
+
for (const el of graph.elements) {
|
|
142
|
+
if (el.type !== 'SCHEMA')
|
|
143
|
+
continue;
|
|
144
|
+
if (el.attributes?.concept === true || el.attributes?.external === true)
|
|
145
|
+
continue;
|
|
146
|
+
const parsed = SchemaRefSchema.safeParse(el.attributes?.schemaRef);
|
|
147
|
+
if (!parsed.success)
|
|
148
|
+
continue; // no/invalid binding → R-26 territory
|
|
149
|
+
const ref = parsed.data;
|
|
150
|
+
if (missingFile(facts, ref.file)) {
|
|
151
|
+
violations.push({
|
|
152
|
+
rule_id: 'RC-03',
|
|
153
|
+
severity: 'error',
|
|
154
|
+
element_id: el.id,
|
|
155
|
+
message: `${el.id} schemaRef.file '${ref.file}' does not exist on disk`,
|
|
156
|
+
fix_hint: 'Re-bind the SCHEMA (graph_realize) to the current source tree, or fix the moved/renamed file path',
|
|
157
|
+
context: { element_type: el.type, element_name: el.name },
|
|
158
|
+
});
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
if (!facts.files[ref.file].declaredSymbols.includes(ref.symbol)) {
|
|
162
|
+
violations.push({
|
|
163
|
+
rule_id: 'RC-03',
|
|
164
|
+
severity: 'error',
|
|
165
|
+
element_id: el.id,
|
|
166
|
+
message: `${el.id} schemaRef.symbol '${ref.symbol}' is not a declared export in '${ref.file}'`,
|
|
167
|
+
fix_hint: 'The schema export was renamed or removed — re-bind the SCHEMA against the current code',
|
|
168
|
+
context: { element_type: el.type, element_name: el.name },
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return violations;
|
|
173
|
+
}
|
|
174
|
+
// RC-04: a bound SCHEMA that the graph says is realized at an interface must
|
|
175
|
+
// actually be parsed there (CR-211). The graph gives the check LOCATIONS: FUNCs
|
|
176
|
+
// io-connected to a FLOW whose data format IS this SCHEMA (FUNC ─io→ FLOW
|
|
177
|
+
// ─relation→ SCHEMA) and that carry a resolvable codeRef. If ≥1 such FUNC exists
|
|
178
|
+
// but NONE of their codeRef files import AND parse (`.parse`/`.safeParse`) the
|
|
179
|
+
// schema symbol, the modelled validation is missing. Severity warn (the parse may
|
|
180
|
+
// legitimately sit in a framework layer, not the FUNC's own file). Skips when the
|
|
181
|
+
// SCHEMA has no schemaRef, or no io-connected realized FUNC to check against.
|
|
182
|
+
function schemaRefMustBeUsed(graph, facts) {
|
|
183
|
+
const typeOf = new Map(graph.elements.map(e => [e.id, e.type]));
|
|
184
|
+
const violations = [];
|
|
185
|
+
for (const el of graph.elements) {
|
|
186
|
+
if (el.type !== 'SCHEMA')
|
|
187
|
+
continue;
|
|
188
|
+
if (el.attributes?.concept === true || el.attributes?.external === true)
|
|
189
|
+
continue;
|
|
190
|
+
const parsed = SchemaRefSchema.safeParse(el.attributes?.schemaRef);
|
|
191
|
+
if (!parsed.success)
|
|
192
|
+
continue;
|
|
193
|
+
const ref = parsed.data;
|
|
194
|
+
// The FLOWs whose data format is this SCHEMA (FLOW ─relation→ SCHEMA).
|
|
195
|
+
const flowIds = new Set(graph.traces
|
|
196
|
+
.filter(t => t.type === 'relation' && t.target === el.id && typeOf.get(t.source) === 'FLOW')
|
|
197
|
+
.map(t => t.source));
|
|
198
|
+
if (flowIds.size === 0)
|
|
199
|
+
continue;
|
|
200
|
+
// FUNCs io-connected to those FLOWs (producer or consumer).
|
|
201
|
+
const funcIds = new Set(graph.traces
|
|
202
|
+
.filter(t => t.type === 'io' &&
|
|
203
|
+
((flowIds.has(t.target) && typeOf.get(t.source) === 'FUNC') ||
|
|
204
|
+
(flowIds.has(t.source) && typeOf.get(t.target) === 'FUNC')))
|
|
205
|
+
.map(t => (flowIds.has(t.target) ? t.source : t.target)));
|
|
206
|
+
// Realized FUNCs among them: a resolvable codeRef whose file exists.
|
|
207
|
+
const realizedFiles = [];
|
|
208
|
+
for (const fnId of funcIds) {
|
|
209
|
+
const fn = graph.elements.find(e => e.id === fnId);
|
|
210
|
+
const cr = CodeRefSchema.safeParse(fn?.attributes?.codeRef);
|
|
211
|
+
if (cr.success && !missingFile(facts, cr.data.file))
|
|
212
|
+
realizedFiles.push(cr.data.file);
|
|
213
|
+
}
|
|
214
|
+
if (realizedFiles.length === 0)
|
|
215
|
+
continue; // nothing realized to check against
|
|
216
|
+
const usedSomewhere = realizedFiles.some(file => {
|
|
217
|
+
const f = facts.files[file];
|
|
218
|
+
return f.importedSymbols.includes(ref.symbol) && f.parsedSymbols.includes(ref.symbol);
|
|
219
|
+
});
|
|
220
|
+
if (!usedSomewhere) {
|
|
221
|
+
violations.push({
|
|
222
|
+
rule_id: 'RC-04',
|
|
223
|
+
severity: 'warning',
|
|
224
|
+
element_id: el.id,
|
|
225
|
+
message: `${el.id} schema '${ref.symbol}' is not parsed in any realized FUNC at its modelled interface`,
|
|
226
|
+
fix_hint: `Import and call ${ref.symbol}.parse()/.safeParse() in one of the io-connected FUNC's code files, or mark the SCHEMA external if it is validated by a framework layer`,
|
|
227
|
+
context: { element_type: el.type, element_name: el.name },
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return violations;
|
|
232
|
+
}
|
|
233
|
+
// ---------------------------------------------------------------------------
|
|
234
|
+
// RC-05: cross-module drift (CR-212) — a file-level import that crosses a MOD
|
|
235
|
+
// boundary the graph does not document. The implementer wires a new dependency
|
|
236
|
+
// between modules (an import over a MOD boundary); if the graph has no io/allocate
|
|
237
|
+
// structure connecting those MODs, the relationship is undocumented and blast-
|
|
238
|
+
// radius tools (graph_impact) under-report. Indicator, not blocker (warn), no
|
|
239
|
+
// auto-fix. Pure over (graph, facts.importEdges):
|
|
240
|
+
// 1. file → MOD via codeRef bindings (a bound file belongs to its FUNC's MOD),
|
|
241
|
+
// then MOD.path longest-prefix for unbound files; the rest are `unassigned`
|
|
242
|
+
// (reported in the message, never silently dropped).
|
|
243
|
+
// 2. graph MOD-adjacency = any io crossing between two MODs' FUNCs
|
|
244
|
+
// (FUNC_A ─io→ FLOW ─io→ FUNC_B), treated UNDIRECTED — the graph models that
|
|
245
|
+
// the relationship exists; a warn rule should not over-fire on io direction.
|
|
246
|
+
// 3. an importEdge whose endpoints map to two DIFFERENT assigned MODs with no
|
|
247
|
+
// such adjacency ⇒ one RC-05 per (fromMod → toMod) pair, evidence = the
|
|
248
|
+
// crossing file imports.
|
|
249
|
+
// ---------------------------------------------------------------------------
|
|
250
|
+
function importDriftConformance(graph, facts) {
|
|
251
|
+
const importEdges = facts.importEdges ?? [];
|
|
252
|
+
if (importEdges.length === 0)
|
|
253
|
+
return [];
|
|
254
|
+
const typeOf = new Map(graph.elements.map(e => [e.id, e.type]));
|
|
255
|
+
const modIds = graph.elements.filter(e => e.type === 'MOD').map(e => e.id);
|
|
256
|
+
if (modIds.length === 0)
|
|
257
|
+
return [];
|
|
258
|
+
// 1. file → MOD. Direct codeRef bindings first.
|
|
259
|
+
const fileToMod = new Map();
|
|
260
|
+
for (const el of graph.elements) {
|
|
261
|
+
if (el.type !== 'FUNC')
|
|
262
|
+
continue;
|
|
263
|
+
const cr = CodeRefSchema.safeParse(el.attributes?.codeRef);
|
|
264
|
+
if (!cr.success)
|
|
265
|
+
continue;
|
|
266
|
+
const modTrace = graph.traces.find(t => t.source === el.id && t.type === 'allocate' && typeOf.get(t.target) === 'MOD');
|
|
267
|
+
if (modTrace)
|
|
268
|
+
fileToMod.set(cr.data.file, modTrace.target);
|
|
269
|
+
}
|
|
270
|
+
// MOD.path longest-prefix fallback for files with no direct binding.
|
|
271
|
+
const modPaths = graph.elements
|
|
272
|
+
.filter(e => e.type === 'MOD' && typeof e.attributes?.path === 'string')
|
|
273
|
+
.map(e => ({ id: e.id, path: e.attributes.path.replace(/[*].*$/, '').replace(/\/+$/, '') }))
|
|
274
|
+
.filter(m => m.path.length > 0);
|
|
275
|
+
const resolveMod = (file) => {
|
|
276
|
+
if (fileToMod.has(file))
|
|
277
|
+
return fileToMod.get(file);
|
|
278
|
+
let best;
|
|
279
|
+
for (const m of modPaths) {
|
|
280
|
+
if ((file === m.path || file.startsWith(m.path + '/')) && (!best || m.path.length > best.len)) {
|
|
281
|
+
best = { id: m.id, len: m.path.length };
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return best?.id;
|
|
285
|
+
};
|
|
286
|
+
// 2. graph MOD-adjacency (undirected) from io crossings FUNC_A ─io→ FLOW ─io→ FUNC_B.
|
|
287
|
+
const funcMod = (funcId) => {
|
|
288
|
+
const t = graph.traces.find(tr => tr.source === funcId && tr.type === 'allocate' && typeOf.get(tr.target) === 'MOD');
|
|
289
|
+
return t?.target;
|
|
290
|
+
};
|
|
291
|
+
const adjacent = new Set(); // key `A|B` with A<B (undirected)
|
|
292
|
+
const io = graph.traces.filter(t => t.type === 'io');
|
|
293
|
+
for (const flow of graph.elements) {
|
|
294
|
+
if (flow.type !== 'FLOW')
|
|
295
|
+
continue;
|
|
296
|
+
const funcs = new Set();
|
|
297
|
+
for (const t of io) {
|
|
298
|
+
if (t.target === flow.id && typeOf.get(t.source) === 'FUNC')
|
|
299
|
+
funcs.add(t.source);
|
|
300
|
+
if (t.source === flow.id && typeOf.get(t.target) === 'FUNC')
|
|
301
|
+
funcs.add(t.target);
|
|
302
|
+
}
|
|
303
|
+
const mods = [...new Set([...funcs].map(funcMod).filter((m) => !!m))];
|
|
304
|
+
for (let i = 0; i < mods.length; i++) {
|
|
305
|
+
for (let j = i + 1; j < mods.length; j++) {
|
|
306
|
+
const [a, b] = [mods[i], mods[j]].sort();
|
|
307
|
+
adjacent.add(`${a}|${b}`);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
const isAdjacent = (a, b) => {
|
|
312
|
+
const [x, y] = [a, b].sort();
|
|
313
|
+
return adjacent.has(`${x}|${y}`);
|
|
314
|
+
};
|
|
315
|
+
// 3. drift per (fromMod → toMod), aggregating evidence; collect unassigned files.
|
|
316
|
+
const drift = new Map();
|
|
317
|
+
const unassigned = new Set();
|
|
318
|
+
for (const edge of importEdges) {
|
|
319
|
+
const fromMod = resolveMod(edge.from);
|
|
320
|
+
const toMod = resolveMod(edge.to);
|
|
321
|
+
if (!fromMod)
|
|
322
|
+
unassigned.add(edge.from);
|
|
323
|
+
if (!toMod)
|
|
324
|
+
unassigned.add(edge.to);
|
|
325
|
+
if (!fromMod || !toMod || fromMod === toMod)
|
|
326
|
+
continue;
|
|
327
|
+
if (isAdjacent(fromMod, toMod))
|
|
328
|
+
continue;
|
|
329
|
+
const key = `${fromMod}->${toMod}`;
|
|
330
|
+
const entry = drift.get(key) ?? { fromMod, toMod, evidence: [] };
|
|
331
|
+
entry.evidence.push(`${edge.from} → ${edge.to}`);
|
|
332
|
+
drift.set(key, entry);
|
|
333
|
+
}
|
|
334
|
+
const nameOf = new Map(graph.elements.map(e => [e.id, e.name]));
|
|
335
|
+
const unassignedNote = unassigned.size > 0 ? ` (unassigned files not mapped to a MOD: ${[...unassigned].sort().join(', ')})` : '';
|
|
336
|
+
return [...drift.values()].map(d => {
|
|
337
|
+
const shown = d.evidence.slice(0, 5);
|
|
338
|
+
const more = d.evidence.length > shown.length ? ` +${d.evidence.length - shown.length} more` : '';
|
|
339
|
+
return {
|
|
340
|
+
rule_id: 'RC-05',
|
|
341
|
+
severity: 'warning',
|
|
342
|
+
element_id: d.fromMod,
|
|
343
|
+
message: `${d.fromMod} imports ${d.toMod} across a module boundary the graph does not document — evidence: ${shown.join('; ')}${more}${unassignedNote}`,
|
|
344
|
+
fix_hint: `Document the ${d.fromMod}→${d.toMod} dependency in the graph (an io/FLOW connection between their FUNCs), or remove the import`,
|
|
345
|
+
context: {
|
|
346
|
+
element_type: 'MOD',
|
|
347
|
+
element_name: nameOf.get(d.fromMod),
|
|
348
|
+
candidate_targets: [{ id: d.toMod, type: 'MOD', name: nameOf.get(d.toMod) ?? d.toMod }],
|
|
349
|
+
},
|
|
350
|
+
};
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
/** All RC conformance rules — evaluated by executors that can supply CodeFacts. */
|
|
354
|
+
export const CODE_CONFORMANCE_RULES = [
|
|
355
|
+
{ id: 'RC-01', name: 'codeRef resolves to a declared symbol', severity: 'error', evaluate: codeRefMustResolve },
|
|
356
|
+
{ id: 'RC-02', name: 'testRef resolves to a runnable test', severity: 'error', evaluate: testRefMustResolve },
|
|
357
|
+
{ id: 'RC-03', name: 'schemaRef resolves to a declared export', severity: 'error', evaluate: schemaRefMustResolve },
|
|
358
|
+
{ id: 'RC-04', name: 'schemaRef is parsed at its interface', severity: 'warning', evaluate: schemaRefMustBeUsed },
|
|
359
|
+
{ id: 'RC-05', name: 'cross-module import drift', severity: 'warning', evaluate: importDriftConformance },
|
|
360
|
+
];
|
|
361
|
+
/** Run all RC rules against a graph + extracted code facts. */
|
|
362
|
+
export function evaluateConformanceRules(graph, facts) {
|
|
363
|
+
return CODE_CONFORMANCE_RULES.flatMap((rule) => rule.evaluate(graph, facts));
|
|
364
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CR-155: CR-specific quality rules for change request traceability.
|
|
3
|
+
* CR is a process node that tracks mutations to structural elements.
|
|
4
|
+
*/
|
|
5
|
+
import type { OntologyGraph } from './ontology.js';
|
|
6
|
+
import type { RuleDefinition, RuleViolation } from './rules.js';
|
|
7
|
+
export declare const CR_RULES: RuleDefinition[];
|
|
8
|
+
export declare function evaluateCRRules(graph: OntologyGraph): RuleViolation[];
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// CR-R01: CR must have at least one outgoing relation trace
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
function crMustTrack(graph) {
|
|
5
|
+
const crs = graph.elements.filter(e => e.type === 'CR');
|
|
6
|
+
return crs
|
|
7
|
+
.filter(cr => !graph.traces.some(t => t.source === cr.id && t.type === 'relation'))
|
|
8
|
+
.map(cr => ({
|
|
9
|
+
rule_id: 'CR-R01',
|
|
10
|
+
severity: 'error',
|
|
11
|
+
element_id: cr.id,
|
|
12
|
+
message: `${cr.id} has no relation traces (tracks nothing)`,
|
|
13
|
+
fix_hint: 'Add relation traces to affected UC, REQ, FUNC, or MOD elements',
|
|
14
|
+
context: {
|
|
15
|
+
element_type: cr.type,
|
|
16
|
+
element_name: cr.name,
|
|
17
|
+
},
|
|
18
|
+
}));
|
|
19
|
+
}
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// CR-R02: CR with status=done must have commitRef (unless architectureOnly)
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
function crDoneRequiresCommit(graph) {
|
|
24
|
+
const crs = graph.elements.filter(e => e.type === 'CR');
|
|
25
|
+
return crs
|
|
26
|
+
.filter(cr => {
|
|
27
|
+
const status = cr.attributes?.status;
|
|
28
|
+
if (status !== 'done')
|
|
29
|
+
return false;
|
|
30
|
+
if (cr.attributes?.architectureOnly === true)
|
|
31
|
+
return false;
|
|
32
|
+
const ref = cr.attributes?.commitRef;
|
|
33
|
+
return !ref || (Array.isArray(ref) && ref.length === 0);
|
|
34
|
+
})
|
|
35
|
+
.map(cr => ({
|
|
36
|
+
rule_id: 'CR-R02',
|
|
37
|
+
severity: 'error',
|
|
38
|
+
element_id: cr.id,
|
|
39
|
+
message: `${cr.id} is done but has no commitRef`,
|
|
40
|
+
fix_hint: 'Add commitRef with Git SHA, or set architectureOnly=true',
|
|
41
|
+
context: {
|
|
42
|
+
element_type: cr.type,
|
|
43
|
+
element_name: cr.name,
|
|
44
|
+
},
|
|
45
|
+
}));
|
|
46
|
+
}
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
// CR-R03: No concurrent mutation — warn if two open CRs track the same node
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
function noConcurrentMutation(graph) {
|
|
51
|
+
const openCrs = graph.elements.filter(e => e.type === 'CR' && (e.attributes?.status === 'open' || e.attributes?.status === 'in-progress'));
|
|
52
|
+
if (openCrs.length < 2)
|
|
53
|
+
return [];
|
|
54
|
+
// Build map: target node → list of open CRs that track it
|
|
55
|
+
const targetToCrs = new Map();
|
|
56
|
+
for (const cr of openCrs) {
|
|
57
|
+
const targets = graph.traces
|
|
58
|
+
.filter(t => t.source === cr.id && t.type === 'relation')
|
|
59
|
+
.map(t => t.target);
|
|
60
|
+
for (const tgt of targets) {
|
|
61
|
+
const list = targetToCrs.get(tgt) ?? [];
|
|
62
|
+
list.push(cr.id);
|
|
63
|
+
targetToCrs.set(tgt, list);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const violations = [];
|
|
67
|
+
const seen = new Set();
|
|
68
|
+
for (const [target, crIds] of targetToCrs) {
|
|
69
|
+
if (crIds.length < 2)
|
|
70
|
+
continue;
|
|
71
|
+
const key = crIds.sort().join(',');
|
|
72
|
+
if (seen.has(key))
|
|
73
|
+
continue;
|
|
74
|
+
seen.add(key);
|
|
75
|
+
violations.push({
|
|
76
|
+
rule_id: 'CR-R03',
|
|
77
|
+
severity: 'warning',
|
|
78
|
+
element_id: target,
|
|
79
|
+
message: `${target} is tracked by ${crIds.length} open CRs: ${crIds.join(', ')}`,
|
|
80
|
+
fix_hint: 'Coordinate changes to avoid merge conflicts',
|
|
81
|
+
context: {
|
|
82
|
+
element_type: graph.elements.find(e => e.id === target)?.type,
|
|
83
|
+
element_name: graph.elements.find(e => e.id === target)?.name,
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
return violations;
|
|
88
|
+
}
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
// CR-R04: CR must have at least one relation→FUNC trace (CR-207)
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
function crMustHaveFunc(graph) {
|
|
93
|
+
const crs = graph.elements.filter(e => e.type === 'CR');
|
|
94
|
+
return crs
|
|
95
|
+
.filter(cr => !graph.traces.some(t => t.source === cr.id && t.type === 'relation' &&
|
|
96
|
+
graph.elements.some(e => e.id === t.target && e.type === 'FUNC')))
|
|
97
|
+
.map(cr => ({
|
|
98
|
+
rule_id: 'CR-R04',
|
|
99
|
+
severity: 'warning',
|
|
100
|
+
element_id: cr.id,
|
|
101
|
+
message: `${cr.id} has no relation→FUNC trace (no implementation scope)`,
|
|
102
|
+
fix_hint: 'Add CR→FUNC [relation] traces to define which functions this CR changes',
|
|
103
|
+
context: {
|
|
104
|
+
element_type: cr.type,
|
|
105
|
+
element_name: cr.name,
|
|
106
|
+
},
|
|
107
|
+
}));
|
|
108
|
+
}
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
// MS-03: CR should have a CR→MS [relation] trace (CR-207)
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
function crShouldHaveMilestone(graph) {
|
|
113
|
+
const crs = graph.elements.filter(e => e.type === 'CR');
|
|
114
|
+
return crs
|
|
115
|
+
.filter(cr => !graph.traces.some(t => t.source === cr.id && t.type === 'relation' &&
|
|
116
|
+
graph.elements.some(e => e.id === t.target && e.type === 'MS')))
|
|
117
|
+
.map(cr => ({
|
|
118
|
+
rule_id: 'MS-03',
|
|
119
|
+
severity: 'info',
|
|
120
|
+
element_id: cr.id,
|
|
121
|
+
message: `${cr.id} is not assigned to any milestone`,
|
|
122
|
+
fix_hint: 'Add CR→MS [relation] trace to assign this CR to a milestone',
|
|
123
|
+
context: {
|
|
124
|
+
element_type: cr.type,
|
|
125
|
+
element_name: cr.name,
|
|
126
|
+
},
|
|
127
|
+
}));
|
|
128
|
+
}
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
// Exports
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
export const CR_RULES = [
|
|
133
|
+
{ id: 'CR-R01', name: 'CR must track', severity: 'error', evaluate: crMustTrack },
|
|
134
|
+
{ id: 'CR-R02', name: 'Done requires commit', severity: 'error', evaluate: crDoneRequiresCommit },
|
|
135
|
+
{ id: 'CR-R03', name: 'No concurrent mutation', severity: 'warning', evaluate: noConcurrentMutation },
|
|
136
|
+
{ id: 'CR-R04', name: 'CR must have FUNC', severity: 'warning', evaluate: crMustHaveFunc },
|
|
137
|
+
{ id: 'MS-03', name: 'CR without milestone', severity: 'info', evaluate: crShouldHaveMilestone },
|
|
138
|
+
];
|
|
139
|
+
export function evaluateCRRules(graph) {
|
|
140
|
+
return CR_RULES.flatMap(rule => rule.evaluate(graph));
|
|
141
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single point of truth for all rule evaluations.
|
|
3
|
+
* Import `evaluateAllRules` and `ALL_RULE_DEFS` instead of individual evaluators.
|
|
4
|
+
*/
|
|
5
|
+
import type { OntologyGraph } from './ontology.js';
|
|
6
|
+
import type { RuleViolation } from './rules.js';
|
|
7
|
+
/** All prescribed rule definitions (single source of truth for the catalog). */
|
|
8
|
+
export declare const ALL_RULE_DEFS: ReadonlyArray<{
|
|
9
|
+
id: string;
|
|
10
|
+
name: string;
|
|
11
|
+
severity: 'error' | 'warning' | 'info';
|
|
12
|
+
}>;
|
|
13
|
+
/** Profile groupings derived from rule prefixes. */
|
|
14
|
+
export type ProfileId = 'default' | 'se' | 'coding';
|
|
15
|
+
export declare function getRuleDefsForProfile(profile: ProfileId): typeof ALL_RULE_DEFS;
|
|
16
|
+
/** Evaluate all rules against a graph. Single call replaces the individual evaluator calls. */
|
|
17
|
+
export declare function evaluateAllRules(graph: OntologyGraph): RuleViolation[];
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { V3_RULES, evaluateRules } from './rules.js';
|
|
2
|
+
import { SC_RULES, evaluateSCRules } from './schema-quality-rules.js';
|
|
3
|
+
import { UC_RULES, evaluateUCRules } from './uc-quality-rules.js';
|
|
4
|
+
import { FC_RULES, evaluateFCRules } from './fchain-quality-rules.js';
|
|
5
|
+
import { MT_RULES, evaluateMTRules } from './metric-rules.js';
|
|
6
|
+
import { FM_RULES, evaluateFMRules } from './fmea-rules.js';
|
|
7
|
+
import { VIEW_RULES, evaluateViewRules } from './view-rules.js';
|
|
8
|
+
import { CR_RULES, evaluateCRRules } from './cr-quality-rules.js';
|
|
9
|
+
import { ND_RULES, evaluateNDRules } from './near-duplicate-rules.js';
|
|
10
|
+
import { AO_RULES, evaluateAORules } from './ao-rules.js';
|
|
11
|
+
import { BQ_RULES, evaluateBQRules } from './quality-rules.js';
|
|
12
|
+
/** All prescribed rule definitions (single source of truth for the catalog). */
|
|
13
|
+
export const ALL_RULE_DEFS = [
|
|
14
|
+
...V3_RULES.map(r => ({ id: r.id, name: r.name, severity: r.severity })),
|
|
15
|
+
...UC_RULES.map(r => ({ id: r.id, name: r.name, severity: r.severity })),
|
|
16
|
+
...FC_RULES.map(r => ({ id: r.id, name: r.name, severity: r.severity })),
|
|
17
|
+
...SC_RULES.map(r => ({ id: r.id, name: r.name, severity: r.severity })),
|
|
18
|
+
...MT_RULES.map(r => ({ id: r.id, name: r.name, severity: r.severity })),
|
|
19
|
+
...BQ_RULES.map(r => ({ id: r.id, name: r.name, severity: r.severity })),
|
|
20
|
+
...ND_RULES.map(r => ({ id: r.id, name: r.name, severity: r.severity })),
|
|
21
|
+
...CR_RULES.map(r => ({ id: r.id, name: r.name, severity: r.severity })),
|
|
22
|
+
...AO_RULES.map(r => ({ id: r.id, name: r.name, severity: r.severity })),
|
|
23
|
+
...FM_RULES.map(r => ({ id: r.id, name: r.name, severity: r.severity })),
|
|
24
|
+
...VIEW_RULES.map(r => ({ id: r.id, name: r.name, severity: r.severity })),
|
|
25
|
+
];
|
|
26
|
+
const SE_PREFIXES = ['R-', 'UC-', 'FC-', 'SC-', 'MT-', 'CR-', 'AO-', 'FM-', 'NFR-', 'RT-', 'PH-', 'CA-', 'IO-', 'VR-', 'CL-'];
|
|
27
|
+
const CODING_PREFIXES = ['BQ-', 'ND-'];
|
|
28
|
+
export function getRuleDefsForProfile(profile) {
|
|
29
|
+
if (profile === 'se')
|
|
30
|
+
return ALL_RULE_DEFS.filter(r => SE_PREFIXES.some(p => r.id.startsWith(p)));
|
|
31
|
+
if (profile === 'coding')
|
|
32
|
+
return ALL_RULE_DEFS.filter(r => CODING_PREFIXES.some(p => r.id.startsWith(p)));
|
|
33
|
+
return ALL_RULE_DEFS;
|
|
34
|
+
}
|
|
35
|
+
/** Evaluate all rules against a graph. Single call replaces the individual evaluator calls. */
|
|
36
|
+
export function evaluateAllRules(graph) {
|
|
37
|
+
return [
|
|
38
|
+
...evaluateRules(graph),
|
|
39
|
+
...evaluateBQRules(graph),
|
|
40
|
+
...evaluateUCRules(graph),
|
|
41
|
+
...evaluateFCRules(graph),
|
|
42
|
+
...evaluateSCRules(graph),
|
|
43
|
+
...evaluateNDRules(graph),
|
|
44
|
+
...evaluateMTRules(graph),
|
|
45
|
+
...evaluateCRRules(graph),
|
|
46
|
+
...evaluateAORules(graph),
|
|
47
|
+
...evaluateFMRules(graph),
|
|
48
|
+
...evaluateViewRules(graph),
|
|
49
|
+
];
|
|
50
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FC-01..FC-03 — FCHAIN quality rules (CR-121 Phase 1).
|
|
3
|
+
*/
|
|
4
|
+
import type { OntologyGraph } from './ontology.js';
|
|
5
|
+
import type { RuleDefinition, RuleViolation } from './rules.js';
|
|
6
|
+
export declare function fc01ActorBoundary(graph: OntologyGraph): RuleViolation[];
|
|
7
|
+
export declare function fc02LeafUcHasFchain(graph: OntologyGraph): RuleViolation[];
|
|
8
|
+
export declare function fc03FchainFlat(graph: OntologyGraph): RuleViolation[];
|
|
9
|
+
export declare const FC_RULES: RuleDefinition[];
|
|
10
|
+
export declare function evaluateFCRules(graph: OntologyGraph): RuleViolation[];
|