@sigloch/contracts 10.12.0 → 10.14.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.js +41 -2
- package/dist/se/conformance-rules.js +40 -3
- package/dist/se/cr-quality-rules.js +72 -0
- package/dist/se/evaluate-all.d.ts +8 -1
- package/dist/se/evaluate-all.js +32 -29
- package/dist/se/fchain-quality-rules.d.ts +2 -1
- package/dist/se/fchain-quality-rules.js +132 -0
- package/dist/se/fmea-rules.js +19 -9
- package/dist/se/grammar-snapshot.d.ts +9 -9
- package/dist/se/grammar-snapshot.js +25 -10
- package/dist/se/index.d.ts +3 -3
- package/dist/se/index.js +3 -3
- package/dist/se/meta-model.js +27 -16
- package/dist/se/ontology.d.ts +42 -21
- package/dist/se/ontology.js +53 -16
- package/dist/se/placeholder.d.ts +1 -0
- package/dist/se/placeholder.js +13 -0
- package/dist/se/quality-rules.js +3 -3
- package/dist/se/readiness.d.ts +14 -0
- package/dist/se/readiness.js +30 -3
- package/dist/se/rule-help.d.ts +17 -3
- package/dist/se/rule-help.js +65 -12
- package/dist/se/rules.d.ts +16 -0
- package/dist/se/rules.js +63 -28
- package/dist/se/uc-quality-rules.js +2 -2
- package/package.json +1 -1
|
@@ -6,6 +6,44 @@ const ARTIFACT_LABEL = {
|
|
|
6
6
|
fmea: 'FMEA',
|
|
7
7
|
implplan: 'Implementation Plan',
|
|
8
8
|
};
|
|
9
|
+
/**
|
|
10
|
+
* CR-SM-382: was der Hinweis nennt — den Task, seinen Skill und den ersten Arbeitsschritt.
|
|
11
|
+
*
|
|
12
|
+
* Bis hierher lautete der fix_hint "Set attributes.analysisFreshness.<id>.graphVersion …". Gemessen im
|
|
13
|
+
* lokalen AgentDiary-Lauf (local-1, 2026-09-30): das Modell setzte fuenf Stempel in einem Zug, ohne ein
|
|
14
|
+
* einziges Artefakt, und meldete die Analysen als erledigt — der Hinweis nannte die Metrik als Handlung.
|
|
15
|
+
* Dazu wich die Artefakt-id vom Task-Namen ab (`implplan` gegen `plan`), der Aufruf
|
|
16
|
+
* `graph_generate {task:'implplan'}` schlug fehl. Der Hinweis nannte darauf den Aufruf woertlich und die
|
|
17
|
+
* Arbeit; den Stempel schreibt der letzte Schritt des Skills, zusammen mit den Funden.
|
|
18
|
+
*
|
|
19
|
+
* CR-SM-387: der Hinweis nennt keinen Werkzeugnamen mehr. `graph_generate` hat nur der Autopilot; der
|
|
20
|
+
* interaktive Agent (todo-local, 2026-10-03) zitierte den Aufruf zweimal als fehlendes Werkzeug und
|
|
21
|
+
* liess die Analyse liegen. Der Skill fuehrt die Analyse in beiden Betriebsarten — er ist der Name, der
|
|
22
|
+
* stimmen muss (`skill` folgt RULE_HELP[…].prompt, der Test haelt beide zusammen); damit entfaellt auch
|
|
23
|
+
* die Fehlerquelle `task` gegen Artefakt-id.
|
|
24
|
+
*/
|
|
25
|
+
const ANALYSIS_WORK = {
|
|
26
|
+
conops: {
|
|
27
|
+
skill: 'se-conops',
|
|
28
|
+
firstStep: 'walk configuration, credentials, user management, deployment, observability and data lifecycle, and write each concern the system must satisfy as a non-functional REQ with its TEST at SYS scope',
|
|
29
|
+
},
|
|
30
|
+
trade: {
|
|
31
|
+
skill: 'se-trade',
|
|
32
|
+
firstStep: 'name the open decision with its options and criteria, then record the chosen option as a CR with relation(label: decides) edges to what it decides',
|
|
33
|
+
},
|
|
34
|
+
'assumption-review': {
|
|
35
|
+
skill: 'se-irr',
|
|
36
|
+
firstStep: 'list every unproven assumption the model rests on (unmeasured numbers, unverified external dependencies) in a record under docs/records/, then promote the load-bearing ones to CRs',
|
|
37
|
+
},
|
|
38
|
+
fmea: {
|
|
39
|
+
skill: 'se-fmea',
|
|
40
|
+
firstStep: 'for each FCHAIN name its failure modes and add each one as a REQ with role risk (severity, occurrence, detection) plus a mitigation REQ and its TEST',
|
|
41
|
+
},
|
|
42
|
+
implplan: {
|
|
43
|
+
skill: 'se-plan',
|
|
44
|
+
firstStep: 'cut one CR per leaf REQ at its carrier and group the CRs into MS milestones in dependency order',
|
|
45
|
+
},
|
|
46
|
+
};
|
|
9
47
|
/** Presence-check for one analysis artifact's freshness stamp, anchored on SYS. */
|
|
10
48
|
function analysisFreshnessPresence(ruleId, artifactId) {
|
|
11
49
|
return (graph) => {
|
|
@@ -16,12 +54,13 @@ function analysisFreshnessPresence(ruleId, artifactId) {
|
|
|
16
54
|
if (AnalysisFreshnessStampSchema.safeParse(stamp).success)
|
|
17
55
|
return [];
|
|
18
56
|
const label = ARTIFACT_LABEL[artifactId];
|
|
57
|
+
const work = ANALYSIS_WORK[artifactId];
|
|
19
58
|
return [{
|
|
20
59
|
rule_id: ruleId,
|
|
21
60
|
severity: 'warning',
|
|
22
61
|
element_id: sys.id,
|
|
23
|
-
message: `${label}
|
|
24
|
-
fix_hint: `
|
|
62
|
+
message: `${label} has not been carried out — no analysis is on record for this system`,
|
|
63
|
+
fix_hint: `Carry out the ${label}: work through the skill ${work.skill} step by step. First step: ${work.firstStep}. The closing step of the skill records the analysis as done, in the same batch as its findings`,
|
|
25
64
|
context: { element_type: sys.type, element_name: sys.name },
|
|
26
65
|
}];
|
|
27
66
|
};
|
|
@@ -104,7 +104,7 @@ function codeRefMustResolve(graph, facts) {
|
|
|
104
104
|
severity: 'warning',
|
|
105
105
|
element_id: el.id,
|
|
106
106
|
message: `${el.id} realRef.file '${ref.file}' does not exist on disk`,
|
|
107
|
-
fix_hint:
|
|
107
|
+
fix_hint: `Re-bind the FUNC to the current source tree via graph_mutate Format-E \`~ ${el.id} @realRef {"file":…,"symbol":…}\` — or restore the moved/renamed file`,
|
|
108
108
|
context: { element_type: el.type, element_name: el.name },
|
|
109
109
|
});
|
|
110
110
|
continue;
|
|
@@ -148,7 +148,7 @@ function testRefMustResolve(graph, facts) {
|
|
|
148
148
|
severity: 'warning',
|
|
149
149
|
element_id: el.id,
|
|
150
150
|
message: `${el.id} testRefs entry file '${ref.file}' does not exist on disk`,
|
|
151
|
-
fix_hint:
|
|
151
|
+
fix_hint: `The test file was moved or deleted — rebind the TEST via graph_mutate Format-E \`~ ${el.id} @testRefs [{"file":…,"tool":…}]\` (the patch replaces the whole list — carry the other entries), or drop the entry`,
|
|
152
152
|
context: { element_type: el.type, element_name: el.name },
|
|
153
153
|
});
|
|
154
154
|
continue;
|
|
@@ -192,7 +192,7 @@ function schemaRefMustResolve(graph, facts) {
|
|
|
192
192
|
severity: 'warning',
|
|
193
193
|
element_id: el.id,
|
|
194
194
|
message: `${el.id} realRef.file '${ref.file}' does not exist on disk`,
|
|
195
|
-
fix_hint:
|
|
195
|
+
fix_hint: `Re-bind the SCHEMA to the current source tree via graph_mutate Format-E \`~ ${el.id} @realRef {"file":…,"symbol":…}\` — or restore the moved/renamed file`,
|
|
196
196
|
context: { element_type: el.type, element_name: el.name },
|
|
197
197
|
});
|
|
198
198
|
continue;
|
|
@@ -653,6 +653,42 @@ function schemaParsedOnlyAtInterface(graph, facts) {
|
|
|
653
653
|
}
|
|
654
654
|
return violations;
|
|
655
655
|
}
|
|
656
|
+
// ---------------------------------------------------------------------------
|
|
657
|
+
// RC-10 (CR-SM-344): a MOD must be resolvable — carry a `path`, or have at least one
|
|
658
|
+
// allocated FUNC with a bound realRef. Those are the only two ways `buildModResolver`
|
|
659
|
+
// maps a file to a MOD; a MOD with neither can never own a file, so RC-05 and the
|
|
660
|
+
// boundary measurement are blind to it BY CONSTRUCTION. Measured at sigllm: 0 RC-05
|
|
661
|
+
// findings at 19 % import coverage, 8 at 81 % — the zero was the absence of a question.
|
|
662
|
+
// The cost in the message comes from `importCoverage` (the same resolution), not from a
|
|
663
|
+
// second count. `concept`/`external` are exempt — the same exemptions as R-20.
|
|
664
|
+
// ABSENT `importEdges` = silence, like RC-05: without an import graph RC-05 has nothing to
|
|
665
|
+
// judge, so there is no blindness to report — and the cost could not be named.
|
|
666
|
+
// ---------------------------------------------------------------------------
|
|
667
|
+
function modMustBeResolvable(graph, facts) {
|
|
668
|
+
if (facts.importEdges === undefined)
|
|
669
|
+
return []; // extractor supplied no import graph — silence
|
|
670
|
+
const typeOf = new Map(graph.elements.map(e => [e.id, e.type]));
|
|
671
|
+
const boundFuncs = new Set(graph.elements.filter(e => e.type === 'FUNC' && readRealRef(e.attributes).state === 'bound').map(e => e.id));
|
|
672
|
+
const resolvedByFunc = new Set(graph.traces
|
|
673
|
+
.filter(t => t.type === 'allocate' && boundFuncs.has(t.source) && typeOf.get(t.target) === 'MOD')
|
|
674
|
+
.map(t => t.target));
|
|
675
|
+
const blind = graph.elements.filter(e => e.type === 'MOD' &&
|
|
676
|
+
e.attributes?.concept !== true && e.attributes?.external !== true &&
|
|
677
|
+
!(typeof e.attributes?.path === 'string' && e.attributes.path.length > 0) &&
|
|
678
|
+
!resolvedByFunc.has(e.id));
|
|
679
|
+
if (blind.length === 0)
|
|
680
|
+
return [];
|
|
681
|
+
const coverage = importCoverage(graph, facts);
|
|
682
|
+
const cost = `${coverage.unassigned.length} of ${coverage.endpoints} import endpoints stay unassigned in this repo`;
|
|
683
|
+
return blind.map(el => ({
|
|
684
|
+
rule_id: 'RC-10',
|
|
685
|
+
severity: 'warning',
|
|
686
|
+
element_id: el.id,
|
|
687
|
+
message: `${el.id} has neither a path nor an allocated FUNC with a realRef — no file can resolve to it, RC-05 is blind here (${cost})`,
|
|
688
|
+
fix_hint: `Set the module's source directory via graph_mutate Format-E \`~ ${el.id} @path <dir>\` — or bind one of its allocated FUNCs (\`~ FUNC-x @realRef {"file":…,"symbol":…}\`); mark it concept:true if no code realizes it yet`,
|
|
689
|
+
context: { element_type: el.type, element_name: el.name },
|
|
690
|
+
}));
|
|
691
|
+
}
|
|
656
692
|
export const CODE_CONFORMANCE_RULES = [
|
|
657
693
|
{ id: 'RC-01', name: 'FUNC realRef resolves to a declared symbol', severity: 'warning', domain: ['FUNC'], evaluate: codeRefMustResolve },
|
|
658
694
|
{ id: 'RC-02', name: 'testRefs entries resolve to runnable tests', severity: 'warning', domain: ['TEST'], evaluate: testRefMustResolve },
|
|
@@ -663,6 +699,7 @@ export const CODE_CONFORMANCE_RULES = [
|
|
|
663
699
|
{ id: 'RC-07', name: 'CR node agrees with docs/cr', severity: 'warning', domain: ['CR', 'SYS'], evaluate: crNodeMatchesFile },
|
|
664
700
|
{ id: 'RC-08', name: 'SCHEMA realRef is a Zod schema', severity: 'warning', domain: ['SCHEMA'], evaluate: schemaRefMustBeZod },
|
|
665
701
|
{ id: 'RC-09', name: 'SCHEMA is parsed only at its modelled interface', severity: 'warning', domain: ['SCHEMA'], evaluate: schemaParsedOnlyAtInterface },
|
|
702
|
+
{ id: 'RC-10', name: 'MOD has resolvable files', severity: 'warning', domain: ['MOD'], evaluate: modMustBeResolvable },
|
|
666
703
|
];
|
|
667
704
|
/** Run all RC rules against a graph + extracted code facts. */
|
|
668
705
|
export function evaluateConformanceRules(graph, facts) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readRealRef } from './ontology.js';
|
|
1
2
|
// ---------------------------------------------------------------------------
|
|
2
3
|
// CR-R01: ein offener CR nennt, WAS er aendert (CR-SM-295)
|
|
3
4
|
//
|
|
@@ -178,6 +179,76 @@ function crShouldHaveMilestone(graph) {
|
|
|
178
179
|
}));
|
|
179
180
|
}
|
|
180
181
|
// ---------------------------------------------------------------------------
|
|
182
|
+
// CR-R05: kein Blatt-REQ ohne Bauauftrag (CR-SM-343) — die REQ-Seite von CR-R01.
|
|
183
|
+
//
|
|
184
|
+
// RD-01 fragt nach einem Traeger, R-01 nach einer Verifikation; ob ein REQ einen BAUAUFTRAG
|
|
185
|
+
// hat, fragte niemand. Gemessen am Fremdlauf sigllm: `se-plan` meldete „20 von 20 geordnet",
|
|
186
|
+
// im selben Graphstand hatten 24 von 64 Blatt-REQ keinen Auftrag — genau die, die alle 16
|
|
187
|
+
// offenen FM-03-Risiken mildern.
|
|
188
|
+
//
|
|
189
|
+
// Beauftragt ist ein Blatt-REQ (kein `compose`→REQ-Kind), wenn ein CR eine `relation` (a)
|
|
190
|
+
// direkt darauf traegt oder (b) auf eine FUNC, die es `satisfy`t. **MOD und SYS zaehlen
|
|
191
|
+
// nicht:** ein Modul ist ein Behaelter — mitgerechnet las sigllm 55/64 statt 40/64 und sah
|
|
192
|
+
// gesund aus. Wer ein REQ an einem MOD-/SYS-Traeger beauftragt, zieht die `relation` direkt
|
|
193
|
+
// auf das REQ. FCHAIN ist kein Weg: `CR -relation-> FCHAIN` ist kein TRACE_PATTERN, eine
|
|
194
|
+
// solche Kante waere R-18 — ein FCHAIN-getragenes REQ braucht also ebenfalls die direkte Kante.
|
|
195
|
+
//
|
|
196
|
+
// Jeder CR zaehlt, auch ein abgeschlossener: gebaut ist beauftragt.
|
|
197
|
+
//
|
|
198
|
+
// **Eine gebaute FUNC deckt ohne CR** (CR-SM-375): traegt die FUNC eine gueltige `realRef`
|
|
199
|
+
// oder ist sie `external`, ist der Bauauftrag erledigt — Bestand vor der CR-Disziplin braucht
|
|
200
|
+
// keinen nachgetragenen Auftrag. `concept: true` ist nicht gebaut, auch mit realRef. Fuer
|
|
201
|
+
// MOD/SYS/FCHAIN aendert das nichts: dort deckt nur die direkte `CR -relation-> REQ`.
|
|
202
|
+
//
|
|
203
|
+
// **Ohne CR-Knoten schweigt die Regel** und zaehlt weder im Zaehler noch im Nenner
|
|
204
|
+
// (`RULE_PRECONDITION`, readiness.ts). Ein Repo ohne Plan im Graphen (`cr: docs`) bekaeme
|
|
205
|
+
// sonst auf jedem REQ einen Befund — die Regel wuerde abgeschaltet und waere danach ueberall
|
|
206
|
+
// still (Fail-open-Klasse ND-01/ND-02 vor CR-SM-286).
|
|
207
|
+
// ---------------------------------------------------------------------------
|
|
208
|
+
function funcIsBuilt(e) {
|
|
209
|
+
if (e.attributes?.concept === true)
|
|
210
|
+
return false;
|
|
211
|
+
return e.attributes?.external === true || readRealRef(e.attributes).state === 'bound';
|
|
212
|
+
}
|
|
213
|
+
function leafReqNeedsBuildOrder(graph) {
|
|
214
|
+
const crIds = new Set(graph.elements.filter(e => e.type === 'CR').map(e => e.id));
|
|
215
|
+
if (crIds.size === 0)
|
|
216
|
+
return [];
|
|
217
|
+
const typeOf = new Map(graph.elements.map(e => [e.id, e.type]));
|
|
218
|
+
const ordered = new Set(); // CR -relation-> X, oder eine bereits gebaute FUNC
|
|
219
|
+
for (const t of graph.traces) {
|
|
220
|
+
if (t.type === 'relation' && crIds.has(t.source))
|
|
221
|
+
ordered.add(t.target);
|
|
222
|
+
}
|
|
223
|
+
for (const e of graph.elements) {
|
|
224
|
+
if (e.type === 'FUNC' && funcIsBuilt(e))
|
|
225
|
+
ordered.add(e.id);
|
|
226
|
+
}
|
|
227
|
+
const covered = new Set();
|
|
228
|
+
const parents = new Set();
|
|
229
|
+
for (const t of graph.traces) {
|
|
230
|
+
if (t.type === 'relation' && crIds.has(t.source) && typeOf.get(t.target) === 'REQ')
|
|
231
|
+
covered.add(t.target);
|
|
232
|
+
if (t.type === 'satisfy' && typeOf.get(t.source) === 'FUNC' && ordered.has(t.source))
|
|
233
|
+
covered.add(t.target);
|
|
234
|
+
if (t.type === 'compose' && typeOf.get(t.source) === 'REQ' && typeOf.get(t.target) === 'REQ')
|
|
235
|
+
parents.add(t.source);
|
|
236
|
+
}
|
|
237
|
+
return graph.elements
|
|
238
|
+
.filter(e => e.type === 'REQ' && !parents.has(e.id) && !covered.has(e.id))
|
|
239
|
+
.map(req => ({
|
|
240
|
+
rule_id: 'CR-R05',
|
|
241
|
+
severity: 'warning',
|
|
242
|
+
element_id: req.id,
|
|
243
|
+
message: `${req.id} is a leaf REQ with no build order (no CR relation to it or to a FUNC that satisfies it, and no built FUNC satisfies it)`,
|
|
244
|
+
fix_hint: `Add a CR relation through graph_mutate Format-E \`CR-x -relation-> ${req.id}\` — or to the FUNC that satisfies it; a CR on its MOD/SYS carrier does not count`,
|
|
245
|
+
context: {
|
|
246
|
+
element_type: req.type,
|
|
247
|
+
element_name: req.name,
|
|
248
|
+
},
|
|
249
|
+
}));
|
|
250
|
+
}
|
|
251
|
+
// ---------------------------------------------------------------------------
|
|
181
252
|
// Exports
|
|
182
253
|
// ---------------------------------------------------------------------------
|
|
183
254
|
export const CR_RULES = [
|
|
@@ -190,6 +261,7 @@ export const CR_RULES = [
|
|
|
190
261
|
// Damit ist sie nach R-08/R-18 die dritte 'all'-Regel — bewusst, nicht vergessen.
|
|
191
262
|
{ id: 'CR-R03', name: 'No concurrent mutation', severity: 'warning', evaluate: noConcurrentMutation, domain: ['all'] },
|
|
192
263
|
{ id: 'MS-03', name: 'CR without milestone', severity: 'info', evaluate: crShouldHaveMilestone, domain: ['CR'] },
|
|
264
|
+
{ id: 'CR-R05', name: 'Leaf REQ has a build order', severity: 'warning', evaluate: leafReqNeedsBuildOrder, domain: ['REQ'] },
|
|
193
265
|
];
|
|
194
266
|
// CR-SM-236: `policy` wird durchgereicht, auch wo diese Familie heute keine Schwelle hat —
|
|
195
267
|
// ein Sonderweg je Familie waere genau der zweite Pfad, den der Regelsatz verbietet.
|
|
@@ -30,5 +30,12 @@ export declare function getRuleDefsForProfile(profile: ProfileId): typeof ALL_RU
|
|
|
30
30
|
* CR-SM-233: `policy` ist **Pflicht und ohne Fallback** — ein Aufruf ohne Policy ist ein
|
|
31
31
|
* Typfehler, keine stille 0.7. Wer keine eigene Quelle hat (Konfiguration, Host), nimmt
|
|
32
32
|
* `DEFAULT_METRIC_POLICY` sichtbar an der Aufrufstelle.
|
|
33
|
+
*
|
|
34
|
+
* CR-SM-378: `only` ist ein REGELFILTER, kein zweiter Auswerter — dieselbe Normalisierung,
|
|
35
|
+
* dieselbe Reihenfolge, dieselben `evaluate`-Aufrufe; eine Regel ausserhalb des Filters laeuft nur
|
|
36
|
+
* nicht. Das Ergebnis ist gleich dem Vollergebnis, nachtraeglich auf `only` gefiltert. Grund:
|
|
37
|
+
* der Steuer-Operator (se-engine `bestBySteer`) bewertete jeden Kandidaten mit dem ganzen
|
|
38
|
+
* Katalog, inklusive des quadratischen ND-01 — 8,9 von 17,1 s bei 2000 Knoten fuer Befunde, die
|
|
39
|
+
* er nie las. Ohne `only` laeuft der ganze Katalog.
|
|
33
40
|
*/
|
|
34
|
-
export declare function evaluateAllRules(graph: OntologyGraph, policy: MetricPolicy): RuleViolation[];
|
|
41
|
+
export declare function evaluateAllRules(graph: OntologyGraph, policy: MetricPolicy, only?: ReadonlySet<string>): RuleViolation[];
|
package/dist/se/evaluate-all.js
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
import { V3_RULES
|
|
2
|
-
import { SC_RULES
|
|
3
|
-
import { UC_RULES
|
|
4
|
-
import { FC_RULES
|
|
5
|
-
import { MT_RULES
|
|
1
|
+
import { V3_RULES } from './rules.js';
|
|
2
|
+
import { SC_RULES } from './schema-quality-rules.js';
|
|
3
|
+
import { UC_RULES } from './uc-quality-rules.js';
|
|
4
|
+
import { FC_RULES } from './fchain-quality-rules.js';
|
|
5
|
+
import { MT_RULES } from './metric-rules.js';
|
|
6
6
|
import { toEvaluableGraph } from './flat-graph.js';
|
|
7
|
-
import { FM_RULES
|
|
8
|
-
import { VIEW_RULES
|
|
9
|
-
import { CR_RULES
|
|
10
|
-
import { ND_RULES
|
|
11
|
-
import { AO_RULES
|
|
12
|
-
import { BQ_RULES
|
|
13
|
-
import { AF_RULES,
|
|
7
|
+
import { FM_RULES } from './fmea-rules.js';
|
|
8
|
+
import { VIEW_RULES } from './view-rules.js';
|
|
9
|
+
import { CR_RULES } from './cr-quality-rules.js';
|
|
10
|
+
import { ND_RULES } from './near-duplicate-rules.js';
|
|
11
|
+
import { AO_RULES } from './ao-rules.js';
|
|
12
|
+
import { BQ_RULES } from './quality-rules.js';
|
|
13
|
+
import { AF_RULES, TASK_OUTCOME_RULES } 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.
|
|
@@ -67,14 +67,31 @@ export function getRuleDefsForProfile(profile) {
|
|
|
67
67
|
return ALL_RULE_DEFS;
|
|
68
68
|
return ALL_RULE_DEFS.filter(r => r.profile === profile);
|
|
69
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* Die ausgewerteten Kataloge in der Reihenfolge der kanonischen Befund-Sequenz (CR-SM-240).
|
|
72
|
+
* Ohne `CODE_CONFORMANCE_RULES` — die brauchen `CodeFacts` (s. CATALOGS). Die Reihenfolge ist
|
|
73
|
+
* Vertrag: das Golden (`se-rule-output-identity.test.ts`) pinnt sie.
|
|
74
|
+
*/
|
|
75
|
+
const EVALUATED_CATALOGS = [
|
|
76
|
+
V3_RULES, BQ_RULES, UC_RULES, FC_RULES, SC_RULES, ND_RULES, MT_RULES, CR_RULES, AO_RULES,
|
|
77
|
+
FM_RULES, VIEW_RULES, AF_RULES,
|
|
78
|
+
TASK_OUTCOME_RULES, // CR-SM-355
|
|
79
|
+
];
|
|
70
80
|
/**
|
|
71
81
|
* Evaluate all rules against a graph. Single call replaces the individual evaluator calls.
|
|
72
82
|
*
|
|
73
83
|
* CR-SM-233: `policy` ist **Pflicht und ohne Fallback** — ein Aufruf ohne Policy ist ein
|
|
74
84
|
* Typfehler, keine stille 0.7. Wer keine eigene Quelle hat (Konfiguration, Host), nimmt
|
|
75
85
|
* `DEFAULT_METRIC_POLICY` sichtbar an der Aufrufstelle.
|
|
86
|
+
*
|
|
87
|
+
* CR-SM-378: `only` ist ein REGELFILTER, kein zweiter Auswerter — dieselbe Normalisierung,
|
|
88
|
+
* dieselbe Reihenfolge, dieselben `evaluate`-Aufrufe; eine Regel ausserhalb des Filters laeuft nur
|
|
89
|
+
* nicht. Das Ergebnis ist gleich dem Vollergebnis, nachtraeglich auf `only` gefiltert. Grund:
|
|
90
|
+
* der Steuer-Operator (se-engine `bestBySteer`) bewertete jeden Kandidaten mit dem ganzen
|
|
91
|
+
* Katalog, inklusive des quadratischen ND-01 — 8,9 von 17,1 s bei 2000 Knoten fuer Befunde, die
|
|
92
|
+
* er nie las. Ohne `only` laeuft der ganze Katalog.
|
|
76
93
|
*/
|
|
77
|
-
export function evaluateAllRules(graph, policy) {
|
|
94
|
+
export function evaluateAllRules(graph, policy, only) {
|
|
78
95
|
// CR-SM-284: die Hebung der flach committeten SSOT laeuft HIER, nicht beim Aufrufer.
|
|
79
96
|
//
|
|
80
97
|
// `toEvaluableGraph` gibt es seit CR-SM-258 — und sie wurde von keinem Produktionspfad und
|
|
@@ -84,20 +101,6 @@ export function evaluateAllRules(graph, policy) {
|
|
|
84
101
|
//
|
|
85
102
|
// Idempotent und identitaetserhaltend (s. dort), also eine Normalisierung am einzigen Eingang
|
|
86
103
|
// und kein Fallback: ein bereits genesteter Graph geht unveraendert und uneingepackt durch.
|
|
87
|
-
|
|
88
|
-
return [
|
|
89
|
-
...evaluateRules(graph, policy),
|
|
90
|
-
...evaluateBQRules(graph, policy),
|
|
91
|
-
...evaluateUCRules(graph, policy),
|
|
92
|
-
...evaluateFCRules(graph, policy),
|
|
93
|
-
...evaluateSCRules(graph, policy),
|
|
94
|
-
...evaluateNDRules(graph),
|
|
95
|
-
...evaluateMTRules(graph, policy),
|
|
96
|
-
...evaluateCRRules(graph, policy),
|
|
97
|
-
...evaluateAORules(graph, policy),
|
|
98
|
-
...evaluateFMRules(graph, policy),
|
|
99
|
-
...evaluateViewRules(graph, policy),
|
|
100
|
-
...evaluateAFRules(graph),
|
|
101
|
-
...evaluateTaskOutcomeRules(graph), // CR-SM-355
|
|
102
|
-
];
|
|
104
|
+
const evaluable = toEvaluableGraph(graph);
|
|
105
|
+
return EVALUATED_CATALOGS.flatMap((rules) => rules.flatMap((rule) => (only === undefined || only.has(rule.id) ? rule.evaluate(evaluable, policy) : [])));
|
|
103
106
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* FC-
|
|
2
|
+
* FC-02..FC-05 — FCHAIN quality rules (CR-121 Phase 1; FC-05 CR-SM-363).
|
|
3
3
|
*/
|
|
4
4
|
import type { OntologyGraph } from './ontology.js';
|
|
5
5
|
import type { RuleDefinition, RuleViolation } from './rules.js';
|
|
@@ -7,5 +7,6 @@ import type { MetricPolicy } from './policy.js';
|
|
|
7
7
|
export declare function fc02LeafUcHasFchain(graph: OntologyGraph): RuleViolation[];
|
|
8
8
|
export declare function fc03FchainFlat(graph: OntologyGraph): RuleViolation[];
|
|
9
9
|
export declare function fc04ActorBounded(graph: OntologyGraph): RuleViolation[];
|
|
10
|
+
export declare function fc05ChainConnected(graph: OntologyGraph): RuleViolation[];
|
|
10
11
|
export declare const FC_RULES: RuleDefinition[];
|
|
11
12
|
export declare function evaluateFCRules(graph: OntologyGraph, policy: MetricPolicy): RuleViolation[];
|
|
@@ -96,6 +96,137 @@ export function fc04ActorBounded(graph) {
|
|
|
96
96
|
}));
|
|
97
97
|
}
|
|
98
98
|
// ---------------------------------------------------------------------------
|
|
99
|
+
// FC-05: FCHAIN ist gerichtet zusammenhaengend (CR-SM-363).
|
|
100
|
+
//
|
|
101
|
+
// Eine FCHAIN ist grammatisch eine Menge (`FCHAIN -compose-> FUNC`); die Reihenfolge entsteht
|
|
102
|
+
// erst aus Erzeuger -> Verbraucher (`FUNC -io-> FLOW -io-> FUNC`, beide Enden Glied). Bilden die
|
|
103
|
+
// Glieder darueber mehr als EINE (schwache) Komponente, beschreibt jede Kettenkennzahl etwas,
|
|
104
|
+
// das es nicht gibt. IO-01 sieht das nicht: es verbindet zwei Glieder schon, wenn beide
|
|
105
|
+
// denselben FLOW konsumieren. Gemessen (graphcode spike-kettenkennzahlen, 76 Ketten): 20
|
|
106
|
+
// zerfallen ohne jede Regel.
|
|
107
|
+
//
|
|
108
|
+
// Je Nebenkomponente (alles ausser der groessten; Gleichstand nach kleinster ID — Gate 5) die
|
|
109
|
+
// Ursache:
|
|
110
|
+
// - `missing-link`: Haupt- und Nebenteil sind ueber eine FUNC AUSSERHALB der Kette verbunden
|
|
111
|
+
// (Glied -> FLOW -> X -> FLOW -> Glied) — der verbindende Schritt gehoert in die Kette.
|
|
112
|
+
// Nur eine FUNC ist ein fehlendes Glied: ein Weg ueber einen ACTOR geht ueber die
|
|
113
|
+
// Systemgrenze, und einen ACTOR kann keine Kette aufnehmen.
|
|
114
|
+
// - `bag`: sonst — keine FUNC verbindet die Teile: ein fehlender FLOW zwischen Gliedern oder
|
|
115
|
+
// parallele Dienste an einer gemeinsamen Quelle (dann ist die Kette zu teilen).
|
|
116
|
+
// Traegt die Kette ein Glied ohne io-Eingang oder -Ausgang, gehoert sie zuerst R-31 — FC-05
|
|
117
|
+
// schweigt, bis alle Glieder verdrahtet sind (keine Doppelmeldung, s. u.). Eingang/Ausgang der Kette fragt FC-04 (streng: ACTOR) — eine Kette ohne
|
|
118
|
+
// jeden Eingang hat auch keinen ACTOR-Eingang, FC-05 schweigt deshalb dazu (eine Ursache, ein
|
|
119
|
+
// Befund). EIN Befund je Kette, die Komponenten im Kontext (CR-SM-242).
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
export function fc05ChainConnected(graph) {
|
|
122
|
+
const idx = indexOf(graph);
|
|
123
|
+
// Erzeuger/Verbraucher je FLOW — ueber alle Typen (FUNC oder ACTOR).
|
|
124
|
+
const producers = new Map();
|
|
125
|
+
const consumers = new Map();
|
|
126
|
+
const wiredIn = new Set();
|
|
127
|
+
const wiredOut = new Set();
|
|
128
|
+
for (const t of idx.tracesOfType('io')) {
|
|
129
|
+
if (idx.typeOf(t.target) === 'FLOW') {
|
|
130
|
+
(producers.get(t.target) ?? producers.set(t.target, []).get(t.target)).push(t.source);
|
|
131
|
+
wiredOut.add(t.source);
|
|
132
|
+
}
|
|
133
|
+
if (idx.typeOf(t.source) === 'FLOW') {
|
|
134
|
+
(consumers.get(t.source) ?? consumers.set(t.source, []).get(t.source)).push(t.target);
|
|
135
|
+
wiredIn.add(t.target);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
const successors = new Map();
|
|
139
|
+
for (const [flow, ps] of producers) {
|
|
140
|
+
for (const p of ps) {
|
|
141
|
+
const set = successors.get(p) ?? successors.set(p, new Set()).get(p);
|
|
142
|
+
for (const c of consumers.get(flow) ?? [])
|
|
143
|
+
if (c !== p)
|
|
144
|
+
set.add(c);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
const succ = (id) => successors.get(id) ?? new Set();
|
|
148
|
+
const violations = [];
|
|
149
|
+
for (const fc of idx.elementsOfType('FCHAIN')) {
|
|
150
|
+
const members = [...new Set(idx.out(fc.id, 'compose').map(t => t.target).filter(id => idx.typeOf(id) === 'FUNC'))].sort();
|
|
151
|
+
if (members.length < 2)
|
|
152
|
+
continue;
|
|
153
|
+
// Ein Glied ohne io-Eingang oder -Ausgang ist R-31s Befund — und er kann genau die Bruecke
|
|
154
|
+
// sein, die fehlt. Solange eines da ist, urteilt FC-05 nicht (eine Ursache, ein Befund):
|
|
155
|
+
// gemessen an graphcode FCHAIN-capture, wo `FUNC-decode` ohne Ausgang sonst einen Sack vortaeuscht.
|
|
156
|
+
if (members.some(f => !wiredIn.has(f) || !wiredOut.has(f)))
|
|
157
|
+
continue;
|
|
158
|
+
const inChain = new Set(members);
|
|
159
|
+
// Schwache Komponenten ueber Glied -> Glied.
|
|
160
|
+
const adj = new Map(members.map(m => [m, new Set()]));
|
|
161
|
+
for (const a of members) {
|
|
162
|
+
for (const b of succ(a))
|
|
163
|
+
if (inChain.has(b)) {
|
|
164
|
+
adj.get(a).add(b);
|
|
165
|
+
adj.get(b).add(a);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
const seen = new Set();
|
|
169
|
+
const comps = [];
|
|
170
|
+
for (const m of members) {
|
|
171
|
+
if (seen.has(m))
|
|
172
|
+
continue;
|
|
173
|
+
const comp = [m];
|
|
174
|
+
seen.add(m);
|
|
175
|
+
for (let i = 0; i < comp.length; i++) {
|
|
176
|
+
for (const n of adj.get(comp[i]))
|
|
177
|
+
if (!seen.has(n)) {
|
|
178
|
+
seen.add(n);
|
|
179
|
+
comp.push(n);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
comps.push(comp.sort());
|
|
183
|
+
}
|
|
184
|
+
if (comps.length < 2)
|
|
185
|
+
continue;
|
|
186
|
+
comps.sort((a, b) => b.length - a.length || a[0].localeCompare(b[0]));
|
|
187
|
+
const main = new Set(comps[0]);
|
|
188
|
+
// Aussen-FUNCs X mit from -> X -> to.
|
|
189
|
+
const bridges = (from, to) => {
|
|
190
|
+
const via = new Set();
|
|
191
|
+
for (const a of from) {
|
|
192
|
+
for (const x of succ(a)) {
|
|
193
|
+
if (inChain.has(x) || idx.typeOf(x) !== 'FUNC')
|
|
194
|
+
continue;
|
|
195
|
+
if ([...succ(x)].some(b => to.has(b)))
|
|
196
|
+
via.add(x);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return [...via];
|
|
200
|
+
};
|
|
201
|
+
const side = [];
|
|
202
|
+
for (const comp of comps.slice(1)) {
|
|
203
|
+
const set = new Set(comp);
|
|
204
|
+
const via = [...new Set([...bridges(main, set), ...bridges(set, main)])].sort();
|
|
205
|
+
side.push({ members: comp, cause: via.length > 0 ? 'missing-link' : 'bag', via });
|
|
206
|
+
}
|
|
207
|
+
if (side.length === 0)
|
|
208
|
+
continue;
|
|
209
|
+
const missing = [...new Set(side.flatMap(c => c.via))].sort();
|
|
210
|
+
const hints = [];
|
|
211
|
+
if (missing.length > 0)
|
|
212
|
+
hints.push(`add the missing link(s) as members: ${missing.map(f => `\`${fc.id} -compose-> ${f}\``).join(', ')}`);
|
|
213
|
+
// `bag` sagt nur: KEINE Aussen-FUNC verbindet die Teile. Das ist entweder ein fehlender FLOW
|
|
214
|
+
// zwischen zwei Gliedern (gemessen: graphcode FCHAIN-generation-states, Uebergabe an
|
|
215
|
+
// graph_suggest nicht modelliert) oder ein Sack paralleler Dienste — der Hinweis nennt beides.
|
|
216
|
+
if (side.some(c => c.cause === 'bag'))
|
|
217
|
+
hints.push('connect the parts with a FLOW from a producer member to a consumer member if they are one chain of effects — otherwise split the chain, one FCHAIN per chain of effects');
|
|
218
|
+
violations.push({
|
|
219
|
+
rule_id: 'FC-05',
|
|
220
|
+
severity: 'warning',
|
|
221
|
+
element_id: fc.id,
|
|
222
|
+
message: `${fc.id} falls apart into ${comps.length} unconnected parts along producer -> consumer (${side.map(c => `${c.members.join('+')}: ${c.cause}`).join('; ')})`,
|
|
223
|
+
fix_hint: `Via graph_mutate Format-E, ${hints.join('; or ')}`,
|
|
224
|
+
context: { element_type: fc.type, element_name: fc.name, side_components: side },
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
return violations;
|
|
228
|
+
}
|
|
229
|
+
// ---------------------------------------------------------------------------
|
|
99
230
|
// Aggregated
|
|
100
231
|
// ---------------------------------------------------------------------------
|
|
101
232
|
export const FC_RULES = [
|
|
@@ -105,6 +236,7 @@ export const FC_RULES = [
|
|
|
105
236
|
// eine Kette mit n verschachtelten FUNCs erzeugt n Verstoesse gegen einen Beitrag von 1.
|
|
106
237
|
{ id: 'FC-03', name: 'FCHAIN is flat', severity: 'warning', evaluate: fc03FchainFlat, domain: ['FUNC'] },
|
|
107
238
|
{ id: 'FC-04', name: 'FCHAIN actor-bounded (trigger+consumer)', severity: 'warning', evaluate: fc04ActorBounded, domain: ['FCHAIN'] },
|
|
239
|
+
{ id: 'FC-05', name: 'FCHAIN is connected (producer -> consumer)', severity: 'warning', evaluate: fc05ChainConnected, domain: ['FCHAIN'] },
|
|
108
240
|
];
|
|
109
241
|
// CR-SM-236: `policy` wird durchgereicht, auch wo diese Familie heute keine Schwelle hat —
|
|
110
242
|
// ein Sonderweg je Familie waere genau der zweite Pfad, den der Regelsatz verbietet.
|
package/dist/se/fmea-rules.js
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* CR-182: FMEA/FTA Risk Rules + NFR Budget Overshoot Detection.
|
|
3
3
|
* FM-01: Risk-REQ missing FMEA attributes (severity, occurrence, detection).
|
|
4
|
-
* FM-02: Risk-REQ without mitigation (compose→REQ(
|
|
4
|
+
* FM-02: Risk-REQ without mitigation (compose→REQ(role=mitigation)).
|
|
5
|
+
* CR-SM-365: "Risk-REQ" heisst `attributes.role === 'risk'` (Leser `readReqRole`), nicht mehr
|
|
6
|
+
* `kinds ∋ risk` — die Rolle ist die Motivation der Anforderung, nicht ihre Art. Kein
|
|
7
|
+
* zweiter Pfad ueber `kinds`: ein Altgraph faellt hier still, bis er migriert ist.
|
|
5
8
|
* FM-03: risk REQ with Action Priority High and no passed verification.
|
|
6
9
|
* NFR-01: measured exceeds budget for an NFR dimension. CR-228 splits the target
|
|
7
10
|
* by dimension nature: physical budgets (weight/power/cost) live on the part
|
|
8
11
|
* (MOD); behavioral budgets (timing/memory-throughput) on the FCHAIN/FUNC.
|
|
9
12
|
*/
|
|
10
|
-
import { readTestRefs } from './ontology.js';
|
|
13
|
+
import { readReqRole, readTestRefs } from './ontology.js';
|
|
11
14
|
import { actionPriority, apMethod } from './action-priority.js';
|
|
12
15
|
const PHYSICAL_BUDGET_PAIRS = [
|
|
13
16
|
['costBudget', 'measuredCost', 'cost'],
|
|
@@ -20,12 +23,19 @@ const BEHAVIORAL_BUDGET_PAIRS = [
|
|
|
20
23
|
];
|
|
21
24
|
const PHYSICAL_BUDGET_TYPES = ['MOD'];
|
|
22
25
|
const BEHAVIORAL_BUDGET_TYPES = ['FUNC', 'FCHAIN'];
|
|
26
|
+
/** Traegt das Element die FMEA-Rolle `role`? Einziger Weg der FM-Regeln (CR-SM-365). */
|
|
27
|
+
const hasRole = (e, role) => {
|
|
28
|
+
if (e.type !== 'REQ')
|
|
29
|
+
return false;
|
|
30
|
+
const r = readReqRole(e.attributes);
|
|
31
|
+
return r.state === 'bound' && r.value === role;
|
|
32
|
+
};
|
|
23
33
|
// ---------------------------------------------------------------------------
|
|
24
34
|
// FM-01: Risk-REQ without FMEA attributes
|
|
25
35
|
// ---------------------------------------------------------------------------
|
|
26
36
|
export function fm01MissingFmeaAttributes(graph) {
|
|
27
37
|
return graph.elements
|
|
28
|
-
.filter(e => e
|
|
38
|
+
.filter(e => hasRole(e, 'risk'))
|
|
29
39
|
.filter(e => {
|
|
30
40
|
const a = e.attributes ?? {};
|
|
31
41
|
return a['severity'] == null || a['occurrence'] == null || a['detection'] == null;
|
|
@@ -52,20 +62,20 @@ export function fm01MissingFmeaAttributes(graph) {
|
|
|
52
62
|
// FM-02: Risk-REQ without mitigation
|
|
53
63
|
// ---------------------------------------------------------------------------
|
|
54
64
|
export function fm02MissingMitigation(graph) {
|
|
55
|
-
const riskReqs = graph.elements.filter(e => e
|
|
65
|
+
const riskReqs = graph.elements.filter(e => hasRole(e, 'risk'));
|
|
56
66
|
return riskReqs
|
|
57
67
|
.filter(riskReq => {
|
|
58
|
-
// Check: compose trace from riskReq to a REQ with
|
|
68
|
+
// Check: compose trace from riskReq to a REQ with role=mitigation
|
|
59
69
|
return !graph.traces.some(t => t.source === riskReq.id &&
|
|
60
70
|
t.type === 'compose' &&
|
|
61
|
-
graph.elements.some(el => el.id === t.target && el
|
|
71
|
+
graph.elements.some(el => el.id === t.target && hasRole(el, 'mitigation')));
|
|
62
72
|
})
|
|
63
73
|
.map(e => ({
|
|
64
74
|
rule_id: 'FM-02',
|
|
65
75
|
severity: 'warning',
|
|
66
76
|
element_id: e.id,
|
|
67
|
-
message: `${e.id} is a risk REQ without a mitigation REQ (compose→REQ[mitigation])`,
|
|
68
|
-
fix_hint: 'Create a mitigation
|
|
77
|
+
message: `${e.id} is a risk REQ without a mitigation REQ (compose→REQ[role=mitigation])`,
|
|
78
|
+
fix_hint: 'Create a REQ with attributes.role="mitigation" (kinds functional or non-functional) and link it via compose trace',
|
|
69
79
|
}));
|
|
70
80
|
}
|
|
71
81
|
// ---------------------------------------------------------------------------
|
|
@@ -80,7 +90,7 @@ export function fm03HighRiskUnverified(graph, policy) {
|
|
|
80
90
|
const threshold = policy.riskRpn;
|
|
81
91
|
if (threshold === null)
|
|
82
92
|
return [];
|
|
83
|
-
const riskReqs = graph.elements.filter(e => e
|
|
93
|
+
const riskReqs = graph.elements.filter(e => hasRole(e, 'risk'));
|
|
84
94
|
return riskReqs
|
|
85
95
|
.filter(riskReq => {
|
|
86
96
|
const a = riskReq.attributes ?? {};
|