@sigloch/contracts 10.12.0 → 10.13.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.
@@ -25,21 +25,25 @@ export const TraceType = z.enum([
25
25
  'compose', 'io', 'satisfy', 'verify', 'allocate', 'relation',
26
26
  ]);
27
27
  /**
28
- * REQ kind — 6 values aligned with SysML 2.0 + FMEA (CR-180).
28
+ * REQ kind — GENAU EIN Wert je REQ, aus zwei (CR-SM-366).
29
29
  *
30
- * CR-SM-266 B: `negative` ENTFAELLT. Ein Verbots-REQ ("das System tut X nie") ist auch eine
31
- * Anforderung — die Implementierung muss Regeln abfragen oder Sicherungen einbauen, und das
32
- * ist `functional`. Der Wert hatte NULL Leser (kein Regel-, Gate- oder View-Konsument; anders
33
- * als risk/mitigation → FM-01..03, pre/postcondition → UC-05/06, non-functional → NFR-01) und
34
- * haette durch die where-Praedikate am satisfy-Pattern per AUSLASSUNG erstmals Wirkung
35
- * bekommen: er stand in keiner der vier Listen und waere damit nur noch per FCHAIN erfuellbar
36
- * gewesen. Die sechs verbleibenden Werte partitionieren die where-Listen VOLLSTAENDIG — kein
37
- * Wert ohne Zuordnung, keine Kante, die durch ein Loch in der Aufzaehlung faellt.
30
+ * `functional` -> erfuellt von FUNC. `non-functional` -> erfuellt von MOD (lokales Budget), SYS
31
+ * (Systemebene) oder FCHAIN (Ende-zu-Ende entlang der Kette). Ohne kinds kein Erfueller (BQ-07,
32
+ * `satisfiesPredicate`). Ein REQ, das beides ist, ist falsch zerlegt -> teilen; die
33
+ * Teilmengen-Semantik der where-Praedikate laesst es von niemandem erfuellen.
34
+ *
35
+ * Entfallen (Major, CR-SM-366):
36
+ * - `precondition` / `postcondition`: die Vorbedingung steckt im Eingangs-FLOW der Kette, die
37
+ * Nachbedingung ist das UC-Ziel — beides prueft R-21 (Integrationstest je Kette). Ihre Leser
38
+ * UC-05/06 sind seit CR-SM-357 gestrichen.
39
+ * - `risk` / `mitigation`: die FMEA-ROLLE einer Anforderung, keine Art — `REQ.attributes.role`
40
+ * (CR-SM-365), satisfy-neutral.
41
+ * Ein Altwert wird nicht still verworfen: `normalizeReqKinds` laesst ihn stehen, das Schema
42
+ * lehnt ihn ab, und R-18 meldet jede satisfy-Kante darauf als `kinds-mismatch` mit `declared`.
43
+ *
44
+ * Vorher CR-SM-266 B: `negative` entfiel (ein Verbots-REQ ist `functional`).
38
45
  */
39
- export const ReqKind = z.enum([
40
- 'functional', 'non-functional', 'risk',
41
- 'mitigation', 'precondition', 'postcondition',
42
- ]);
46
+ export const ReqKind = z.enum(['functional', 'non-functional']);
43
47
  /**
44
48
  * `kinds` in der Form, die WIRKLICH auf Platte liegt — als Liste.
45
49
  *
@@ -183,6 +187,36 @@ export function readRealRef(attributes) {
183
187
  const r = RealRefSchema.safeParse(raw);
184
188
  return r.success ? { state: 'bound', value: r.data } : { state: 'invalid', error: r.error.message };
185
189
  }
190
+ /**
191
+ * CR-SM-364: die Kopplung eines Uebergangs — `FLOW.attributes.sync`. Fehlend heisst UNBEKANNT,
192
+ * nie stillschweigend `sync`: eine Kettenkennzahl (synchrone Tiefe, Engstellen-Grad) weist eine
193
+ * unmarkierte Kette als „nicht bewertbar" aus, statt sie ueber eine Annahme zu beurteilen.
194
+ */
195
+ export const FlowSync = z.enum(['sync', 'async']);
196
+ /** Der EINE Leser von `FLOW.attributes.sync` (CR-SM-364) — ungueltig ist nicht fehlend. */
197
+ export function readFlowSync(attributes) {
198
+ const raw = attributes?.sync;
199
+ if (raw === undefined || raw === null)
200
+ return { state: 'absent' };
201
+ const r = FlowSync.safeParse(raw);
202
+ return r.success ? { state: 'bound', value: r.data } : { state: 'invalid', error: r.error.message };
203
+ }
204
+ /**
205
+ * CR-SM-365: die ROLLE einer Anforderung im FMEA-Sinn — `REQ.attributes.role`. `risk` und
206
+ * `mitigation` waren bis hierher `kinds`-Werte und damit satisfy-relevant (MOD/SYS erfuellten
207
+ * sie). Sie sind aber keine ART der Anforderung, sondern ihre Motivation: eine Gegenmassnahme ist
208
+ * funktional oder nicht-funktional wie jede andere REQ. Die Rolle ist satisfy-neutral — die
209
+ * Grammatik (`isValidTrace`) liest sie nicht; FM-01..03 lesen NUR sie.
210
+ */
211
+ export const ReqRole = z.enum(['risk', 'mitigation']);
212
+ /** Der EINE Leser von `REQ.attributes.role` (CR-SM-365) — ungueltig ist nicht fehlend. */
213
+ export function readReqRole(attributes) {
214
+ const raw = attributes?.role;
215
+ if (raw === undefined || raw === null)
216
+ return { state: 'absent' };
217
+ const r = ReqRole.safeParse(raw);
218
+ return r.success ? { state: 'bound', value: r.data } : { state: 'invalid', error: r.error.message };
219
+ }
186
220
  /**
187
221
  * The five judgment-work artifact ids (CR-GC-221's creation keys) that graphcode's
188
222
  * skills produce and that a phase gate checks for presence+freshness (CR-SM-227).
@@ -263,7 +297,7 @@ export const OntologyElement = z.object({
263
297
  description: z.string(),
264
298
  /** INCOSE TIAD verification method — only for TEST elements (CR-057). */
