@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.
Files changed (44) hide show
  1. package/LICENSE +21 -0
  2. package/dist/harness/index.d.ts +185 -0
  3. package/dist/harness/index.js +185 -0
  4. package/dist/index.d.ts +13 -0
  5. package/dist/index.js +14 -0
  6. package/dist/se/ao-rules.d.ts +59 -0
  7. package/dist/se/ao-rules.js +341 -0
  8. package/dist/se/conformance-rules.d.ts +64 -0
  9. package/dist/se/conformance-rules.js +364 -0
  10. package/dist/se/cr-quality-rules.d.ts +8 -0
  11. package/dist/se/cr-quality-rules.js +141 -0
  12. package/dist/se/evaluate-all.d.ts +17 -0
  13. package/dist/se/evaluate-all.js +50 -0
  14. package/dist/se/fchain-quality-rules.d.ts +10 -0
  15. package/dist/se/fchain-quality-rules.js +105 -0
  16. package/dist/se/fmea-rules.d.ts +17 -0
  17. package/dist/se/fmea-rules.js +137 -0
  18. package/dist/se/format-e-parser.d.ts +28 -0
  19. package/dist/se/format-e-parser.js +217 -0
  20. package/dist/se/index.d.ts +28 -0
  21. package/dist/se/index.js +28 -0
  22. package/dist/se/meta-model.d.ts +26 -0
  23. package/dist/se/meta-model.js +60 -0
  24. package/dist/se/metric-rules.d.ts +45 -0
  25. package/dist/se/metric-rules.js +208 -0
  26. package/dist/se/near-duplicate-rules.d.ts +44 -0
  27. package/dist/se/near-duplicate-rules.js +106 -0
  28. package/dist/se/ontology.d.ts +327 -0
  29. package/dist/se/ontology.js +216 -0
  30. package/dist/se/quality-rules.d.ts +28 -0
  31. package/dist/se/quality-rules.js +206 -0
  32. package/dist/se/readiness.d.ts +62 -0
  33. package/dist/se/readiness.js +79 -0
  34. package/dist/se/rules.d.ts +159 -0
  35. package/dist/se/rules.js +854 -0
  36. package/dist/se/schema-quality-rules.d.ts +11 -0
  37. package/dist/se/schema-quality-rules.js +73 -0
  38. package/dist/se/semantic-id.d.ts +30 -0
  39. package/dist/se/semantic-id.js +90 -0
  40. package/dist/se/uc-quality-rules.d.ts +13 -0
  41. package/dist/se/uc-quality-rules.js +123 -0
  42. package/dist/se/view-rules.d.ts +11 -0
  43. package/dist/se/view-rules.js +56 -0
  44. 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[];