@sigloch/contracts 10.13.0 → 11.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 (37) hide show
  1. package/dist/se/analysis-freshness-rules.d.ts +49 -15
  2. package/dist/se/analysis-freshness-rules.js +109 -34
  3. package/dist/se/ao-rules.js +3 -3
  4. package/dist/se/conformance-rules.d.ts +4 -0
  5. package/dist/se/conformance-rules.js +37 -32
  6. package/dist/se/cr-quality-rules.js +4 -76
  7. package/dist/se/evaluate-all.d.ts +5 -0
  8. package/dist/se/evaluate-all.js +2 -1
  9. package/dist/se/fchain-quality-rules.js +4 -4
  10. package/dist/se/fmea-rules.js +4 -4
  11. package/dist/se/format-e-parser.js +1 -1
  12. package/dist/se/grammar-snapshot.d.ts +6 -6
  13. package/dist/se/grammar-snapshot.js +91 -90
  14. package/dist/se/index.d.ts +3 -2
  15. package/dist/se/index.js +4 -2
  16. package/dist/se/metric-rules.d.ts +3 -0
  17. package/dist/se/metric-rules.js +3 -3
  18. package/dist/se/near-duplicate-rules.d.ts +2 -0
  19. package/dist/se/near-duplicate-rules.js +2 -2
  20. package/dist/se/ontology.d.ts +23 -4
  21. package/dist/se/ontology.js +37 -14
  22. package/dist/se/placeholder.d.ts +1 -0
  23. package/dist/se/placeholder.js +13 -0
  24. package/dist/se/quality-rules.js +8 -8
  25. package/dist/se/readiness.d.ts +45 -47
  26. package/dist/se/readiness.js +57 -120
  27. package/dist/se/rule-due.d.ts +35 -0
  28. package/dist/se/rule-due.js +83 -0
  29. package/dist/se/rule-help.d.ts +19 -3
  30. package/dist/se/rule-help.js +97 -26
  31. package/dist/se/rules.d.ts +18 -0
  32. package/dist/se/rules.js +119 -73
  33. package/dist/se/schema-quality-rules.d.ts +1 -1
  34. package/dist/se/schema-quality-rules.js +1 -1
  35. package/dist/se/uc-quality-rules.js +6 -6
  36. package/dist/se/view-rules.js +6 -4
  37. package/package.json +1 -1
@@ -1,64 +1,98 @@
1
1
  /**
2
- * CR-SM-227: Analysis-Freshness-Legs — the third leg-kind a review gate checks
3
- * (rule legs, layer-presence legs [R-28, entfallen mit CR-SM-247], now analysis-freshness legs).
4
- * Presence/Resolution-Split (wie R-19/R-20/R-26/R-27 vs. RC-01..05): these five
5
- * rules are the PRÜFBARE FORM only — does a `graphVersion` stamp exist for the
6
- * artifact, as a graph attribute. They never compare the stamp against the
7
- * live graphVersion (that staleness/currency check is I/O — a consumer
8
- * concern, `CreationCurrencyProvider` in `@sigloch/graphcode-client`).
2
+ * CR-SM-227: the analysis rules — was each judgment-work artifact carried out?
3
+ *
4
+ * AF-01..04 ask for the stamp `SYS.attributes.analysisFreshness.<artifact>`: its presence is the
5
+ * only record that the analysis was done (an FMEA may legitimately end without a single risk, so
6
+ * the result cannot stand in for it). They never compare the stamp against the live graphVersion.
7
+ *
8
+ * AF-05 is different since CR-SM-395: the implementation plan is not a stamp but the set of open
9
+ * CRs. The rule reads "there is something unbuilt, but no open order" and falls due again with
10
+ * every change — which a one-time stamp could not express.
9
11
  */
10
12
  import type { OntologyGraph } from './ontology.js';
