@sigloch/contracts 10.14.0 → 12.0.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.
Files changed (40) hide show
  1. package/dist/se/analysis-freshness-rules.d.ts +37 -30
  2. package/dist/se/analysis-freshness-rules.js +41 -81
  3. package/dist/se/ao-rules.js +3 -3
  4. package/dist/se/chain-metrics.d.ts +116 -0
  5. package/dist/se/chain-metrics.js +311 -0
  6. package/dist/se/conformance-rules.d.ts +4 -0
  7. package/dist/se/conformance-rules.js +37 -32
  8. package/dist/se/cr-quality-rules.js +4 -76
  9. package/dist/se/evaluate-all.d.ts +7 -3
  10. package/dist/se/evaluate-all.js +3 -4
  11. package/dist/se/fchain-quality-rules.js +4 -4
  12. package/dist/se/fmea-rules.js +4 -4
  13. package/dist/se/format-e-parser.js +1 -1
  14. package/dist/se/grammar-snapshot.d.ts +6 -6
  15. package/dist/se/grammar-snapshot.js +90 -103
  16. package/dist/se/index.d.ts +4 -2
  17. package/dist/se/index.js +6 -2
  18. package/dist/se/metric-rules.d.ts +9 -5
  19. package/dist/se/metric-rules.js +10 -9
  20. package/dist/se/module-crossings.d.ts +10 -0
  21. package/dist/se/module-crossings.js +3 -1
  22. package/dist/se/near-duplicate-rules.d.ts +2 -0
  23. package/dist/se/near-duplicate-rules.js +2 -2
  24. package/dist/se/ontology.d.ts +23 -7
  25. package/dist/se/ontology.js +29 -26
  26. package/dist/se/quality-rules.js +5 -5
  27. package/dist/se/readiness.d.ts +41 -149
  28. package/dist/se/readiness.js +50 -280
  29. package/dist/se/rule-due.d.ts +26 -0
  30. package/dist/se/rule-due.js +72 -0
  31. package/dist/se/rule-help.d.ts +3 -1
  32. package/dist/se/rule-help.js +47 -31
  33. package/dist/se/rules.d.ts +23 -13
  34. package/dist/se/rules.js +112 -72
  35. package/dist/se/schema-quality-rules.d.ts +1 -1
  36. package/dist/se/schema-quality-rules.js +1 -1
  37. package/dist/se/similarity.js +4 -10
  38. package/dist/se/uc-quality-rules.js +4 -4
  39. package/dist/se/view-rules.js +6 -4
  40. package/package.json +1 -1
