@sigloch/contracts 1.0.0 → 3.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.
@@ -79,6 +79,15 @@ export declare const MutateCommandSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
79
79
  targetUid: z.ZodString;
80
80
  }, z.core.$strip>], "op">;
81
81
  export type MutateCommand = z.infer<typeof MutateCommandSchema>;
82
+ export interface OpRisk {
83
+ destructive: boolean;
84
+ structural: boolean;
85
+ }
86
+ export declare const OP_RISK: Record<MutateCommand['op'], OpRisk>;
87
+ /** True iff `op` removes data that is not recoverable by re-running the same op. */
88
+ export declare function isDestructiveOp(op: MutateCommand['op']): boolean;
89
+ /** True iff `op` changes graph topology (nodes/edges), not just attributes. */
90
+ export declare function isStructuralOp(op: MutateCommand['op']): boolean;
82
91
  /** Monotone graph revision; incremented by +1 per successful mutate(). */
83
92
  export declare const GraphVersionSchema: z.ZodNumber;
84
93
  export type GraphVersion = z.infer<typeof GraphVersionSchema>;
@@ -93,6 +93,23 @@ export const MutateCommandSchema = z.discriminatedUnion('op', [
93
93
  targetUid: z.string(),
94
94
  }),
95
95
  ]);
96
+ export const OP_RISK = {
97
+ 'add-node': { destructive: false, structural: true },
98
+ 'update-node': { destructive: false, structural: false },
99
+ 'delete-node': { destructive: true, structural: true },
100
+ 'add-edge': { destructive: false, structural: true },
101
+ 'delete-edge': { destructive: true, structural: true },
102
+ 'update-edge': { destructive: false, structural: true },
103
+ 'merge-nodes': { destructive: true, structural: true },
104
+ };
105
+ /** True iff `op` removes data that is not recoverable by re-running the same op. */
106
+ export function isDestructiveOp(op) {
107
+ return OP_RISK[op].destructive;
108
+ }
109
+ /** True iff `op` changes graph topology (nodes/edges), not just attributes. */
110
+ export function isStructuralOp(op) {
111
+ return OP_RISK[op].structural;
112
+ }
96
113
  // ---------------------------------------------------------------------------
97
114
  // CR-199: OCC — graph revision + optional baseVersion on mutate().
98
115
  // Schema-only here; revision counter/stale-check/delta live in the harness
@@ -52,7 +52,7 @@ export declare const AO_RULES: readonly [{
52
52
  readonly evaluate: typeof ca01CapabilityAllocation;
53
53
  }, {
54
54
  readonly id: "IO-01";
55
- readonly name: "CrossModuleIOCompleteness";
55
+ readonly name: "FuncPairIOCompleteness";
56
56
  readonly severity: "warning";
57
57
  readonly evaluate: typeof io01CrossModuleCompleteness;
58
58
  }];
@@ -256,8 +256,12 @@ export function ca01CapabilityAllocation(graph) {
256
256
  return violations;
257
257
  }
258
258
  // ---------------------------------------------------------------------------
259
- // IO-01: Cross-module IO completeness (CR-192)
260
- // For each FCHAIN, FUNCs in different MODs must have a FLOW path between them.
259
+ // IO-01: FUNC-pair IO completeness (CR-192, extended CR-SM-226)
260
+ // For each FCHAIN with ≥2 FUNCs, every pair of FUNCs in the chain must have a
261
+ // FLOW path between them — not just pairs allocated to different MODs.
262
+ // CR-SM-226 removed the cross-module-only restriction: same-module FUNC
263
+ // pairs need an explicit FLOW just as much (module allocation is an
264
+ // orthogonal concern to whether the interaction itself is wired up).
261
265
  // ---------------------------------------------------------------------------
