@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.
@@ -2,6 +2,7 @@
2
2
  * CR-121 P3: Architecture Metrics MT-01..MT-03.
3
3
  * Module-level metrics computed from graph structure.
4
4
  */
5
+ import { z } from 'zod/v4';
5
6
  import type { OntologyGraph } from './ontology.js';
6
7
  import type { RuleViolation } from './rules.js';
7
8
  import type { MetricPolicy } from './policy.js';
@@ -82,66 +83,24 @@ export interface AllocationCohesion {
82
83
  * is deferred (CR-SM-223).
83
84
  */
84
85
  /** Per-module architecture metrics — the numbers behind MT-01/MT-02 (CR-SM-232). */
85
- export interface ModuleMetrics {
86
- moduleId: string;
87
- moduleName: string;
88
- /** FUNCs allocated to this module (`FUNC —allocate→ MOD`). */
89
- allocatedFuncs: number;
90
- /** MT-01 core: coupling. `instability = fanOut / (fanIn + fanOut)`. */
91
- fanIn: number;
92
- fanOut: number;
93
- /** null when fanIn + fanOut === 0 — no signal, no substitute value. */
94
- instability: number | null;
95
- /**
96
- * MT-02 core: connected-component count ueber die BOXEN dieser Modul-Whitebox (CR-SM-326).
97
- * `null`, wo nichts zu messen ist: unter zwei Boxen, oder wenn keine einzige Box einen
98
- * Vertrag traegt — dann meldet R-31 die unverdrahteten Funktionen am Element, und eine Zahl
99
- * hier waere keine Aussage ueber Kohaesion, sondern dieselbe Aussage ein zweites Mal.
100
- */
101
- lcom4: number | null;
102
- /**
103
- * CR-SM-326: die Zahl der BOXEN, ueber die `lcom4` gerechnet wurde — Sub-MODs
104
- * (`MOD -compose-> MOD`) plus die direkt allozierten FUNC. Sie ist der Nenner der MT-02-Meldung
105
- * und NICHT `allocatedFuncs`: ein Eltern-MOD hat Boxen ohne eine einzige eigene Allokation.
106
- */
107
- lcom4Boxes: number;
108
- /**
109
- * CR-SM-263: JEDE allozierte FUNC ist ein zerlegter Block (Rollup), das MOD also ein
110
- * Container statt eines Implementierungsmoduls.
111
- * `false` unterhalb von 2 allozierten FUNCs (kein Container, nur zu klein zum Messen).
112
- *
113
- * CR-SM-326: seither eine reine MESSUNG. Das Urteil haengt nicht mehr daran — eine Box erbt
114
- * die Vertraege ihres Teilbaums, also ist LCOM4 an einem Container genauso aussagekraeftig
115
- * wie an einem Implementierungsmodul. Der Wert steht hier, weil er die Lesart der Zeile
116
- * erklaert (Bloecke statt Blaetter), nicht weil er etwas stilllegt.
117
- */
118
- rollupContainer: boolean;
119
- /** The CR-SM-223 measurement, deliberately threshold-free. null where contracts omits it. */
120
- cohesion: {
121
- internal: number;
122
- external: number;
123
- ratio: number;
124
- } | null;
125
- /**
126
- * CR-SM-301 — Martins *Stable Dependencies Principle*, als MESSUNG und ausdruecklich als
127
- * KEINE Regel: die Zahl der Vertraege, die dieses Modul von einem WENIGER stabilen Lieferanten
128
- * bezieht (`I_Lieferant > I_Bezieher`). Abhaengigkeiten „bergauf".
129
- *
130
- * Warum keine Regel: CR-SM-298 hat genau das versucht und ist an Gate 7 gescheitert. Ueber
131
- * 19 Familiengraphen laufen 25 von 250 Abhaengigkeiten bergauf, aber der MEDIAN der
132
- * Stabilitaetsluecke ist **0.10** — bei einer Vertragsmasse von im Mittel 5 ist das ein
133
- * Vertrag Unterschied, also Rauschen. Filtert man auf Luecke > 0.2 und beide Enden >= 3
134
- * Vertraege, bleiben 3 von 25, davon zwei in einem Wegwerf-Trial und einer Demo. Eine Regel
135
- * haette zwei erfundene Schwellen gebraucht, um EINEN Befund zu produzieren.
136
- *
137
- * Was bleibt, ist die Zahl. `graph_metrics` traegt sie in jeder Modulzeile, ein Mensch liest
138
- * sie neben `instability` und deutet sie — Praezedenz MT-03 (CR-SM-223) und `cohesion` eine
139
- * Zeile weiter oben: messen, nicht urteilen, ohne Katalogeintrag und ohne Nenner-Anteil.
140
- *
141
- * `null`, wo `instability` es auch ist — ohne Kopplung gibt es keine Richtung.
142
- */
143
- uphillDependencies: number | null;
144
- }
86
+ export declare const ModuleMetrics: z.ZodObject<{
87
+ moduleId: z.ZodString;
88
+ moduleName: z.ZodString;
89
+ allocatedFuncs: z.ZodNumber;
90
+ fanIn: z.ZodNumber;
91
+ fanOut: z.ZodNumber;
92
+ instability: z.ZodNullable<z.ZodNumber>;
93
+ lcom4: z.ZodNullable<z.ZodNumber>;
94
+ lcom4Boxes: z.ZodNumber;
95
+ rollupContainer: z.ZodBoolean;
96
+ cohesion: z.ZodNullable<z.ZodObject<{
97
+ internal: z.ZodNumber;
98
+ external: z.ZodNumber;
99
+ ratio: z.ZodNumber;
100
+ }, z.core.$strip>>;
101
+ uphillDependencies: z.ZodNullable<z.ZodNumber>;
102
+ }, z.core.$strip>;
103
+ export type ModuleMetrics = z.infer<typeof ModuleMetrics>;
145
104
  /**
146
105
  * Per-module architecture metrics as NUMBERS — one row per MOD, threshold or not
147
106
  * (CR-SM-232).
@@ -166,7 +125,7 @@ export declare const MT_RULES: readonly [{
166
125
  }, {
167
126
  readonly id: "MT-02";
168
127
  readonly name: "Module cohesion (LCOM4)";
169
- readonly severity: "info";
128
+ readonly severity: "warning";
170
129
  readonly evaluate: typeof mt02Lcom4;
171
130
  readonly domain: readonly ["MOD"];
172
131
  }, {
@@ -1,3 +1,8 @@
1
+ /**
2
+ * CR-121 P3: Architecture Metrics MT-01..MT-03.
3
+ * Module-level metrics computed from graph structure.
4
+ */
5
+ import { z } from 'zod/v4';
1
6
  import { decomposedFuncs } from './rules.js';
2
7
  import { indexOf } from './graph-index.js';
3
8
  import { moduleCrossings, subtreeFuncs, funcSubtree, boxContracts } from './module-crossings.js';
@@ -185,6 +190,87 @@ function connectionPairs(graph) {
185
190
  }
186
191
  return pairs;
187
192
  }