@@ -0,0 +1,311 @@
1
+ /**
2
+ * CR-SM-404 — Kennzahlen der Wirkkette: je FCHAIN eine Zahl, oder der Grund, warum es keine gibt.
3
+ *
4
+ * Die Leitlinie verlangt, die Realisierungsarchitektur AUF DER WIRKKETTE zu bewerten (graphcode
5
+ * `docs/graphcode_architektur_konzept.md`, „Kennzahlen der Wirkkette"). Bis hierher rechnete das
6
+ * nur ein Spike in graphcode (`scripts/spike-kettenkennzahlen.mjs`); von dort ist die Rechnung
7
+ * portiert, nicht neu erfunden. Gemessen 2026-10-09 ueber 10 Familie-Graphen: 76 Ketten, 31
8
+ * bewertbar.
9
+ *
10
+ * ## Diese Datei URTEILT NICHT
11
+ *
12
+ * Keine Regel, keine Schwelle, kein Katalogeintrag — dieselbe Trennung wie `moduleMetrics` und
13
+ * `functionCriticality`. Ob eine Laenge von 6 gut ist, sagt erst ein Profil.
14
+ *
15
+ * ## Nicht bewertbar heisst: KEINE Zahl
16
+ *
17
+ * Eine FCHAIN ist grammatisch eine Menge; die Reihenfolge entsteht erst aus Erzeuger ->
18
+ * Verbraucher (`FUNC -io-> FLOW -io-> FUNC`, beide Enden Glied). Zerfaellt die Kette, beschriebe
19
+ * jede Kennzahl etwas, das es nicht gibt. Eine solche Kette traegt deshalb ihre Gruende und kein
20
+ * einziges Zahlenfeld — das Schema laesst es nicht zu. Sie zaehlt im NENNER der Quote
21
+ * (`measurability`), die neben jedem Wert stehen muss wie die Bindungsquote neben RC-*.
22
+ *
23
+ * ## Wer urteilt, ob eine Kette eine Kette ist
24
+ *
25
+ * - `FC-05`: die Regel FC-05, aufgerufen, nicht nachgebaut. Ursache (fehlendes Glied, Sack) und
26
+ * die Nebenkomponenten stehen in ihrem Befund (`context.side_components`), nicht hier.
27
+ * - `loose-member`: ein Glied ohne io-Eingang oder -Ausgang. Das ist die Bedingung, unter der
28
+ * FC-05 sich ENTHAELT (R-31 meldet das Glied) — ohne diesen Grund haette eine solche Kette weder
29
+ * einen FC-05-Befund noch einen Grund und bekaeme Zahlen.
30
+ * - `no-entry` / `no-exit`: kein FLOW von bzw. zu einem Knoten AUSSERHALB der Kette. Weiter als
31
+ * FC-04, das nur einen ACTOR gelten laesst: eine Kette, die eine andere fortsetzt, ist messbar.
32
+ * - `empty`: keine FUNC (R-15).
33
+ *
34
+ * ## Ein Kettenbegriff, ein Modulbegriff
35
+ *
36
+ * Glied = direkte `FCHAIN -compose-> FUNC`-Kante, ueber `chainsByFunc` (R-30s Definition).
37
+ * Modul einer FUNC = direkte `allocate`-Kante, wie `module-crossings.ts` und `moduleMetrics`.
38
+ * Der Spike erbte das Modul ueber compose-Vorfahren; gemessen sind 326 von 326 Kettengliedern
39
+ * des Korpus direkt alloziert — die Vererbung kauft nichts und waere eine zweite Vorstellung von
40
+ * Zugehoerigkeit im selben Paket.
41
+ *
42
+ * ## CR-SM-406 — die ORTE je Kette, nur erkannt
43
+ *
44
+ * Neben jeder Zahl steht, WO sie herkommt: die Mitglieder je Rueckkopplung, die geteilten
45
+ * Funktionen, die Grenzen je gerichtetem Modulpaar mit ihren Vertraegen, dazu Zulauf, Importe und
46
+ * Uebergaben. Reine Zusatzfelder — die Zahlen aus CR-SM-404 bleiben und sind die Laenge ihrer
47
+ * Liste. Weiter kein Urteil.
48
+ *
49
+ * - Import und Uebergabe zaehlen nur an Gliedern, die ALLEIN dieser Kette gehoeren. Was an einer
50
+ * geteilten Funktion haengt, steht an der geteilten Funktion — sie ist ein eigener Ort. Ohne
51
+ * diese Grenze meldete jede Kette durch Gate und Store deren Zufluesse als ihre eigenen
52
+ * (gemessen am graphcode-Modell 2026-10-10: 333 statt 16).
53
+ * - Ein ACTOR als Erzeuger oder Verbraucher ist der Ein- und Ausgang der Kette, kein Ort.
54
+ * - Vertrag = `contractsOfFlow` aus `module-crossings.ts` (SCHEMA, sonst `UNBOUND:<flow>`), hier je
55
+ * Kettenkante gelesen. `moduleCrossings().pairs` selbst traegt die Vertraege des GANZEN Graphen
56
+ * je ungerichtetem Paar und kann „elf Kanten auf einem Vertrag" in EINER Kette nicht zeigen.
57
+ *
58
+ * @author andreas@siglochconsulting
59
+ */
60
+ import { z } from 'zod/v4';
61
+ import { indexOf } from './graph-index.js';
62
+ import { chainsByFunc } from './function-criticality.js';
63
+ import { fc05ChainConnected } from './fchain-quality-rules.js';
64
+ import { contractsOfFlow } from './module-crossings.js';
65
+ const count = z.number().int().nonnegative();
66
+ const identity = { chainId: z.string(), chainName: z.string() };
67
+ /** CR-SM-406: ein Fluss an der Kettengrenze und das Glied der Kette, an dem er haengt. */
68
+ const FlowAtFunc = z.strictObject({ flow: z.string(), func: z.string() });
69
+ /**
70
+ * Eine bewertbare Kette: die fuenf heute rechenbaren Kennzahlen plus die Engstellen-Schranke,
71
+ * dazu die Orte (CR-SM-406). Jede Liste ist uid-sortiert.
72
+ */
73
+ const MeasuredChain = z.strictObject({
74
+ ...identity,
75
+ measurable: z.literal(true),
76
+ /** Gesamtlaenge: FUNC-Schritte auf dem laengsten Pfad; eine Schleife zaehlt als EIN Schritt. */
77
+ length: count,
78
+ /** Verzweigungsgrad: groesster Ausgangsgrad eines Glieds INNERHALB der Kette. */
79
+ branching: count,
80
+ /**
81
+ * Modulgrenzen: Kettenkanten, deren Enden in verschiedenen MOD liegen.
82
+ * `null`, sobald ein Glied kein Modul hat (R-22 meldet es) — keine geratene Zahl.
83
+ */
84
+ moduleBoundaries: count.nullable(),
85
+ /** Rueckkopplungen: nicht-triviale starke Komponenten ueber die Kettenkanten. */
86
+ feedbackLoops: count,
87
+ /** Geteilte Knoten: Glieder, die in mehr als einer Kette liegen. */
88
+ sharedFuncs: count,
89
+ /**
90
+ * Engstellen — OBERE SCHRANKE: geteilte Glieder mit Ein- UND Ausgang in der Kette. Der
91
+ * Konzeptbegriff verlangt „synchron durchlaufen"; ohne `FLOW.sync` zaehlt jeder Durchgang.
92
+ */
93
+ bottlenecks: count,
94
+ /** Synchrone Tiefe — in dieser Stufe nie berechnet: braucht den Leser von `FLOW.sync` (CR-SM-364). */
95
+ syncDepth: z.null(),
96
+ /** Fehlerpfad-Tiefe — nicht berechenbar: Fehlerfluesse traegt kein Attribut (CR-SM-364, `kind` verworfen). */
97
+ errorPathDepth: z.null(),
98
+ /** Zahl der Glieder. */
99
+ memberCount: count,
100
+ /** Zulauf: groesster Eingangsgrad eines Glieds INNERHALB der Kette — das Gegenstueck zu `branching`. */
101
+ fanIn: count,
102
+ /**
103
+ * Die Rueckkopplungen, die `feedbackLoops` zaehlt. `id` entsteht allein aus den Mitgliedern
104
+ * (uids mit `+` verbunden) — dieselbe Schleife traegt in jeder Kette dieselbe Kennung.
105
+ */
106
+ loops: z.array(z.strictObject({ id: z.string(), members: z.array(z.string()).min(2) })),
107
+ /** Die geteilten Glieder, die `sharedFuncs` zaehlt. */
108
+ shared: z.array(z.string()),
109
+ /**
110
+ * Die Modulgrenzen je GERICHTETEM Modulpaar: `edges` Kettenkanten von `from` nach `to` (ihre
111
+ * Summe ist `moduleBoundaries`) auf den verschiedenen Vertraegen `contracts`. `null` wie dort.
112
+ */
113
+ boundaries: z.array(z.strictObject({
114
+ from: z.string(), to: z.string(), edges: count.min(1), contracts: z.array(z.string()).min(1),
115
+ })).nullable(),
116
+ /**
117
+ * Importe: ein Glied, das NUR in dieser Kette liegt, verbraucht einen Fluss, den mindestens
118
+ * eine FUNC erzeugt und kein Glied der Kette.
119
+ */
120
+ imports: z.array(FlowAtFunc),
121
+ /**
122
+ * Uebergaben, gespiegelt: ein Glied, das nur in dieser Kette liegt, erzeugt einen Fluss, den
123
+ * mindestens eine FUNC verbraucht und kein Glied der Kette.
124
+ */
125
+ handovers: z.array(FlowAtFunc),
126
+ });
127
+ /** Eine nicht bewertbare Kette: der Grund statt der Zahl. Reihenfolge der Gruende ist fest. */
128
+ const UnmeasurableChain = z.strictObject({
129
+ ...identity,
130
+ measurable: z.literal(false),
131
+ reasons: z.array(z.enum(['FC-05', 'loose-member', 'no-entry', 'no-exit', 'empty'])).min(1),
132
+ });
133
+ // CR-SM-361: ein Zod-Vertrag, kein Typ — der Erzeuger prueft sein Ergebnis.
134
+ export const ChainMetrics = z.object({
135
+ /** Eine Zeile je FCHAIN, in GRAPH-Reihenfolge — keine Rangfolge, die Zahl ist kein Urteil. */
136
+ chains: z.array(z.discriminatedUnion('measurable', [MeasuredChain, UnmeasurableChain])),
137
+ /** Die Reichweite der Aussage. `ratio` ist `null` ohne Ketten — kein Ersatzwert. */
138
+ measurability: z.object({ chains: count, measurable: count, ratio: z.number().min(0).max(1).nullable() }),
139
+ });
140
+ /** Die Kennzahlen aller FCHAINs dieses Graphen und die Quote der bewertbaren. */
141
+ export function chainMetrics(graph) {
142
+ const idx = indexOf(graph);
143
+ const chainsOf = chainsByFunc(graph);
144
+ // FC-05 ist der EINE Leser fuer „gerichtet zusammenhaengend" — hier zaehlt nur, ob er meldet.
145
+ const fc05 = new Set(fc05ChainConnected(graph).map((v) => v.element_id));
146
+ const flowsInto = (id) => idx.in(id, 'io').map((t) => t.source).filter((s) => idx.typeOf(s) === 'FLOW');
147
+ const flowsOutOf = (id) => idx.out(id, 'io').map((t) => t.target).filter((s) => idx.typeOf(s) === 'FLOW');
148
+ const moduleOf = (func) => idx.out(func, 'allocate').map((t) => t.target).find((m) => idx.typeOf(m) === 'MOD');
149
+ const chains = idx.elementsOfType('FCHAIN').map((fc) => {
150
+ const who = { chainId: fc.id, chainName: fc.name };
151
+ const members = [...new Set(idx.out(fc.id, 'compose').map((t) => t.target).filter((id) => idx.typeOf(id) === 'FUNC'))].sort();
152
+ if (members.length === 0)
153
+ return { ...who, measurable: false, reasons: ['empty'] };
154
+ const inChain = new Set(members);
155
+ // Kettenkanten Erzeuger -> Verbraucher; dazu, wo die Kette von aussen gespeist wird und wo sie hinausliefert.
156
+ const next = new Map(members.map((m) => [m, new Set()]));
157
+ const carriers = new Map(); // Kettenkante `a b` -> die FLOWs, die sie tragen
158
+ let entry = false;
159
+ let exit = false;
160
+ let loose = false;
161
+ for (const m of members) {
162
+ const ins = flowsInto(m);
163
+ const outs = flowsOutOf(m);
164
+ if (ins.length === 0 || outs.length === 0)
165
+ loose = true;
166
+ for (const flow of ins)
167
+ if (idx.in(flow, 'io').some((t) => !inChain.has(t.source)))
168
+ entry = true;
169
+ for (const flow of outs) {
170
+ for (const t of idx.out(flow, 'io')) {
171
+ if (t.target === m)
172
+ continue;
173
+ if (!inChain.has(t.target)) {
174
+ exit = true;
175
+ continue;
176
+ }
177
+ next.get(m).add(t.target);
178
+ const edge = `${m} ${t.target}`;
179
+ (carriers.get(edge) ?? carriers.set(edge, new Set()).get(edge)).add(flow);
180
+ }
181
+ }
182
+ }
183
+ const reasons = [];
184
+ if (fc05.has(fc.id))
185
+ reasons.push('FC-05');
186
+ if (loose)
187
+ reasons.push('loose-member');
188
+ if (!entry)
189
+ reasons.push('no-entry');
190
+ if (!exit)
191
+ reasons.push('no-exit');
192
+ if (reasons.length > 0)
193
+ return { ...who, measurable: false, reasons };
194
+ // Rueckkopplungen: Tarjan — jede starke Komponente mit mehr als einem Glied ist eine Schleife.
195
+ const order = new Map();
196
+ const low = new Map();
197
+ const onStack = new Set();
198
+ const stack = [];
199
+ const sccOf = new Map();
200
+ const loops = [];
201
+ const visit = (v) => {
202
+ order.set(v, order.size);
203
+ low.set(v, order.get(v));
204
+ stack.push(v);
205
+ onStack.add(v);
206
+ for (const w of next.get(v)) {
207
+ if (!order.has(w)) {
208
+ visit(w);
209
+ low.set(v, Math.min(low.get(v), low.get(w)));
210
+ }
211
+ else if (onStack.has(w))
212
+ low.set(v, Math.min(low.get(v), order.get(w)));
213
+ }
214
+ if (low.get(v) !== order.get(v))
215
+ return;
216
+ const comp = [];
217
+ let w;
218
+ do {
219
+ w = stack.pop();
220
+ onStack.delete(w);
221
+ comp.push(w);
222
+ } while (w !== v);
223
+ if (comp.length > 1)
224
+ loops.push({ id: comp.sort().join('+'), members: comp });
225
+ for (const c of comp)
226
+ sccOf.set(c, v);
227
+ };
228
+ for (const m of members)
229
+ if (!order.has(m))
230
+ visit(m);
231
+ loops.sort((a, b) => (a.id < b.id ? -1 : 1));
232
+ // Gesamtlaenge: laengster Pfad auf dem Kondensat — eine Schleife ist dort ein Knoten.
233
+ const condensed = new Map();
234
+ for (const [a, targets] of next) {
235
+ for (const b of targets) {
236
+ const ca = sccOf.get(a);
237
+ const cb = sccOf.get(b);
238
+ if (ca !== cb)
239
+ (condensed.get(ca) ?? condensed.set(ca, new Set()).get(ca)).add(cb);
240
+ }
241
+ }
242
+ const longestFrom = new Map();
243
+ const longest = (c) => {
244
+ const known = longestFrom.get(c);
245
+ if (known !== undefined)
246
+ return known;
247
+ let best = 1;
248
+ for (const n of condensed.get(c) ?? [])
249
+ best = Math.max(best, 1 + longest(n));
250
+ longestFrom.set(c, best);
251
+ return best;
252
+ };
253
+ const length = Math.max(...members.map((m) => longest(sccOf.get(m))));
254
+ const allocated = members.every((m) => moduleOf(m) !== undefined);
255
+ let branching = 0;
256
+ const inDegree = new Map();
257
+ const pairs = new Map();
258
+ for (const [a, targets] of next) {
259
+ branching = Math.max(branching, targets.size);
260
+ for (const b of targets) {
261
+ inDegree.set(b, (inDegree.get(b) ?? 0) + 1);
262
+ const from = moduleOf(a);
263
+ const to = moduleOf(b);
264
+ if (from === undefined || to === undefined || from === to)
265
+ continue;
266
+ const key = `${from} ${to}`;
267
+ const pair = pairs.get(key) ?? pairs.set(key, { from, to, edges: 0, contracts: new Set() }).get(key);
268
+ pair.edges++;
269
+ for (const flow of carriers.get(`${a} ${b}`))
270
+ for (const c of contractsOfFlow(idx, flow))
271
+ pair.contracts.add(c);
272
+ }
273
+ }
274
+ const boundaries = [...pairs.entries()].sort(([a], [b]) => (a < b ? -1 : 1))
275
+ .map(([, p]) => ({ ...p, contracts: [...p.contracts].sort() }));
276
+ const shared = members.filter((m) => (chainsOf.get(m)?.size ?? 0) > 1);
277
+ // Importe und Uebergaben: nur an Gliedern, die allein dieser Kette gehoeren; die andere Seite
278
+ // des Flusses ist mindestens eine FUNC und kein Glied (ein ACTOR ist Ein-/Ausgang, kein Ort).
279
+ const own = members.filter((m) => !shared.includes(m));
280
+ const foreign = (ends) => {
281
+ const funcs = ends.filter((id) => idx.typeOf(id) === 'FUNC');
282
+ return funcs.length > 0 && !funcs.some((f) => inChain.has(f));
283
+ };
284
+ const at = (flows, farEnds) => own.flatMap((func) => [...new Set(flows(func))].sort()
285
+ .filter((flow) => foreign(farEnds(flow))).map((flow) => ({ flow, func })));
286
+ return {
287
+ ...who,
288
+ measurable: true,
289
+ length,
290
+ branching,
291
+ moduleBoundaries: allocated ? boundaries.reduce((n, p) => n + p.edges, 0) : null,
292
+ feedbackLoops: loops.length,
293
+ sharedFuncs: shared.length,
294
+ bottlenecks: shared.filter((m) => inDegree.has(m) && next.get(m).size > 0).length,
295
+ syncDepth: null,
296
+ errorPathDepth: null,
297
+ memberCount: members.length,
298
+ fanIn: Math.max(0, ...inDegree.values()),
299
+ loops,
300
+ shared,
301
+ boundaries: allocated ? boundaries : null,
302
+ imports: at(flowsInto, (flow) => idx.in(flow, 'io').map((t) => t.source)),
303
+ handovers: at(flowsOutOf, (flow) => idx.out(flow, 'io').map((t) => t.target)),
304
+ };
305
+ });
306
+ const measurable = chains.filter((c) => c.measurable).length;
307
+ return ChainMetrics.parse({
308
+ chains,
309
+ measurability: { chains: chains.length, measurable, ratio: chains.length > 0 ? measurable / chains.length : null },
310
+ });
311
+ }
@@ -17,6 +17,7 @@
17
17
  import { z } from 'zod/v4';