265
299
  method: VerificationMethod.optional(),
266
- /** REQ kinds: multi-valued classification (CR-180). Only for REQ elements. */
300
+ /** REQ kinds: exactly ONE of functional|non-functional (CR-SM-366); a list for the on-disk form. Only for REQ elements. */
267
301
  kinds: z.array(ReqKind).optional(),
268
302
  attributes: z.record(z.string(), z.unknown()).optional(),
269
303
  status: z.enum(['draft', 'reviewed', 'open', 'done']).default('draft'),
@@ -335,6 +369,7 @@ export const ELEMENT_ATTRIBUTES = {
335
369
  { key: 'concept', type: 'boolean', description: 'Concept-only TEST: no run artifact yet; exempt from the R-19 testRefs-binding requirement (CR-GC-205)' },
336
370
  ],
337
371
  REQ: [
372
+ { key: 'role', type: 'enum', enumValues: ReqRole.options, description: "FMEA role: 'risk' | 'mitigation' — satisfy-neutral, read by FM-01..03 via readReqRole. A mitigation is functional or non-functional like any REQ; the role is its motivation, not its kind (CR-SM-365)" },
338
373
  { key: 'severity', type: 'number', description: 'FMEA severity (1-10)' },
339
374
  { key: 'occurrence', type: 'number', description: 'FMEA occurrence (1-10)' },
340
375
  { key: 'detection', type: 'number', description: 'FMEA detection (1-10)' },
@@ -356,9 +391,10 @@ export const ELEMENT_ATTRIBUTES = {
356
391
  { key: 'rationale', type: 'string', description: 'Reason for the change' },
357
392
  { key: 'spike', type: 'boolean', description: 'CR is a spike/exploration' },
358
393
  ],
394
+ // CR-SM-364: genau ein Realisierungsattribut. `protocol`/`qos` entfielen — freie Strings, in
395
+ // 0 von 186 Familien-FLOWs gesetzt, ohne Leser. Abweichende Kopplung je Verbraucher ⇒ FLOW teilen.
359
396
  FLOW: [
360
- { key: 'protocol', type: 'string', description: 'ICD protocol (e.g. REST, gRPC)' },
361
- { key: 'qos', type: 'string', description: 'Quality of Service level' },
397
+ { key: 'sync', type: 'enum', enumValues: FlowSync.options, description: "Coupling of the transition: 'sync' | 'async'. Missing = unknown, never an implicit 'sync' — read with readFlowSync (CR-SM-364)" },
362
398
  ],
363
399
  MOD: [
364
400
  { key: 'path', type: 'string', description: 'Source file or glob the module owns, e.g. src/harness.ts — anchors the MOD<->file mapping for graph<->code conformance (CR-GC-205)' },
@@ -170,6 +170,20 @@ export type PhaseGateType = z.infer<typeof PhaseGate>;
170
170
  * single-owner convention as RULE_TO_DIMENSION.
171
171
  */
172
172
  export declare const RULE_TO_PHASE: Record<string, PhaseGateType>;
173
+ /**
174
+ * CR-SM-343 — die VORBEDINGUNG einer Regel: der Elementtyp, von dem der Graph mindestens ein
175
+ * Element tragen muss, damit die Regel ueberhaupt ausgewertet ist.
176
+ *
177
+ * Der Nenner einer Dimension ist `Σ Grundgesamtheit` ueber ihre Regeln (`domain`). Eine Regel,
178
+ * die mangels Gegenstand schweigt, truege dort ihre ganze Grundgesamtheit bei und liefert null
179
+ * Verstoesse — der Score STIEGE, weil nicht geprueft wurde. CR-R05 fragt nach einem Bauauftrag;
180
+ * ein Graph ohne einen einzigen CR (`cr: docs`) hat keinen Plan im Graphen, also ist die Frage
181
+ * dort NICHT GESTELLT, nicht bestanden. Wer einen Anteil bildet, fragt `ruleApplies` und laesst
182
+ * eine nicht anwendbare Regel aus Zaehler UND Nenner.
183
+ */
184
+ export declare const RULE_PRECONDITION: Readonly<Record<string, string>>;
185
+ /** Ist die Regel an einem Graphen mit diesen Typ-Zaehlungen ausgewertet? (CR-SM-343) */
186
+ export declare function ruleApplies(ruleId: string, countByType: Readonly<Record<string, number>>): boolean;
173
187
  /**
174
188
  * Die Eigentuemer-Spalte (CR-SM-350, Entscheidung 2026-09-22): welcher TASK eine Regel bearbeitet.
175
189
  *
@@ -152,6 +152,9 @@ export const RULE_TO_DIMENSION = {
152
152
  'BQ-01': 'req', 'BQ-02': 'req', 'BQ-04': 'req',
153
153
  'BQ-06': 'req', 'BQ-07': 'req',
154
154
  'RD-01': 'req', 'RD-02': 'req',
155
+ // CR-SM-343: CR-R05 (Blatt-REQ ohne Bauauftrag) — ein Mangel der Anforderungsdeckung, nicht
156
+ // der CR-Hygiene; zaehlt nur, wenn der Graph ueberhaupt CR-Knoten traegt (RULE_PRECONDITION).
157
+ 'CR-R05': 'req',
155
158
  // RD-04 is decomposition *breadth* — an architecture concern, not a requirement one
156
159
  'RD-04': 'arch',
157
160
  // CR-SM-311: die Untergrenze derselben Breite — ebenfalls Architektur
@@ -171,6 +174,7 @@ export const RULE_TO_DIMENSION = {
171
174
  'R-15': 'uc', 'R-16': 'uc', 'R-17': 'uc',
172
175
  // FC-04 (CR-SM-226): FCHAIN actor-bounded (trigger+consumer) — same dimension as FC-01..03.
173
176
  'FC-02': 'uc', 'FC-03': 'uc', 'FC-04': 'uc',
177
+ 'FC-05': 'uc', // CR-SM-363: Zusammenhang der Kette — dieselbe Dimension wie FC-04
174
178
  // arch
175
179
  'R-02': 'arch', 'R-10': 'arch', 'R-12': 'arch',
176
180
  // alloc
@@ -262,7 +266,7 @@ export const RULE_TO_PHASE = {
262
266
  'R-02': 'PDR', 'R-08': 'PDR', 'R-10': 'PDR', 'R-12': 'PDR', 'R-18': 'PDR',
263
267
  'R-04': 'PDR', 'R-22': 'PDR', 'R-23': 'PDR',
264
268
  'R-15': 'PDR',
265
- 'FC-03': 'PDR', 'FC-04': 'PDR',
269
+ 'FC-03': 'PDR', 'FC-04': 'PDR', 'FC-05': 'PDR',
266
270
  // CR-GC-366: Anschluss des Funktionsbaus — gehoert an dasselbe Gate wie R-15/IO-01,
267
271
  // die auf derselben Kette aufsetzen.
268
272
  'R-30': 'PDR', 'R-31': 'PDR',
@@ -282,7 +286,27 @@ export const RULE_TO_PHASE = {
282
286
  'VR-01': 'TRR',
283
287
  'FM-03': 'TRR',
284
288
  'CR-R02': 'TRR',
289
+ 'CR-R05': 'TRR', // CR-SM-343: dieselbe Phase wie AF-05 (Implementierungsplan)
285
290
  };
291
+ /**
292
+ * CR-SM-343 — die VORBEDINGUNG einer Regel: der Elementtyp, von dem der Graph mindestens ein
293
+ * Element tragen muss, damit die Regel ueberhaupt ausgewertet ist.
294
+ *
295
+ * Der Nenner einer Dimension ist `Σ Grundgesamtheit` ueber ihre Regeln (`domain`). Eine Regel,
296
+ * die mangels Gegenstand schweigt, truege dort ihre ganze Grundgesamtheit bei und liefert null
297
+ * Verstoesse — der Score STIEGE, weil nicht geprueft wurde. CR-R05 fragt nach einem Bauauftrag;
298
+ * ein Graph ohne einen einzigen CR (`cr: docs`) hat keinen Plan im Graphen, also ist die Frage
299
+ * dort NICHT GESTELLT, nicht bestanden. Wer einen Anteil bildet, fragt `ruleApplies` und laesst
300
+ * eine nicht anwendbare Regel aus Zaehler UND Nenner.
301
+ */
302
+ export const RULE_PRECONDITION = {
303
+ 'CR-R05': 'CR',
304
+ };
305
+ /** Ist die Regel an einem Graphen mit diesen Typ-Zaehlungen ausgewertet? (CR-SM-343) */
306
+ export function ruleApplies(ruleId, countByType) {
307
+ const required = RULE_PRECONDITION[ruleId];
308
+ return required === undefined || (countByType[required] ?? 0) > 0;
309
+ }
286
310
  /**
287
311
  * Die Eigentuemer-Spalte (CR-SM-350, Entscheidung 2026-09-22): welcher TASK eine Regel bearbeitet.
288
312
  *
@@ -302,10 +326,13 @@ export const TASK_OWNED_RULES = {
302
326
  trade: ['TR-01'], // CR-SM-355: Trade-Entscheidung als CR
303
327
  irr: ['IR-01'], // CR-SM-355: Annahmen-Review promoviert zu CR
304
328
  fmea: ['FM-01', 'FM-02', 'FM-03'],
305
- plan: ['MS-01', 'MS-02', 'MS-03', 'CR-R01', 'CR-R02', 'CR-R03'],
329
+ // CR-SM-377: CR-R05 (Blatt-REQ ohne Bauauftrag, CR-SM-343) fragt nach einem CR — die Antwort
330
+ // schreibt der Plan-Task (se-plan), nicht die Generierungsschleife. FC-05 bleibt Kern: die
331
+ // Kette zu schliessen ist Spezifikationsarbeit wie FC-04.
332
+ plan: ['MS-01', 'MS-02', 'MS-03', 'CR-R01', 'CR-R02', 'CR-R03', 'CR-R05'],
306
333
  // Annahme (CR-SM-350): BQ bekommt einen eigenen Task, vorerst ohne automatischen Eintrittspunkt.
307
334
  anforderungsqualitaet: ['BQ-01', 'BQ-02', 'BQ-04', 'BQ-06', 'BQ-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'],
335
+ 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', 'RC-10'],
309
336
  };
310
337
  /**
311
338
  * Der Eintrittspunkt eines Tasks im Kern: die Regel, die "dieses Artefakt fehlt" sagt. `null` = der
@@ -115,6 +115,11 @@ export const RULE_HELP = {
115
115
  plain: "Several open changes touch the same thing, so they will collide → sequence them or merge them.",
116
116
  se: "One element tracked by more than one `CR` with `status` open/in-progress.",
117
117
  },
118
+ 'CR-R05': {
119
+ plain: "This smallest-piece requirement has no work order — no change request points at it or at the function that fulfils it, and that function is not built yet, so no plan will ever build it → attach it to the change request that builds it.",
120
+ se: "Leaf `REQ` (no `compose`→`REQ` children) with no build order: no `CR -relation->` on the REQ itself nor on a `FUNC` that `satisfy`s it (CR-SM-343; the REQ side of CR-R01). A `FUNC` that is already built — valid `realRef` or `external`, not `concept` — covers the REQ without a CR (CR-SM-375). A CR on a `MOD`/`SYS` carrier does not count — a module is a container; such REQs take a direct `CR -relation-> REQ`. Silent, and not scored, on a graph without any `CR` node (`RULE_PRECONDITION`).",
121
+ prompt: "se-plan",
122
+ },
118
123
  'FC-02': {
119
124
  plain: "A scenario that is not broken into sub-scenarios has no described sequence of steps → add one, even if the steps are done by hand.",
120
125
  se: "Leaf `UC` (no `UC -compose-> UC`) with no `FCHAIN`. A chain may consist of EXISTING FUNCs an actor strings together — it costs a node plus compose edges, not code.",
@@ -128,19 +133,23 @@ export const RULE_HELP = {
128
133
  plain: "A sequence either has nobody starting it or nothing coming back out → wire both ends to whoever uses it.",
129
134
  se: "`FCHAIN` lacking an entry (`ACTOR -io-> FLOW -io-> FUNC∈chain`) or an exit (`FUNC∈chain -io-> FLOW -io-> ACTOR`). Stricter than FC-01: both directions, at FUNC/FLOW level, no UC-level bypass.",
130
135
  },
136
+ 'FC-05': {
137
+ plain: "The steps of this sequence do not hand their results to each other — it falls apart into pieces, so nobody can say how long it is or where it gets stuck → add the missing step, connect the pieces with a data flow, or split it into separate sequences.",
138
+ se: "`FCHAIN` whose member `FUNC`s do not form one (weakly) connected component along producer → consumer (`FUNC -io-> FLOW -io-> FUNC`, both ends members). Per side component the cause: `missing-link` (main and side part are joined through a `FUNC` outside the chain — compose it) or `bag` (no FUNC joins them: a missing FLOW between members, or parallel services at a shared source — split). One finding per chain, components in `context.side_components` (CR-SM-242). Silent while any member lacks an io input or output (R-31 first); entry/exit belong to FC-04 (CR-SM-363).",
139
+ },
131
140
  'FM-01': {
132
141
  plain: "A requirement is marked as a risk but carries no risk ratings → rate how bad, how likely and how detectable it is (1-10 each).",
133
- se: "Risk `REQ` missing `severity` / `occurrence` / `detection` attributes (AIAG-VDA).",
142
+ se: "`REQ` with `role:\"risk\"` missing `severity` / `occurrence` / `detection` attributes (AIAG-VDA). The role is the requirement's motivation, not its kind — satisfy-neutral (CR-SM-365).",
134
143
  prompt: "se-fmea",
135
144
  },
136
145
  'FM-02': {
137
146
  plain: "A known risk has nothing planned against it → write the countermeasure as its own requirement.",
138
- se: "Risk `REQ` with no `compose`d mitigation `REQ` (`kinds:[\"mitigation\"]`).",
147
+ se: "`REQ` with `role:\"risk\"` and no `compose`d `REQ` with `role:\"mitigation\"`. The mitigation keeps its own kind (functional or non-functional) and is satisfied like any REQ of that kind (CR-SM-365).",
139
148
  prompt: "se-fmea",
140
149
  },
141
150
  'FM-03': {
142
151
  plain: "A high risk has no test that actually passed → add a test proving the countermeasure works.",
143
- se: "Risk `REQ` with RPN > 100 and no `TEST` carrying `testResult:\"passed\"` verifying it.",
152
+ se: "`REQ` with `role:\"risk\"`, Action Priority High (AIAG-VDA, RPN band as interim) and no verifying `TEST` whose every `testRefs` entry has `result:\"passed\"`.",
144
153
  prompt: "se-fmea",
145
154
  },
146
155
  'IO-01': {
@@ -247,8 +256,8 @@ export const RULE_HELP = {
247
256
  se: "Realized `FUNC` with no valid `realRef` `{file, symbol}` (graph↔code binding); else `concept:true` / `external:true`.",
248
257
  },
249
258
  'R-21': {
250
- plain: "Two functions hand data over, but they are not in one chain together — or they are, and nothing tests the hand-off → put both into the same chain, and give that chain an integration test.",
251
- se: "FUNC↔FUNC handover (`FUNC` ─io→ `FLOW` ─io→ `FUNC`), two findings. (a) endpoints share NO `FCHAIN` → finding at the PRODUCER: no integration scope is declared. (b) endpoints share one, but no shared chain carries a verified integration test (`TEST` ─verify→ `REQ` ←satisfy─ `FCHAIN`) → finding at the chain. SILENT when either endpoint is in no chain at all (that is R-30's statement) and when either is infrastructure (`chains >= policy.criticality.infrastructure`): a handover into a function that runs through every chain is a bus, not a missing test.",
259
+ plain: "Two functions hand data over, but they are not in one chain together — or they are, and the chain is not covered: some function in it has no requirement, and no end-to-end test checks the chain → give each function of the chain a requirement it fulfils, or give the chain an end-to-end quality requirement and a test for it.",
260
+ se: "FUNC↔FUNC handover (`FUNC` ─io→ `FLOW` ─io→ `FUNC`), two findings. (a) endpoints share NO `FCHAIN` → finding at the PRODUCER: no integration scope is declared. (b) endpoints share one, but no shared chain is covered → finding at the chain, naming the member FUNCs without a REQ. A chain is covered either (1) through its members — EVERY member FUNC (`FCHAIN` ─compose→ `FUNC`) satisfies at least one `REQ`; verifying those REQs is R-01's job (CR-SM-376) — or (2) end-to-end — a verified integration test (`TEST` ─verify→ `REQ` ←satisfy─ `FCHAIN`); an `FCHAIN` satisfies only non-functional REQs (CR-SM-366). SILENT when either endpoint is in no chain at all (that is R-30's statement) and when either is infrastructure (`chains >= policy.criticality.infrastructure`): a handover into a function that runs through every chain is a bus, not a missing test.",
252
261
  },
253
262
  'R-22': {
254
263
  plain: "A function isn't assigned to any building block, so it has no home in the structure → put it on a module.",
@@ -310,6 +319,10 @@ export const RULE_HELP = {
310
319
  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
320
  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).",
312
321
  },
322
+ 'RC-10': {
323
+ plain: "No file in the code can be assigned to this building block, so nobody can check whether its imports cross boundaries the model does not know → give the block its source folder, or point one of its functions at its code.",
324
+ se: "`MOD` with neither a `path` attribute nor an allocated `FUNC` carrying a bound `realRef` — the only two ways a file resolves to a module. Without either, RC-05 and the boundary measurement are blind for this module by construction (sigllm: 0 RC-05 findings at 19 % import coverage, 8 at 81 %). The message names the cost from `importCoverage`. `concept`/`external` modules are exempt, as for R-20 (CR-SM-344).",
325
+ },
313
326
  'RD-01': {
314
327
  plain: "A smallest-piece feature has nothing built to fulfil it → add what implements it.",
315
328
  se: "Leaf `REQ` (no `compose`→`REQ` children) with no `satisfy` from a `FUNC`/`FCHAIN`/`MOD`/`SYS`.",
@@ -338,8 +351,7 @@ export const RULE_HELP = {
338
351
  },
339
352
  'UC-02': {
340
353
  plain: "A scenario has nobody who triggers it → name who or what starts it.",
341
- se: "`UC` with no `ACTOR` connected by an `io` trace (directly or via a `FLOW` of its chain).",
342
- prompt: "se:author-uc",
354
+ se: "`UC` that no `ACTOR` reaches: the one legal path is `ACTOR -io-> FLOW -io-> FUNC` (or back, `FUNC -io-> FLOW -io-> ACTOR`), the FUNC a member of one of the UC's chains (`UC -compose-> FCHAIN -compose-> FUNC`). A direct `ACTOR -io-> UC` is rejected by R-18. No skill prompt (CR-SM-370): `se:author-uc` shows creating UCs and `se:author-actor` says to leave actors unwired — neither is this work.",
343
355
  },
344
356
  'UC-03': {
345
357
  plain: "A scenario says what should be possible but not how it runs → describe the steps as a chain of functions.",
@@ -81,6 +81,14 @@ export declare const ViolationContext: z.ZodObject<{
81
81
  parent_module: z.ZodOptional<z.ZodString>;
82
82
  current_description: z.ZodOptional<z.ZodString>;
83
83
  also_affects: z.ZodOptional<z.ZodArray<z.ZodString>>;
84
+ side_components: z.ZodOptional<z.ZodArray<z.ZodObject<{
85
+ members: z.ZodArray<z.ZodString>;
86
+ cause: z.ZodEnum<{
87
+ "missing-link": "missing-link";
88
+ bag: "bag";
89
+ }>;
90
+ via: z.ZodArray<z.ZodString>;
91
+ }, z.core.$strip>>>;
84
92
  trace_rejection: z.ZodOptional<z.ZodObject<{
85
93
  reason: z.ZodEnum<{
86
94
  "no-pattern": "no-pattern";
@@ -212,6 +220,14 @@ export declare const RuleViolation: z.ZodObject<{
212
220
  parent_module: z.ZodOptional<z.ZodString>;
213
221
  current_description: z.ZodOptional<z.ZodString>;
214
222
  also_affects: z.ZodOptional<z.ZodArray<z.ZodString>>;
223
+ side_components: z.ZodOptional<z.ZodArray<z.ZodObject<{
224
+ members: z.ZodArray<z.ZodString>;
225
+ cause: z.ZodEnum<{
226
+ "missing-link": "missing-link";
227
+ bag: "bag";
228
+ }>;
229
+ via: z.ZodArray<z.ZodString>;
230
+ }, z.core.$strip>>>;
215
231
  trace_rejection: z.ZodOptional<z.ZodObject<{
216
232
  reason: z.ZodEnum<{
217
233
  "no-pattern": "no-pattern";
package/dist/se/rules.js CHANGED
@@ -35,6 +35,17 @@ export const ViolationContext = z.object({
35
35
  * unterschlagen.
36
36
  */
37
37
  also_affects: z.array(z.string()).optional(),
38
+ /**
39
+ * CR-SM-363 (FC-05): die NEBENKOMPONENTEN einer zerfallenen FCHAIN, je mit Ursache —
40
+ * `missing-link` (Haupt- und Nebenteil sind ueber eine FUNC ausserhalb der Kette verbunden;
41
+ * `via` nennt sie) oder `bag` (nur eine gemeinsame Quelle, parallele Dienste). EIN Befund je
42
+ * Kette (CR-SM-242), die Komponenten hier statt als n Befunde.
43
+ */
44
+ side_components: z.array(z.object({
45
+ members: z.array(z.string()),
46
+ cause: z.enum(['missing-link', 'bag']),
47
+ via: z.array(z.string()),
48
+ })).optional(),
38
49
  /**
39
50
  * CR-SM-341: der Ablehnungsgrund von R-18, maschinenlesbar statt nur als Satz.
40
51
  *
@@ -242,24 +253,15 @@ function funcMustSatisfyReq(graph) {
242
253
  .map(st => idx.byId.get(st.target))
243
254
  .filter((e) => !!e)
244
255
  : [];
245
- // Also include constraints whose description mentions this func's name
246
- const namePattern = fn.name.toLowerCase();
247
- const matchingConstraints = allReqs.filter(r => r.kinds?.includes('non-functional') && r.description.toLowerCase().includes(namePattern));
248
256
  // CR-SM-346 (ITEM-2026-299): der Kandidatenkreis wird am META-MODELL gefiltert, nicht
249
- // an einer zweiten, hier abgeschriebenen kinds-Liste. `FUNC -satisfy-> REQ` laesst nur
250
- // BEHAVIOURAL_KINDS zu; bis hierher bot die Regel ALLE REQ an — gemessen an sigllm hatte
251
- // damit KEINER von 33 Befunden einen legalen Kandidaten, und die Vorlage aus CR-SM-342
252
- // gab korrekt `null` zurueck. 86 Befunde, 0 Empfehlungen: der Befund war unbedienbar.
253
- //
254
- // Besonders der `matchingConstraints`-Zweig zog gezielt `non-functional` herein — genau
255
- // die Sorte, die das Muster abweist. Er bleibt stehen (ein REQ kann mehrere kinds tragen,
256
- // und die Namensnennung ist ein echtes Relevanzsignal), aber er entscheidet nicht mehr
257
- // ueber die Legalitaet.
257
+ // an einer zweiten, hier abgeschriebenen kinds-Liste — `isValidTrace` statt Eigenbau: eine
258
+ // Regel, die Kandidaten anbietet, die ihr eigenes Meta-Modell abweist, erzeugt Waste statt
259
+ // Fuehrung. `FUNC -satisfy-> REQ` laesst nur `functional` zu (CR-SM-366).
258
260
  //
259
- // `isValidTrace` statt Eigenbau: eine Regel, die Kandidaten anbietet, die ihr eigenes
260
- // Meta-Modell abweist, erzeugt Waste statt Fuehrung — und zwei Wahrheiten ueber dieselbe
261
- // Grammatik sind genau der Defekt, den CR-SM-286/-302 beseitigt haben.
262
- const candidates = [...new Map([...siblingReqs, ...matchingConstraints, ...allReqs].map(e => [e.id, e])).values()]
261
+ // CR-SM-366: der fruehere `matchingConstraints`-Zweig (non-functional-REQ, deren Text den
262
+ // FUNC-Namen nennt) ist entfallen. Er zog genau die Sorte herein, die das Muster abweist;
263
+ // seit ein REQ genau EIN kind traegt, konnte kein Treffer den Filter je passieren.
264
+ const candidates = [...new Map([...siblingReqs, ...allReqs].map(e => [e.id, e])).values()]
263
265
  .filter(r => isValidTrace({ source: 'FUNC', target: 'REQ', type: 'satisfy', targetKinds: r.kinds }));
264
266
  return {
265
267
  rule_id: 'R-02',
@@ -827,7 +829,9 @@ function actorMustHaveTrace(graph) {
827
829
  severity: 'warning',
828
830
  element_id: a.id,
829
831
  message: `${a.id} has no io traces (disconnected actor)`,
830
- fix_hint: 'Link to a UC or FLOW via io trace',
832
+ // CR-SM-371: ein ACTOR hat genau zwei legale Kanten (ACTOR -io-> FLOW, FLOW -io-> ACTOR);
833
+ // der UC wird ueber den FLOW an einer FUNC seiner Kette erreicht, nie direkt (R-18).
834
+ fix_hint: `Wire the actor through a FLOW via graph_mutate Format-E: \`${a.id} -io-> FLOW-x\` (it sends) or \`FLOW-x -io-> ${a.id}\` (it receives), plus \`FLOW-x -io-> FUNC-y\` or \`FUNC-y -io-> FLOW-x\` for a FUNC in the use case's chain — never an io edge to the UC itself`,
831
835
  context: {
832
836
  element_type: a.type,
833
837
  element_name: a.name,
@@ -927,7 +931,6 @@ function validTracePattern(graph) {
927
931
  });
928
932
  if (!why)
929
933
  return [];
930
- const { detail, hint } = explainRejection(why, t, src, tgt);
931
934
  // CR-SM-341: derselbe Grund, den `detail` in Prosa giesst — hier zusaetzlich als Struktur.
932
935
  // Keine zweite Rechnung: `why` liegt vor, die Alternativen befragen `isValidTrace`.
933
936
  const shape = {
@@ -936,6 +939,7 @@ function validTracePattern(graph) {
936
939
  targetKinds: idx.byId.get(t.target)?.kinds,
937
940
  };
938
941
  const alt = admittingAlternatives(shape);
942
+ const { detail, hint } = explainRejection(why, t, src, tgt, alt.sources);
939
943
  const where = why.reason === 'no-pattern' ? undefined : why.pattern.where;
940
944
  return [{
941
945
  rule_id: 'R-18',
@@ -970,7 +974,7 @@ function validTracePattern(graph) {
970
974
  * `kinds` steht da und passt nicht (Kante heben oder Kind korrigieren). Wer das nicht
971
975
  * unterscheiden kann, sucht am falschen Ende.
972
976
  */
973
- function explainRejection(why, t, src, tgt) {
977
+ function explainRejection(why, t, src, tgt, admittingSources) {
974
978
  if (why.reason === 'no-pattern') {
975
979
  return {
976
980
  detail: `${src} → ${tgt} is not a valid ${t.type} pattern`,
@@ -991,7 +995,11 @@ function explainRejection(why, t, src, tgt) {
991
995
  }
992
996
  : {
993
997
  detail: `${admits}, but ${end} declares {${[...why.declared].sort().join(', ')}}`,
994
- hint: `Correct '${where.field}' on ${end}, or use a source type whose ${t.type} pattern admits them (FCHAIN carries every kind)`,
998
+ // CR-SM-366: den Erfueller NENNEN statt "FCHAIN carries every kind" — das galt nie fuer
999
+ // die Code-Bindung und gilt seit dem FCHAIN-where gar nicht mehr.
1000
+ hint: admittingSources.length > 0
1001
+ ? `Correct '${where.field}' on ${end}, or ${t.type} it from ${admittingSources.join(' | ')} — the pattern admitting {${[...why.declared].sort().join(', ')}}`
1002
+ : `Correct '${where.field}' on ${end} to a subset of {${allowed}} — no source type admits {${[...why.declared].sort().join(', ')}} (retired kind? split a mixed REQ)`,
995
1003
  };
996
1004
  }
997
1005
  /**
@@ -1270,7 +1278,7 @@ function funcMustHaveCodeBinding(graph) {
1270
1278
  severity: 'warning',
1271
1279
  element_id: fn.id,
1272
1280
  message: `${fn.id} is not fully realized — unbound compose ${leafList.length === 1 ? 'child' : 'children'}: ${leafList.join(', ')}`,
1273
- fix_hint: `Realize the leaves ${leafList.join(', ')} (graph_realize) — the parent inherits their binding`,
1281
+ fix_hint: `Bind the leaves via graph_mutate Format-E (${leafList.map((id) => `\`~ ${id} @realRef {"file":…,"symbol":…}\``).join(', ')}) — the parent inherits their binding`,
1274
1282
  context: { element_type: fn.type, element_name: fn.name },
1275
1283
  });
1276
1284
  }
@@ -1300,6 +1308,10 @@ function funcMustHaveCodeBinding(graph) {
1300
1308
  // Co-adjacency at a shared FLOW is not an asserted interface — treating it
1301
1309
  // as one produced P·C findings per hub FLOW and made reuse the expensive
1302
1310
  // choice. The FCHAIN is the modelled claim; the test is owed on the claim.
1311
+ // CR-SM-376: second way to cover a chain. Since CR-SM-366 an FCHAIN carries
1312
+ // only NFRs; its effect is covered through its members when EVERY member
1313
+ // FUNC (FCHAIN ─compose→ FUNC) satisfies at least one REQ. Verifying those
1314
+ // member REQs is R-01's job, not R-21's.
1303
1315
  // ---------------------------------------------------------------------------
1304
1316
  function fchainMustHaveIntegrationTest(graph, policy) {
1305
1317
  const idx = indexOf(graph);
@@ -1333,6 +1345,22 @@ function fchainMustHaveIntegrationTest(graph, policy) {
1333
1345
  testedChains.add(t.source);
1334
1346
  }
1335
1347
  }
1348
+ // CR-SM-376: der zweite Weg — die Kette ist ueber ihre Glieder gedeckt, wenn JEDE Glied-FUNC
1349
+ // (direkte FCHAIN -compose-> FUNC, dieselbe Definition wie `chainsByFunc`) eine REQ erfuellt.
1350
+ // `unmetMembers` liefert die Glieder ohne REQ; leer = gedeckt. Eine Kette ohne Glied kann hier
1351
+ // nicht auftauchen: ZWEIG 2 hat mindestens die beiden Enden der Uebergabe als Glieder.
1352
+ const funcsWithReq = new Set(idx.tracesOfType('satisfy').filter(t => isFunc(t.source) && typeOf.get(t.target) === 'REQ').map(t => t.source));
1353
+ const membersOf = new Map();
1354
+ for (const [fn, chains] of chainsOf) {
1355
+ for (const ch of chains) {
1356
+ const list = membersOf.get(ch);
1357
+ if (list)
1358
+ list.push(fn);
1359
+ else
1360
+ membersOf.set(ch, [fn]);
1361
+ }
1362
+ }
1363
+ const unmetMembers = (ch) => (membersOf.get(ch) ?? []).filter(fn => !funcsWithReq.has(fn)).sort();
1336
1364
  // CR-SM-313: die Infrastruktur-Ausnahme. Die ZAHL kommt aus der Kennzahl (CR-SM-314), nicht
1337
1365
  // aus einer zweiten Rechnung — eine Uebergabe an eine Funktion, die quer durch alle Ketten
1338
1366
  // laeuft, ist kein fehlender Integrationstest, sondern ein Bus. `null` = keine Ausnahme.
@@ -1372,8 +1400,9 @@ function fchainMustHaveIntegrationTest(graph, policy) {
1372
1400
  });
1373
1401
  continue;
1374
1402
  }
1375
- // ZWEIG 2 — gemeinsame Kette, aber keine davon geprueft: Befund an der Kette. Unveraendert.
1376
- if (shared.some(ch => testedChains.has(ch)))
1403
+ // ZWEIG 2 — gemeinsame Kette, aber keine davon gedeckt: Befund an der Kette. Gedeckt heisst
1404
+ // (a) verifizierte Ketten-REQ oder (b, CR-SM-376) jede Glied-FUNC erfuellt eine REQ.
1405
+ if (shared.some(ch => testedChains.has(ch) || unmetMembers(ch).length === 0))
1377
1406
  continue;
1378
1407
  const anchor = shared[0];
1379
1408
  const key = `${anchor}|${p}>${c}`;
@@ -1385,8 +1414,8 @@ function fchainMustHaveIntegrationTest(graph, policy) {
1385
1414
  rule_id: 'R-21',
1386
1415
  severity: 'warning',
1387
1416
  element_id: anchor,
1388
- message: `${anchor} has no integration test covering connection ${p} → ${c}`,
1389
- fix_hint: 'Verify an FCHAIN-satisfied REQ with an integration TEST',
1417
+ message: `${anchor} has no integration test covering connection ${p} → ${c}; member FUNCs without a REQ: ${unmetMembers(anchor).join(', ')}`,
1418
+ fix_hint: 'Give each member FUNC of the chain a REQ it satisfies (FUNC -satisfy-> REQ), or attach an end-to-end non-functional REQ to the chain (FCHAIN -satisfy-> REQ) and verify it with an integration TEST',
1390
1419
  context: { element_type: anchorEl?.type, element_name: anchorEl?.name },
1391
1420
  });
1392
1421
  }
@@ -1650,7 +1679,7 @@ export const V3_RULES = [
1650
1679
  { id: 'R-19', name: 'Runnable TEST binding', severity: 'warning', evaluate: testMustHaveRunnableBinding, domain: ['TEST'] },
1651
1680
  { id: 'R-29', name: 'Test file exclusivity', severity: 'error', evaluate: testFileExclusivity, domain: ['TEST'] },
1652
1681
  { id: 'R-20', name: 'FUNC realRef binding', severity: 'warning', evaluate: funcMustHaveCodeBinding, domain: ['FUNC'] },
1653
- { id: 'R-21', name: 'FUNC↔FUNC handover needs a shared chain and an integration test', severity: 'warning', evaluate: fchainMustHaveIntegrationTest, domain: ['FCHAIN', 'FUNC'] },
1682
+ { id: 'R-21', name: 'FUNC↔FUNC handover needs a shared chain covered by member REQs or an integration test', severity: 'warning', evaluate: fchainMustHaveIntegrationTest, domain: ['FCHAIN', 'FUNC'] },
1654
1683
  { id: 'R-22', name: 'FUNC must be allocated to MOD', severity: 'warning', evaluate: funcMustBeAllocated, domain: ['FUNC'] },
1655
1684
  { id: 'R-23', name: 'MOD must have allocated FUNC', severity: 'warning', evaluate: modMustHaveAllocatedFunc, domain: ['MOD'] },
1656
1685
  { id: 'R-26', name: 'SCHEMA must have realRef', severity: 'warning', evaluate: schemaMustHaveSchemaRef, domain: ['SCHEMA'] },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sigloch/contracts",
3
- "version": "10.12.0",
3
+ "version": "10.13.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",