193
+ /**
194
+ * Allocation cohesion — a **measurement, not a rule** (CR-SM-223, decision 2026-07-29).
195
+ *
196
+ * It used to be MT-03 with an 80 % threshold, and it fired on nearly every module of
197
+ * every real graph: 6 of 7 on graphcode, 4 of 4 on graph-view-edit, 10 of 11 on the
198
+ * family graph. `CR-SM-221` first suspected the edge definition and made it
199
+ * FLOW-transitive — the hit rate did not move. The threshold was the miscalibration:
200
+ * in a flow-routed layered architecture, module interaction crosses boundaries by
201
+ * design, so "80 % of interaction is internal" describes a monolith, not a healthy
202
+ * module.
203
+ *
204
+ * Rather than fit a cut-off to 11 data points, this reports the number and lets the
205
+ * architect judge. Returned worst-first, so the head of the list is where to look.
206
+ * Modules with fewer than two allocated FUNCs, or with no external connection at all,
207
+ * carry no signal and are omitted.
208
+ *
209
+ * Deliberately NOT a `RuleDefinition`: `computeReadiness` counts every violation into
210
+ * its dimension score regardless of severity, so a per-module advisory would depress
211
+ * the `alloc` score permanently. A measurement must not masquerade as a defect.
212
+ *
213
+ * Validation of this metric — and of MT-01/MT-02, which are thresholded the same way —
214
+ * is deferred (CR-SM-223).
215
+ */
216
+ /** Per-module architecture metrics — the numbers behind MT-01/MT-02 (CR-SM-232). */
217
+ // CR-SM-361: ein Zod-Vertrag, kein Typ — gemessen wird einmal je Graph, dort wird geprueft.
218
+ export const ModuleMetrics = z.object({
219
+ moduleId: z.string(),
220
+ moduleName: z.string(),
221
+ /** FUNCs allocated to this module (`FUNC —allocate→ MOD`). */
222
+ allocatedFuncs: z.number(),
223
+ /** MT-01 core: coupling. `instability = fanOut / (fanIn + fanOut)`. */
224
+ fanIn: z.number(),
225
+ fanOut: z.number(),
226
+ /** null when fanIn + fanOut === 0 — no signal, no substitute value. */
227
+ instability: z.number().nullable(),
228
+ /**
229
+ * MT-02 core: connected-component count ueber die BOXEN dieser Modul-Whitebox (CR-SM-326).
230
+ * `null`, wo nichts zu messen ist: unter zwei Boxen, oder wenn keine einzige Box einen
231
+ * Vertrag traegt — dann meldet R-31 die unverdrahteten Funktionen am Element, und eine Zahl
232
+ * hier waere keine Aussage ueber Kohaesion, sondern dieselbe Aussage ein zweites Mal.
233
+ */
234
+ lcom4: z.number().nullable(),
235
+ /**
236
+ * CR-SM-326: die Zahl der BOXEN, ueber die `lcom4` gerechnet wurde — Sub-MODs
237
+ * (`MOD -compose-> MOD`) plus die direkt allozierten FUNC. Sie ist der Nenner der MT-02-Meldung
238
+ * und NICHT `allocatedFuncs`: ein Eltern-MOD hat Boxen ohne eine einzige eigene Allokation.
239
+ */
240
+ lcom4Boxes: z.number(),
241
+ /**
242
+ * CR-SM-263: JEDE allozierte FUNC ist ein zerlegter Block (Rollup), das MOD also ein
243
+ * Container statt eines Implementierungsmoduls.
244
+ * `false` unterhalb von 2 allozierten FUNCs (kein Container, nur zu klein zum Messen).
245
+ *
246
+ * CR-SM-326: seither eine reine MESSUNG. Das Urteil haengt nicht mehr daran — eine Box erbt
247
+ * die Vertraege ihres Teilbaums, also ist LCOM4 an einem Container genauso aussagekraeftig
248
+ * wie an einem Implementierungsmodul. Der Wert steht hier, weil er die Lesart der Zeile
249
+ * erklaert (Bloecke statt Blaetter), nicht weil er etwas stilllegt.
250
+ */
251
+ rollupContainer: z.boolean(),
252
+ /** The CR-SM-223 measurement, deliberately threshold-free. null where contracts omits it. */
253
+ cohesion: z.object({ internal: z.number(), external: z.number(), ratio: z.number() }).nullable(),
254
+ /**
255
+ * CR-SM-301 — Martins *Stable Dependencies Principle*, als MESSUNG und ausdruecklich als
256
+ * KEINE Regel: die Zahl der Vertraege, die dieses Modul von einem WENIGER stabilen Lieferanten
257
+ * bezieht (`I_Lieferant > I_Bezieher`). Abhaengigkeiten „bergauf".
258
+ *
259
+ * Warum keine Regel: CR-SM-298 hat genau das versucht und ist an Gate 7 gescheitert. Ueber
260
+ * 19 Familiengraphen laufen 25 von 250 Abhaengigkeiten bergauf, aber der MEDIAN der
261
+ * Stabilitaetsluecke ist **0.10** — bei einer Vertragsmasse von im Mittel 5 ist das ein
262
+ * Vertrag Unterschied, also Rauschen. Filtert man auf Luecke > 0.2 und beide Enden >= 3
263
+ * Vertraege, bleiben 3 von 25, davon zwei in einem Wegwerf-Trial und einer Demo. Eine Regel
264
+ * haette zwei erfundene Schwellen gebraucht, um EINEN Befund zu produzieren.
265
+ *
266
+ * Was bleibt, ist die Zahl. `graph_metrics` traegt sie in jeder Modulzeile, ein Mensch liest
267
+ * sie neben `instability` und deutet sie — Praezedenz MT-03 (CR-SM-223) und `cohesion` eine
268
+ * Zeile weiter oben: messen, nicht urteilen, ohne Katalogeintrag und ohne Nenner-Anteil.
269
+ *
270
+ * `null`, wo `instability` es auch ist — ohne Kopplung gibt es keine Richtung.
271
+ */
272
+ uphillDependencies: z.number().nullable(),
273
+ });
188
274
  /**
189
275
  * The one per-module computation (CR-SM-232) — in GRAPH ORDER, unsorted.
190
276
  *
@@ -312,6 +398,7 @@ function measureModulesUncached(graph) {
312
398
  }
313
399
  row.uphillDependencies = uphill;
314
400
  }
401
+ z.array(ModuleMetrics).parse(rows); // CR-SM-361: der Erzeuger prueft seinen Vertrag (einmal je Graph, gecacht)
315
402
  return rows;
316
403
  }
317
404
  /**
@@ -451,7 +538,9 @@ export function allocationCohesion(graph) {
451
538
  }
452
539
  export const MT_RULES = [
453
540
  { id: 'MT-01', name: 'Module instability', severity: 'warning', evaluate: mt01Instability, domain: ['MOD'] },
454
- { id: 'MT-02', name: 'Module cohesion (LCOM4)', severity: 'info', evaluate: mt02Lcom4, domain: ['MOD'] },
541
+ // CR-SM-357: warning — MT-02 ist Steuerregel (STEER_RULES), und eine Steuerregel darf nicht info sein.
542
+ // Die info-Stufe unterhalb der Schwelle bleibt als Vorwarnung (Masse 0), das Urteil der Regel ist warning.
543
+ { id: 'MT-02', name: 'Module cohesion (LCOM4)', severity: 'warning', evaluate: mt02Lcom4, domain: ['MOD'] },
455
544
  // CR-SM-327: dieselbe Masszahl an der FUNC-Whitebox. Eigene ID, weil die Grundgesamtheit
456
545
  // die Readiness-Dimension bestimmt (Begruendung an `mt04WhiteboxLcom4`).
457
546
  { id: 'MT-04', name: 'Whitebox cohesion (LCOM4)', severity: 'info', evaluate: mt04WhiteboxLcom4, domain: ['FUNC'] },
@@ -17,20 +17,20 @@
17
17
  */