262
266
  export function io01CrossModuleCompleteness(graph) {
263
267
  const violations = [];
@@ -270,23 +274,21 @@ export function io01CrossModuleCompleteness(graph) {
270
274
  .filter(id => graph.elements.some(e => e.id === id && e.type === 'FUNC'));
271
275
  if (funcIds.length < 2)
272
276
  continue;
273
- // Map FUNC→MOD via allocate
277
+ // Map FUNC→MOD via allocate (kept for the message context; no longer a filter).
274
278
  const funcToMod = new Map();
275
279
  for (const fid of funcIds) {
276
280
  const allocTrace = graph.traces.find(t => t.source === fid && t.type === 'allocate');
277
281
  if (allocTrace)
278
282
  funcToMod.set(fid, allocTrace.target);
279
283
  }
280
- // For each pair of FUNCs in different MODs, check for FLOW path
284
+ // For each pair of FUNCs in the chain, check for a FLOW path.
281
285
  const checked = new Set();
282
286
  for (const fA of funcIds) {
283
287
  for (const fB of funcIds) {
284
288
  if (fA >= fB)
285
289
  continue;
286
- const modA = funcToMod.get(fA);
287
- const modB = funcToMod.get(fB);
288
- if (!modA || !modB || modA === modB)
289
- continue;
290
+ const modA = funcToMod.get(fA) ?? 'unallocated';
291
+ const modB = funcToMod.get(fB) ?? 'unallocated';
290
292
  const pairKey = `${fA}:${fB}`;
291
293
  if (checked.has(pairKey))
292
294
  continue;
@@ -314,7 +316,7 @@ export function io01CrossModuleCompleteness(graph) {
314
316
  severity: 'warning',
315
317
  element_id: fA,
316
318
  message: `${fA} (${modA}) and ${fB} (${modB}) in FCHAIN ${fc.id} have no IO path — missing FLOW?`,
317
- fix_hint: 'Add a FLOW element with io traces between these cross-module functions',
319
+ fix_hint: 'Add a FLOW element with io traces between these functions',
318
320
  context: {
319
321
  element_type: 'FUNC',
320
322
  element_name: elA?.name ?? fA,
@@ -334,7 +336,7 @@ export const AO_RULES = [
334
336
  { id: 'RT-01', name: 'PhysicalBoundaryIntegrity', severity: 'error', evaluate: rt01PhysicalBoundaryIntegrity },
335
337
  { id: 'PH-01', name: 'PhysicalModCompleteness', severity: 'info', evaluate: ph01PhysicalModCompleteness },
336
338
  { id: 'CA-01', name: 'CapabilityAllocation', severity: 'error', evaluate: ca01CapabilityAllocation },
337
- { id: 'IO-01', name: 'CrossModuleIOCompleteness', severity: 'warning', evaluate: io01CrossModuleCompleteness },
339
+ { id: 'IO-01', name: 'FuncPairIOCompleteness', severity: 'warning', evaluate: io01CrossModuleCompleteness },
338
340
  ];
339
341
  export function evaluateAORules(graph) {
340
342
  return AO_RULES.flatMap(r => r.evaluate(graph));
@@ -6,5 +6,6 @@ import type { RuleDefinition, RuleViolation } from './rules.js';
6
6
  export declare function fc01ActorBoundary(graph: OntologyGraph): RuleViolation[];
7
7
  export declare function fc02LeafUcHasFchain(graph: OntologyGraph): RuleViolation[];
8
8
  export declare function fc03FchainFlat(graph: OntologyGraph): RuleViolation[];
9
+ export declare function fc04ActorBounded(graph: OntologyGraph): RuleViolation[];
9
10
  export declare const FC_RULES: RuleDefinition[];
10
11
  export declare function evaluateFCRules(graph: OntologyGraph): RuleViolation[];
@@ -93,12 +93,55 @@ export function fc03FchainFlat(graph) {
93
93
  return violations;
94
94
  }
95
95
  // ---------------------------------------------------------------------------
96
+ // FC-04: FCHAIN is actor-bounded — trigger AND consumer (CR-SM-226).
97
+ // Distinct from FC-01: FC-01 is satisfied by ANY single actor connection (one
98
+ // direction only), and even allows a UC-level bypass (the parent UC's own
99
+ // ACTOR io counts, with no FUNC-level connection at all). FC-04 is stricter —
100
+ // it requires BOTH an entry (ACTOR→FLOW→FUNC∈chain, something triggers the
101
+ // chain) AND an exit (FUNC∈chain→FLOW→ACTOR, the chain produces something
102
+ // back to an actor), evaluated purely at the FUNC/FLOW level. This catches
103
+ // the "hollow chain" FC-01 cannot see: a chain with an entry but no exit (or
104
+ // vice versa), or one that only looks bounded because its parent UC happens
105
+ // to have unrelated ACTOR io.
106
+ // ---------------------------------------------------------------------------
107
+ export function fc04ActorBounded(graph) {
108
+ return graph.elements
109
+ .filter(e => e.type === 'FCHAIN')
110
+ .filter(fc => {
111
+ const funcIds = new Set(graph.traces
112
+ .filter(t => t.source === fc.id && t.type === 'compose')
113
+ .map(t => t.target)
114
+ .filter(id => graph.elements.some(e => e.id === id && e.type === 'FUNC')));
115
+ if (funcIds.size === 0)
116
+ return false; // R-15 already flags the empty-chain case
117
+ const io = graph.traces.filter(t => t.type === 'io');
118
+ const isActor = (id) => graph.elements.some(e => e.id === id && e.type === 'ACTOR');
119
+ const isFlow = (id) => graph.elements.some(e => e.id === id && e.type === 'FLOW');
120
+ // Entry: ACTOR→FLOW→FUNC∈chain (something triggers the chain).
121
+ const entry = io.some(e => funcIds.has(e.target) && isFlow(e.source) &&
122
+ io.some(a => a.target === e.source && isActor(a.source)));
123
+ // Exit: FUNC∈chain→FLOW→ACTOR (the chain produces something back to an actor).
124
+ const exit = io.some(e => funcIds.has(e.source) && isFlow(e.target) &&
125
+ io.some(a => a.source === e.target && isActor(a.target)));
126
+ return !(entry && exit);
127
+ })
128
+ .map(fc => ({
129
+ rule_id: 'FC-04',
130
+ severity: 'warning',
131
+ element_id: fc.id,
132
+ message: `${fc.id} is not actor-bounded (needs an ACTOR trigger into AND an ACTOR consumer out of the chain)`,
133
+ fix_hint: 'Link an ACTOR→FLOW→FUNC entry and a FUNC→FLOW→ACTOR exit for at least one FUNC in the chain',
134
+ context: { element_type: fc.type, element_name: fc.name },
135
+ }));
136
+ }
137
+ // ---------------------------------------------------------------------------
96
138
  // Aggregated
97
139
  // ---------------------------------------------------------------------------
98
140
  export const FC_RULES = [
99
141
  { id: 'FC-01', name: 'FCHAIN has actor boundary', severity: 'warning', evaluate: fc01ActorBoundary },
100
142
  { id: 'FC-02', name: 'Leaf UC has FCHAIN', severity: 'warning', evaluate: fc02LeafUcHasFchain },
101
143
  { id: 'FC-03', name: 'FCHAIN is flat', severity: 'warning', evaluate: fc03FchainFlat },
144
+ { id: 'FC-04', name: 'FCHAIN actor-bounded (trigger+consumer)', severity: 'warning', evaluate: fc04ActorBounded },
102
145
  ];
103
146
  export function evaluateFCRules(graph) {
104
147
  return FC_RULES.flatMap(rule => rule.evaluate(graph));
@@ -5,7 +5,7 @@
5
5
  /** Ontology schema version (element types + trace types). */
6
6
  export declare const ONTOLOGY_VERSION = "4.0.0";
7
7
  /** Rules engine version (validation rules incl. RC conformance). */
8
- export declare const RULES_VERSION = "2.20.0";
8
+ export declare const RULES_VERSION = "2.22.0";
9
9
  /** Meta-model version (trace pattern constraints + format-e parser). */
10
10
  export declare const META_MODEL_VERSION = "1.4.0";
11
11
  export * from './ontology.js';
package/dist/se/index.js CHANGED
@@ -5,7 +5,7 @@
5
5
  /** Ontology schema version (element types + trace types). */
6
6
  export const ONTOLOGY_VERSION = '4.0.0'; // BREAKING: `SemanticId` (`Name.TypeAbbr.Counter`) deleted, `ElementUid` (`<TYPE>-<slug>`) is the family canon — the old canon was used by no production graph of the family while 626 of 1145 elements already carried TYPE-slug (CR-SM-217); realRef unifies codeRef+schemaRef (+physical-MOD CAD ref), symbol optional; testRef stays separate (CR-228 C); +RepoRelativePathSchema on testRef/realRef .file — no absolute/`..` paths (CR-GC-255)
7
7
  /** Rules engine version (validation rules incl. RC conformance). */
8
- export const RULES_VERSION = '2.20.0'; // +RD-04 decomposition breadth (>11 children per level warning) and MT-03 recalibrated to FLOW-transitive connection pairs counting raw io traces reported internal=0 on every real SE graph (15/15 false positives), because the meta-model routes FUNC↔FUNC through FLOW; 'RD-' added to the se profile prefixes, where RD-01..04 were silently missing (CR-SM-221); -SC-01/-SC-03 deleted (BOK-CR-026): `realRef` is the single SCHEMA binding truth (R-26 presence, RC-03/RC-04 resolution); the legacy `zodDefinition`/`sourceFile`/`sourceExport` attributes are gone from every producer, SC_RULES = [SC-02]; R-20/R-26/RC-01/RC-03/RC-04 read realRef (unified codeRef+schemaRef); +R-27 physical-MOD realRef presence (CR-228 C); -R-24/R-25 REQMOD allocation rules deleted (CR-228 A: REQMOD allocate no longer a valid pattern, R-18 flags residual edges); NFR-01 budget target split physicalMOD / behavioral→FCHAIN; RULE_TO_DIMENSION completeness (R-18..R-23/R-26/MS-03/CR-R04 mapped, no advisory fall-through) (CR-228 B/D); +BQ-01/02/04/06/07 base-quality rules promoted from aimpro (K2-b, completes rule consolidation); +ND-01/02 near-duplicate + AO-D01/D03/CR-01/RT-01/PH-01/CA-01/IO-01 architecture rules promoted from aimpro (K2-b); +CR-R01..04/MS-03 change-request rules promoted from aimpro (K2-b); +FM-01..03/NFR-01 FMEA + VR-01/CL-01 view rules promoted from aimpro (K2-b); +MT-01..03 architecture metrics promoted from aimpro (K2-b); +SC-01..03/UC-01..06/FC-01..03 quality rules promoted from aimpro (K2-a); +RC-05 cross-module import drift (CR-212); +R-26/RC-03/RC-04 schemaRef (CR-211); +R-22..R-25/R-10/R-20 (CR-201/202/208/209/210)
8
+ export const RULES_VERSION = '2.22.0'; // CR-SM-226: +RULE_TO_PHASE (readiness.ts) ruleSRR/PDR/CDR/TRR gate mapping analogous to RULE_TO_DIMENSION, exported PhaseGate enum, completeness-tested (no advisory fall-through); +DIMENSION_READINESS_NAME/PHASE_READINESS_NAME/DIMENSION_READINESS_DELTA_NAME Sprachregelung constants; -emergentPhase/-phaseScore (CR-120/146) DELETED from ReadinessReport a third, undocumented phase construct next to RULE_TO_DIMENSION and the phase-gate grouping, no computation ever lived in contracts; +R-28 Ebenen-Präsenz (funcCount>1 requires ≥1 FLOW AND ≥1 SCHEMA, ONE combined rule closes the vacuous-complete hole where 0 FLOWs/SCHEMAs read as "complete" because no per-element rule had anything to iterate) → PDR; +FC-04 FCHAIN-actor-bounded (ACTORFLOW→FUNC∈chain entry AND FUNC∈chainFLOW→ACTOR exit, both required stricter than FC-01's any-direction/UC-bypass check) → PDR; +SC-04 FLOWSCHEMA sharp rule (every FLOW must relation→SCHEMA, the per-FLOW inverse of SC-02's per-SCHEMA check) CDR; IO-01 extended to ALL FUNC pairs within an FCHAIN (removed the same-module skip same-module pairs need an explicit FLOW just as much as cross-module ones) and added to RULE_TO_PHASE PDR (was RULE_TO_DIMENSION-only); harness: +OP_RISK/isDestructiveOp/isStructuralOp destructive/structural classification per MutateCommand op (GVE-Audit F7) informational only, does not drive the Apply-Gate verdict.
9
9
  /** Meta-model version (trace pattern constraints + format-e parser). */
10
10
  export const META_MODEL_VERSION = '1.4.0'; // -REQ→MOD allocate pattern removed (CR-228 A); +FUNC→FUNC compose (blackbox function decomposition)
11
11
  export * from './ontology.js';
@@ -18,14 +18,41 @@ export declare function mt01Instability(graph: OntologyGraph): RuleViolation[];
18
18
  * Components > 1 → info.
19
19
  */
20
20
  export declare function mt02Lcom4(graph: OntologyGraph): RuleViolation[];
21
+ /** One module's allocation-cohesion measurement (CR-SM-223). */
22
+ export interface AllocationCohesion {
23
+ moduleId: string;
24
+ moduleName: string;
25
+ /** Connection pairs with both endpoints allocated to this module. */
26
+ internal: number;
27
+ /** Connection pairs with exactly one endpoint allocated to this module. */
28
+ external: number;
29
+ /** internal / (internal + external), in [0, 1]. */
30
+ cohesion: number;
31
+ }
21
32
  /**
22
- * MT-03: Allocation Cohesion (CR-191 reformulated, CR-SM-221 recalibrated).
23
- * cohesion = internal / (internal + external), over FLOW-transitive connection pairs.
24
- * internal = both endpoints allocated to this module.
25
- * external = exactly one endpoint allocated to this module.
26
- * If external === 0 cohesion = 100% OK.
33
+ * Allocation cohesion a **measurement, not a rule** (CR-SM-223, decision 2026-07-29).
34
+ *
35
+ * It used to be MT-03 with an 80 % threshold, and it fired on nearly every module of
36
+ * every real graph: 6 of 7 on graphcode, 4 of 4 on graph-view-edit, 10 of 11 on the
37
+ * family graph. `CR-SM-221` first suspected the edge definition and made it
38
+ * FLOW-transitive — the hit rate did not move. The threshold was the miscalibration:
39
+ * in a flow-routed layered architecture, module interaction crosses boundaries by
40
+ * design, so "80 % of interaction is internal" describes a monolith, not a healthy
41
+ * module.
42
+ *
43
+ * Rather than fit a cut-off to 11 data points, this reports the number and lets the
44
+ * architect judge. Returned worst-first, so the head of the list is where to look.
45
+ * Modules with fewer than two allocated FUNCs, or with no external connection at all,
46
+ * carry no signal and are omitted.
47
+ *
48
+ * Deliberately NOT a `RuleDefinition`: `computeReadiness` counts every violation into
49
+ * its dimension score regardless of severity, so a per-module advisory would depress
50
+ * the `alloc` score permanently. A measurement must not masquerade as a defect.
51
+ *
52
+ * Validation of this metric — and of MT-01/MT-02, which are thresholded the same way —
53
+ * is deferred (CR-SM-223).
27
54
  */
28
- export declare function mt03AllocationCohesion(graph: OntologyGraph): RuleViolation[];
55
+ export declare function allocationCohesion(graph: OntologyGraph): AllocationCohesion[];
29
56
  export declare const MT_RULES: readonly [{
30
57
  readonly id: "MT-01";
31
58
  readonly name: "Module instability";
@@ -36,10 +63,5 @@ export declare const MT_RULES: readonly [{
36
63
  readonly name: "Module cohesion (LCOM4)";
37
64
  readonly severity: "info";
38
65
  readonly evaluate: typeof mt02Lcom4;
39
- }, {
40
- readonly id: "MT-03";
41
- readonly name: "Allocation cohesion";
42
- readonly severity: "info";
43
- readonly evaluate: typeof mt03AllocationCohesion;
44
66
  }];
45
67
  export declare function evaluateMTRules(graph: OntologyGraph): RuleViolation[];
@@ -204,20 +204,34 @@ function connectionPairs(graph) {
204
204
  return pairs;
205
205
  }
206
206
  /**
207
- * MT-03: Allocation Cohesion (CR-191 reformulated, CR-SM-221 recalibrated).
208
- * cohesion = internal / (internal + external), over FLOW-transitive connection pairs.
209
- * internal = both endpoints allocated to this module.
210
- * external = exactly one endpoint allocated to this module.
211
- * If external === 0 cohesion = 100% OK.
207
+ * Allocation cohesion a **measurement, not a rule** (CR-SM-223, decision 2026-07-29).
208
+ *
209
+ * It used to be MT-03 with an 80 % threshold, and it fired on nearly every module of
210
+ * every real graph: 6 of 7 on graphcode, 4 of 4 on graph-view-edit, 10 of 11 on the
211
+ * family graph. `CR-SM-221` first suspected the edge definition and made it
212
+ * FLOW-transitive — the hit rate did not move. The threshold was the miscalibration:
213
+ * in a flow-routed layered architecture, module interaction crosses boundaries by
214
+ * design, so "80 % of interaction is internal" describes a monolith, not a healthy
215
+ * module.
216
+ *
217
+ * Rather than fit a cut-off to 11 data points, this reports the number and lets the
218
+ * architect judge. Returned worst-first, so the head of the list is where to look.
219
+ * Modules with fewer than two allocated FUNCs, or with no external connection at all,
220
+ * carry no signal and are omitted.
221
+ *
222
+ * Deliberately NOT a `RuleDefinition`: `computeReadiness` counts every violation into
223
+ * its dimension score regardless of severity, so a per-module advisory would depress
224
+ * the `alloc` score permanently. A measurement must not masquerade as a defect.
225
+ *
226
+ * Validation of this metric — and of MT-01/MT-02, which are thresholded the same way —
227
+ * is deferred (CR-SM-223).
212
228
  */
213
- export function mt03AllocationCohesion(graph) {
214
- const violations = [];
229
+ export function allocationCohesion(graph) {
230
+ const measurements = [];
215
231
  const mods = graph.elements.filter(e => e.type === 'MOD');
216
- const COHESION_THRESHOLD = 0.8;
217
232
  const pairs = [...connectionPairs(graph)].map(p => p.split('|'));
218
233
  for (const mod of mods) {
219
- const allocTraces = graph.traces.filter(t => t.type === 'allocate' && t.target === mod.id);
220
- const funcIds = new Set(allocTraces.map(t => t.source));
234
+ const funcIds = new Set(graph.traces.filter(t => t.type === 'allocate' && t.target === mod.id).map(t => t.source));
221
235
  if (funcIds.size < 2)
222
236
  continue;
223
237
  let internal = 0;
@@ -230,25 +244,24 @@ export function mt03AllocationCohesion(graph) {
230
244
  else if (aIn || bIn)
231
245
  external++;
232
246
  }
233
- // No external connections → cohesion = 100% OK
247
+ // No external connections → nothing to compare against, no signal.
234
248
  if (external === 0)
235
249
  continue;
236
- const cohesion = internal / (internal + external);
237
- if (cohesion < COHESION_THRESHOLD) {
238
- violations.push({
239
- rule_id: 'MT-03',
240
- severity: 'info',
241
- element_id: mod.id,
242
- message: `${mod.name} allocation cohesion ${Math.round(cohesion * 100)}% (<${COHESION_THRESHOLD * 100}%). internal=${internal}, external=${external}`,
243
- });
244
- }
250
+ measurements.push({
251
+ moduleId: mod.id,
252
+ moduleName: mod.name,
253
+ internal,
254
+ external,
255
+ cohesion: internal / (internal + external),
256
+ });
245
257
  }
246
- return violations;
258
+ // Worst first; stable by id so the ranking is deterministic.
259
+ return measurements.sort((a, b) => a.cohesion - b.cohesion || (a.moduleId < b.moduleId ? -1 : a.moduleId > b.moduleId ? 1 : 0));
247
260
  }
248
261
  export const MT_RULES = [
249
262
  { id: 'MT-01', name: 'Module instability', severity: 'warning', evaluate: mt01Instability },
250
263
  { id: 'MT-02', name: 'Module cohesion (LCOM4)', severity: 'info', evaluate: mt02Lcom4 },
251
- { id: 'MT-03', name: 'Allocation cohesion', severity: 'info', evaluate: mt03AllocationCohesion },
264
+ // MT-03 retired as a rule (CR-SM-223) see `allocationCohesion` above.
252
265
  ];
253
266
  export function evaluateMTRules(graph) {
254
267
  return MT_RULES.flatMap(r => r.evaluate(graph));
@@ -1,6 +1,11 @@
1
1
  /**
2
2
  * CR-120: Readiness dimension schemas for dynamic phase readiness.
3
3
  * Dimensions emerge from graph state — no state machine.
4
+ * CR-SM-226: `emergentPhase`/`phaseScore` (CR-120/146) removed — a third,
5
+ * undocumented phase construct next to RULE_TO_DIMENSION and the (then still
6
+ * ad-hoc) phase-gate grouping. `RULE_TO_PHASE` below is the one replacement:
7
+ * a rule → SRR/PDR/CDR/TRR mapping, same shape and same completeness
8
+ * guarantee as `RULE_TO_DIMENSION`, exported so no consumer reinvents it.
4
9
  */
5
10
  import { z } from 'zod/v4';
6
11
  export declare const ReadinessDimension: z.ZodEnum<{
@@ -48,8 +53,6 @@ export declare const ReadinessReport: z.ZodObject<{
48
53
  applicable: z.ZodNumber;
49
54
  ready: z.ZodBoolean;
50
55
  }, z.core.$strip>>;
51
- emergentPhase: z.ZodString;
52
- phaseScore: z.ZodNumber;
53
56
  overallScore: z.ZodNumber;
54
57
  timestamp: z.ZodISODateTime;
55
58
  }, z.core.$strip>;
@@ -60,3 +63,24 @@ export type ReadinessReportType = z.infer<typeof ReadinessReport>;
60
63
  * since its core concern is test verification coverage.
61
64
  */
62
65
  export declare const RULE_TO_DIMENSION: Record<string, ReadinessDimensionType>;
66
+ /** INCOSE technical-review gates, in lifecycle order. */
67
+ export declare const PhaseGate: z.ZodEnum<{
68
+ SRR: "SRR";
69
+ PDR: "PDR";
70
+ CDR: "CDR";
71
+ TRR: "TRR";
72
+ }>;
73
+ export type PhaseGateType = z.infer<typeof PhaseGate>;
74
+ /**
75
+ * Rule → phase-gate mapping (CR-SM-226). SRR = requirements/scope clarity,
76
+ * PDR = architecture/functional completeness, CDR = critical design/schema
77
+ * completeness, TRR = test readiness. Primary-gate assignment, same
78
+ * single-owner convention as RULE_TO_DIMENSION.
79
+ */
80
+ export declare const RULE_TO_PHASE: Record<string, PhaseGateType>;
81
+ /** The 8 RULE_TO_DIMENSION topic scores (req/uc/arch/alloc/ver/schema/cr/ms). */
82
+ export declare const DIMENSION_READINESS_NAME = "dimension_readiness";
83
+ /** The 4 RULE_TO_PHASE gate scores (SRR/PDR/CDR/TRR). */
84
+ export declare const PHASE_READINESS_NAME = "phase_readiness";
85
+ /** Best-of-N ranking KPI: candidate dimension_readiness minus baseline. */
86
+ export declare const DIMENSION_READINESS_DELTA_NAME = "dimension_readiness_delta";
@@ -1,15 +1,20 @@
1
1
  /**
2
2
  * CR-120: Readiness dimension schemas for dynamic phase readiness.
3
3
  * Dimensions emerge from graph state — no state machine.
4
+ * CR-SM-226: `emergentPhase`/`phaseScore` (CR-120/146) removed — a third,
5
+ * undocumented phase construct next to RULE_TO_DIMENSION and the (then still
6
+ * ad-hoc) phase-gate grouping. `RULE_TO_PHASE` below is the one replacement:
7
+ * a rule → SRR/PDR/CDR/TRR mapping, same shape and same completeness
8
+ * guarantee as `RULE_TO_DIMENSION`, exported so no consumer reinvents it.
4
9
  */
5
10
  import { z } from 'zod/v4';
6
11
  export const ReadinessDimension = z.enum([
7
12
  'req', // Requirements quality (BQ-01..07, RD-01..03)
8
- 'uc', // UC completeness (UC-01..06, R-14, FC-01..03)
13
+ 'uc', // UC completeness (UC-01..06, R-14, FC-01..04)
9
14
  'arch', // Functional architecture (R-02, R-03, R-10, R-12)
10
15
  'alloc', // Module allocation (R-04)
11
16
  'ver', // Test coverage (R-01, R-05)
12
- 'schema', // Interface completeness (R-26 binding, SC-02 usage)
17
+ 'schema', // Interface completeness (R-26 binding, SC-02/SC-04 usage)
13
18
  'cr', // CR traceability (CR-R01..R03)
14
19
  'ms', // Milestone planning (MS-01..02)
15
20
  ]);
@@ -22,8 +27,6 @@ export const ReadinessScore = z.object({
22
27
  });
23
28
  export const ReadinessReport = z.object({
24
29
  scores: z.array(ReadinessScore),
25
- emergentPhase: z.string(),
26
- phaseScore: z.number().min(0).max(1), // CR-146: % progress of current phase
27
30
  overallScore: z.number().min(0).max(1),
28
31
  timestamp: z.iso.datetime(),
29
32
  });
@@ -42,11 +45,14 @@ export const RULE_TO_DIMENSION = {
42
45
  // trace/realization/allocation completeness rules (CR-228 D: previously unmapped → advisory fall-through)
43
46
  'R-18': 'arch', 'R-19': 'ver', 'R-20': 'arch', 'R-21': 'ver',
44
47
  'R-22': 'alloc', 'R-23': 'alloc', 'R-26': 'schema', 'R-27': 'arch',
48
+ // CR-SM-226: R-28 Ebenen-Präsenz (>1 FUNC needs FLOW+SCHEMA) is an architecture rule.
49
+ 'R-28': 'arch',
45
50
  // uc
46
51
  'UC-01': 'uc', 'UC-02': 'uc', 'UC-03': 'uc', 'UC-04': 'uc',
47
52
  'UC-05': 'uc', 'UC-06': 'uc',
48
53
  'R-14': 'uc', 'R-15': 'uc', 'R-16': 'uc', 'R-17': 'uc',
49
- 'FC-01': 'uc', 'FC-02': 'uc', 'FC-03': 'uc',
54
+ // FC-04 (CR-SM-226): FCHAIN actor-bounded (trigger+consumer) — same dimension as FC-01..03.
55
+ 'FC-01': 'uc', 'FC-02': 'uc', 'FC-03': 'uc', 'FC-04': 'uc',
50
56
  // arch
51
57
  'R-02': 'arch', 'R-03': 'arch', 'R-10': 'arch', 'R-12': 'arch',
52
58
  // alloc
@@ -55,12 +61,16 @@ export const RULE_TO_DIMENSION = {
55
61
  'R-01': 'ver', 'R-05': 'ver',
56
62
  // schema
57
63
  'SC-02': 'schema', // SC-01/SC-03 deleted (BOK-CR-026) — R-26 is the binding rule
64
+ // CR-SM-226: SC-04 sharp per-FLOW SCHEMA-binding check — same dimension as SC-02.
65
+ 'SC-04': 'schema',
58
66
  // structural rules without primary dimension → assigned by closest concern
59
67
  'R-08': 'arch',
60
68
  // near-duplicate detection
61
69
  'ND-01': 'arch', 'ND-02': 'schema',
62
70
  // architecture metrics
63
- 'MT-01': 'alloc', 'MT-02': 'alloc', 'MT-03': 'alloc',
71
+ // MT-03 is no longer here: it became a measurement (`allocationCohesion`), not a
72
+ // rule (CR-SM-223) — a per-module advisory would depress this score permanently.
73
+ 'MT-01': 'alloc', 'MT-02': 'alloc',
64
74
  // CR traceability
65
75
  'CR-R01': 'cr', 'CR-R02': 'cr', 'CR-R03': 'cr', 'CR-R04': 'cr',
66
76
  // architecture optimization
@@ -79,3 +89,65 @@ export const RULE_TO_DIMENSION = {
79
89
  'VR-01': 'ver',
80
90
  'CL-01': 'uc',
81
91
  };
92
+ // ---------------------------------------------------------------------------
93
+ // Phase-gate readiness (CR-SM-226) — the INCOSE technical-review axis.
94
+ // Orthogonal to RULE_TO_DIMENSION (8 topic scores): RULE_TO_PHASE groups the
95
+ // SAME rule violations into the 4 lifecycle gates SRR/PDR/CDR/TRR. Both are
96
+ // projections of one rule stream — "three views over the same rule
97
+ // violations" (docs/articles/07-the-scoring-landscape.md), not three
98
+ // independent scoring systems. No advisory fall-through: every rule in
99
+ // ALL_RULE_DEFS must appear here (enforced by a completeness test analogous
100
+ // to the RULE_TO_DIMENSION one).
101
+ // ---------------------------------------------------------------------------
102
+ /** INCOSE technical-review gates, in lifecycle order. */
103
+ export const PhaseGate = z.enum(['SRR', 'PDR', 'CDR', 'TRR']);
104
+ /**
105
+ * Rule → phase-gate mapping (CR-SM-226). SRR = requirements/scope clarity,
106
+ * PDR = architecture/functional completeness, CDR = critical design/schema
107
+ * completeness, TRR = test readiness. Primary-gate assignment, same
108
+ * single-owner convention as RULE_TO_DIMENSION.
109
+ */
110
+ export const RULE_TO_PHASE = {
111
+ // SRR — requirements/scope clarity, system+UC boundary definition.
112
+ 'BQ-01': 'SRR', 'BQ-02': 'SRR', 'BQ-04': 'SRR', 'BQ-06': 'SRR', 'BQ-07': 'SRR',
113
+ 'RD-01': 'SRR', 'RD-02': 'SRR', 'RD-03': 'SRR',
114
+ 'UC-01': 'SRR', 'UC-02': 'SRR', 'UC-03': 'SRR', 'UC-04': 'SRR', 'UC-05': 'SRR', 'UC-06': 'SRR',
115
+ 'R-14': 'SRR', 'R-16': 'SRR', 'R-17': 'SRR',
116
+ 'FC-02': 'SRR',
117
+ 'CL-01': 'SRR',
118
+ 'FM-01': 'SRR', 'FM-02': 'SRR',
119
+ 'CR-R01': 'SRR', 'CR-R03': 'SRR',
120
+ 'MS-01': 'SRR', 'MS-02': 'SRR', 'MS-03': 'SRR',
121
+ // PDR — architecture/functional completeness.
122
+ 'RD-04': 'PDR',
123
+ 'R-02': 'PDR', 'R-03': 'PDR', 'R-08': 'PDR', 'R-10': 'PDR', 'R-12': 'PDR', 'R-18': 'PDR',
124
+ 'R-04': 'PDR', 'R-22': 'PDR', 'R-23': 'PDR',
125
+ 'R-15': 'PDR',
126
+ 'FC-01': 'PDR', 'FC-03': 'PDR', 'FC-04': 'PDR',
127
+ 'R-28': 'PDR', // Ebenen-Präsenz (CR-SM-226)
128
+ 'MT-01': 'PDR', 'MT-02': 'PDR',
129
+ 'ND-01': 'PDR',
130
+ 'AO-D01': 'PDR', 'AO-D03': 'PDR', 'CR-01': 'PDR', 'RT-01': 'PDR', 'PH-01': 'PDR', 'CA-01': 'PDR',
131
+ 'IO-01': 'PDR', // CR-SM-226: extended to all FCHAIN FUNC-pairs, added to the phase axis.
132
+ 'CR-R04': 'PDR',
133
+ // CDR — critical design/schema completeness.
134
+ 'R-26': 'CDR', 'R-27': 'CDR',
135
+ 'SC-02': 'CDR', 'SC-04': 'CDR', // SC-04: FLOW→SCHEMA sharp rule (CR-SM-226)
136
+ 'ND-02': 'CDR',
137
+ 'NFR-01': 'CDR',
138
+ // TRR — test readiness.
139
+ 'R-01': 'TRR', 'R-05': 'TRR', 'R-19': 'TRR', 'R-20': 'TRR', 'R-21': 'TRR',
140
+ 'VR-01': 'TRR',
141
+ 'FM-03': 'TRR',
142
+ 'CR-R02': 'TRR',
143
+ };
144
+ // ---------------------------------------------------------------------------
145
+ // Sprachregelung (CR-SM-226) — naming SSOT so graphcode/graph-view-edit/
146
+ // article render the same terms instead of re-inventing string literals.
147
+ // ---------------------------------------------------------------------------
148
+ /** The 8 RULE_TO_DIMENSION topic scores (req/uc/arch/alloc/ver/schema/cr/ms). */
149
+ export const DIMENSION_READINESS_NAME = 'dimension_readiness';
150
+ /** The 4 RULE_TO_PHASE gate scores (SRR/PDR/CDR/TRR). */
151
+ export const PHASE_READINESS_NAME = 'phase_readiness';
152
+ /** Best-of-N ranking KPI: candidate dimension_readiness minus baseline. */
153
+ export const DIMENSION_READINESS_DELTA_NAME = 'dimension_readiness_delta';
package/dist/se/rules.js CHANGED
@@ -898,6 +898,41 @@ function physicalModMustHaveRealRef(graph) {
898
898
  context: { element_type: mod.type, element_name: mod.name },
899
899
  }));
900
900
  }
901
+ // ---------------------------------------------------------------------------
902
+ // R-28: Ebenen-Präsenz — architecture-level presence (CR-SM-226).
903
+ // Closes the vacuous-complete hole: with 0 or 1 FUNC, per-element rules like
904
+ // R-15/FC-04/SC-04 fire 0 times and every completeness leg reads "complete" —
905
+ // not because the architecture is done, but because there is nothing to check
906
+ // yet. Once >1 FUNC exists, the graph must ALSO carry at least one FLOW (data
907
+ // moves between functions) AND at least one SCHEMA (that data has a
908
+ // contract) — ONE combined rule, not staged PDR/CDR checks (the CR draft's
909
+ // two-stage proposal was simplified in Familie-Review 2026-08-04).
910
+ // ---------------------------------------------------------------------------
911
+ function ebenenPraesenz(graph) {
912
+ const funcs = graph.elements.filter(e => e.type === 'FUNC');
913
+ if (funcs.length <= 1)
914
+ return [];
915
+ const flows = graph.elements.filter(e => e.type === 'FLOW');
916
+ const schemas = graph.elements.filter(e => e.type === 'SCHEMA');
917
+ const missingLevels = [];
918
+ if (flows.length === 0)
919
+ missingLevels.push('FLOW');
920
+ if (schemas.length === 0)
921
+ missingLevels.push('SCHEMA');
922
+ if (missingLevels.length === 0)
923
+ return [];
924
+ // Anchor the graph-wide violation on SYS (the root) if present, else the
925
+ // first FUNC — there is no single "population" element this rule checks.
926
+ const anchor = graph.elements.find(e => e.type === 'SYS') ?? funcs[0];
927
+ return [{
928
+ rule_id: 'R-28',
929
+ severity: 'warning',
930
+ element_id: anchor.id,
931
+ message: `Graph has ${funcs.length} FUNCs but no ${missingLevels.join(' and no ')} element — architecture level(s) absent`,
932
+ fix_hint: `Add at least one ${missingLevels.join(' and one ')} element to bind the functional and data-level architecture`,
933
+ context: { element_type: anchor.type, element_name: anchor.name },
934
+ }];
935
+ }
901
936
  export const V3_RULES = [
902
937
  { id: 'R-01', name: 'REQ must have verification', severity: 'error', evaluate: reqMustHaveVerification },
903
938
  { id: 'R-02', name: 'FUNC must satisfy REQ', severity: 'warning', evaluate: funcMustSatisfyReq },
@@ -925,6 +960,7 @@ export const V3_RULES = [
925
960
  { id: 'RD-04', name: 'Decomposition breadth', severity: 'warning', evaluate: decompositionBreadth },
926
961
  { id: 'MS-01', name: 'Milestone empty scope', severity: 'warning', evaluate: msEmptyScope },
927
962
  { id: 'MS-02', name: 'Milestone dangling dependency', severity: 'error', evaluate: msDanglingDependency },
963
+ { id: 'R-28', name: 'Ebenen-Präsenz (FLOW+SCHEMA when funcCount>1)', severity: 'warning', evaluate: ebenenPraesenz },
928
964
  ];
929
965
  /** Run all rules against a graph */
930
966
  export function evaluateRules(graph) {
@@ -15,5 +15,6 @@
15
15
  import type { OntologyGraph } from './ontology.js';
16
16
  import type { RuleDefinition, RuleViolation } from './rules.js';
17
17
  export declare function sc02IsReferenced(graph: OntologyGraph): RuleViolation[];
18
+ export declare function sc04FlowHasSchema(graph: OntologyGraph): RuleViolation[];
18
19
  export declare const SC_RULES: RuleDefinition[];
19
20
  export declare function evaluateSCRules(graph: OntologyGraph): RuleViolation[];
@@ -20,10 +20,37 @@ export function sc02IsReferenced(graph) {
20
20
  }));
21
21
  }
22
22
  // ---------------------------------------------------------------------------
23
+ // SC-04: FLOW must reference a SCHEMA (CR-SM-226) — the sharp per-FLOW
24
+ // inverse of SC-02. SC-02 catches an orphan SCHEMA (nobody uses it); SC-04
25
+ // catches a FLOW with no data contract at all — until now only caught
26
+ // loosely, around the SC-02-adjacent CDR completeness leg, never as its own
27
+ // rule.
28
+ // ---------------------------------------------------------------------------
29
+ export function sc04FlowHasSchema(graph) {
30
+ const schemas = graph.elements.filter(e => e.type === 'SCHEMA');
31
+ return graph.elements
32
+ .filter(e => e.type === 'FLOW')
33
+ .filter(f => !graph.traces.some(t => t.source === f.id && t.type === 'relation' &&
34
+ graph.elements.some(e => e.id === t.target && e.type === 'SCHEMA')))
35
+ .map(f => ({
36
+ rule_id: 'SC-04',
37
+ severity: 'warning',
38
+ element_id: f.id,
39
+ message: `${f.id} has no SCHEMA binding`,
40
+ fix_hint: 'Link a SCHEMA via relation trace to define this FLOW\'s data contract',
41
+ context: {
42
+ element_type: f.type,
43
+ element_name: f.name,
44
+ candidate_targets: schemas.map(s => ({ id: s.id, type: s.type, name: s.name })),
45
+ },
46
+ }));
47
+ }
48
+ // ---------------------------------------------------------------------------
23
49
  // Aggregated array & convenience runner
24
50
  // ---------------------------------------------------------------------------
25
51
  export const SC_RULES = [
26
52
  { id: 'SC-02', name: 'Schema referenced by FLOW', severity: 'warning', evaluate: sc02IsReferenced },
53
+ { id: 'SC-04', name: 'FLOW has SCHEMA binding', severity: 'warning', evaluate: sc04FlowHasSchema },
27
54
  ];
28
55
  export function evaluateSCRules(graph) {
29
56
  return SC_RULES.flatMap(rule => rule.evaluate(graph));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sigloch/contracts",
3
- "version": "1.0.0",
3
+ "version": "3.0.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",