11
- import type { RuleViolation } from './rules.js';
13
+ import { type RuleViolation } from './rules.js';
14
+ /**
15
+ * AF-05 (CR-SM-395): es gibt Ungebautes, aber keinen offenen Auftrag → der Bauplan ist faellig.
16
+ *
17
+ * Ungebaut ist, woran die Bindungsregeln melden wuerden (`unbuiltElementIds`: TEST, FUNC, SCHEMA
18
+ * ohne Bindung, `external` und Zerlegungs-Eltern ausgenommen wie dort). Es reicht, dass EIN offener
19
+ * Auftrag existiert; er muss nicht auf die Elemente zeigen — ob er die richtigen trifft, zeigt der
20
+ * Git-Diff. Mit dem Auftrag verstummt diese Regel, und die Bindungsregeln der Stufe 11 werden
21
+ * faellig: ihre Befunde sind die Arbeitsliste. Beim ersten Bau sind das alle Elemente, bei einer
22
+ * Aenderung die neuen.
23
+ *
24
+ * EIN Befund je Graph, am SYS: dort wird er abgenommen („Bau nicht beauftragt"), und eine Abnahme
25
+ * je Element waere mit jedem neuen Element wieder offen. Die Meldung nennt die ungebauten Elemente.
26
+ */
27
+ declare function buildPlanDue(graph: OntologyGraph): RuleViolation[];
12
28
  export declare const AF_RULES: readonly [{
13
29
  readonly id: "AF-01";
14
30
  readonly name: "ConOps freshness stamp present";
15
31
  readonly severity: "warning";
32
+ readonly stage: 3;
33
+ readonly role: "analysis";
16
34
  readonly evaluate: (graph: OntologyGraph) => RuleViolation[];
17
35
  readonly domain: readonly ["graph"];
18
36
  }, {
19
37
  readonly id: "AF-02";
20
38
  readonly name: "Trade Study freshness stamp present";
21
39
  readonly severity: "warning";
40
+ readonly stage: 7;
41
+ readonly role: "analysis";
22
42
  readonly evaluate: (graph: OntologyGraph) => RuleViolation[];
23
43
  readonly domain: readonly ["graph"];
24
44
  }, {
25
45
  readonly id: "AF-03";
26
46
  readonly name: "Assumption Review freshness stamp present";
27
47
  readonly severity: "warning";
48
+ readonly stage: 7;
49
+ readonly role: "analysis";
28
50
  readonly evaluate: (graph: OntologyGraph) => RuleViolation[];
29
51
  readonly domain: readonly ["graph"];
30
52
  }, {
31
53
  readonly id: "AF-04";
32
54
  readonly name: "FMEA freshness stamp present";
33
55
  readonly severity: "warning";
56
+ readonly stage: 8;
57
+ readonly role: "analysis";
34
58
  readonly evaluate: (graph: OntologyGraph) => RuleViolation[];
35
59
  readonly domain: readonly ["graph"];
36
60
  }, {
37
61
  readonly id: "AF-05";
38
- readonly name: "Implementation Plan freshness stamp present";
62
+ readonly name: "Unbuilt elements have an open CR";
39
63
  readonly severity: "warning";
40
- readonly evaluate: (graph: OntologyGraph) => RuleViolation[];
64
+ readonly stage: 10;
65
+ readonly role: "existence";
66
+ readonly evaluate: typeof buildPlanDue;
41
67
  readonly domain: readonly ["graph"];
42
68
  }];
43
69
  /** TR-01: a stamped trade study names >=1 existing CR, each carrying a relation(label: decides) edge. */
44
70
  export declare function tradeDecisionRecorded(graph: OntologyGraph): RuleViolation[];