18
18
  import type { OntologyGraph } from './ontology.js';
19
19
  import type { RuleViolation } from './rules.js';
20
- /** ND-01: FUNC pairs with similarity >= 0.85 -> error. */
20
+ /** ND-01: FUNC pairs with similarity >= 0.85 -> warning. */
21
21
  export declare function nd01FuncNearDuplicate(graph: OntologyGraph): RuleViolation[];
22
- /** ND-02: SCHEMA pairs with similarity >= 0.85 -> error. */
22
+ /** ND-02: SCHEMA pairs with similarity >= 0.85 -> warning. */
23
23
  export declare function nd02SchemaNearDuplicate(graph: OntologyGraph): RuleViolation[];
24
24
  export declare const ND_RULES: readonly [{
25
25
  readonly id: "ND-01";
26
26
  readonly name: "FuncNearDuplicate";
27
- readonly severity: "error";
27
+ readonly severity: "warning";
28
28
  readonly evaluate: typeof nd01FuncNearDuplicate;
29
29
  readonly domain: readonly ["FUNC"];
30
30
  }, {
31
31
  readonly id: "ND-02";
32
32
  readonly name: "SchemaNearDuplicate";
33
- readonly severity: "error";
33
+ readonly severity: "warning";
34
34
  readonly evaluate: typeof nd02SchemaNearDuplicate;
35
35
  readonly domain: readonly ["SCHEMA"];
36
36
  }];
@@ -9,7 +9,7 @@ function nearDuplicates(graph, sim, ruleId, what, fixHint) {
9
9
  const pct = Math.round(similarity * 100);
10
10
  return {
11
11
  rule_id: ruleId,
12
- severity: 'error',
12
+ severity: 'warning',
13
13
  element_id: b,
14
14
  message: `${b}${elB?.name ? ' (' + elB.name + ')' : ''} is ${pct}% similar to ${a}${elA?.name ? ' (' + elA.name + ')' : ''} — potential duplicate ${what}`,
15
15
  fix_hint: fixHint,
@@ -17,17 +17,17 @@ function nearDuplicates(graph, sim, ruleId, what, fixHint) {
17
17
  };
18
18
  });
19
19
  }
20
- /** ND-01: FUNC pairs with similarity >= 0.85 -> error. */
20
+ /** ND-01: FUNC pairs with similarity >= 0.85 -> warning. */
21
21
  export function nd01FuncNearDuplicate(graph) {
22
22
  return nearDuplicates(graph, funcSimilarity(graph), 'ND-01', 'function', 'Merge into single FUNC or differentiate responsibilities');
23
23
  }
24
- /** ND-02: SCHEMA pairs with similarity >= 0.85 -> error. */
24
+ /** ND-02: SCHEMA pairs with similarity >= 0.85 -> warning. */
25
25
  export function nd02SchemaNearDuplicate(graph) {
26
26
  return nearDuplicates(graph, schemaSimilarity(graph), 'ND-02', 'schema', 'Merge schemas or differentiate field sets');
27
27
  }