18
18
  import type { OntologyGraph } from './ontology.js';
19
19
  import type { RuleSeverity, RuleViolation } from './rules.js';
20
+ import type { RuleRole, RuleStage } from './readiness.js';
20
21
  /** Parser facts about one source file (extracted by the executor). */
21
22
  export declare const FileFactsSchema: z.ZodObject<{
22
23
  exists: z.ZodBoolean;
@@ -67,6 +68,9 @@ export interface ConformanceRuleDefinition {
67
68
  id: string;
68
69
  name: string;
69
70
  severity: RuleSeverity;
71
+ /** CR-SM-395: Stufe und Rolle wie an jeder Katalogregel (`RuleDefinition`). */
72
+ stage: RuleStage;
73
+ role?: RuleRole;
70
74
  /**
71
75
  * CR-SM-305: die Grundgesamtheit, ueber die die Regel meldet — dieselbe Bedeutung wie an
72
76
  * jeder Katalogregel (CR-SM-235), damit RC in `ALL_RULE_DEFS` stehen kann, ohne dass ein
@@ -17,6 +17,7 @@
17
17
  import { z } from 'zod/v4';
18
18
  import { readTestRefs, readRealRef } from './ontology.js';
19
19
  import { CLOSED_STATUS } from './cr-quality-rules.js';
20
+ import { whenDue } from './rule-due.js';
20
21
  /** Parser facts about one source file (extracted by the executor). */
21
22
  export const FileFactsSchema = z.object({
22
23
  /** File exists on disk (repo-relative path). */
@@ -83,7 +84,7 @@ export const CodeFactsSchema = z.object({
83
84
  });
84
85
  const missingFile = (facts, file) => facts.files[file]?.exists !== true;
85
86
  // RC-01: every valid FUNC realRef must resolve — file on disk, symbol declared in
86
- // it. Presence is R-20's concern (incl. concept/external/decomposition-parent
87
+ // it. Presence is R-20's concern (incl. external/decomposition-parent
87
88
  // exemptions, CR-GC-244); RC-01 only judges bindings that exist. `lang:'prompt'`
88
89
  // is realized by a skill file — file-exists is the whole binding. A realRef with
89
90
  // no `symbol` (CR-228) is also file-only: resolution stops at file-exists.
@@ -92,7 +93,7 @@ function codeRefMustResolve(graph, facts) {
92
93
  for (const el of graph.elements) {
93
94
  if (el.type !== 'FUNC')
94
95
  continue;
95
- if (el.attributes?.concept === true || el.attributes?.external === true)
96
+ if (el.attributes?.external === true)
96
97
  continue;
97
98
  const parsed = readRealRef(el.attributes);
98
99
  if (parsed.state !== 'bound')
@@ -136,8 +137,6 @@ function testRefMustResolve(graph, facts) {
136
137
  for (const el of graph.elements) {
137
138
  if (el.type !== 'TEST')
138
139
  continue;
139
- if (el.attributes?.concept === true)
140
- continue;
141
140
  const parsed = readTestRefs(el.attributes);
142
141
  if (parsed.state !== 'bound')
143
142
  continue; // no/invalid binding → R-19 territory
@@ -172,7 +171,7 @@ function testRefMustResolve(graph, facts) {
172
171
  }
173
172
  // RC-03: every valid SCHEMA realRef must resolve — file on disk, symbol declared
174
173
  // in it (CR-211, unified CR-228). Presence (a SCHEMA with NO realRef) is the
175
- // concern of the R-26 presence rule, not RC-03; concept/external SCHEMAs are exempt
174
+ // concern of the R-26 presence rule, not RC-03; external SCHEMAs are exempt
176
175
  // there and here. Severity error, like RC-01: a bound-but-broken schema IS a defect.
177
176
  // A symbol-less realRef stops at file-exists. Fires 0× until realRefs exist.
178
177
  function schemaRefMustResolve(graph, facts) {
@@ -180,7 +179,7 @@ function schemaRefMustResolve(graph, facts) {
180
179
  for (const el of graph.elements) {
181
180
  if (el.type !== 'SCHEMA')
182
181
  continue;
183
- if (el.attributes?.concept === true || el.attributes?.external === true)
182
+ if (el.attributes?.external === true)
184
183
  continue;
185
184
  const parsed = readRealRef(el.attributes);
186
185
  if (parsed.state !== 'bound')
@@ -274,7 +273,7 @@ function boundToNonZod(facts, ref) {
274
273
  // but NONE of their realRef files import AND parse (`.parse`/`.safeParse`) the
275
274
  // schema symbol, the modelled validation is missing. Severity warn (the parse may
276
275
  // legitimately sit in a framework layer, not the FUNC's own file). Skips when the
277
- // SCHEMA is concept-only, has no realRef (or a symbol-less one), or no io-connected
276
+ // SCHEMA has no realRef (or a symbol-less one), or no io-connected
278
277
  // realized FUNC. `external` is NOT exempt (CR-SM-318, ITEM-2026-064): an external SCHEMA
279
278
  // with a realRef is a foreign-API contract held as a Zod schema in OUR code — the
280
279
  // boundary to the foreign system is exactly where that parse belongs.
@@ -284,8 +283,6 @@ function schemaRefMustBeUsed(graph, facts) {
284
283
  for (const el of graph.elements) {
285
284
  if (el.type !== 'SCHEMA')
286
285
  continue;
287
- if (el.attributes?.concept === true)
288
- continue;
289
286
  const parsed = readRealRef(el.attributes);
290
287
  if (parsed.state !== 'bound' || parsed.value.symbol === undefined)
291
288
  continue;
@@ -308,7 +305,7 @@ function schemaRefMustBeUsed(graph, facts) {
308
305
  (actual.length > 0 ? ` — it is parsed in: ${actual.join(', ')}` : ''),
309
306
  fix_hint: actual.length > 0
310
307
  ? `The parse sits in ${actual.join(', ')}: point the FUNC realRef at that file, or move the parse into the modelled FUNC`
311
- : `Import and call ${ref.symbol}.parse()/.safeParse() in one of the io-connected FUNC's code files — or mark the SCHEMA concept:true if no code realizes it yet`,
308
+ : `Import and call ${ref.symbol}.parse()/.safeParse() in one of the io-connected FUNC's code files`,
312
309
  context: { element_type: el.type, element_name: el.name },
313
310
  });
314
311
  }
@@ -490,8 +487,7 @@ function externalRefMustNameDependency(graph, facts) {
490
487
  const known = new Set(declared);
491
488
  const violations = [];
492
489
  for (const el of graph.elements) {
493
- // `concept: true` has no binding that could rot — a concept node claims nothing about code.
494
- if (el.attributes?.external !== true || el.attributes?.concept === true)
490
+ if (el.attributes?.external !== true)
495
491
  continue;
496
492
  const parsed = readRealRef(el.attributes);
497
493
  if (parsed.state !== 'bound')
@@ -523,8 +519,12 @@ function externalRefMustNameDependency(graph, facts) {
523
519
  // directory;
524
520
  // 2. an OPEN file with no node — an open CR the graph cannot see. The missing node has no
525
521
  // element to carry the finding, so it anchors at the first SYS (sorted, Gate 5); without
526
- // a SYS the branch is silent — R-17 already reports that graph.
522
+ // a SYS the branch is silent — R-33 already reports that graph.
527
523
  // A node WITHOUT a file is not reported: that is history (archived CRs, pre-docs/cr numbering).
524
+ //
525
+ // CR-SM-396: stage 10 (plan), not 12. The rule needs the ORDERS and the directory, not a binding —
526
+ // it holds always, also in a draft: an open CR file without a node must be reported before the
527
+ // build is opened, not after. Its findings hold the mark Bau like every finding of stages 10–12.
528
528
  // ---------------------------------------------------------------------------
529
529
  function crNodeMatchesFile(graph, facts) {
530
530
  const crFiles = facts.crFiles;
@@ -585,8 +585,6 @@ function schemaRefMustBeZod(graph, facts) {
585
585
  for (const el of graph.elements) {
586
586
  if (el.type !== 'SCHEMA')
587
587
  continue;
588
- if (el.attributes?.concept === true)
589
- continue;
590
588
  const parsed = readRealRef(el.attributes);
591
589
  if (parsed.state !== 'bound' || parsed.value.symbol === undefined)
592
590
  continue;
@@ -620,8 +618,6 @@ function schemaParsedOnlyAtInterface(graph, facts) {
620
618
  for (const el of graph.elements) {
621
619
  if (el.type !== 'SCHEMA')
622
620
  continue;
623
- if (el.attributes?.concept === true)
624
- continue;
625
621
  const parsed = readRealRef(el.attributes);
626
622
  if (parsed.state !== 'bound' || parsed.value.symbol === undefined)
627
623
  continue;
@@ -660,7 +656,7 @@ function schemaParsedOnlyAtInterface(graph, facts) {
660
656
  // boundary measurement are blind to it BY CONSTRUCTION. Measured at sigllm: 0 RC-05
661
657
  // findings at 19 % import coverage, 8 at 81 % — the zero was the absence of a question.
662
658
  // The cost in the message comes from `importCoverage` (the same resolution), not from a
663
- // second count. `concept`/`external` are exempt — the same exemptions as R-20.
659
+ // second count. `external` is exempt — the same exemption as R-20.
664
660
  // ABSENT `importEdges` = silence, like RC-05: without an import graph RC-05 has nothing to
665
661
  // judge, so there is no blindness to report — and the cost could not be named.
666
662
  // ---------------------------------------------------------------------------
@@ -673,7 +669,7 @@ function modMustBeResolvable(graph, facts) {
673
669
  .filter(t => t.type === 'allocate' && boundFuncs.has(t.source) && typeOf.get(t.target) === 'MOD')
674
670
  .map(t => t.target));
675
671
  const blind = graph.elements.filter(e => e.type === 'MOD' &&
676
- e.attributes?.concept !== true && e.attributes?.external !== true &&
672
+ e.attributes?.external !== true &&
677
673
  !(typeof e.attributes?.path === 'string' && e.attributes.path.length > 0) &&
678
674
  !resolvedByFunc.has(e.id));
679
675
  if (blind.length === 0)
@@ -685,22 +681,31 @@ function modMustBeResolvable(graph, facts) {
685
681
  severity: 'warning',
686
682
  element_id: el.id,
687
683
  message: `${el.id} has neither a path nor an allocated FUNC with a realRef — no file can resolve to it, RC-05 is blind here (${cost})`,
688
- fix_hint: `Set the module's source directory via graph_mutate Format-E \`~ ${el.id} @path <dir>\` — or bind one of its allocated FUNCs (\`~ FUNC-x @realRef {"file":…,"symbol":…}\`); mark it concept:true if no code realizes it yet`,
684
+ fix_hint: `Set the module's source directory via graph_mutate Format-E \`~ ${el.id} @path <dir>\` — or bind one of its allocated FUNCs (\`~ FUNC-x @realRef {"file":…,"symbol":…}\`)`,
689
685
  context: { element_type: el.type, element_name: el.name },
690
686
  }));
691
687
  }
692
- export const CODE_CONFORMANCE_RULES = [
693
- { id: 'RC-01', name: 'FUNC realRef resolves to a declared symbol', severity: 'warning', domain: ['FUNC'], evaluate: codeRefMustResolve },
694
- { id: 'RC-02', name: 'testRefs entries resolve to runnable tests', severity: 'warning', domain: ['TEST'], evaluate: testRefMustResolve },
695
- { id: 'RC-03', name: 'SCHEMA realRef resolves to a declared export', severity: 'warning', domain: ['SCHEMA'], evaluate: schemaRefMustResolve },
696
- { id: 'RC-04', name: 'SCHEMA realRef is parsed at its interface', severity: 'warning', domain: ['SCHEMA'], evaluate: schemaRefMustBeUsed },
697
- { id: 'RC-05', name: 'cross-module import drift', severity: 'warning', domain: ['MOD'], evaluate: importDriftConformance },
698
- { id: 'RC-06', name: 'external realRef names a declared dependency', severity: 'warning', domain: ['FUNC', 'MOD', 'SCHEMA'], evaluate: externalRefMustNameDependency },
699
- { id: 'RC-07', name: 'CR node agrees with docs/cr', severity: 'warning', domain: ['CR', 'SYS'], evaluate: crNodeMatchesFile },
700
- { id: 'RC-08', name: 'SCHEMA realRef is a Zod schema', severity: 'warning', domain: ['SCHEMA'], evaluate: schemaRefMustBeZod },
701
- { id: 'RC-09', name: 'SCHEMA is parsed only at its modelled interface', severity: 'warning', domain: ['SCHEMA'], evaluate: schemaParsedOnlyAtInterface },
702
- { id: 'RC-10', name: 'MOD has resolvable files', severity: 'warning', domain: ['MOD'], evaluate: modMustBeResolvable },
703
- ];
688
+ // CR-SM-395: Stufe 12 — der Abgleich ist faellig, sobald der Bau eroeffnet ist (s. rule-due.ts).
689
+ // Zwei Regeln warten nicht auf den Bau und liegen deshalb auf Stufe 10, der letzten Stufe, deren
690
+ // Faelligkeit die `domain` ist; ihre Befunde halten weiter die Marke Bau:
691
+ // - RC-07 (CR-SM-396) braucht die Auftraege und `docs/cr`, keine Bindung.
692
+ // - RC-05 (CR-SM-397, Entscheidung des Autors 2026-10-06) braucht Module und den Import-Graphen.
693
+ // Ein Modul ist schon ueber `path` aufloesbar, ohne Bindung und ohne Auftrag. Die Stufenregel
694
+ // ergaebe 7 (Modul) — dort hielte eine Warnung KEINE Marke mehr (nur Fehler und Existenz-Regeln
695
+ // halten vor TRR). Stufe 10 ist die benannte Ausnahme: der Befund bleibt ein Befund des Baus.
696
+ // Ohne `importEdges` und ohne aufloesbares Modul schweigt die Regel von selbst.
697
+ export const CODE_CONFORMANCE_RULES = whenDue([
698
+ { id: 'RC-01', name: 'FUNC realRef resolves to a declared symbol', severity: 'warning', stage: 12, domain: ['FUNC'], evaluate: codeRefMustResolve },
699
+ { id: 'RC-02', name: 'testRefs entries resolve to runnable tests', severity: 'warning', stage: 12, domain: ['TEST'], evaluate: testRefMustResolve },
700
+ { id: 'RC-03', name: 'SCHEMA realRef resolves to a declared export', severity: 'warning', stage: 12, domain: ['SCHEMA'], evaluate: schemaRefMustResolve },
701
+ { id: 'RC-04', name: 'SCHEMA realRef is parsed at its interface', severity: 'warning', stage: 12, domain: ['SCHEMA'], evaluate: schemaRefMustBeUsed },
702
+ { id: 'RC-05', name: 'cross-module import drift', severity: 'warning', stage: 10, domain: ['MOD'], evaluate: importDriftConformance },
703
+ { id: 'RC-06', name: 'external realRef names a declared dependency', severity: 'warning', stage: 12, domain: ['FUNC', 'MOD', 'SCHEMA'], evaluate: externalRefMustNameDependency },
704
+ { id: 'RC-07', name: 'CR node agrees with docs/cr', severity: 'warning', stage: 10, domain: ['CR', 'SYS'], evaluate: crNodeMatchesFile },
705
+ { id: 'RC-08', name: 'SCHEMA realRef is a Zod schema', severity: 'warning', stage: 12, domain: ['SCHEMA'], evaluate: schemaRefMustBeZod },
706
+ { id: 'RC-09', name: 'SCHEMA is parsed only at its modelled interface', severity: 'warning', stage: 12, domain: ['SCHEMA'], evaluate: schemaParsedOnlyAtInterface },
707
+ { id: 'RC-10', name: 'MOD has resolvable files', severity: 'warning', stage: 12, domain: ['MOD'], evaluate: modMustBeResolvable },
708
+ ]);
704
709
  /** Run all RC rules against a graph + extracted code facts. */
705
710
  export function evaluateConformanceRules(graph, facts) {
706
711
  return CODE_CONFORMANCE_RULES.flatMap((rule) => rule.evaluate(graph, facts));
@@ -1,4 +1,3 @@
1
- import { readRealRef } from './ontology.js';
2
1
  // ---------------------------------------------------------------------------
3
2
  // CR-R01: ein offener CR nennt, WAS er aendert (CR-SM-295)
4
3
  //
@@ -179,89 +178,18 @@ function crShouldHaveMilestone(graph) {
179
178
  }));
180
179
  }
181
180
  // ---------------------------------------------------------------------------
182
- // CR-R05: kein Blatt-REQ ohne Bauauftrag (CR-SM-343) — die REQ-Seite von CR-R01.
183
- //
184
- // RD-01 fragt nach einem Traeger, R-01 nach einer Verifikation; ob ein REQ einen BAUAUFTRAG
185
- // hat, fragte niemand. Gemessen am Fremdlauf sigllm: `se-plan` meldete „20 von 20 geordnet",
186
- // im selben Graphstand hatten 24 von 64 Blatt-REQ keinen Auftrag — genau die, die alle 16
187
- // offenen FM-03-Risiken mildern.
188
- //
189
- // Beauftragt ist ein Blatt-REQ (kein `compose`→REQ-Kind), wenn ein CR eine `relation` (a)
190
- // direkt darauf traegt oder (b) auf eine FUNC, die es `satisfy`t. **MOD und SYS zaehlen
191
- // nicht:** ein Modul ist ein Behaelter — mitgerechnet las sigllm 55/64 statt 40/64 und sah
192
- // gesund aus. Wer ein REQ an einem MOD-/SYS-Traeger beauftragt, zieht die `relation` direkt
193
- // auf das REQ. FCHAIN ist kein Weg: `CR -relation-> FCHAIN` ist kein TRACE_PATTERN, eine
194
- // solche Kante waere R-18 — ein FCHAIN-getragenes REQ braucht also ebenfalls die direkte Kante.
195
- //
196
- // Jeder CR zaehlt, auch ein abgeschlossener: gebaut ist beauftragt.
197
- //
198
- // **Eine gebaute FUNC deckt ohne CR** (CR-SM-375): traegt die FUNC eine gueltige `realRef`
199
- // oder ist sie `external`, ist der Bauauftrag erledigt — Bestand vor der CR-Disziplin braucht
200
- // keinen nachgetragenen Auftrag. `concept: true` ist nicht gebaut, auch mit realRef. Fuer
201
- // MOD/SYS/FCHAIN aendert das nichts: dort deckt nur die direkte `CR -relation-> REQ`.
202
- //
203
- // **Ohne CR-Knoten schweigt die Regel** und zaehlt weder im Zaehler noch im Nenner
204
- // (`RULE_PRECONDITION`, readiness.ts). Ein Repo ohne Plan im Graphen (`cr: docs`) bekaeme
205
- // sonst auf jedem REQ einen Befund — die Regel wuerde abgeschaltet und waere danach ueberall
206
- // still (Fail-open-Klasse ND-01/ND-02 vor CR-SM-286).
207
- // ---------------------------------------------------------------------------
208
- function funcIsBuilt(e) {
209
- if (e.attributes?.concept === true)
210
- return false;
211
- return e.attributes?.external === true || readRealRef(e.attributes).state === 'bound';
212
- }
213
- function leafReqNeedsBuildOrder(graph) {
214
- const crIds = new Set(graph.elements.filter(e => e.type === 'CR').map(e => e.id));
215
- if (crIds.size === 0)
216
- return [];
217
- const typeOf = new Map(graph.elements.map(e => [e.id, e.type]));
218
- const ordered = new Set(); // CR -relation-> X, oder eine bereits gebaute FUNC
219
- for (const t of graph.traces) {
220
- if (t.type === 'relation' && crIds.has(t.source))
221
- ordered.add(t.target);
222
- }
223
- for (const e of graph.elements) {
224
- if (e.type === 'FUNC' && funcIsBuilt(e))
225
- ordered.add(e.id);
226
- }
227
- const covered = new Set();
228
- const parents = new Set();
229
- for (const t of graph.traces) {
230
- if (t.type === 'relation' && crIds.has(t.source) && typeOf.get(t.target) === 'REQ')
231
- covered.add(t.target);
232
- if (t.type === 'satisfy' && typeOf.get(t.source) === 'FUNC' && ordered.has(t.source))
233
- covered.add(t.target);
234
- if (t.type === 'compose' && typeOf.get(t.source) === 'REQ' && typeOf.get(t.target) === 'REQ')
235
- parents.add(t.source);
236
- }
237
- return graph.elements
238
- .filter(e => e.type === 'REQ' && !parents.has(e.id) && !covered.has(e.id))
239
- .map(req => ({
240
- rule_id: 'CR-R05',
241
- severity: 'warning',
242
- element_id: req.id,
243
- message: `${req.id} is a leaf REQ with no build order (no CR relation to it or to a FUNC that satisfies it, and no built FUNC satisfies it)`,
244
- fix_hint: `Add a CR relation through graph_mutate Format-E \`CR-x -relation-> ${req.id}\` — or to the FUNC that satisfies it; a CR on its MOD/SYS carrier does not count`,
245
- context: {
246
- element_type: req.type,
247
- element_name: req.name,
248
- },
249
- }));
250
- }
251
- // ---------------------------------------------------------------------------
252
181
  // Exports
253
182
  // ---------------------------------------------------------------------------
254
183
  export const CR_RULES = [
255
- { id: 'CR-R01', name: 'CR must track', severity: 'warning', evaluate: crMustTrack, domain: ['CR'] },
256
- { id: 'CR-R02', name: 'Done requires commit', severity: 'warning', evaluate: crDoneRequiresCommit, domain: ['CR'] },
184
+ { id: 'CR-R01', name: 'CR must track', severity: 'warning', stage: 10, evaluate: crMustTrack, domain: ['CR'] },
185
+ { id: 'CR-R02', name: 'Done requires commit', severity: 'warning', stage: 10, evaluate: crDoneRequiresCommit, domain: ['CR'] },
257
186
  // CR-SM-243: domain ist 'all', nicht CR — die Regel meldet am getrackten ZIELKNOTEN
258
187
  // ("dieser Knoten wird von 2 offenen CRs angefasst"), und der kann jeden Typ haben. Eine
259
188
  // Aufzaehlung waere eine Momentaufnahme der heute getrackten Typen, kein Modell; 'all'
260
189
  // (= alle Elemente) ist die einzige Menge, aus der der Zielknoten garantiert stammt.
261
190
  // Damit ist sie nach R-08/R-18 die dritte 'all'-Regel — bewusst, nicht vergessen.
262
- { id: 'CR-R03', name: 'No concurrent mutation', severity: 'warning', evaluate: noConcurrentMutation, domain: ['all'] },
263
- { id: 'MS-03', name: 'CR without milestone', severity: 'info', evaluate: crShouldHaveMilestone, domain: ['CR'] },
264
- { id: 'CR-R05', name: 'Leaf REQ has a build order', severity: 'warning', evaluate: leafReqNeedsBuildOrder, domain: ['REQ'] },
191
+ { id: 'CR-R03', name: 'No concurrent mutation', severity: 'warning', stage: 'immer', evaluate: noConcurrentMutation, domain: ['all'] },
192
+ { id: 'MS-03', name: 'CR without milestone', severity: 'info', stage: 10, evaluate: crShouldHaveMilestone, domain: ['CR'] },
265
193
  ];
266
194
  // CR-SM-236: `policy` wird durchgereicht, auch wo diese Familie heute keine Schwelle hat —
267
195
  // ein Sonderweg je Familie waere genau der zweite Pfad, den der Regelsatz verbietet.
@@ -5,13 +5,13 @@
5
5
  import type { OntologyGraph } from './ontology.js';
6
6
  import type { RuleViolation } from './rules.js';
7
7
  import type { MetricPolicy } from './policy.js';
8
+ import { type MarkType, type RuleRole, type RuleStage } from './readiness.js';
8
9
  /**
9
10
  * All prescribed rule definitions (single source of truth for the catalog).
10
11
  *
11
12
  * CR-SM-235: `domain` reicht mit durch — die Grundgesamtheit, ueber die eine Regel feuert.
12
- * Konsumenten, die einen Anteil bilden (`se-steering`s `computeApplicable`), lesen den Nenner
13
- * hier statt eine eigene Tabelle zu fuehren. Eine zweite Tabelle kann nicht hinterherhinken,
14
- * wenn es keine zweite gibt.
13
+ * Die Faelligkeit (`isDue`) liest sie hier statt eine eigene Tabelle zu fuehren. Eine zweite
14
+ * Tabelle kann nicht hinterherhinken, wenn es keine zweite gibt.
15
15
  */
16
16
  /** Profile groupings — see CATALOGS. */
17
17
  export type ProfileId = 'default' | 'se' | 'coding' | 'conformance';
@@ -22,6 +22,10 @@ export declare const ALL_RULE_DEFS: ReadonlyArray<{
22
22
  domain: readonly string[];
23
23
  /** CR-SM-285: das Profil reist mit der Regel, statt aus ihrer ID geraten zu werden. */
24
24
  profile: Exclude<ProfileId, 'default'>;
25
+ /** CR-SM-395: Stufe und Rolle von der Regeldefinition; `mark` ist daraus BERECHNET (`null` bei `'immer'`). */
26
+ stage: RuleStage;
27
+ role?: RuleRole;
28
+ mark: MarkType | null;
25
29
  }>;
26
30
  export declare function getRuleDefsForProfile(profile: ProfileId): typeof ALL_RULE_DEFS;
27
31
  /**