45
- /** IR-01: every CR named by a stamped assumption review exists; an empty list is a legitimate outcome. */
46
- export declare function assumptionReviewPromoted(graph: OntologyGraph): RuleViolation[];
47
- /** CR-SM-355: the task-outcome rules — owned by trade / irr (TASK_OWNED_RULES), never in the kern focus. */
71
+ /**
72
+ * IR-01 (CR-SM-397): every REQ named by a stamped assumption review exists; an empty list is a
73
+ * legitimate outcome. That the REQ carries a verifying TEST is R-01's finding, not repeated here.
74
+ */
75
+ export declare function assumptionReviewRequirementsExist(graph: OntologyGraph): RuleViolation[];
76
+ /** CR-SM-355: the outcome rules of trade / irr — silent until the analysis is stamped. */
48
77
  export declare const TASK_OUTCOME_RULES: readonly [{
49
78
  readonly id: "TR-01";
50
79
  readonly name: "Trade decision recorded as CR";
51
80
  readonly severity: "warning";
81
+ readonly stage: 7;
82
+ readonly role: "analysis";
52
83
  readonly evaluate: typeof tradeDecisionRecorded;
53
84
  readonly domain: readonly ["graph"];
54
85
  }, {
55
86
  readonly id: "IR-01";
56
- readonly name: "Assumption review promoted to CR";
87
+ readonly name: "Assumption review requirements exist";
57
88
  readonly severity: "warning";
58
- readonly evaluate: typeof assumptionReviewPromoted;
89
+ readonly stage: 7;
90
+ readonly role: "analysis";
91
+ readonly evaluate: typeof assumptionReviewRequirementsExist;
59
92
  readonly domain: readonly ["graph"];
60
93
  }];
61
94
  /** Evaluate the task-outcome rules (CR-SM-355). */
62
95
  export declare function evaluateTaskOutcomeRules(graph: OntologyGraph): RuleViolation[];
63
96
  /** Evaluate all analysis-freshness-presence rules. */
64
97
  export declare function evaluateAFRules(graph: OntologyGraph): RuleViolation[];
98
+ export {};
@@ -1,10 +1,45 @@
1
- import { AnalysisFreshnessStampSchema } from './ontology.js';
1
+ import { readAnalysisStamp } from './ontology.js';
2
+ import { unbuiltElementIds } from './rules.js';
3
+ import { hasOpenOrder } from './rule-due.js';
2
4
  const ARTIFACT_LABEL = {
3
5
  conops: 'Concept of Operations',
4
6
  trade: 'Trade Study',
5
7
  'assumption-review': 'Assumption Review',
6
8
  fmea: 'FMEA',
7
- implplan: 'Implementation Plan',
9
+ };
10
+ /**
11
+ * CR-SM-382: was der Hinweis nennt — den Task, seinen Skill und den ersten Arbeitsschritt.
12
+ *
13
+ * Bis hierher lautete der fix_hint "Set attributes.analysisFreshness.<id>.graphVersion …". Gemessen im
14
+ * lokalen AgentDiary-Lauf (local-1, 2026-09-30): das Modell setzte fuenf Stempel in einem Zug, ohne ein
15
+ * einziges Artefakt, und meldete die Analysen als erledigt — der Hinweis nannte die Metrik als Handlung.
16
+ * Dazu wich die Artefakt-id vom Task-Namen ab (`implplan` gegen `plan`), der Aufruf
17
+ * `graph_generate {task:'implplan'}` schlug fehl. Der Hinweis nannte darauf den Aufruf woertlich und die
18
+ * Arbeit; den Stempel schreibt der letzte Schritt des Skills, zusammen mit den Funden.
19
+ *
20
+ * CR-SM-387: der Hinweis nennt keinen Werkzeugnamen mehr. `graph_generate` hat nur der Autopilot; der
21
+ * interaktive Agent (todo-local, 2026-10-03) zitierte den Aufruf zweimal als fehlendes Werkzeug und
22
+ * liess die Analyse liegen. Der Skill fuehrt die Analyse in beiden Betriebsarten — er ist der Name, der
23
+ * stimmen muss (`skill` folgt RULE_HELP[…].prompt, der Test haelt beide zusammen); damit entfaellt auch
24
+ * die Fehlerquelle `task` gegen Artefakt-id.
25
+ */
26
+ const ANALYSIS_WORK = {
27
+ conops: {
28
+ skill: 'se-conops',
29
+ firstStep: 'walk configuration, credentials, user management, deployment, observability and data lifecycle, and write each concern the system must satisfy as a non-functional REQ with its TEST at SYS scope',
30
+ },
31
+ trade: {
32
+ skill: 'se-trade',
33
+ firstStep: 'name the open decision with its options and criteria, then record the chosen option as a CR with relation(label: decides) edges to what it decides',
34
+ },
35
+ 'assumption-review': {
36
+ skill: 'se-irr',
37
+ firstStep: 'list every unproven assumption the model rests on (unmeasured numbers, unverified external dependencies) in a record under docs/records/, then write each load-bearing one as a REQ with its verifying TEST',
38
+ },
39
+ fmea: {
40
+ skill: 'se-fmea',
41
+ firstStep: 'for each FCHAIN name its failure modes and add each one as a REQ with role risk (severity, occurrence, detection) plus a mitigation REQ and its TEST',
42
+ },
8
43
  };
