@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.
@@ -31,8 +31,11 @@
31
31
  * umformulierter Satz darf keinen Major ausloesen, deshalb steht im Golden-File nur die
32
32
  * Schluesselmenge, nicht die Prosa.
33
33
  *
34
- * Sprache: Englisch, wie die Regelbeschreibungen selbst. 18,6 KB reine Strings — der
35
- * `./browser`-Eintrag bleibt transitiv `node:`-frei (CR-SM-230), Strings ziehen nichts nach.
34
+ * Sprache: Englisch, wie die Regelbeschreibungen selbst — ausser `vorschlag`, der Satz, den ein Nutzer
35
+ * seinem Agenten schickt (CR-SM-384): Deutsch, weil er in der Sprache des Nutzers steht, nicht in der
36
+ * des Katalogs. Bis CR-SM-384 lag er als `VORSCHLAG_REGEL` in graphcode, ein paralleler Pfad zu
37
+ * `plain` und `fix_hint` mit derselben Driftgefahr wie oben. Reine Strings — der `./browser`-Eintrag
38
+ * bleibt transitiv `node:`-frei (CR-SM-230), Strings ziehen nichts nach.
36
39
  *
37
40
  * @author andreas@siglochconsulting
38
41
  */
@@ -93,6 +96,7 @@ export const RULE_HELP = {
93
96
  'BW-02': {
94
97
  plain: "This block hands out many different kinds of data at its edge → whoever uses it has to understand all of them, so either bundle them or split the block.",
95
98
  se: "Whitebox boundary width: distinct `SCHEMA` contracts on `FUNC` `io` `FLOW` `io` `FUNC` paths with one endpoint inside the `compose` subtree and one outside, judged against `metricPolicy.boundaryWidth.warning`. Parnas, information hiding — what a boundary HIDES is what makes it worth having. Rolled up over the subtree because a decomposed `FUNC` carries no `io` edges of its own (CR-SM-283).",
99
+ vorschlag: 'Bündle die Datenformate an der Grenze von {n} oder teile den Block.',
96
100
  },
97
101
  'CL-01': {
98
102
  plain: "Someone only ever uses the system in one way, which usually means their other situations are missing → describe the scenarios you left out.",
@@ -102,6 +106,7 @@ export const RULE_HELP = {
102
106
  'CR-01': {
103
107
  plain: "Two parts exchange an unusually large amount of data, which usually means the boundary is in the wrong place → reconsider the cut.",
104
108
  se: "High crossing `io` FLOW count between two `MOD`s — a coupling metric, advisory.",
109
+ vorschlag: 'Prüfe den Schnitt zwischen {n}, dort fließen ungewöhnlich viele Daten.',
105
110
  },
106
111
  'CR-R01': {
107
112
  plain: "A change is recorded but says nothing about what it changes → link it to what it touches.",
@@ -115,41 +120,56 @@ export const RULE_HELP = {
115
120
  plain: "Several open changes touch the same thing, so they will collide → sequence them or merge them.",
116
121
  se: "One element tracked by more than one `CR` with `status` open/in-progress.",
117
122
  },
123
+ 'CR-R05': {
124
+ 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.",
125
+ 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`).",
126
+ prompt: "se-plan",
127
+ },
118
128
  'FC-02': {
119
129
  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
130
  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.",
121
131
  prompt: "se:author-uc",
132
+ vorschlag: 'Beschreibe die Abläufe {n} als Kette von Funktionen.',
122
133
  },
123
134
  'FC-03': {
124
135
  plain: "A step inside a sequence contains further steps, so the sequence has hidden depth → lift them to the same level.",
125
136
  se: "`FUNC` inside an `FCHAIN` that itself `compose`s other `FUNC`s. Chains are flat by construction.",
137
+ vorschlag: 'Hebe die inneren Schritte der Funktionen {n} auf die Ebene ihrer Kette.',
126
138
  },
127
139
  'FC-04': {
128
140
  plain: "A sequence either has nobody starting it or nothing coming back out → wire both ends to whoever uses it.",
129
141
  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.",
142
+ vorschlag: 'Verbinde Anfang und Ende der Funktionsketten {n} mit dem Nutzer.',
143
+ },
144
+ 'FC-05': {
145
+ 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.",
146
+ 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).",
147
+ vorschlag: 'Verbinde die Schritte der Funktionsketten {n} über Datenflüsse.',
130
148
  },
131
149
  'FM-01': {
132
150
  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).",
151
+ 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
152
  prompt: "se-fmea",
135
153
  },
136
154
  'FM-02': {
137
155
  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\"]`).",
156
+ 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
157
  prompt: "se-fmea",
140
158
  },
141
159
  'FM-03': {
142
160
  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.",
161
+ 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
162
  prompt: "se-fmea",
145
163
  },
146
164
  'IO-01': {
147
165
  plain: "Two steps in the same sequence have no described data passing between them → add the data one hands to the other.",
148
166
  se: "A `FUNC` pair inside one `FCHAIN` with no `io` path (`FUNC -io-> FLOW -io-> FUNC`) connecting them.",
167
+ vorschlag: 'Ergänze den Datenfluss zwischen den Schritten {n}.',
149
168
  },
150
169
  'IO-02': {
151
170
  plain: "Two different places write into the same flow, so a reader cannot tell which version arrives \u2192 give each source its own flow; they may keep sharing one contract.",
152
171
  se: "`FLOW` with more than one producer (`FUNC`/`ACTOR -io-> FLOW`). R-10 checks the LOWER bound of the same axis (at least one producer, at least one consumer); the two can never fire on the same FLOW, so there is no double count. Only the producer side is bounded: several consumers are the normal shape of a shared contract \u2014 one source, many readers, which is exactly how a central configuration looks. One finding per FLOW, `value` = number of producers, `threshold` 1. Severity `error` (hygiene) and deliberately NOT in STEER_RULES — a decision, not a measured conflict: a split flow keeps its `SCHEMA`, so BW-02 (distinct contracts per boundary) does not rise. Whether a gate blocks a new finding is the consumer's gating choice, not part of the rule.",
172
+ vorschlag: 'Gib jeder Quelle der Datenflüsse {n} einen eigenen Datenfluss.',
153
173
  },
154
174
  'MS-01': {
155
175
  plain: "A milestone has no work assigned to it → assign the work items that belong to it.",
@@ -167,10 +187,12 @@ export const RULE_HELP = {
167
187
  'MT-01': {
168
188
  plain: "This module draws more from others than others draw from it → it will keep changing whenever they do. Measured, not judged: no threshold is set by default.",
169
189
  se: "Instability I = fan_out / (fan_in + fan_out), counted in DISTINCT CONTRACTS crossing the module boundary — the same `moduleCrossings` definition CR-01, R-04 and BW-02 use (CR-SM-274/276). CR-SM-293 fixed both halves: it used to count every trace touching the module, of which only 34 % was coupling (`allocate` is module SIZE, `satisfy` is specification); and the direction was inverted — the CONSUMER depends, so supplying outward is fan_in (Martin's Ca) and drawing inward is fan_out (Ce). `metricPolicy.instability` defaults to `null`: the distribution is bimodal, so no threshold can be read from it. The number stays in every `graph_metrics` module row, next to `uphillDependencies` (CR-SM-301).",
190
+ vorschlag: 'Prüfe die Abhängigkeiten des Moduls {n}.',
170
191
  },
171
192
  'MT-02': {
172
193
  plain: "The parts inside this module never talk to each other → it is really several modules in one.",
173
194
  se: "LCOM4 over the BOXES of this module whitebox: its sub-MODs (`MOD -compose-> MOD`) plus its directly allocated `FUNC`s. A box owns its whole subtree, so a decomposed block carries the contracts of its leaves — the same ownership chain R-04 (CR-SM-282), BW-02 (CR-SM-283) and R-23 (CR-SM-321) use; without it a container counted as an empty group of its own and the number grew with decomposition DEPTH (CR-SM-326). Connected means a shared CONTRACT (`FLOW -relation-> SCHEMA`, else the untyped flow) — the same counting base as CR-01/R-04/MT-01 since CR-SM-274/276, not FLOW identity. CR-SM-297 dropped `satisfy`: two FUNCs meeting the same requirement can be fully decoupled at runtime, and if they share one, the REQUIREMENT is what needs decomposing. `null` (no judgement) below two boxes and where no box carries a contract at all — an unwired FUNC is R-31's statement, at the element. `info` from `metricPolicy.lcom4.info`, `warning` from `.warning` (CR-GC-329).",
195
+ vorschlag: 'Teile das Modul {n}, seine Teile arbeiten nicht zusammen.',
174
196
  },
175
197
  'MT-04': {
176
198
  plain: "This block's parts never talk to each other → it is really two blocks that happen to sit under one name.",
@@ -179,32 +201,39 @@ export const RULE_HELP = {
179
201
  'ND-01': {
180
202
  plain: "Two functions look like the same function written twice → merge them, or make clear what each one does differently.",
181
203
  se: "Near-duplicate `FUNC` above 0.85 similarity (name, description and legal trace partners). Since CR-SM-286 the similarity is computed IN the rule, cached per graph — before that it had to be injected, and without injection the rule returned green, indistinguishable from \"no duplicates\".",
204
+ vorschlag: 'Führe die doppelten Funktionen {n} zusammen oder grenze sie ab.',
182
205
  },
183
206
  'ND-02': {
184
207
  plain: "Two data contracts describe the same thing twice → merge them, or say what distinguishes them.",
185
208
  se: "Near-duplicate `SCHEMA` above 0.85 similarity (name, description, fields and legal trace partners). Partners are filtered through `isValidTrace`: a rule must not reach its verdict via an edge R-18 rejects (CR-SM-286).",
209
+ vorschlag: 'Führe die doppelten Datenformate {n} zusammen oder grenze sie ab.',
186
210
  },
187
211
  'NFR-01': {
188
212
  plain: "Something measured exceeds the limit that was set for it → fix it or change the limit deliberately.",
189
213
  se: "Measured value above its declared budget on a `MOD` (physical) or `FUNC`/`FCHAIN` (behavioural).",
214
+ vorschlag: 'Bringe {n} unter sein Budget oder ändere das Budget bewusst.',
190
215
  },
191
216
  'R-01': {
192
217
  plain: "A feature you've promised has no test proving it's met, so you can't show it works → add or author a test for it.",
193
218
  se: "`REQ` with no incoming `verify` trace (test→requirement coverage, INCOSE V&V).",
194
219
  prompt: "se:close-violations",
220
+ vorschlag: 'Ergänze Tests für die Anforderungen {n}.',
195
221
  },
196
222
  'R-02': {
197
223
  plain: "A function isn't linked to any feature it's meant to build, so it may be dead code → link it to the feature it serves, or delete it.",
198
224
  se: "`FUNC` with no `satisfy` trace to a `REQ` (design→requirement traceability).",
225
+ vorschlag: 'Ordne die Funktionen {n} den Anforderungen zu, die sie erfüllen.',
199
226
  },
200
227
  'R-04': {
201
228
  plain: "Too many different data formats cross this module's boundary → merge formats, or move the functions that carry them.",
202
229
  se: "`MOD` whose boundary is crossed by `metricPolicy.boundaryWidth.warning` or more DISTINCT contracts (`SCHEMA` on the io path `FUNC` ─io→ `FLOW` ─io→ `FUNC` across the boundary, union over all neighbours — CR-SM-276). Same question and same threshold as BW-02 on the FUNC tree (CR-SM-312); module SIZE is RD-04's question since CR-SM-311.",
203
230
  prompt: "se-view:arch",
231
+ vorschlag: 'Verringere die Datenformate an der Grenze des Moduls {n}.',
204
232
  },
205
233
  'R-05': {
206
234
  plain: "A test doesn't check any feature you promised → link it to the feature it tests, or remove it.",
207
235
  se: "`TEST` with no `verify` trace at all — neither to a `REQ` (test→requirement coverage) nor to a `SCHEMA` (contract test, CR-SM-317).",
236
+ vorschlag: 'Ordne die Tests {n} den Anforderungen zu, die sie prüfen.',
208
237
  },
209
238
  'R-32': {
210
239
  plain: "A data contract exists in code but no test exercises it as a contract → add a test that tries every variant, including the ones it must reject, and link it to the schema.",
@@ -213,30 +242,37 @@ export const RULE_HELP = {
213
242
  'R-08': {
214
243
  plain: "A link points at something that no longer exists → repair or remove the broken link.",
215
244
  se: "Trace whose source or target element is missing (dangling reference).",
245
+ vorschlag: 'Repariere oder entferne die Verbindungen an {n}, deren Ziel fehlt.',
216
246
  },
217
247
  'R-10': {
218
248
  plain: "A piece of data goes nowhere — nothing produces or consumes it → connect it to a function or a person/outside system.",
219
249
  se: "`FLOW` with no `io` trace to a `FUNC` or `ACTOR`.",
250
+ vorschlag: 'Vervollständige die Datenflüsse {n}: woher sie kommen und wohin sie gehen.',
220
251
  },
221
252
  'R-12': {
222
253
  plain: "Two items depend on each other in a loop, so neither can stand alone → remove or redirect one of the two links.",
223
254
  se: "Direct cycle: A→B and B→A via the same trace type, checked on `compose` / `allocate` / `relation` only. Data (`io`) is exempt — a function that reads and writes the same `FLOW` is normal reuse, not a dependency cycle.",
255
+ vorschlag: 'Löse die zyklische Abhängigkeit um {n} auf.',
224
256
  },
225
257
  'R-15': {
226
- plain: "A sequence of steps for a use case is empty → add the functions that make it up.",
227
- se: "`FCHAIN` with no `compose` to any `FUNC`.",
258
+ plain: "A sequence of steps is empty, or serves no use case → add the functions that make it up, and attach it to the use case it belongs to.",
259
+ se: "`FCHAIN` with no `compose` to any `FUNC`, or with no incoming `compose` from a `UC` — one rule_id, up to two findings per chain (pattern of R-10).",
260
+ vorschlag: 'Gib den Funktionsketten {n} ihre Funktionen und ordne sie ihrem Ablauf zu.',
228
261
  },
229
262
  'R-16': {
230
263
  plain: "A person or outside system isn't connected to anything → connect it to the data it sends or receives.",
231
264
  se: "`ACTOR` with no `io` trace to a `FLOW`.",
265
+ vorschlag: 'Verbinde den Nutzer {n} über Datenflüsse mit den Funktionen.',
232
266
  },
233
267
  'R-17': {
234
268
  plain: "The top level of your project is empty — nothing is inside it → add the main use cases, features, or modules.",
235
269
  se: "`SYS` with no `compose` to `UC` / `REQ` / `MOD`.",
270
+ vorschlag: 'Lege die Abläufe des Systems {n} an.',
236
271
  },
237
272
  'R-18': {
238
273
  plain: "You connected two items in a combination that isn't allowed → use an allowed link, or fix what's at each end.",
239
274
  se: "Trace whose (source-type, target-type) pair isn't an allowed combination in the metamodel of legal links.",
275
+ vorschlag: 'Korrigiere die unzulässigen Verbindungen an {n}.',
240
276
  },
241
277
  'R-19': {
242
278
  plain: "A test that's meant to run doesn't point to a test file → add the link to the test file, or, if it isn't written yet, mark it not-yet-written.",
@@ -247,16 +283,19 @@ export const RULE_HELP = {
247
283
  se: "Realized `FUNC` with no valid `realRef` `{file, symbol}` (graph↔code binding); else `concept:true` / `external:true`.",
248
284
  },
249
285
  '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.",
286
+ 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.",
287
+ 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.",
288
+ vorschlag: 'Bringe die Übergaben an {n} in eine Funktionskette, deren Funktionen Anforderungen erfüllen oder die ein Integrationstest prüft.',
252
289
  },
253
290
  'R-22': {
254
291
  plain: "A function isn't assigned to any building block, so it has no home in the structure → put it on a module.",
255
292
  se: "`FUNC` with no `allocate` trace to a `MOD` (deployment assignment); every function lives on exactly one module.",
293
+ vorschlag: 'Ordne die Funktionen {n} Modulen zu.',
256
294
  },
257
295
  'R-23': {
258
296
  plain: "A building block is empty — no function is assigned to it or to any block inside it → put a function on it, or remove the empty block.",
259
297
  se: "`MOD` with no `FUNC` ─allocate→ trace anywhere in its `compose` subtree (`subtreeFuncs`, the same count R-04 uses, CR-SM-321); the module-side complement of R-22 (empty-container signal like R-14/R-16/R-17). A container `MOD` whose sub-modules hold functions is not empty.",
298
+ vorschlag: 'Gib den Modulen {n} Funktionen oder entferne sie.',
260
299
  },
261
300
  'R-26': {
262
301
  plain: "A data format in the model isn't linked to the schema code that defines it → link it to the schema (or mark it concept/outside).",
@@ -269,10 +308,12 @@ export const RULE_HELP = {
269
308
  'R-30': {
270
309
  plain: "This function sits in no chain of effects, so nobody can say which use case it serves — and the checks that would prove its wiring never look at it → add it to the chain of the use case it belongs to.",
271
310
  se: "FUNC belongs to a function chain (CR-GC-366): a `FUNC` needs an incoming `FCHAIN -compose-> FUNC`, directly or inherited from a parent `FUNC` it decomposes from. R-15 demanded the opposite direction — that a chain has functions — and nothing demanded that a function has a chain. That gap is load-bearing: IO-01 (FLOW paths between chain members) and R-21 (integration test per chain) both scope themselves to a chain, so a function outside every chain falls through both nets silently. Severity `warning`, not error: on a real model this fires on the majority of functions, and an error would block every further mutation through the delta gate.",
311
+ vorschlag: 'Ordne die Funktionen {n} der Kette ihres Ablaufs zu.',
272
312
  },
273
313
  'R-31': {
274
314
  plain: "This function has no input or no output, so it is a dead block in the picture — only an actor is allowed to be an end point → connect it to a flow on the missing side.",
275
315
  se: "FUNC is wired (CR-GC-366): a `FUNC` needs at least one incoming `FLOW -io-> FUNC` and one outgoing `FUNC -io-> FLOW`. Only an `ACTOR` may terminate a chain. R-10 asks the same question from the FLOW side ('does this flow have a producer and a consumer?') and therefore never sees a function with no io edge at all — it does not appear in the FLOW loop. IO-01 presupposes chain membership and misses it too. One finding per FUNC naming the missing side(s), not one per side: otherwise the counter exceeds its own denominator contribution, the mis-measurement documented in CR-SM-242.",
316
+ vorschlag: 'Verbinde die Funktionen {n} auf der fehlenden Seite mit einem Datenfluss.',
276
317
  },
277
318
  'RC-01': {
278
319
  plain: "A function points to code that isn't there anymore (file moved or name changed) → repoint it to the current code.",
@@ -310,46 +351,58 @@ export const RULE_HELP = {
310
351
  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
352
  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
353
  },
354
+ 'RC-10': {
355
+ 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.",
356
+ 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).",
357
+ },
313
358
  'RD-01': {
314
359
  plain: "A smallest-piece feature has nothing built to fulfil it → add what implements it.",
315
360
  se: "Leaf `REQ` (no `compose`→`REQ` children) with no `satisfy` from a `FUNC`/`FCHAIN`/`MOD`/`SYS`.",
316
361
  prompt: "se:close-violations",
362
+ vorschlag: 'Lege an, was die Anforderungen {n} erfüllt.',
317
363
  },
318
364
  'RD-02': {
319
365
  plain: "You split a feature into smaller features but you're also building the big one directly → build only the small pieces, not both.",
320
366
  se: "Parent `REQ` (has `compose`→`REQ` children) carrying a direct `FUNC` `satisfy`; the satisfy belongs on the children.",
367
+ vorschlag: 'Lass die Anforderungen {n} nur über ihre Teilanforderungen erfüllen.',
321
368
  },
322
369
  'RD-04': {
323
370
  plain: "One thing has more than 9 parts directly under it → group them, so each level stays readable.",
324
371
  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.",
372
+ vorschlag: 'Gruppiere die Teile unter {n}, es sind zu viele auf einer Ebene.',
325
373
  },
326
374
  'RD-05': {
327
375
  plain: "A level has only one or two parts under it → it groups nothing; fold it into the level above, or gather the parts that belong together under it.",
328
- se: "Decomposition breadth below `metricPolicy.decompositionBreadth.min` (default 3, exclusive) on one level, counted exactly like RD-04 (CR-SM-311, Z3). A level abstracts nothing below it. The finding carries no `value`/`threshold` and does not enter the steering score: it is readability, not boundary width. A level with no children at all is not this rule (empty MOD: R-23; FUNC without children is a leaf).",
376
+ se: "Decomposition breadth below `metricPolicy.decompositionBreadth.min` (default 3, exclusive) on one level, counted exactly like RD-04 (CR-SM-311, Z3). A level abstracts nothing below it. The finding carries no `value`/`threshold` and does not enter the steering score: it is readability, not boundary width. A level with no children at all is not this rule (empty MOD: R-23; FUNC without children is a leaf). The root level directly under `SYS` (`SYS` `compose` `MOD`, root `FUNC` forest) is exempt — there is no level above it, and one module or one root function is an architecture decision (CR-SM-388); RD-04 still applies there.",
377
+ vorschlag: 'Löse die Ebene {n} in die Ebene darüber auf oder sammle unter ihr, was zusammengehört.',
329
378
  },
330
379
  'SC-02': {
331
380
  plain: "A data format is defined but nothing uses it → connect it to the data it describes, or drop it.",
332
381
  se: "`SCHEMA` not referenced by any `FLOW` via a `relation` trace.",
382
+ vorschlag: 'Verbinde das Datenformat {n} mit seinem Datenfluss oder entferne es.',
333
383
  },
334
384
  'UC-01': {
335
385
  plain: "A scenario says what someone wants to do but never says what the system must provide → write down the requirements it needs.",
336
386
  se: "`UC` with no `compose` trace to any `REQ`. The use case carries no requirement content.",
337
387
  prompt: "se:author-req",
388
+ vorschlag: 'Lege für die Abläufe {n} Anforderungen mit Test an: fachliche je Funktion, Ende-zu-Ende-Vorgaben an der Kette.',
338
389
  },
339
390
  'UC-02': {
340
391
  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",
392
+ 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.",
393
+ vorschlag: 'Lege für die Abläufe {n} die Funktionen an, die der Nutzer über einen Datenfluss auslöst.',
343
394
  },
344
395
  'UC-03': {
345
396
  plain: "A scenario says what should be possible but not how it runs → describe the steps as a chain of functions.",
346
397
  se: "`UC` with no `compose` trace to an `FCHAIN`. No behavioural scenario is declared.",
347
398
  prompt: "se:author-uc",
399
+ vorschlag: 'Beschreibe die Abläufe {n} als Kette von Funktionen.',
348
400
  },
349
401
  'UC-04': {
350
402
  plain: "A scenario has no real description, or still carries a placeholder like TBD → write what the user actually wants to achieve.",
351
403
  se: "`UC` description shorter than 10 characters or containing TBD/TODO/FIXME/placeholder/XXX. Description IS the goal (CR-150).",
352
404
  prompt: "se:author-uc",
405
+ vorschlag: 'Beschreibe das Ziel der Abläufe {n}.',
353
406
  },
354
407
  'VR-01': {
355
408
  plain: "A test exists but no result was ever recorded, so nobody knows if it passed → record the outcome.",
@@ -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.
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).
253
260
  //
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.
258
- //
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',
@@ -744,6 +746,12 @@ function decompositionBreadth(graph, policy) {
744
746
  // deshalb keine Zahl im Kontext und steuert nicht — sie ist Lesbarkeit, nicht Randbreite.
745
747
  // Eine Ebene ohne jedes Kind ist nicht ihr Fall: ein leeres MOD meldet R-23, eine FUNC ohne Kinder
746
748
  // ist ein Blatt.
749
+ //
750
+ // CR-SM-388 (ITEM-2026-731): die WURZEL ist ausgenommen — die Ebene direkt unter SYS, also beide
751
+ // Zaehlungen mit SYS als Eltern (`SYS compose MOD` und der FUNC-Wurzelwald). Der Rat „in die Ebene
752
+ // darueber aufloesen" ist dort nicht ausfuehrbar, ueber SYS liegt keine; und ein System aus einem
753
+ // Modul oder einer Wurzelfunktion ist eine Architekturentscheidung, keine leere Abstraktion.
754
+ // RD-04 (Obergrenze) gilt an der Wurzel weiter.
747
755
  // ---------------------------------------------------------------------------
748
756
  function decompositionNarrow(graph, policy) {
749
757
  const band = policy.decompositionBreadth;
@@ -751,7 +759,7 @@ function decompositionNarrow(graph, policy) {
751
759
  return [];
752
760
  const byId = indexOf(graph).byId;
753
761
  return breadthCounts(graph)
754
- .filter(c => c.n < band.min)
762
+ .filter(c => c.n < band.min && byId.get(c.parentId)?.type !== 'SYS')
755
763
  .map(c => {
756
764
  const parent = byId.get(c.parentId);
757
765
  return {
@@ -827,7 +835,9 @@ function actorMustHaveTrace(graph) {
827
835
  severity: 'warning',
828
836
  element_id: a.id,
829
837
  message: `${a.id} has no io traces (disconnected actor)`,
830
- fix_hint: 'Link to a UC or FLOW via io trace',
838
+ // CR-SM-371: ein ACTOR hat genau zwei legale Kanten (ACTOR -io-> FLOW, FLOW -io-> ACTOR);
839
+ // der UC wird ueber den FLOW an einer FUNC seiner Kette erreicht, nie direkt (R-18).
840
+ 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
841
  context: {
832
842
  element_type: a.type,
833
843
  element_name: a.name,
@@ -927,7 +937,6 @@ function validTracePattern(graph) {
927
937
  });
928
938
  if (!why)
929
939
  return [];
930
- const { detail, hint } = explainRejection(why, t, src, tgt);
931
940
  // CR-SM-341: derselbe Grund, den `detail` in Prosa giesst — hier zusaetzlich als Struktur.
932
941
  // Keine zweite Rechnung: `why` liegt vor, die Alternativen befragen `isValidTrace`.
933
942
  const shape = {
@@ -936,6 +945,7 @@ function validTracePattern(graph) {
936
945
  targetKinds: idx.byId.get(t.target)?.kinds,
937
946
  };
938
947
  const alt = admittingAlternatives(shape);
948
+ const { detail, hint } = explainRejection(why, t, src, tgt, alt.sources);
939
949
  const where = why.reason === 'no-pattern' ? undefined : why.pattern.where;
940
950
  return [{
941
951
  rule_id: 'R-18',
@@ -970,7 +980,7 @@ function validTracePattern(graph) {
970
980
  * `kinds` steht da und passt nicht (Kante heben oder Kind korrigieren). Wer das nicht
971
981
  * unterscheiden kann, sucht am falschen Ende.
972
982
  */
973
- function explainRejection(why, t, src, tgt) {
983
+ function explainRejection(why, t, src, tgt, admittingSources) {
974
984
  if (why.reason === 'no-pattern') {
975
985
  return {
976
986
  detail: `${src} → ${tgt} is not a valid ${t.type} pattern`,
@@ -991,7 +1001,11 @@ function explainRejection(why, t, src, tgt) {
991
1001
  }
992
1002
  : {
993
1003
  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)`,
1004
+ // CR-SM-366: den Erfueller NENNEN statt "FCHAIN carries every kind" — das galt nie fuer
1005
+ // die Code-Bindung und gilt seit dem FCHAIN-where gar nicht mehr.
1006
+ hint: admittingSources.length > 0
1007
+ ? `Correct '${where.field}' on ${end}, or ${t.type} it from ${admittingSources.join(' | ')} — the pattern admitting {${[...why.declared].sort().join(', ')}}`
1008
+ : `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
1009
  };
996
1010
  }
997
1011
  /**
@@ -1270,7 +1284,7 @@ function funcMustHaveCodeBinding(graph) {
1270
1284
  severity: 'warning',
1271
1285
  element_id: fn.id,
1272
1286
  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`,
1287
+ fix_hint: `Bind the leaves via graph_mutate Format-E (${leafList.map((id) => `\`~ ${id} @realRef {"file":…,"symbol":…}\``).join(', ')}) — the parent inherits their binding`,
1274
1288
  context: { element_type: fn.type, element_name: fn.name },
1275
1289
  });
1276
1290
  }
@@ -1300,6 +1314,10 @@ function funcMustHaveCodeBinding(graph) {
1300
1314
  // Co-adjacency at a shared FLOW is not an asserted interface — treating it
1301
1315
  // as one produced P·C findings per hub FLOW and made reuse the expensive
1302
1316
  // choice. The FCHAIN is the modelled claim; the test is owed on the claim.
1317
+ // CR-SM-376: second way to cover a chain. Since CR-SM-366 an FCHAIN carries
1318
+ // only NFRs; its effect is covered through its members when EVERY member
1319
+ // FUNC (FCHAIN ─compose→ FUNC) satisfies at least one REQ. Verifying those
1320
+ // member REQs is R-01's job, not R-21's.
1303
1321
  // ---------------------------------------------------------------------------
1304
1322
  function fchainMustHaveIntegrationTest(graph, policy) {
1305
1323
  const idx = indexOf(graph);
@@ -1333,6 +1351,22 @@ function fchainMustHaveIntegrationTest(graph, policy) {
1333
1351
  testedChains.add(t.source);
1334
1352
  }
1335
1353
  }
1354
+ // CR-SM-376: der zweite Weg — die Kette ist ueber ihre Glieder gedeckt, wenn JEDE Glied-FUNC
1355
+ // (direkte FCHAIN -compose-> FUNC, dieselbe Definition wie `chainsByFunc`) eine REQ erfuellt.
1356
+ // `unmetMembers` liefert die Glieder ohne REQ; leer = gedeckt. Eine Kette ohne Glied kann hier
1357
+ // nicht auftauchen: ZWEIG 2 hat mindestens die beiden Enden der Uebergabe als Glieder.
1358
+ const funcsWithReq = new Set(idx.tracesOfType('satisfy').filter(t => isFunc(t.source) && typeOf.get(t.target) === 'REQ').map(t => t.source));
1359
+ const membersOf = new Map();
1360
+ for (const [fn, chains] of chainsOf) {
1361
+ for (const ch of chains) {
1362
+ const list = membersOf.get(ch);
1363
+ if (list)
1364
+ list.push(fn);
1365
+ else
1366
+ membersOf.set(ch, [fn]);
1367
+ }
1368
+ }
1369
+ const unmetMembers = (ch) => (membersOf.get(ch) ?? []).filter(fn => !funcsWithReq.has(fn)).sort();
1336
1370
  // CR-SM-313: die Infrastruktur-Ausnahme. Die ZAHL kommt aus der Kennzahl (CR-SM-314), nicht
1337
1371
  // aus einer zweiten Rechnung — eine Uebergabe an eine Funktion, die quer durch alle Ketten
1338
1372
  // laeuft, ist kein fehlender Integrationstest, sondern ein Bus. `null` = keine Ausnahme.
@@ -1372,8 +1406,9 @@ function fchainMustHaveIntegrationTest(graph, policy) {
1372
1406
  });
1373
1407
  continue;
1374
1408
  }
1375
- // ZWEIG 2 — gemeinsame Kette, aber keine davon geprueft: Befund an der Kette. Unveraendert.
1376
- if (shared.some(ch => testedChains.has(ch)))
1409
+ // ZWEIG 2 — gemeinsame Kette, aber keine davon gedeckt: Befund an der Kette. Gedeckt heisst
1410
+ // (a) verifizierte Ketten-REQ oder (b, CR-SM-376) jede Glied-FUNC erfuellt eine REQ.
1411
+ if (shared.some(ch => testedChains.has(ch) || unmetMembers(ch).length === 0))
1377
1412
  continue;
1378
1413
  const anchor = shared[0];
1379
1414
  const key = `${anchor}|${p}>${c}`;
@@ -1385,8 +1420,8 @@ function fchainMustHaveIntegrationTest(graph, policy) {
1385
1420
  rule_id: 'R-21',
1386
1421
  severity: 'warning',
1387
1422
  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',
1423
+ message: `${anchor} has no integration test covering connection ${p} → ${c}; member FUNCs without a REQ: ${unmetMembers(anchor).join(', ')}`,
1424
+ 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
1425
  context: { element_type: anchorEl?.type, element_name: anchorEl?.name },
1391
1426
  });
1392
1427
  }
@@ -1650,7 +1685,7 @@ export const V3_RULES = [
1650
1685
  { id: 'R-19', name: 'Runnable TEST binding', severity: 'warning', evaluate: testMustHaveRunnableBinding, domain: ['TEST'] },
1651
1686
  { id: 'R-29', name: 'Test file exclusivity', severity: 'error', evaluate: testFileExclusivity, domain: ['TEST'] },
1652
1687
  { 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'] },
1688
+ { 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
1689
  { id: 'R-22', name: 'FUNC must be allocated to MOD', severity: 'warning', evaluate: funcMustBeAllocated, domain: ['FUNC'] },
1655
1690
  { id: 'R-23', name: 'MOD must have allocated FUNC', severity: 'warning', evaluate: modMustHaveAllocatedFunc, domain: ['MOD'] },
1656
1691
  { id: 'R-26', name: 'SCHEMA must have realRef', severity: 'warning', evaluate: schemaMustHaveSchemaRef, domain: ['SCHEMA'] },
@@ -1658,7 +1693,7 @@ export const V3_RULES = [
1658
1693
  { id: 'RD-01', name: 'Unresolved requirement', severity: 'warning', evaluate: unresolvedRequirement, domain: ['REQ'] },
1659
1694
  { id: 'RD-02', name: 'Decomposition consistency', severity: 'warning', evaluate: decompositionConsistency, domain: ['REQ'] },
1660
1695
  { id: 'RD-04', name: 'Decomposition breadth', severity: 'warning', evaluate: decompositionBreadth, domain: ['FUNC', 'MOD', 'SYS'] },
1661
- { id: 'RD-05', name: 'Decomposition too narrow', severity: 'warning', evaluate: decompositionNarrow, domain: ['FUNC', 'MOD', 'SYS'] },
1696
+ { id: 'RD-05', name: 'Decomposition too narrow', severity: 'warning', evaluate: decompositionNarrow, domain: ['FUNC', 'MOD'] },
1662
1697
  { id: 'MS-01', name: 'Milestone empty scope', severity: 'warning', evaluate: msEmptyScope, domain: ['MS'] },
1663
1698
  { id: 'MS-02', name: 'Milestone dangling dependency', severity: 'warning', evaluate: msDanglingDependency, domain: ['MS'] },
1664
1699
  { id: 'R-30', name: 'FUNC leaf must belong to a function chain', severity: 'warning', evaluate: funcMustBeInEffectChain, domain: ['FUNC'] },
@@ -1,4 +1,5 @@
1
1
  import { indexOf } from './graph-index.js';
2
+ import { hasPlaceholder } from './placeholder.js';
2
3
  // ---------------------------------------------------------------------------
3
4
  // UC-01: UC must have compose→REQ trace (CR-165: satisfy UC→REQ is invalid per meta-model)
4
5
  // ---------------------------------------------------------------------------
@@ -94,13 +95,12 @@ export function uc03HasScenario(graph) {
94
95
  // UC-04: UC must have meaningful description (CR-150: description = goal)
95
96
  // ---------------------------------------------------------------------------
96
97
  const UC_DESC_MIN_LEN = 10;
97
- const PLACEHOLDER_RE = /\b(tbd|todo|fixme|placeholder|xxx)\b/i;
98
98
  export function uc04GoalDefined(graph) {
99
99
  return graph.elements
100
100
  .filter(e => e.type === 'UC')
101
101
  .filter(uc => {
102
102
  const d = uc.description?.trim() ?? '';
103
- return d.length < UC_DESC_MIN_LEN || PLACEHOLDER_RE.test(d);
103
+ return d.length < UC_DESC_MIN_LEN || hasPlaceholder(d);
104
104
  })
105
105
  .map(uc => ({
106
106
  rule_id: 'UC-04',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sigloch/contracts",
3
- "version": "10.12.0",
3
+ "version": "10.14.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",