@sigloch/contracts 10.9.0 → 10.12.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/analysis-freshness-rules.d.ts +20 -0
- package/dist/se/analysis-freshness-rules.js +71 -0
- package/dist/se/conformance-rules.d.ts +6 -1
- package/dist/se/conformance-rules.js +196 -55
- package/dist/se/cr-quality-rules.d.ts +5 -0
- package/dist/se/cr-quality-rules.js +10 -5
- package/dist/se/evaluate-all.js +3 -1
- package/dist/se/fmea-rules.js +6 -6
- package/dist/se/function-criticality.d.ts +8 -22
- package/dist/se/function-criticality.js +71 -1
- package/dist/se/grammar-snapshot.d.ts +6 -6
- package/dist/se/grammar-snapshot.js +35 -25
- package/dist/se/index.d.ts +2 -2
- package/dist/se/index.js +2 -2
- package/dist/se/metric-rules.d.ts +20 -61
- package/dist/se/metric-rules.js +90 -1
- package/dist/se/near-duplicate-rules.d.ts +4 -4
- package/dist/se/near-duplicate-rules.js +5 -5
- package/dist/se/ontology.d.ts +28 -0
- package/dist/se/ontology.js +23 -0
- package/dist/se/readiness.js +18 -13
- package/dist/se/rule-help.js +20 -13
- package/dist/se/rules.js +10 -40
- package/dist/se/uc-quality-rules.d.ts +0 -2
- package/dist/se/uc-quality-rules.js +6 -44
- package/dist/se/view-rules.js +5 -5
- package/package.json +1 -1
|
@@ -40,5 +40,25 @@ export declare const AF_RULES: readonly [{
|
|
|
40
40
|
readonly evaluate: (graph: OntologyGraph) => RuleViolation[];
|
|
41
41
|
readonly domain: readonly ["graph"];
|
|
42
42
|
}];
|
|
43
|
+
/** TR-01: a stamped trade study names >=1 existing CR, each carrying a relation(label: decides) edge. */
|
|
44
|
+
export declare function tradeDecisionRecorded(graph: OntologyGraph): RuleViolation[];
|
|
45
|
+
/** IR-01: every CR named by a stamped assumption review exists; an empty list is a legitimate outcome. */
|
|
46
|
+
export declare function assumptionReviewPromoted(graph: OntologyGraph): RuleViolation[];
|
|
47
|
+
/** CR-SM-355: the task-outcome rules — owned by trade / irr (TASK_OWNED_RULES), never in the kern focus. */
|
|
48
|
+
export declare const TASK_OUTCOME_RULES: readonly [{
|
|
49
|
+
readonly id: "TR-01";
|
|
50
|
+
readonly name: "Trade decision recorded as CR";
|
|
51
|
+
readonly severity: "warning";
|
|
52
|
+
readonly evaluate: typeof tradeDecisionRecorded;
|
|
53
|
+
readonly domain: readonly ["graph"];
|
|
54
|
+
}, {
|
|
55
|
+
readonly id: "IR-01";
|
|
56
|
+
readonly name: "Assumption review promoted to CR";
|
|
57
|
+
readonly severity: "warning";
|
|
58
|
+
readonly evaluate: typeof assumptionReviewPromoted;
|
|
59
|
+
readonly domain: readonly ["graph"];
|
|
60
|
+
}];
|
|
61
|
+
/** Evaluate the task-outcome rules (CR-SM-355). */
|
|
62
|
+
export declare function evaluateTaskOutcomeRules(graph: OntologyGraph): RuleViolation[];
|
|
43
63
|
/** Evaluate all analysis-freshness-presence rules. */
|
|
44
64
|
export declare function evaluateAFRules(graph: OntologyGraph): RuleViolation[];
|
|
@@ -38,6 +38,77 @@ export const AF_RULES = [
|
|
|
38
38
|
{ id: 'AF-04', name: 'FMEA freshness stamp present', severity: 'warning', evaluate: fmeaFreshnessPresent, domain: ['graph'] },
|
|
39
39
|
{ id: 'AF-05', name: 'Implementation Plan freshness stamp present', severity: 'warning', evaluate: implplanFreshnessPresent, domain: ['graph'] },
|
|
40
40
|
];
|
|
41
|
+
/**
|
|
42
|
+
* CR-SM-355: Task-Ausgangsregeln fuer trade und irr — die aus dem Task entstandenen Massnahmen sind
|
|
43
|
+
* als CR dokumentiert. Traeger ist `crRefs` im Frischestempel. Ohne Stempel feuern sie nicht (dann
|
|
44
|
+
* feuert der Eintritt AF-02/AF-03, und der Task ist nicht durch). `label` wird als `Trace.label`
|
|
45
|
+
* gelesen wie bei MS-02 — `decides` ist kein TraceType, sondern das SP-1-Label einer relation-Kante.
|
|
46
|
+
*/
|
|
47
|
+
function stampedCrRefs(graph, artifactId) {
|
|
48
|
+
const sys = graph.elements.find(e => e.type === 'SYS');
|
|
49
|
+
if (!sys)
|
|
50
|
+
return null;
|
|
51
|
+
const raw = sys.attributes?.analysisFreshness?.[artifactId];
|
|
52
|
+
const parsed = AnalysisFreshnessStampSchema.safeParse(raw);
|
|
53
|
+
if (!parsed.success)
|
|
54
|
+
return null;
|
|
55
|
+
return { sys, crRefs: parsed.data.crRefs ?? [] };
|
|
56
|
+
}
|
|
57
|
+
/** TR-01: a stamped trade study names >=1 existing CR, each carrying a relation(label: decides) edge. */
|
|
58
|
+
export function tradeDecisionRecorded(graph) {
|
|
59
|
+
const stamped = stampedCrRefs(graph, 'trade');
|
|
60
|
+
if (!stamped)
|
|
61
|
+
return [];
|
|
62
|
+
const { sys, crRefs } = stamped;
|
|
63
|
+
const base = { rule_id: 'TR-01', severity: 'warning', context: { element_type: sys.type, element_name: sys.name } };
|
|
64
|
+
if (crRefs.length === 0) {
|
|
65
|
+
return [{
|
|
66
|
+
...base,
|
|
67
|
+
element_id: sys.id,
|
|
68
|
+
message: 'Trade Study is stamped but names no decision CR (analysisFreshness.trade.crRefs is empty) — a trade without a recorded decision is a list, not a trade',
|
|
69
|
+
fix_hint: 'Record the decision as a CR with relation(label: decides) edges and list its id in analysisFreshness.trade.crRefs (se-trade)',
|
|
70
|
+
}];
|
|
71
|
+
}
|
|
72
|
+
const byId = new Map(graph.elements.map(e => [e.id, e]));
|
|
73
|
+
const decidesFrom = new Set(graph.traces.filter(t => t.type === 'relation' && t.label === 'decides').map(t => t.source));
|
|
74
|
+
return crRefs.flatMap(id => {
|
|
75
|
+
const cr = byId.get(id);
|
|
76
|
+
if (!cr || cr.type !== 'CR') {
|
|
77
|
+
return [{ ...base, element_id: sys.id, message: `Trade Study names ${id} as its decision, but no CR with that id exists`, fix_hint: 'Point analysisFreshness.trade.crRefs at an existing CR, or add the CR through graph_mutate (se-trade)' }];
|
|
78
|
+
}
|
|
79
|
+
if (!decidesFrom.has(id)) {
|
|
80
|
+
return [{ ...base, element_id: id, message: `${id} is named as a trade decision but carries no relation(label: decides) edge`, fix_hint: 'Add a relation edge from the CR to the element it decides, with label "decides" (se-trade)' }];
|
|
81
|
+
}
|
|
82
|
+
return [];
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
/** IR-01: every CR named by a stamped assumption review exists; an empty list is a legitimate outcome. */
|
|
86
|
+
export function assumptionReviewPromoted(graph) {
|
|
87
|
+
const stamped = stampedCrRefs(graph, 'assumption-review');
|
|
88
|
+
if (!stamped)
|
|
89
|
+
return [];
|
|
90
|
+
const { sys, crRefs } = stamped;
|
|
91
|
+
const byId = new Map(graph.elements.map(e => [e.id, e]));
|
|
92
|
+
return crRefs
|
|
93
|
+
.filter(id => byId.get(id)?.type !== 'CR')
|
|
94
|
+
.map(id => ({
|
|
95
|
+
rule_id: 'IR-01',
|
|
96
|
+
severity: 'warning',
|
|
97
|
+
element_id: sys.id,
|
|
98
|
+
message: `Assumption Review names ${id} as a promoted assumption, but no CR with that id exists`,
|
|
99
|
+
fix_hint: 'Point analysisFreshness.assumption-review.crRefs at an existing CR, or add the CR through graph_mutate (se-irr)',
|
|
100
|
+
context: { element_type: sys.type, element_name: sys.name },
|
|
101
|
+
}));
|
|
102
|
+
}
|
|
103
|
+
/** CR-SM-355: the task-outcome rules — owned by trade / irr (TASK_OWNED_RULES), never in the kern focus. */
|
|
104
|
+
export const TASK_OUTCOME_RULES = [
|
|
105
|
+
{ id: 'TR-01', name: 'Trade decision recorded as CR', severity: 'warning', evaluate: tradeDecisionRecorded, domain: ['graph'] },
|
|
106
|
+
{ id: 'IR-01', name: 'Assumption review promoted to CR', severity: 'warning', evaluate: assumptionReviewPromoted, domain: ['graph'] },
|
|
107
|
+
];
|
|
108
|
+
/** Evaluate the task-outcome rules (CR-SM-355). */
|
|
109
|
+
export function evaluateTaskOutcomeRules(graph) {
|
|
110
|
+
return TASK_OUTCOME_RULES.flatMap(r => r.evaluate(graph));
|
|
111
|
+
}
|
|
41
112
|
/** Evaluate all analysis-freshness-presence rules. */
|
|
42
113
|
export function evaluateAFRules(graph) {
|
|
43
114
|
return AF_RULES.flatMap(r => r.evaluate(graph));
|
|
@@ -24,6 +24,7 @@ export declare const FileFactsSchema: z.ZodObject<{
|
|
|
24
24
|
testCases: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
25
25
|
importedSymbols: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
26
26
|
parsedSymbols: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
27
|
+
zodSymbols: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
27
28
|
}, z.core.$strip>;
|
|
28
29
|
export type FileFacts = z.infer<typeof FileFactsSchema>;
|
|
29
30
|
/** One file→file import edge (repo-relative paths) — the module-drift evidence (CR-212). */
|
|
@@ -44,6 +45,7 @@ export declare const CodeFactsSchema: z.ZodObject<{
|
|
|
44
45
|
testCases: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
45
46
|
importedSymbols: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
46
47
|
parsedSymbols: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
48
|
+
zodSymbols: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
47
49
|
}, z.core.$strip>>;
|
|
48
50
|
importEdges: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
49
51
|
from: z.ZodString;
|
|
@@ -54,6 +56,10 @@ export declare const CodeFactsSchema: z.ZodObject<{
|
|
|
54
56
|
open: "open";
|
|
55
57
|
done: "done";
|
|
56
58
|
}>>>;
|
|
59
|
+
fileScope: z.ZodOptional<z.ZodEnum<{
|
|
60
|
+
all: "all";
|
|
61
|
+
referenced: "referenced";
|
|
62
|
+
}>>;
|
|
57
63
|
}, z.core.$strip>;
|
|
58
64
|
export type CodeFacts = z.infer<typeof CodeFactsSchema>;
|
|
59
65
|
/** A conformance rule: pure over (graph, facts) — never touches I/O itself. */
|
|
@@ -103,7 +109,6 @@ export interface ImportCoverage {
|
|
|
103
109
|
unassigned: string[];
|
|
104
110
|
}
|
|
105
111
|
export declare function importCoverage(graph: OntologyGraph, facts: CodeFacts): ImportCoverage;
|
|
106
|
-
/** All RC conformance rules — evaluated by executors that can supply CodeFacts. */
|
|
107
112
|
export declare const CODE_CONFORMANCE_RULES: ConformanceRuleDefinition[];
|
|
108
113
|
/** Run all RC rules against a graph + extracted code facts. */
|
|
109
114
|
export declare function evaluateConformanceRules(graph: OntologyGraph, facts: CodeFacts): RuleViolation[];
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* @sigloch/contracts/se — single source of truth for SE validation rules.
|
|
16
16
|
*/
|
|
17
17
|
import { z } from 'zod/v4';
|
|
18
|
-
import {
|
|
18
|
+
import { readTestRefs, readRealRef } from './ontology.js';
|
|
19
19
|
import { CLOSED_STATUS } from './cr-quality-rules.js';
|
|
20
20
|
/** Parser facts about one source file (extracted by the executor). */
|
|
21
21
|
export const FileFactsSchema = z.object({
|
|
@@ -29,6 +29,12 @@ export const FileFactsSchema = z.object({
|
|
|
29
29
|
importedSymbols: z.array(z.string()).default([]),
|
|
30
30
|
/** Symbols X invoked as `X.parse(` / `X.safeParse(` — the schema is actually used (CR-211). */
|
|
31
31
|
parsedSymbols: z.array(z.string()).default([]),
|
|
32
|
+
/**
|
|
33
|
+
* Exported consts whose initializer is a Zod schema (`export const X = z.…`) — for RC-08
|
|
34
|
+
* (CR-SM-358). Optional, ABSENT = silence: the extractor did not look, which is not the
|
|
35
|
+
* same as "this file declares no schema" (same asymmetry as `declaredDependencies`).
|
|
36
|
+
*/
|
|
37
|
+
zodSymbols: z.array(z.string()).optional(),
|
|
32
38
|
});
|
|
33
39
|
/** One file→file import edge (repo-relative paths) — the module-drift evidence (CR-212). */
|
|
34
40
|
export const ImportEdgeSchema = z.object({
|
|
@@ -67,6 +73,13 @@ export const CodeFactsSchema = z.object({
|
|
|
67
73
|
* `docs/cr/` was never looked at, it does not "have no CRs".
|
|
68
74
|
*/
|
|
69
75
|
crFiles: z.record(z.string(), z.enum(['open', 'done'])).optional(),
|
|
76
|
+
/**
|
|
77
|
+
* Which files `files` covers (CR-SM-358): `referenced` = only files a realRef/testRefs
|
|
78
|
+
* entry names (the contract above), `all` = every source file of the repo. RC-09 asks
|
|
79
|
+
* "who ELSE parses this contract" and can only answer it over `all` — a partial scan
|
|
80
|
+
* would report the absence of what it never looked at. ABSENT = `referenced`.
|
|
81
|
+
*/
|
|
82
|
+
fileScope: z.enum(['referenced', 'all']).optional(),
|
|
70
83
|
});
|
|
71
84
|
const missingFile = (facts, file) => facts.files[file]?.exists !== true;
|
|
72
85
|
// RC-01: every valid FUNC realRef must resolve — file on disk, symbol declared in
|
|
@@ -81,14 +94,14 @@ function codeRefMustResolve(graph, facts) {
|
|
|
81
94
|
continue;
|
|
82
95
|
if (el.attributes?.concept === true || el.attributes?.external === true)
|
|
83
96
|
continue;
|
|
84
|
-
const parsed =
|
|
85
|
-
if (
|
|
97
|
+
const parsed = readRealRef(el.attributes);
|
|
98
|
+
if (parsed.state !== 'bound')
|
|
86
99
|
continue; // no/invalid binding → R-20 territory
|
|
87
|
-
const ref = parsed.
|
|
100
|
+
const ref = parsed.value;
|
|
88
101
|
if (missingFile(facts, ref.file)) {
|
|
89
102
|
violations.push({
|
|
90
103
|
rule_id: 'RC-01',
|
|
91
|
-
severity: '
|
|
104
|
+
severity: 'warning',
|
|
92
105
|
element_id: el.id,
|
|
93
106
|
message: `${el.id} realRef.file '${ref.file}' does not exist on disk`,
|
|
94
107
|
fix_hint: 'Re-realize the FUNC (graph_realize) against the current source tree, or fix the moved/renamed file path',
|
|
@@ -101,7 +114,7 @@ function codeRefMustResolve(graph, facts) {
|
|
|
101
114
|
if (!facts.files[ref.file].declaredSymbols.includes(ref.symbol)) {
|
|
102
115
|
violations.push({
|
|
103
116
|
rule_id: 'RC-01',
|
|
104
|
-
severity: '
|
|
117
|
+
severity: 'warning',
|
|
105
118
|
element_id: el.id,
|
|
106
119
|
message: `${el.id} realRef.symbol '${ref.symbol}' is not declared in '${ref.file}'`,
|
|
107
120
|
fix_hint: 'The symbol was renamed or removed — re-realize the FUNC against the current code',
|
|
@@ -125,14 +138,14 @@ function testRefMustResolve(graph, facts) {
|
|
|
125
138
|
continue;
|
|
126
139
|
if (el.attributes?.concept === true)
|
|
127
140
|
continue;
|
|
128
|
-
const parsed =
|
|
129
|
-
if (
|
|
141
|
+
const parsed = readTestRefs(el.attributes);
|
|
142
|
+
if (parsed.state !== 'bound')
|
|
130
143
|
continue; // no/invalid binding → R-19 territory
|
|
131
|
-
for (const ref of parsed.
|
|
144
|
+
for (const ref of parsed.value) {
|
|
132
145
|
if (missingFile(facts, ref.file)) {
|
|
133
146
|
violations.push({
|
|
134
147
|
rule_id: 'RC-02',
|
|
135
|
-
severity: '
|
|
148
|
+
severity: 'warning',
|
|
136
149
|
element_id: el.id,
|
|
137
150
|
message: `${el.id} testRefs entry file '${ref.file}' does not exist on disk`,
|
|
138
151
|
fix_hint: 'The test file was moved or deleted — rebind the TEST (graph_realize) to the current file, or drop the entry',
|
|
@@ -146,7 +159,7 @@ function testRefMustResolve(graph, facts) {
|
|
|
146
159
|
if (caseName !== undefined && !facts.files[ref.file].testCases.some((name) => name.includes(caseName))) {
|
|
147
160
|
violations.push({
|
|
148
161
|
rule_id: 'RC-02',
|
|
149
|
-
severity: '
|
|
162
|
+
severity: 'warning',
|
|
150
163
|
element_id: el.id,
|
|
151
164
|
message: `${el.id} testRefs entry case '${ref.case}' is not declared in '${ref.file}'`,
|
|
152
165
|
fix_hint: 'The test case was renamed or removed — rebind the TEST case name',
|
|
@@ -169,14 +182,14 @@ function schemaRefMustResolve(graph, facts) {
|
|
|
169
182
|
continue;
|
|
170
183
|
if (el.attributes?.concept === true || el.attributes?.external === true)
|
|
171
184
|
continue;
|
|
172
|
-
const parsed =
|
|
173
|
-
if (
|
|
185
|
+
const parsed = readRealRef(el.attributes);
|
|
186
|
+
if (parsed.state !== 'bound')
|
|
174
187
|
continue; // no/invalid binding → R-26 territory
|
|
175
|
-
const ref = parsed.
|
|
188
|
+
const ref = parsed.value;
|
|
176
189
|
if (missingFile(facts, ref.file)) {
|
|
177
190
|
violations.push({
|
|
178
191
|
rule_id: 'RC-03',
|
|
179
|
-
severity: '
|
|
192
|
+
severity: 'warning',
|
|
180
193
|
element_id: el.id,
|
|
181
194
|
message: `${el.id} realRef.file '${ref.file}' does not exist on disk`,
|
|
182
195
|
fix_hint: 'Re-bind the SCHEMA (graph_realize) to the current source tree, or fix the moved/renamed file path',
|
|
@@ -189,7 +202,7 @@ function schemaRefMustResolve(graph, facts) {
|
|
|
189
202
|
if (!facts.files[ref.file].declaredSymbols.includes(ref.symbol)) {
|
|
190
203
|
violations.push({
|
|
191
204
|
rule_id: 'RC-03',
|
|
192
|
-
severity: '
|
|
205
|
+
severity: 'warning',
|
|
193
206
|
element_id: el.id,
|
|
194
207
|
message: `${el.id} realRef.symbol '${ref.symbol}' is not a declared export in '${ref.file}'`,
|
|
195
208
|
fix_hint: 'The schema export was renamed or removed — re-bind the SCHEMA against the current code',
|
|
@@ -199,6 +212,61 @@ function schemaRefMustResolve(graph, facts) {
|
|
|
199
212
|
}
|
|
200
213
|
return violations;
|
|
201
214
|
}
|
|
215
|
+
// The files the MODEL names as a SCHEMA's interface: realRef files of the FUNCs io-connected
|
|
216
|
+
// (producer or consumer) to a FLOW whose data format is this SCHEMA (FUNC ─io→ FLOW
|
|
217
|
+
// ─relation→ SCHEMA), restricted to files that exist. RC-04 asks whether one of them parses
|
|
218
|
+
// the schema, RC-09 whether anyone ELSE does — one derivation, so both ask the same question.
|
|
219
|
+
function interfaceFiles(graph, schemaId, facts, typeOf) {
|
|
220
|
+
const flowIds = new Set(graph.traces
|
|
221
|
+
.filter(t => t.type === 'relation' && t.target === schemaId && typeOf.get(t.source) === 'FLOW')
|
|
222
|
+
.map(t => t.source));
|
|
223
|
+
if (flowIds.size === 0)
|
|
224
|
+
return [];
|
|
225
|
+
const funcIds = new Set(graph.traces
|
|
226
|
+
.filter(t => t.type === 'io' &&
|
|
227
|
+
((flowIds.has(t.target) && typeOf.get(t.source) === 'FUNC') ||
|
|
228
|
+
(flowIds.has(t.source) && typeOf.get(t.target) === 'FUNC')))
|
|
229
|
+
.map(t => (flowIds.has(t.target) ? t.source : t.target)));
|
|
230
|
+
const files = [];
|
|
231
|
+
for (const fnId of funcIds) {
|
|
232
|
+
const fn = graph.elements.find(e => e.id === fnId);
|
|
233
|
+
const cr = readRealRef(fn?.attributes);
|
|
234
|
+
if (cr.state === 'bound' && !missingFile(facts, cr.value.file))
|
|
235
|
+
files.push(cr.value.file);
|
|
236
|
+
}
|
|
237
|
+
return files;
|
|
238
|
+
}
|
|
239
|
+
// Does `file` parse the schema? It must call `.parse`/`.safeParse` on the symbol AND have it in
|
|
240
|
+
// scope — imported, or declared right there: the definition file does not import its own schema
|
|
241
|
+
// (CR-SM-358, found by the positive control on graphcode's Format-E door, where the modelled
|
|
242
|
+
// translator lives in the schema's own file).
|
|
243
|
+
function parsesAt(facts, file, ref) {
|
|
244
|
+
const f = facts.files[file];
|
|
245
|
+
if (f?.exists !== true || !f.parsedSymbols.includes(ref.symbol))
|
|
246
|
+
return false;
|
|
247
|
+
return f.importedSymbols.includes(ref.symbol) || file === ref.file;
|
|
248
|
+
}
|
|
249
|
+
// Files outside `allowed` that import AND parse the symbol — the parsers the model does not
|
|
250
|
+
// name. Test files (testCases present) are exempt; consumer ratchets cover them. Empty unless
|
|
251
|
+
// the extractor saw every file (`fileScope: 'all'`): a partial scan cannot name who else parses.
|
|
252
|
+
function foreignParsers(facts, symbol, allowed) {
|
|
253
|
+
if (facts.fileScope !== 'all')
|
|
254
|
+
return [];
|
|
255
|
+
return Object.entries(facts.files)
|
|
256
|
+
.filter(([file, f]) => f.exists && !allowed.has(file) && f.testCases.length === 0 &&
|
|
257
|
+
f.importedSymbols.includes(symbol) && f.parsedSymbols.includes(symbol))
|
|
258
|
+
.map(([file]) => file)
|
|
259
|
+
.sort();
|
|
260
|
+
}
|
|
261
|
+
// A SCHEMA whose realRef symbol is declared but, by the extractor's own account, not a Zod
|
|
262
|
+
// schema. False when the extractor did not report `zodSymbols` (silence, not a verdict) or
|
|
263
|
+
// when the symbol is not declared at all (RC-03's finding, not this one).
|
|
264
|
+
function boundToNonZod(facts, ref) {
|
|
265
|
+
const f = facts.files[ref.file];
|
|
266
|
+
if (f?.exists !== true || f.zodSymbols === undefined)
|
|
267
|
+
return false;
|
|
268
|
+
return f.declaredSymbols.includes(ref.symbol) && !f.zodSymbols.includes(ref.symbol);
|
|
269
|
+
}
|
|
202
270
|
// RC-04: a bound SCHEMA that the graph says is realized at an interface must
|
|
203
271
|
// actually be parsed there (CR-211). The graph gives the check LOCATIONS: FUNCs
|
|
204
272
|
// io-connected to a FLOW whose data format IS this SCHEMA (FUNC ─io→ FLOW
|
|
@@ -218,43 +286,29 @@ function schemaRefMustBeUsed(graph, facts) {
|
|
|
218
286
|
continue;
|
|
219
287
|
if (el.attributes?.concept === true)
|
|
220
288
|
continue;
|
|
221
|
-
const parsed =
|
|
222
|
-
if (
|
|
289
|
+
const parsed = readRealRef(el.attributes);
|
|
290
|
+
if (parsed.state !== 'bound' || parsed.value.symbol === undefined)
|
|
223
291
|
continue;
|
|
224
|
-
const ref = parsed.
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
.map(t => t.source));
|
|
229
|
-
if (flowIds.size === 0)
|
|
230
|
-
continue;
|
|
231
|
-
// FUNCs io-connected to those FLOWs (producer or consumer).
|
|
232
|
-
const funcIds = new Set(graph.traces
|
|
233
|
-
.filter(t => t.type === 'io' &&
|
|
234
|
-
((flowIds.has(t.target) && typeOf.get(t.source) === 'FUNC') ||
|
|
235
|
-
(flowIds.has(t.source) && typeOf.get(t.target) === 'FUNC')))
|
|
236
|
-
.map(t => (flowIds.has(t.target) ? t.source : t.target)));
|
|
237
|
-
// Realized FUNCs among them: a resolvable realRef whose file exists.
|
|
238
|
-
const realizedFiles = [];
|
|
239
|
-
for (const fnId of funcIds) {
|
|
240
|
-
const fn = graph.elements.find(e => e.id === fnId);
|
|
241
|
-
const cr = RealRefSchema.safeParse(fn?.attributes?.realRef);
|
|
242
|
-
if (cr.success && !missingFile(facts, cr.data.file))
|
|
243
|
-
realizedFiles.push(cr.data.file);
|
|
244
|
-
}
|
|
292
|
+
const ref = parsed.value;
|
|
293
|
+
if (boundToNonZod(facts, ref))
|
|
294
|
+
continue; // RC-08 owns this cause (CR-SM-358)
|
|
295
|
+
const realizedFiles = interfaceFiles(graph, el.id, facts, typeOf);
|
|
245
296
|
if (realizedFiles.length === 0)
|
|
246
297
|
continue; // nothing realized to check against
|
|
247
|
-
const usedSomewhere = realizedFiles.some(file =>
|
|
248
|
-
const f = facts.files[file];
|
|
249
|
-
return f.importedSymbols.includes(ref.symbol) && f.parsedSymbols.includes(ref.symbol);
|
|
250
|
-
});
|
|
298
|
+
const usedSomewhere = realizedFiles.some(file => parsesAt(facts, file, ref));
|
|
251
299
|
if (!usedSomewhere) {
|
|
300
|
+
// CR-SM-358: where it IS parsed, if the extractor saw every file — then the finding says
|
|
301
|
+
// where the binding has to point, and RC-09 stays quiet about the same cause.
|
|
302
|
+
const actual = foreignParsers(facts, ref.symbol, new Set([ref.file, ...realizedFiles]));
|
|
252
303
|
violations.push({
|
|
253
304
|
rule_id: 'RC-04',
|
|
254
305
|
severity: 'warning',
|
|
255
306
|
element_id: el.id,
|
|
256
|
-
message: `${el.id} schema '${ref.symbol}' is not parsed in any realized FUNC at its modelled interface
|
|
257
|
-
|
|
307
|
+
message: `${el.id} schema '${ref.symbol}' is not parsed in any realized FUNC at its modelled interface` +
|
|
308
|
+
(actual.length > 0 ? ` — it is parsed in: ${actual.join(', ')}` : ''),
|
|
309
|
+
fix_hint: actual.length > 0
|
|
310
|
+
? `The parse sits in ${actual.join(', ')}: point the FUNC realRef at that file, or move the parse into the modelled FUNC`
|
|
311
|
+
: `Import and call ${ref.symbol}.parse()/.safeParse() in one of the io-connected FUNC's code files — or mark the SCHEMA concept:true if no code realizes it yet`,
|
|
258
312
|
context: { element_type: el.type, element_name: el.name },
|
|
259
313
|
});
|
|
260
314
|
}
|
|
@@ -295,12 +349,12 @@ function buildModResolver(graph) {
|
|
|
295
349
|
for (const el of graph.elements) {
|
|
296
350
|
if (el.type !== 'FUNC')
|
|
297
351
|
continue;
|
|
298
|
-
const cr =
|
|
299
|
-
if (
|
|
352
|
+
const cr = readRealRef(el.attributes);
|
|
353
|
+
if (cr.state !== 'bound')
|
|
300
354
|
continue;
|
|
301
355
|
const modTrace = graph.traces.find(t => t.source === el.id && t.type === 'allocate' && typeOf.get(t.target) === 'MOD');
|
|
302
356
|
if (modTrace)
|
|
303
|
-
fileToMod.set(cr.
|
|
357
|
+
fileToMod.set(cr.value.file, modTrace.target);
|
|
304
358
|
}
|
|
305
359
|
// MOD.path longest-prefix fallback for files with no direct binding.
|
|
306
360
|
const modPaths = graph.elements
|
|
@@ -439,10 +493,10 @@ function externalRefMustNameDependency(graph, facts) {
|
|
|
439
493
|
// `concept: true` has no binding that could rot — a concept node claims nothing about code.
|
|
440
494
|
if (el.attributes?.external !== true || el.attributes?.concept === true)
|
|
441
495
|
continue;
|
|
442
|
-
const parsed =
|
|
443
|
-
if (
|
|
496
|
+
const parsed = readRealRef(el.attributes);
|
|
497
|
+
if (parsed.state !== 'bound')
|
|
444
498
|
continue;
|
|
445
|
-
const match = WORKSPACE_PATH.exec(parsed.
|
|
499
|
+
const match = WORKSPACE_PATH.exec(parsed.value.file);
|
|
446
500
|
if (!match)
|
|
447
501
|
continue; // not the shape this rule can decide
|
|
448
502
|
const pkg = `${WORKSPACE_SCOPE}/${match[1]}`;
|
|
@@ -452,7 +506,7 @@ function externalRefMustNameDependency(graph, facts) {
|
|
|
452
506
|
rule_id: 'RC-06',
|
|
453
507
|
severity: 'warning',
|
|
454
508
|
element_id: el.id,
|
|
455
|
-
message: `${el.id} binds to '${parsed.
|
|
509
|
+
message: `${el.id} binds to '${parsed.value.file}', but '${pkg}' is not a declared dependency`,
|
|
456
510
|
fix_hint: `Add '${pkg}' to dependencies, or re-point the realRef at the package that now owns the symbol`,
|
|
457
511
|
context: { element_type: el.type, element_name: el.name },
|
|
458
512
|
});
|
|
@@ -514,14 +568,101 @@ function crNodeMatchesFile(graph, facts) {
|
|
|
514
568
|
return violations;
|
|
515
569
|
}
|
|
516
570
|
/** All RC conformance rules — evaluated by executors that can supply CodeFacts. */
|
|
571
|
+
// ---------------------------------------------------------------------------
|
|
572
|
+
// RC-08 / RC-09 (CR-SM-358) — the contract has a checker, and only the modelled places use it.
|
|
573
|
+
//
|
|
574
|
+
// Why these two replace ND-01 in substance: parallel paths are not similar to each other
|
|
575
|
+
// (spike CR-GC-637: name similarity 0.000 in 7 of 7 documented pairs); they share a CONTRACT.
|
|
576
|
+
// A second translator of the same contract is found by asking who parses it, not what it
|
|
577
|
+
// looks like. RC-01..RC-07 ask model → code ("does the binding resolve?"); RC-09 is the
|
|
578
|
+
// first rule that asks code → model ("does the model know everyone who reads this?").
|
|
579
|
+
// ---------------------------------------------------------------------------
|
|
580
|
+
// RC-08: a bound SCHEMA must bind a Zod schema, not a type. A type is checked by the
|
|
581
|
+
// compiler and gone at runtime: whatever produces the data cannot prove it, and whoever
|
|
582
|
+
// reads it cannot check it. Data that nobody parses yet only has a reader nobody knows yet.
|
|
583
|
+
function schemaRefMustBeZod(graph, facts) {
|
|
584
|
+
const violations = [];
|
|
585
|
+
for (const el of graph.elements) {
|
|
586
|
+
if (el.type !== 'SCHEMA')
|
|
587
|
+
continue;
|
|
588
|
+
if (el.attributes?.concept === true)
|
|
589
|
+
continue;
|
|
590
|
+
const parsed = readRealRef(el.attributes);
|
|
591
|
+
if (parsed.state !== 'bound' || parsed.value.symbol === undefined)
|
|
592
|
+
continue;
|
|
593
|
+
const ref = parsed.value;
|
|
594
|
+
if (!boundToNonZod(facts, ref))
|
|
595
|
+
continue;
|
|
596
|
+
violations.push({
|
|
597
|
+
rule_id: 'RC-08',
|
|
598
|
+
severity: 'warning',
|
|
599
|
+
element_id: el.id,
|
|
600
|
+
message: `${el.id} realRef '${ref.symbol}' in '${ref.file}' is not a Zod schema — the contract cannot be checked at runtime`,
|
|
601
|
+
fix_hint: `Declare the contract as a Zod schema (export const ${ref.symbol}Schema = z.…) and bind the SCHEMA to it — a producer needs a checkable schema even while no reader is known`,
|
|
602
|
+
context: { element_type: el.type, element_name: el.name },
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
return violations;
|
|
606
|
+
}
|
|
607
|
+
// RC-09: a Zod-bound SCHEMA may be parsed only where the model says — in the realRef files
|
|
608
|
+
// of its producers/translators (the same set RC-04 checks) or in its own definition file.
|
|
609
|
+
// Any other file that imports AND parses the symbol is a translator the model does not
|
|
610
|
+
// know: a second path. The definition may live in another package (no facts entry) — only a
|
|
611
|
+
// proven type binding (RC-08) silences the rule. One finding per SCHEMA, listing the files — the count must not grow
|
|
612
|
+
// with usage (Gate 7, class CR-GC-315). Test files (testCases present) are exempt; ratchets
|
|
613
|
+
// in the consumer cover them. Runs only when the extractor saw every file (`fileScope:'all'`).
|
|
614
|
+
function schemaParsedOnlyAtInterface(graph, facts) {
|
|
615
|
+
if (facts.fileScope !== 'all')
|
|
616
|
+
return [];
|
|
617
|
+
// (foreignParsers repeats this guard; checking here skips the graph walk entirely.)
|
|
618
|
+
const typeOf = new Map(graph.elements.map(e => [e.id, e.type]));
|
|
619
|
+
const violations = [];
|
|
620
|
+
for (const el of graph.elements) {
|
|
621
|
+
if (el.type !== 'SCHEMA')
|
|
622
|
+
continue;
|
|
623
|
+
if (el.attributes?.concept === true)
|
|
624
|
+
continue;
|
|
625
|
+
const parsed = readRealRef(el.attributes);
|
|
626
|
+
if (parsed.state !== 'bound' || parsed.value.symbol === undefined)
|
|
627
|
+
continue;
|
|
628
|
+
const ref = parsed.value;
|
|
629
|
+
// Only a PROVEN type binding silences RC-09 (RC-08 owns it). A definition outside this
|
|
630
|
+
// repo — the family's shared contracts live in @sigloch/contracts — has no facts entry,
|
|
631
|
+
// and those are exactly the contracts reused most; a `.parse()` on the symbol is already
|
|
632
|
+
// the evidence that it is a runtime checker.
|
|
633
|
+
if (boundToNonZod(facts, ref))
|
|
634
|
+
continue;
|
|
635
|
+
const iface = interfaceFiles(graph, el.id, facts, typeOf);
|
|
636
|
+
// One cause, one finding (Gate 4): if the model names an interface and NONE of it parses,
|
|
637
|
+
// the parse merely sits elsewhere — that is RC-04's finding (which names the file), not a
|
|
638
|
+
// second path. A second path needs a first one: a modelled parser AND a foreign one.
|
|
639
|
+
const ifaceParses = iface.some(file => parsesAt(facts, file, ref));
|
|
640
|
+
if (iface.length > 0 && !ifaceParses)
|
|
641
|
+
continue;
|
|
642
|
+
const foreign = foreignParsers(facts, ref.symbol, new Set([ref.file, ...iface]));
|
|
643
|
+
if (foreign.length === 0)
|
|
644
|
+
continue;
|
|
645
|
+
violations.push({
|
|
646
|
+
rule_id: 'RC-09',
|
|
647
|
+
severity: 'warning',
|
|
648
|
+
element_id: el.id,
|
|
649
|
+
message: `${el.id} schema '${ref.symbol}' is parsed in ${foreign.length} file(s) the model does not know as its producer or translator: ${foreign.join(', ')}`,
|
|
650
|
+
fix_hint: 'Route the caller through the modelled translator — or, if it is a translator in its own right, model it: FUNC ─io→ FLOW ─relation→ this SCHEMA, with the FUNC realRef on that file',
|
|
651
|
+
context: { element_type: el.type, element_name: el.name },
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
return violations;
|
|
655
|
+
}
|
|
517
656
|
export const CODE_CONFORMANCE_RULES = [
|
|
518
|
-
{ id: 'RC-01', name: 'FUNC realRef resolves to a declared symbol', severity: '
|
|
519
|
-
{ id: 'RC-02', name: 'testRefs entries resolve to runnable tests', severity: '
|
|
520
|
-
{ id: 'RC-03', name: 'SCHEMA realRef resolves to a declared export', severity: '
|
|
657
|
+
{ id: 'RC-01', name: 'FUNC realRef resolves to a declared symbol', severity: 'warning', domain: ['FUNC'], evaluate: codeRefMustResolve },
|
|
658
|
+
{ id: 'RC-02', name: 'testRefs entries resolve to runnable tests', severity: 'warning', domain: ['TEST'], evaluate: testRefMustResolve },
|
|
659
|
+
{ id: 'RC-03', name: 'SCHEMA realRef resolves to a declared export', severity: 'warning', domain: ['SCHEMA'], evaluate: schemaRefMustResolve },
|
|
521
660
|
{ id: 'RC-04', name: 'SCHEMA realRef is parsed at its interface', severity: 'warning', domain: ['SCHEMA'], evaluate: schemaRefMustBeUsed },
|
|
522
661
|
{ id: 'RC-05', name: 'cross-module import drift', severity: 'warning', domain: ['MOD'], evaluate: importDriftConformance },
|
|
523
662
|
{ id: 'RC-06', name: 'external realRef names a declared dependency', severity: 'warning', domain: ['FUNC', 'MOD', 'SCHEMA'], evaluate: externalRefMustNameDependency },
|
|
524
663
|
{ id: 'RC-07', name: 'CR node agrees with docs/cr', severity: 'warning', domain: ['CR', 'SYS'], evaluate: crNodeMatchesFile },
|
|
664
|
+
{ id: 'RC-08', name: 'SCHEMA realRef is a Zod schema', severity: 'warning', domain: ['SCHEMA'], evaluate: schemaRefMustBeZod },
|
|
665
|
+
{ id: 'RC-09', name: 'SCHEMA is parsed only at its modelled interface', severity: 'warning', domain: ['SCHEMA'], evaluate: schemaParsedOnlyAtInterface },
|
|
525
666
|
];
|
|
526
667
|
/** Run all RC rules against a graph + extracted code facts. */
|
|
527
668
|
export function evaluateConformanceRules(graph, facts) {
|
|
@@ -8,6 +8,11 @@ import type { MetricPolicy } from './policy.js';
|
|
|
8
8
|
/**
|
|
9
9
|
* Abgeschlossen heisst: nicht mehr steuerbar. Alles andere ist Grundgesamtheit.
|
|
10
10
|
* Exportiert fuer RC-07 (CR-SM-329), das dieselbe Frage gegen `docs/cr/done/` stellt.
|
|
11
|
+
*
|
|
12
|
+
* CR-SM-362 (ITEM-2026-536): nur noch `done`. Die Menge kannte `dropped`/`rejected`, der
|
|
13
|
+
* Element-Vertrag (`OntologyElement.status`) nicht — die Regeln rechneten mit Werten, die der
|
|
14
|
+
* Vertrag verbietet. Verworfen ist eine Begruendung im CR-Text, kein eigener Zustand: das
|
|
15
|
+
* Verzeichnis `done/` schliesst ihn, der Knoten spiegelt `done`.
|
|
11
16
|
*/
|
|
12
17
|
export declare const CLOSED_STATUS: ReadonlySet<string>;
|
|
13
18
|
export declare const CR_RULES: RuleDefinition[];
|
|
@@ -36,8 +36,13 @@ const SCOPE_TYPES = new Set(['FUNC', 'MOD', 'SCHEMA', 'REQ', 'UC']);
|
|
|
36
36
|
/**
|
|
37
37
|
* Abgeschlossen heisst: nicht mehr steuerbar. Alles andere ist Grundgesamtheit.
|
|
38
38
|
* Exportiert fuer RC-07 (CR-SM-329), das dieselbe Frage gegen `docs/cr/done/` stellt.
|
|
39
|
+
*
|
|
40
|
+
* CR-SM-362 (ITEM-2026-536): nur noch `done`. Die Menge kannte `dropped`/`rejected`, der
|
|
41
|
+
* Element-Vertrag (`OntologyElement.status`) nicht — die Regeln rechneten mit Werten, die der
|
|
42
|
+
* Vertrag verbietet. Verworfen ist eine Begruendung im CR-Text, kein eigener Zustand: das
|
|
43
|
+
* Verzeichnis `done/` schliesst ihn, der Knoten spiegelt `done`.
|
|
39
44
|
*/
|
|
40
|
-
export const CLOSED_STATUS = new Set(['done'
|
|
45
|
+
export const CLOSED_STATUS = new Set(['done']);
|
|
41
46
|
function crMustTrack(graph) {
|
|
42
47
|
const typeOf = new Map(graph.elements.map(e => [e.id, e.type]));
|
|
43
48
|
const crs = graph.elements.filter(e => {
|
|
@@ -49,7 +54,7 @@ function crMustTrack(graph) {
|
|
|
49
54
|
.filter(cr => !graph.traces.some(t => t.source === cr.id && t.type === 'relation' && SCOPE_TYPES.has(typeOf.get(t.target) ?? '')))
|
|
50
55
|
.map(cr => ({
|
|
51
56
|
rule_id: 'CR-R01',
|
|
52
|
-
severity: '
|
|
57
|
+
severity: 'warning',
|
|
53
58
|
element_id: cr.id,
|
|
54
59
|
message: `${cr.id} names no scope (no relation to FUNC/MOD/SCHEMA/REQ/UC)`,
|
|
55
60
|
fix_hint: 'Add a relation trace to the FUNC, MOD, SCHEMA, REQ or UC this CR changes — a milestone alone is a date, not a scope',
|
|
@@ -76,7 +81,7 @@ function crDoneRequiresCommit(graph) {
|
|
|
76
81
|
})
|
|
77
82
|
.map(cr => ({
|
|
78
83
|
rule_id: 'CR-R02',
|
|
79
|
-
severity: '
|
|
84
|
+
severity: 'warning',
|
|
80
85
|
element_id: cr.id,
|
|
81
86
|
message: `${cr.id} is done but has no commitRef`,
|
|
82
87
|
fix_hint: 'Add commitRef with Git SHA, or set architectureOnly=true',
|
|
@@ -176,8 +181,8 @@ function crShouldHaveMilestone(graph) {
|
|
|
176
181
|
// Exports
|
|
177
182
|
// ---------------------------------------------------------------------------
|
|
178
183
|
export const CR_RULES = [
|
|
179
|
-
{ id: 'CR-R01', name: 'CR must track', severity: '
|
|
180
|
-
{ id: 'CR-R02', name: 'Done requires commit', severity: '
|
|
184
|
+
{ id: 'CR-R01', name: 'CR must track', severity: 'warning', evaluate: crMustTrack, domain: ['CR'] },
|
|
185
|
+
{ id: 'CR-R02', name: 'Done requires commit', severity: 'warning', evaluate: crDoneRequiresCommit, domain: ['CR'] },
|
|
181
186
|
// CR-SM-243: domain ist 'all', nicht CR — die Regel meldet am getrackten ZIELKNOTEN
|
|
182
187
|
// ("dieser Knoten wird von 2 offenen CRs angefasst"), und der kann jeden Typ haben. Eine
|
|
183
188
|
// Aufzaehlung waere eine Momentaufnahme der heute getrackten Typen, kein Modell; 'all'
|
package/dist/se/evaluate-all.js
CHANGED
|
@@ -10,7 +10,7 @@ import { CR_RULES, evaluateCRRules } from './cr-quality-rules.js';
|
|
|
10
10
|
import { ND_RULES, evaluateNDRules } from './near-duplicate-rules.js';
|
|
11
11
|
import { AO_RULES, evaluateAORules } from './ao-rules.js';
|
|
12
12
|
import { BQ_RULES, evaluateBQRules } from './quality-rules.js';
|
|
13
|
-
import { AF_RULES, evaluateAFRules } from './analysis-freshness-rules.js';
|
|
13
|
+
import { AF_RULES, evaluateAFRules, TASK_OUTCOME_RULES, evaluateTaskOutcomeRules } from './analysis-freshness-rules.js';
|
|
14
14
|
import { CODE_CONFORMANCE_RULES } from './conformance-rules.js';
|
|
15
15
|
/**
|
|
16
16
|
* CR-SM-285: das Profil haengt am KATALOG, nicht am ID-Praefix.
|
|
@@ -36,6 +36,7 @@ const CATALOGS = [
|
|
|
36
36
|
{ profile: 'se', rules: FM_RULES },
|
|
37
37
|
{ profile: 'se', rules: VIEW_RULES },
|
|
38
38
|
{ profile: 'se', rules: AF_RULES },
|
|
39
|
+
{ profile: 'se', rules: TASK_OUTCOME_RULES }, // CR-SM-355: Task-Ausgaenge trade / irr
|
|
39
40
|
// Sprach-/Textqualitaet statt SE-Struktur — das ist die Trennlinie, die die Praefixliste meinte.
|
|
40
41
|
{ profile: 'coding', rules: BQ_RULES },
|
|
41
42
|
{ profile: 'coding', rules: ND_RULES },
|
|
@@ -97,5 +98,6 @@ export function evaluateAllRules(graph, policy) {
|
|
|
97
98
|
...evaluateFMRules(graph, policy),
|
|
98
99
|
...evaluateViewRules(graph, policy),
|
|
99
100
|
...evaluateAFRules(graph),
|
|
101
|
+
...evaluateTaskOutcomeRules(graph), // CR-SM-355
|
|
100
102
|
];
|
|
101
103
|
}
|
package/dist/se/fmea-rules.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* by dimension nature: physical budgets (weight/power/cost) live on the part
|
|
8
8
|
* (MOD); behavioral budgets (timing/memory-throughput) on the FCHAIN/FUNC.
|
|
9
9
|
*/
|
|
10
|
-
import {
|
|
10
|
+
import { readTestRefs } from './ontology.js';
|
|
11
11
|
import { actionPriority, apMethod } from './action-priority.js';
|
|
12
12
|
const PHYSICAL_BUDGET_PAIRS = [
|
|
13
13
|
['costBudget', 'measuredCost', 'cost'],
|
|
@@ -106,10 +106,10 @@ export function fm03HighRiskUnverified(graph, policy) {
|
|
|
106
106
|
graph.elements.some(el => {
|
|
107
107
|
if (el.id !== t.source || el.type !== 'TEST')
|
|
108
108
|
return false;
|
|
109
|
-
const parsed =
|
|
110
|
-
if (
|
|
109
|
+
const parsed = readTestRefs(el.attributes);
|
|
110
|
+
if (parsed.state !== 'bound')
|
|
111
111
|
return false; // keine Bindung → keine Evidenz
|
|
112
|
-
return parsed.
|
|
112
|
+
return parsed.value.every(ref => ref.result === 'passed');
|
|
113
113
|
}));
|
|
114
114
|
return !hasPassedTest;
|
|
115
115
|
})
|
|
@@ -121,7 +121,7 @@ export function fm03HighRiskUnverified(graph, policy) {
|
|
|
121
121
|
// dieselbe Quelle. `rpn-interim` heisst „bestaetigtes AP, wo bekannt; RPN, wo nicht".
|
|
122
122
|
return {
|
|
123
123
|
rule_id: 'FM-03',
|
|
124
|
-
severity: '
|
|
124
|
+
severity: 'warning',
|
|
125
125
|
element_id: e.id,
|
|
126
126
|
message: `${e.id} has Action Priority High (S${s}/O${o}/D${d}, ${method}) without passed test verification`,
|
|
127
127
|
fix_hint: 'Add a TEST whose every testRefs entry has result=passed, and a verify trace to this risk REQ',
|
|
@@ -166,7 +166,7 @@ export function nfr01BudgetOvershoot(graph) {
|
|
|
166
166
|
export const FM_RULES = [
|
|
167
167
|
{ id: 'FM-01', name: 'RiskReqFmeaAttributes', severity: 'warning', evaluate: fm01MissingFmeaAttributes, domain: ['REQ'] },
|
|
168
168
|
{ id: 'FM-02', name: 'RiskReqMitigation', severity: 'warning', evaluate: fm02MissingMitigation, domain: ['REQ'] },
|
|
169
|
-
{ id: 'FM-03', name: 'HighRiskVerification', severity: '
|
|
169
|
+
{ id: 'FM-03', name: 'HighRiskVerification', severity: 'warning', evaluate: fm03HighRiskUnverified, domain: ['REQ'] },
|
|
170
170
|
// CR-SM-243: die Regel prueft drei Typen, nicht einen — physische Budgets am MOD
|
|
171
171
|
// (PHYSICAL_BUDGET_TYPES), Verhaltensbudgets an FUNC und FCHAIN (BEHAVIORAL_BUDGET_TYPES).
|
|
172
172
|
// Die domain nennt dieselben Mengen wie die beiden Konstanten oben; wer eine davon
|