9
44
  /** Presence-check for one analysis artifact's freshness stamp, anchored on SYS. */
10
45
  function analysisFreshnessPresence(ruleId, artifactId) {
@@ -12,16 +47,16 @@ function analysisFreshnessPresence(ruleId, artifactId) {
12
47
  const sys = graph.elements.find(e => e.type === 'SYS');
13
48
  if (!sys)
14
49
  return []; // nothing to anchor on yet (vacuous-complete exemption)
15
- const stamp = sys.attributes?.analysisFreshness?.[artifactId];
16
- if (AnalysisFreshnessStampSchema.safeParse(stamp).success)
50
+ if (readAnalysisStamp(graph, artifactId).state === 'bound')
17
51
  return [];
18
52
  const label = ARTIFACT_LABEL[artifactId];
53
+ const work = ANALYSIS_WORK[artifactId];
19
54
  return [{
20
55
  rule_id: ruleId,
21
56
  severity: 'warning',
22
57
  element_id: sys.id,
23
- message: `${label} (${artifactId}) has no freshness stamp — was it ever written with a graphVersion stamp?`,
24
- fix_hint: `Set attributes.analysisFreshness.${artifactId}.graphVersion to the current graphVersion() when writing/refreshing the ${label} artifact`,
58
+ message: `${label} has not been carried out — no analysis is on record for this system`,
59
+ fix_hint: `Carry out the ${label}: work through the skill ${work.skill} step by step. First step: ${work.firstStep}. The closing step of the skill records the analysis as done, in the same batch as its findings`,
25
60
  context: { element_type: sys.type, element_name: sys.name },
26
61
  }];
27
62
  };
@@ -30,36 +65,73 @@ const conopsFreshnessPresent = analysisFreshnessPresence('AF-01', 'conops');
30
65
  const tradeFreshnessPresent = analysisFreshnessPresence('AF-02', 'trade');
31
66
  const assumptionReviewFreshnessPresent = analysisFreshnessPresence('AF-03', 'assumption-review');
32
67
  const fmeaFreshnessPresent = analysisFreshnessPresence('AF-04', 'fmea');
33
- const implplanFreshnessPresent = analysisFreshnessPresence('AF-05', 'implplan');
68
+ /**
69
+ * AF-05 (CR-SM-395): es gibt Ungebautes, aber keinen offenen Auftrag → der Bauplan ist faellig.
70
+ *
71
+ * Ungebaut ist, woran die Bindungsregeln melden wuerden (`unbuiltElementIds`: TEST, FUNC, SCHEMA
72
+ * ohne Bindung, `external` und Zerlegungs-Eltern ausgenommen wie dort). Es reicht, dass EIN offener
73
+ * Auftrag existiert; er muss nicht auf die Elemente zeigen — ob er die richtigen trifft, zeigt der
74
+ * Git-Diff. Mit dem Auftrag verstummt diese Regel, und die Bindungsregeln der Stufe 11 werden
75
+ * faellig: ihre Befunde sind die Arbeitsliste. Beim ersten Bau sind das alle Elemente, bei einer
76
+ * Aenderung die neuen.
77
+ *
78
+ * EIN Befund je Graph, am SYS: dort wird er abgenommen („Bau nicht beauftragt"), und eine Abnahme
79
+ * je Element waere mit jedem neuen Element wieder offen. Die Meldung nennt die ungebauten Elemente.
80
+ */
81
+ function buildPlanDue(graph) {
82
+ const sys = graph.elements.find(e => e.type === 'SYS');
83
+ if (!sys)
84
+ return []; // kein Anker — R-33 meldet das fehlende System
85
+ if (hasOpenOrder(graph))
86
+ return [];
87
+ const unbuilt = unbuiltElementIds(graph);
88
+ if (unbuilt.length === 0)
89
+ return [];
90
+ const shown = unbuilt.slice(0, 3).join(', ') + (unbuilt.length > 3 ? ` and ${unbuilt.length - 3} more` : '');
91
+ return [{
92
+ rule_id: 'AF-05',
93
+ severity: 'warning',
94
+ element_id: sys.id,
95
+ message: `${unbuilt.length} ${unbuilt.length === 1 ? 'element is' : 'elements are'} not built yet (${shown}) and no open CR orders the work — the implementation plan is due`,
96
+ fix_hint: 'Carry out the Implementation Plan: work through the skill se-plan step by step. First step: cut one CR per leaf REQ at its carrier and group the CRs into MS milestones in dependency order. The open CRs are the plan — with the first one the binding rules list what to build',
97
+ context: { element_type: sys.type, element_name: sys.name },
98
+ }];
99
+ }
100
+ // CR-SM-396: AF-01 liegt auf Stufe 3, HINTER den Anforderungen — eine Ausnahme von „Stufe = spaeteste
101
+ // Menge, die die Regel braucht" (das Einsatzkonzept braucht nur die Anwendungsfaelle, Stufe 2).
102
+ // Entscheidung des Autors 2026-10-06: zuerst sagt der Anwendungsfall, was verlangt ist (UC-01), dann
103
+ // folgt die Analyse des Betriebs. In einer Stufe steht die Existenz-Regel vor der Analyse.
34
104
  export const AF_RULES = [
35
- { id: 'AF-01', name: 'ConOps freshness stamp present', severity: 'warning', evaluate: conopsFreshnessPresent, domain: ['graph'] },
36
- { id: 'AF-02', name: 'Trade Study freshness stamp present', severity: 'warning', evaluate: tradeFreshnessPresent, domain: ['graph'] },
37
- { id: 'AF-03', name: 'Assumption Review freshness stamp present', severity: 'warning', evaluate: assumptionReviewFreshnessPresent, domain: ['graph'] },
38
- { id: 'AF-04', name: 'FMEA freshness stamp present', severity: 'warning', evaluate: fmeaFreshnessPresent, domain: ['graph'] },
39
- { id: 'AF-05', name: 'Implementation Plan freshness stamp present', severity: 'warning', evaluate: implplanFreshnessPresent, domain: ['graph'] },
105
+ { id: 'AF-01', name: 'ConOps freshness stamp present', severity: 'warning', stage: 3, role: 'analysis', evaluate: conopsFreshnessPresent, domain: ['graph'] },
106
+ { id: 'AF-02', name: 'Trade Study freshness stamp present', severity: 'warning', stage: 7, role: 'analysis', evaluate: tradeFreshnessPresent, domain: ['graph'] },
107
+ { id: 'AF-03', name: 'Assumption Review freshness stamp present', severity: 'warning', stage: 7, role: 'analysis', evaluate: assumptionReviewFreshnessPresent, domain: ['graph'] },
108
+ { id: 'AF-04', name: 'FMEA freshness stamp present', severity: 'warning', stage: 8, role: 'analysis', evaluate: fmeaFreshnessPresent, domain: ['graph'] },
109
+ { id: 'AF-05', name: 'Unbuilt elements have an open CR', severity: 'warning', stage: 10, role: 'existence', evaluate: buildPlanDue, domain: ['graph'] },
40
110
  ];
41
111
  /**
42
- * CR-SM-355: Task-Ausgangsregeln fuer trade und irr — die aus dem Task entstandenen Massnahmen sind
43
- * als CR dokumentiert. Traeger ist `crRefs` im Frischestempel. Ohne Stempel feuern sie nicht (dann
44
- * feuert der Eintritt AF-02/AF-03, und der Task ist nicht durch). `label` wird als `Trace.label`
45
- * gelesen wie bei MS-02 — `decides` ist kein TraceType, sondern das SP-1-Label einer relation-Kante.
112
+ * Task-Ausgangsregeln fuer trade und irr (CR-SM-355, CR-SM-397): was der Task hinterlassen hat, ist
113
+ * im Frischestempel genannt und existiert. Ein Variantenvergleich hinterlaesst Entscheidungs-CRs
114
+ * (`crRefs`, TR-01), ein Annahmen-Review Anforderungen (`reqRefs`, IR-01) — eine Analyse legt keinen
115
+ * Bauauftrag an. Ohne Stempel feuern die Regeln nicht (dann feuert der Eintritt AF-02/AF-03).
116
+ * `label` wird als `Trace.label` gelesen wie bei MS-02 — `decides` ist kein TraceType, sondern das
117
+ * SP-1-Label einer relation-Kante.
46
118
  */
47
- function stampedCrRefs(graph, artifactId) {
119
+ function stampedOutcome(graph, artifactId) {
48
120
  const sys = graph.elements.find(e => e.type === 'SYS');
49
121
  if (!sys)
50
122
  return null;
51
- const raw = sys.attributes?.analysisFreshness?.[artifactId];
52
- const parsed = AnalysisFreshnessStampSchema.safeParse(raw);
53
- if (!parsed.success)
123
+ const stamp = readAnalysisStamp(graph, artifactId);
124
+ if (stamp.state !== 'bound')
54
125
  return null;
55
- return { sys, crRefs: parsed.data.crRefs ?? [] };
126
+ return { sys, stamp: stamp.value };
56
127
  }
57
128
  /** TR-01: a stamped trade study names >=1 existing CR, each carrying a relation(label: decides) edge. */
58
129
  export function tradeDecisionRecorded(graph) {
59
- const stamped = stampedCrRefs(graph, 'trade');
130
+ const stamped = stampedOutcome(graph, 'trade');
60
131
  if (!stamped)
61
132
  return [];
62
- const { sys, crRefs } = stamped;
133
+ const { sys } = stamped;
134
+ const crRefs = stamped.stamp.crRefs ?? [];
63
135
  const base = { rule_id: 'TR-01', severity: 'warning', context: { element_type: sys.type, element_name: sys.name } };
64
136
  if (crRefs.length === 0) {
65
137
  return [{
@@ -82,28 +154,31 @@ export function tradeDecisionRecorded(graph) {
82
154
  return [];
83
155
  });
84
156
  }
85
- /** IR-01: every CR named by a stamped assumption review exists; an empty list is a legitimate outcome. */
86
- export function assumptionReviewPromoted(graph) {
87
- const stamped = stampedCrRefs(graph, 'assumption-review');
157
+ /**
158
+ * IR-01 (CR-SM-397): every REQ named by a stamped assumption review exists; an empty list is a
159
+ * legitimate outcome. That the REQ carries a verifying TEST is R-01's finding, not repeated here.
160
+ */
161
+ export function assumptionReviewRequirementsExist(graph) {
162
+ const stamped = stampedOutcome(graph, 'assumption-review');
88
163
  if (!stamped)
89
164
  return [];
90
- const { sys, crRefs } = stamped;
165
+ const { sys } = stamped;
91
166
  const byId = new Map(graph.elements.map(e => [e.id, e]));
92
- return crRefs
93
- .filter(id => byId.get(id)?.type !== 'CR')
167
+ return (stamped.stamp.reqRefs ?? [])
168
+ .filter(id => byId.get(id)?.type !== 'REQ')
94
169
  .map(id => ({
95
170
  rule_id: 'IR-01',
96
171
  severity: 'warning',
97
172
  element_id: sys.id,
98
- message: `Assumption Review names ${id} as a promoted assumption, but no CR with that id exists`,
99
- fix_hint: 'Point analysisFreshness.assumption-review.crRefs at an existing CR, or add the CR through graph_mutate (se-irr)',
173
+ message: `Assumption Review names ${id} as the requirement of a load-bearing assumption, but no REQ with that id exists`,
174
+ fix_hint: 'Point analysisFreshness.assumption-review.reqRefs at an existing REQ, or add the REQ with its verifying TEST through graph_mutate (se-irr)',
100
175
  context: { element_type: sys.type, element_name: sys.name },
101
176
  }));
102
177
  }
103
- /** CR-SM-355: the task-outcome rules — owned by trade / irr (TASK_OWNED_RULES), never in the kern focus. */
178
+ /** CR-SM-355: the outcome rules of trade / irr — silent until the analysis is stamped. */
104
179
  export const TASK_OUTCOME_RULES = [
105
- { id: 'TR-01', name: 'Trade decision recorded as CR', severity: 'warning', evaluate: tradeDecisionRecorded, domain: ['graph'] },
106
- { id: 'IR-01', name: 'Assumption review promoted to CR', severity: 'warning', evaluate: assumptionReviewPromoted, domain: ['graph'] },
180
+ { id: 'TR-01', name: 'Trade decision recorded as CR', severity: 'warning', stage: 7, role: 'analysis', evaluate: tradeDecisionRecorded, domain: ['graph'] },
181
+ { id: 'IR-01', name: 'Assumption review requirements exist', severity: 'warning', stage: 7, role: 'analysis', evaluate: assumptionReviewRequirementsExist, domain: ['graph'] },
107
182
  ];
108
183
  /** Evaluate the task-outcome rules (CR-SM-355). */
109
184
  export function evaluateTaskOutcomeRules(graph) {
@@ -260,12 +260,12 @@ export function io01CrossModuleCompleteness(graph) {
260
260
  return violations;
261
261
  }
262
262
  export const AO_RULES = [
263
- { id: 'BW-02', name: 'Whitebox boundary width', severity: 'warning', evaluate: bw02WhiteboxWidth, domain: ['FUNC'] },
264
- { id: 'CR-01', name: 'CrossingFlowCount', severity: 'warning', evaluate: cr01CrossingFlowCount, domain: ['MOD'] },
263
+ { id: 'BW-02', name: 'Whitebox boundary width', severity: 'warning', stage: 5, evaluate: bw02WhiteboxWidth, domain: ['FUNC'] },
264
+ { id: 'CR-01', name: 'CrossingFlowCount', severity: 'warning', stage: 7, evaluate: cr01CrossingFlowCount, domain: ['MOD'] },
265
265
  // CR-SM-242: domain ist FUNC, nicht MOD — die Regel laeuft ueber FUNC-Paare in FCHAINs und
266
266
  // meldet am FUNC. Mit ['MOD'] uebertraf ihr Zaehler (21) ihren eigenen Nenner-Beitrag (10);
267
267
  // nur der Clamp verhinderte einen negativen Score.
268
- { id: 'IO-01', name: 'FuncPairIOCompleteness', severity: 'warning', evaluate: io01CrossModuleCompleteness, domain: ['FUNC'] },
268
+ { id: 'IO-01', name: 'FuncPairIOCompleteness', severity: 'warning', stage: 5, evaluate: io01CrossModuleCompleteness, domain: ['FUNC'] },
269
269
  ];
270
270
  export function evaluateAORules(graph, policy) {
271
271
  return AO_RULES.flatMap(r => r.evaluate(graph, policy));
@@ -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));