28
28
  export const ND_RULES = [
29
- { id: 'ND-01', name: 'FuncNearDuplicate', severity: 'error', evaluate: nd01FuncNearDuplicate, domain: ['FUNC'] },
30
- { id: 'ND-02', name: 'SchemaNearDuplicate', severity: 'error', evaluate: nd02SchemaNearDuplicate, domain: ['SCHEMA'] },
29
+ { id: 'ND-01', name: 'FuncNearDuplicate', severity: 'warning', evaluate: nd01FuncNearDuplicate, domain: ['FUNC'] },
30
+ { id: 'ND-02', name: 'SchemaNearDuplicate', severity: 'warning', evaluate: nd02SchemaNearDuplicate, domain: ['SCHEMA'] },
31
31
  ];
32
32
  /** Evaluate all near-duplicate rules. */
33
33
  export function evaluateNDRules(graph) {
@@ -196,6 +196,32 @@ export declare const RealRefSchema: z.ZodObject<{
196
196
  lang: z.ZodOptional<z.ZodString>;
197
197
  }, z.core.$strip>;
198
198
  export type RealRef = z.infer<typeof RealRefSchema>;
199
+ /**
200
+ * Was ein Attributvertrag an einem Element aussagt — dreiwertig (CR-SM-360).
201
+ *
202
+ * `absent` : das Attribut fehlt (auch als `null`-Grabstein — Attribute sind nur mergebar).
203
+ * `invalid` : es ist da, aber verletzt den Vertrag; `error` sagt wie.
204
+ * `bound` : es ist da und gueltig.
205
+ *
206
+ * Bis hierher parste jeder Leser selbst (25 Stellen in contracts und graphcode), und die Leser
207
+ * wichen ab: einer meldete ungueltige testRefs, die anderen behandelten „ungueltig" still wie
208
+ * „fehlt" — die Klasse aus CR-GC-338. Ein Aufrufer, der beides gleich behandeln will, tut das jetzt
209
+ * sichtbar (`state !== 'bound'`), statt dass der Unterschied beim Parsen verloren geht.
210
+ */
211
+ export type BindingRead<T> = {
212
+ state: 'absent';
213
+ } | {
214
+ state: 'invalid';
215
+ error: string;
216
+ } | {
217
+ state: 'bound';
218
+ value: T;
219
+ };
220
+ type Attributes = Record<string, unknown> | undefined;
221
+ /** Der EINE Leser von `attributes.testRefs` (CR-SM-360). */
222
+ export declare function readTestRefs(attributes: Attributes): BindingRead<TestRefs>;
223
+ /** Der EINE Leser von `attributes.realRef` (CR-SM-360). */
224
+ export declare function readRealRef(attributes: Attributes): BindingRead<RealRef>;
199
225
  /**
200
226
  * The five judgment-work artifact ids (CR-GC-221's creation keys) that graphcode's
201
227
  * skills produce and that a phase gate checks for presence+freshness (CR-SM-227).
@@ -222,6 +248,7 @@ export type AnalysisArtifactId = z.infer<typeof AnalysisArtifactId>;
222
248
  */
223
249
  export declare const AnalysisFreshnessStampSchema: z.ZodObject<{
224
250
  graphVersion: z.ZodNumber;
251
+ crRefs: z.ZodOptional<z.ZodArray<z.ZodString>>;
225
252
  }, z.core.$strip>;
226
253
  export type AnalysisFreshnessStamp = z.infer<typeof AnalysisFreshnessStampSchema>;
227
254
  /**
@@ -408,3 +435,4 @@ export interface AttributeSpec {
408
435
  * the prescribed keys that rules, views, and extractors rely on.
409
436
  */
410
437
  export declare const ELEMENT_ATTRIBUTES: Partial<Record<ElementType, AttributeSpec[]>>;
438
+ export {};
@@ -167,6 +167,22 @@ export const RealRefSchema = z.object({
167
167
  symbol: z.string().optional(),
168
168
  lang: z.string().optional(),
169
169
  });
170
+ /** Der EINE Leser von `attributes.testRefs` (CR-SM-360). */
171
+ export function readTestRefs(attributes) {
172
+ const raw = attributes?.testRefs;
173
+ if (raw === undefined || raw === null)
174
+ return { state: 'absent' };
175
+ const r = TestRefsSchema.safeParse(raw);
176
+ return r.success ? { state: 'bound', value: r.data } : { state: 'invalid', error: r.error.message };
177
+ }
178
+ /** Der EINE Leser von `attributes.realRef` (CR-SM-360). */
179
+ export function readRealRef(attributes) {
180
+ const raw = attributes?.realRef;
181
+ if (raw === undefined || raw === null)
182
+ return { state: 'absent' };
183
+ const r = RealRefSchema.safeParse(raw);
184
+ return r.success ? { state: 'bound', value: r.data } : { state: 'invalid', error: r.error.message };
185
+ }
170
186
  /**
171
187
  * The five judgment-work artifact ids (CR-GC-221's creation keys) that graphcode's
172
188
  * skills produce and that a phase gate checks for presence+freshness (CR-SM-227).
@@ -186,6 +202,13 @@ export const AnalysisArtifactId = z.enum(['conops', 'fmea', 'trade', 'implplan',
186
202
  */
187
203
  export const AnalysisFreshnessStampSchema = z.object({
188
204
  graphVersion: z.number().int().nonnegative(),
205
+ /**
206
+ * CR-SM-355: die CRs, die aus dem Task hervorgegangen sind (Trade: die Entscheidungs-CRs,
207
+ * Assumption Review: die promovierten Annahmen). Der Verweis liegt dort, wo der Task endet,
208
+ * atomar mit dem Stempel; TR-01 / IR-01 pruefen ihn. Optional, weil conops/fmea/implplan keine
209
+ * Massnahmen-CRs erzeugen.
210
+ */
211
+ crRefs: z.array(z.string()).optional(),
189
212
  });
190
213
  /**
191
214
  * Eine benannte Abweichung (CR-SM-349): ein Fund, den der Mensch bewusst offen laesst.
@@ -151,7 +151,7 @@ export const RULE_TO_DIMENSION = {
151
151
  // req
152
152
  'BQ-01': 'req', 'BQ-02': 'req', 'BQ-04': 'req',
153
153
  'BQ-06': 'req', 'BQ-07': 'req',
154
- 'RD-01': 'req', 'RD-02': 'req', 'RD-03': 'req',
154
+ 'RD-01': 'req', 'RD-02': 'req',
155
155
  // RD-04 is decomposition *breadth* — an architecture concern, not a requirement one
156
156
  'RD-04': 'arch',
157
157
  // CR-SM-311: die Untergrenze derselben Breite — ebenfalls Architektur
@@ -168,7 +168,6 @@ export const RULE_TO_DIMENSION = {
168
168
  'R-30': 'arch', 'R-31': 'arch',
169
169
  // uc
170
170
  'UC-01': 'uc', 'UC-02': 'uc', 'UC-03': 'uc', 'UC-04': 'uc',
171
- 'UC-05': 'uc', 'UC-06': 'uc',
172
171
  'R-15': 'uc', 'R-16': 'uc', 'R-17': 'uc',
173
172
  // FC-04 (CR-SM-226): FCHAIN actor-bounded (trigger+consumer) — same dimension as FC-01..03.
174
173
  'FC-02': 'uc', 'FC-03': 'uc', 'FC-04': 'uc',
@@ -206,7 +205,9 @@ export const RULE_TO_DIMENSION = {
206
205
  'FM-01': 'req', 'FM-02': 'req', 'FM-03': 'ver',
207
206
  // CR-SM-227: analysis-artifact freshness presence — closest topical fit per artifact.
208
207
  'AF-01': 'req', 'AF-03': 'req', // ConOps/Assumption-Review: requirements-adjacent judgment work
208
+ 'IR-01': 'req', // CR-SM-355: Annahmen-Review promoviert zu CR — Task-Ausgang von irr
209
209
  'AF-02': 'arch', // Trade Study: architecture decision record
210
+ 'TR-01': 'arch', // CR-SM-355: Trade-Entscheidung als CR — Task-Ausgang von trade
210
211
  'AF-04': 'ver', // FMEA: risk/verification, same bucket as FM-03
211
212
  'AF-05': 'ms', // Implementation Plan: milestone/planning
212
213
  // NFR budget
@@ -237,16 +238,19 @@ export const PhaseGate = z.enum(['SRR', 'PDR', 'CDR', 'TRR']);
237
238
  * single-owner convention as RULE_TO_DIMENSION.
238
239
  */
239
240
  export const RULE_TO_PHASE = {
240
- // CR-SM-227: Analysis-Freshness-Legs (AF-01..05) — Familie-Review 2026-08-05
241
- // gate assignment, final: ConOps/Trade/Assumption-Review → PDR, FMEA → CDR,
242
- // Implementation Plan → TRR. SRR gets no analysis-freshness leg.
243
- 'AF-01': 'PDR', 'AF-02': 'PDR', 'AF-03': 'PDR', // ConOps, Trade Study, Assumption Review
241
+ // CR-SM-227: Analysis-Freshness-Legs (AF-01..05) — Familie-Review 2026-08-05; revidiert durch
242
+ // CR-SM-354 (2026-09-22) gegen NASA NPR 7123.1D Appendix G: das Operations Concept ist ein
243
+ // SRR-Eintrittskriterium (AF-01 → SRR), Integrationsplan und Master Schedule werden zur PDR
244
+ // baselined (AF-05 → PDR). Trade / Assumption Review PDR und FMEA CDR bleiben.
245
+ 'AF-01': 'SRR', // ConOps
246
+ 'AF-02': 'PDR', 'AF-03': 'PDR', // Trade Study, Assumption Review
244
247
  'AF-04': 'CDR', // FMEA
245
- 'AF-05': 'TRR', // Implementation Plan
248
+ 'AF-05': 'PDR', // Implementation Plan
249
+ 'TR-01': 'PDR', 'IR-01': 'PDR', // CR-SM-355: Task-Ausgaenge von trade / irr — dasselbe Gate wie ihr Eintritt
246
250
  // SRR — requirements/scope clarity, system+UC boundary definition.
247
251
  'BQ-01': 'SRR', 'BQ-02': 'SRR', 'BQ-04': 'SRR', 'BQ-06': 'SRR', 'BQ-07': 'SRR',
248
- 'RD-01': 'SRR', 'RD-02': 'SRR', 'RD-03': 'SRR',
249
- 'UC-01': 'SRR', 'UC-02': 'SRR', 'UC-03': 'SRR', 'UC-04': 'SRR', 'UC-05': 'SRR', 'UC-06': 'SRR',
252
+ 'RD-01': 'SRR', 'RD-02': 'SRR',
253
+ 'UC-01': 'SRR', 'UC-02': 'SRR', 'UC-03': 'SRR', 'UC-04': 'SRR',
250
254
  'R-16': 'SRR', 'R-17': 'SRR',
251
255
  'FC-02': 'SRR',
252
256
  'CL-01': 'SRR',
@@ -266,11 +270,12 @@ export const RULE_TO_PHASE = {
266
270
  'ND-01': 'PDR',
267
271
  'BW-02': 'PDR', 'CR-01': 'PDR', 'IO-01': 'PDR', 'IO-02': 'PDR', // CR-SM-226: extended to all FCHAIN FUNC-pairs, added to the phase axis.
268
272
  // CDR — critical design/schema completeness.
269
- 'R-26': 'CDR', 'SC-02': 'CDR', // SC-04 entfiel mit CR-SM-271 — der FLOW-ohne-SCHEMA-Fall haengt an R-18/PDR
273
+ 'SC-02': 'CDR', // SC-04 entfiel mit CR-SM-271 — der FLOW-ohne-SCHEMA-Fall haengt an R-18/PDR
270
274
  'ND-02': 'CDR',
271
275
  'NFR-01': 'CDR',
272
276
  // TRR — test readiness.
273
277
  'R-01': 'TRR', 'R-05': 'TRR', 'R-32': 'TRR', 'R-19': 'TRR', 'R-20': 'TRR', 'R-21': 'TRR',
278
+ 'R-26': 'TRR', // CR-SM-354: dieselbe Aussage wie R-20 (Praesenz von Code) — as-built ist TRR, nicht CDR
274
279
  // CR-SM-231: eine doppelt beanspruchte Testdatei laesst den TRR-Gate dieselbe Evidenz
275
280
  // doppelt zaehlen — der Befund gehoert genau an dieses Gate.
276
281
  'R-29': 'TRR',
@@ -294,13 +299,13 @@ export const RULE_TASKS = ['kern', 'conops', 'trade', 'irr', 'fmea', 'plan', 'an
294
299
  /** Die Regeln je Task (ohne Kern). Jede Regel hoechstens einmal — der Test haelt das fest. */
295
300
  export const TASK_OWNED_RULES = {
296
301
  conops: ['CL-01'],
297
- trade: [],
298
- irr: [],
302
+ trade: ['TR-01'], // CR-SM-355: Trade-Entscheidung als CR
303
+ irr: ['IR-01'], // CR-SM-355: Annahmen-Review promoviert zu CR
299
304
  fmea: ['FM-01', 'FM-02', 'FM-03'],
300
305
  plan: ['MS-01', 'MS-02', 'MS-03', 'CR-R01', 'CR-R02', 'CR-R03'],
301
306
  // Annahme (CR-SM-350): BQ bekommt einen eigenen Task, vorerst ohne automatischen Eintrittspunkt.
302
307
  anforderungsqualitaet: ['BQ-01', 'BQ-02', 'BQ-04', 'BQ-06', 'BQ-07'],
303
- realisierung: ['R-19', 'R-20', 'R-26', 'R-27', 'R-29', 'R-32', 'VR-01', 'RC-01', 'RC-02', 'RC-03', 'RC-04', 'RC-05', 'RC-06', 'RC-07'],
308
+ realisierung: ['R-19', 'R-20', 'R-26', 'R-29', 'R-32', 'VR-01', 'RC-01', 'RC-02', 'RC-03', 'RC-04', 'RC-05', 'RC-06', 'RC-07', 'RC-08', 'RC-09'],
304
309
  };
305
310
  /**
306
311
  * Der Eintrittspunkt eines Tasks im Kern: die Regel, die "dieses Artefakt fehlt" sagt. `null` = der
@@ -300,7 +300,15 @@ export const RULE_HELP = {
300
300
  },
301
301
  'RC-07': {
302
302
  plain: "A change request is marked done in the model but still open in the folder (or the other way round), or an open change request has no entry in the model → the folder decides; fix the model.",
303
- se: "`CR` node whose `status` (closed = done/dropped/rejected) contradicts `docs/cr/open|done/`, or an OPEN CR file with no `CR` node (anchored at the first `SYS`). Nodes without a file are not reported — history. Absent `crFiles` is SILENCE (CR-SM-329).",
303
+ se: "`CR` node whose `status` (closed = done; CR-SM-362) contradicts `docs/cr/open|done/`, or an OPEN CR file with no `CR` node (anchored at the first `SYS`). Nodes without a file are not reported — history. Absent `crFiles` is SILENCE (CR-SM-329).",
304
+ },
305
+ 'RC-08': {
306
+ plain: "A data contract in the model points at a type, which vanishes at runtime → write it as a Zod schema, so whoever produces the data can prove it and whoever reads it can check it.",
307
+ se: "`SCHEMA` whose `realRef` symbol is declared but is not a Zod schema (extractor field `zodSymbols`). Every producer needs a checkable schema — \"nobody parses it\" only means the reader is not known yet. Absent `zodSymbols` is SILENCE. Owns the type-binding cause: RC-04 stays quiet on such a SCHEMA (CR-SM-358).",
308
+ },
309
+ 'RC-09': {
310
+ plain: "Code somewhere reads a data contract on its own, but the model does not know that place → go through the one translator the model names, or add the new translator to the model.",
311
+ se: "Zod-bound `SCHEMA` parsed (import + `.parse`/`.safeParse`) in a file that is neither its definition nor the `realRef` of a `FUNC` io-connected to a `FLOW` of this SCHEMA. The first code → model check: a second translator of one contract is a parallel path (spike CR-GC-637 — such paths share a contract, they are not similar). One finding per SCHEMA; test files exempt; runs only with `fileScope: 'all'` (CR-SM-358).",
304
312
  },
305
313
  'RD-01': {
306
314
  plain: "A smallest-piece feature has nothing built to fulfil it → add what implements it.",
@@ -311,10 +319,6 @@ export const RULE_HELP = {
311
319
  plain: "You split a feature into smaller features but you're also building the big one directly → build only the small pieces, not both.",
312
320
  se: "Parent `REQ` (has `compose`→`REQ` children) carrying a direct `FUNC` `satisfy`; the satisfy belongs on the children.",
313
321
  },
314
- 'RD-03': {
315
- plain: "You split a feature into pieces, but all the pieces are handled by the same one thing — the split may be pointless → consider merging them.",
316
- se: "Parent `REQ` whose children all share one satisfier.",
317
- },
318
322
  'RD-04': {
319
323
  plain: "One thing has more than 9 parts directly under it → group them, so each level stays readable.",
320
324
  se: "Decomposition breadth above `metricPolicy.decompositionBreadth.warning` children on one level — `FUNC` `compose` `FUNC`, `SYS` `compose` `MOD`, a `MOD`'s sub-`MOD`s plus its own leaf `FUNC` (`allocate`), and the root `FUNC` forest anchored at `SYS` (CR-SM-282). Default 9, the upper end of 7±2 (CR-SM-296). CR-SM-311: a module level counts its own functions again — breadth, not size; module coupling is R-04. The lower bound is RD-05.",
@@ -347,16 +351,19 @@ export const RULE_HELP = {
347
351
  se: "`UC` description shorter than 10 characters or containing TBD/TODO/FIXME/placeholder/XXX. Description IS the goal (CR-150).",
348
352
  prompt: "se:author-uc",
349
353
  },
350
- 'UC-05': {
351
- plain: "A scenario does not say what must be true once it has finished → add that as a requirement.",
352
- se: "`UC` with no `compose`d `REQ` carrying `kinds:[\"postcondition\"]`.",
353
- },
354
- 'UC-06': {
355
- plain: "A scenario does not say what must be true before it can start → add that as a requirement.",
356
- se: "`UC` with no `compose`d `REQ` carrying `kinds:[\"precondition\"]`.",
357
- },
358
354
  'VR-01': {
359
355
  plain: "A test exists but no result was ever recorded, so nobody knows if it passed → record the outcome.",
360
356
  se: "`TEST` with no `testResult` attribute — assumed pending, never assumed green.",
361
357
  },
358
+ // CR-SM-355: Task-Ausgaenge — sichtbar nur im Task trade / irr.
359
+ 'TR-01': {
360
+ plain: "The trade study is stamped, but the decision it produced is not on record as a CR (or the CR it names is missing or carries no decides edge) → record the decision as a CR and list its id in the stamp.",
361
+ se: "Task outcome rule (CR-SM-355): `analysisFreshness.trade.crRefs` names existing CR elements, each carrying a `relation` edge labelled `decides` (the SP-1 decision record).",
362
+ prompt: "se-trade",
363
+ },
364
+ 'IR-01': {
365
+ plain: "The assumption review names a CR that does not exist → fix the reference or add the CR; an empty list is fine when no assumption was load-bearing.",
366
+ se: "Task outcome rule (CR-SM-355): every id in `analysisFreshness.assumption-review.crRefs` resolves to a CR element; empty is a legitimate outcome (low-risk assumptions stay in the record).",
367
+ prompt: "se-irr",
368
+ },
362
369
  };
package/dist/se/rules.js CHANGED
@@ -4,7 +4,7 @@
4
4
  * @sigloch/contracts/se — single source of truth for SE validation rules.
5
5
  */
6
6
  import { z } from 'zod/v4';
7
- import { ElementType, TraceType, TestRefsSchema, RealRefSchema } from './ontology.js';
7
+ import { ElementType, TraceType, readTestRefs, readRealRef } from './ontology.js';
8
8
  import { traceRejection, admittingAlternatives, isValidTrace, BOUNDED_PATTERNS, REQUIRED_PATTERNS, maxOccurs } from './meta-model.js';
9
9
  import { indexOf } from './graph-index.js';
10
10
  import { moduleCrossings, subtreeFuncs } from './module-crossings.js';
@@ -663,35 +663,6 @@ function decompositionConsistency(graph) {
663
663
  context: { element_type: req.type, element_name: req.name },
664
664
  }));
665
665
  }
666
- // ---------------------------------------------------------------------------
667
- // RD-03: No premature decomposition (all children same satisfy target)
668
- // ---------------------------------------------------------------------------
669
- function noPrematureDecomposition(graph) {
670
- const idx = indexOf(graph);
671
- const reqs = idx.elementsOfType('REQ');
672
- const parentReqs = reqs.filter(req => idx.out(req.id, 'compose').some(t => idx.typeOf(t.target) === 'REQ'));
673
- return parentReqs
674
- .filter(parent => {
675
- const childIds = graph.traces
676
- .filter(t => t.source === parent.id && t.type === 'compose' &&
677
- idx.typeOf(t.target) === 'REQ')
678
- .map(t => t.target);
679
- if (childIds.length < 2)
680
- return false;
681
- const satisfyTargets = childIds.map(cid => idx.in(cid, 'satisfy').filter(t => t.source !== cid).map(t => t.source));
682
- // All children satisfied by the same single source
683
- const allSame = satisfyTargets.every(targets => targets.length === 1 && targets[0] === satisfyTargets[0]?.[0]);
684
- return allSame && satisfyTargets[0]?.length === 1;
685
- })
686
- .map(req => ({
687
- rule_id: 'RD-03',
688
- severity: 'info',
689
- element_id: req.id,
690
- message: `${req.id} children all have the same satisfy target — decomposition may be unnecessary`,
691
- fix_hint: 'Consider merging sub-REQs back into parent if they share the same verifiable level',
692
- context: { element_type: req.type, element_name: req.name },
693
- }));
694
- }
695
666
  function breadthCounts(graph) {
696
667
  const idx = indexOf(graph);
697
668
  // CR-SM-264: `typeOf` und `byId` baute diese Regel je Aufruf selbst — der Index hat beide.
@@ -919,7 +890,7 @@ function msDanglingDependency(graph) {
919
890
  .filter(t => !msIds.has(t.target))
920
891
  .map(t => ({
921
892
  rule_id: 'MS-02',
922
- severity: 'error',
893
+ severity: 'warning',
923
894
  element_id: t.source,
924
895
  message: `${t.source} depends on non-existent milestone ${t.target}`,
925
896
  fix_hint: 'Fix the depends-on relation target to reference an existing MS element',
@@ -1162,7 +1133,7 @@ function testMustHaveRunnableBinding(graph) {
1162
1133
  .filter(e => e.attributes?.concept !== true) // concept-only TESTs are exempt
1163
1134
  // CR-SM-231: `testRefs` (Array, min 1) statt `testRef` (Objekt). Semantik unveraendert —
1164
1135
  // Praesenz und Form, keine Datei-Existenz; die pruefst RC-02.
1165
- .filter(e => !TestRefsSchema.safeParse(e.attributes?.testRefs).success)
1136
+ .filter(e => readTestRefs(e.attributes).state !== 'bound')
1166
1137
  .map(test => ({
1167
1138
  rule_id: 'R-19',
1168
1139
  severity: 'warning',
@@ -1194,12 +1165,12 @@ function testFileExclusivity(graph) {
1194
1165
  for (const el of graph.elements) {
1195
1166
  if (el.type !== 'TEST')
1196
1167
  continue;
1197
- const parsed = TestRefsSchema.safeParse(el.attributes?.testRefs);
1198
- if (!parsed.success)
1168
+ const parsed = readTestRefs(el.attributes);
1169
+ if (parsed.state !== 'bound')
1199
1170
  continue; // keine/ungueltige Bindung → R-19-Gebiet
1200
1171
  // Innerhalb EINES Knotens ist dieselbe Datei zweimal kein Konflikt, sondern eine
1201
1172
  // Redundanz — sie wuerde sonst als Selbstkollision gemeldet.
1202
- for (const file of new Set(parsed.data.map(r => r.file))) {
1173
+ for (const file of new Set(parsed.value.map(r => r.file))) {
1203
1174
  const owners = claimedBy.get(file) ?? [];
1204
1175
  owners.push(el.id);
1205
1176
  claimedBy.set(file, owners);
@@ -1244,7 +1215,7 @@ function funcMustHaveCodeBinding(graph) {
1244
1215
  const composeFuncChildren = (id) => graph.traces
1245
1216
  .filter(t => t.source === id && t.type === 'compose' && funcById.has(t.target))
1246
1217
  .map(t => t.target);
1247
- const hasCodeRef = (el) => RealRefSchema.safeParse(el.attributes?.realRef).success;
1218
+ const hasCodeRef = (el) => readRealRef(el.attributes).state === 'bound';
1248
1219
  const isExempt = (el) => el.attributes?.concept === true || el.attributes?.external === true;
1249
1220
  // A FUNC is bound iff it is exempt, has a codeRef, or is a parent whose
1250
1221
  // compose→FUNC children are all bound (recursive, cycle-safe).
@@ -1501,7 +1472,7 @@ function schemaMustHaveSchemaRef(graph) {
1501
1472
  const idx = indexOf(graph);
1502
1473
  return idx.elementsOfType('SCHEMA')
1503
1474
  .filter(e => e.attributes?.concept !== true && e.attributes?.external !== true)
1504
- .filter(e => !RealRefSchema.safeParse(e.attributes?.realRef).success)
1475
+ .filter(e => readRealRef(e.attributes).state !== 'bound')
1505
1476
  .map(sc => ({
1506
1477
  rule_id: 'R-26',
1507
1478
  severity: 'warning',
@@ -1526,7 +1497,7 @@ function schemaMustHaveContractTest(graph) {
1526
1497
  const tests = idx.elementsOfType('TEST');
1527
1498
  return idx.elementsOfType('SCHEMA')
1528
1499
  .filter(sc => sc.attributes?.concept !== true)
1529
- .filter(sc => { const r = RealRefSchema.safeParse(sc.attributes?.realRef); return r.success && r.data.symbol !== undefined; })
1500
+ .filter(sc => { const r = readRealRef(sc.attributes); return r.state === 'bound' && r.value.symbol !== undefined; })
1530
1501
  .filter(sc => !graph.traces.some(t => t.type === 'verify' && t.target === sc.id && typeOf.get(t.source) === 'TEST'))
1531
1502
  .map(sc => ({
1532
1503
  rule_id: 'R-32',
@@ -1686,11 +1657,10 @@ export const V3_RULES = [
1686
1657
  { id: 'R-32', name: 'SCHEMA must have contract TEST', severity: 'warning', evaluate: schemaMustHaveContractTest, domain: ['SCHEMA'] },
1687
1658
  { id: 'RD-01', name: 'Unresolved requirement', severity: 'warning', evaluate: unresolvedRequirement, domain: ['REQ'] },
1688
1659
  { id: 'RD-02', name: 'Decomposition consistency', severity: 'warning', evaluate: decompositionConsistency, domain: ['REQ'] },
1689
- { id: 'RD-03', name: 'No premature decomposition', severity: 'info', evaluate: noPrematureDecomposition, domain: ['REQ'] },
1690
1660
  { id: 'RD-04', name: 'Decomposition breadth', severity: 'warning', evaluate: decompositionBreadth, domain: ['FUNC', 'MOD', 'SYS'] },
1691
1661
  { id: 'RD-05', name: 'Decomposition too narrow', severity: 'warning', evaluate: decompositionNarrow, domain: ['FUNC', 'MOD', 'SYS'] },
1692
1662
  { id: 'MS-01', name: 'Milestone empty scope', severity: 'warning', evaluate: msEmptyScope, domain: ['MS'] },
1693
- { id: 'MS-02', name: 'Milestone dangling dependency', severity: 'error', evaluate: msDanglingDependency, domain: ['MS'] },
1663
+ { id: 'MS-02', name: 'Milestone dangling dependency', severity: 'warning', evaluate: msDanglingDependency, domain: ['MS'] },
1694
1664
  { id: 'R-30', name: 'FUNC leaf must belong to a function chain', severity: 'warning', evaluate: funcMustBeInEffectChain, domain: ['FUNC'] },
1695
1665
  { id: 'R-31', name: 'FUNC must be wired (io input + output)', severity: 'warning', evaluate: funcMustBeWired, domain: ['FUNC'] },
1696
1666
  ];
@@ -8,7 +8,5 @@ export declare function uc01HasRequirements(graph: OntologyGraph): RuleViolation
8
8
  export declare function uc02HasActor(graph: OntologyGraph): RuleViolation[];
9
9
  export declare function uc03HasScenario(graph: OntologyGraph): RuleViolation[];
10
10
  export declare function uc04GoalDefined(graph: OntologyGraph): RuleViolation[];
11
- export declare function uc05HasPostcondition(graph: OntologyGraph): RuleViolation[];
12
- export declare function uc06HasPrecondition(graph: OntologyGraph): RuleViolation[];
13
11
  export declare const UC_RULES: RuleDefinition[];
14
12
  export declare function evaluateUCRules(graph: OntologyGraph, policy: MetricPolicy): RuleViolation[];