@sigloch/contracts 2.0.0 → 3.1.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
@@ -0,0 +1,39 @@
1
+ /**
2
+ * CR-SM-227: Analysis-Freshness-Legs — the third leg-kind a review gate checks
3
+ * (rule legs, layer-presence legs [R-28], 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`).
9
+ */
10
+ import type { OntologyGraph } from './ontology.js';
11
+ import type { RuleViolation } from './rules.js';
12
+ export declare const AF_RULES: readonly [{
13
+ readonly id: "AF-01";
14
+ readonly name: "ConOps freshness stamp present";
15
+ readonly severity: "warning";
16
+ readonly evaluate: (graph: OntologyGraph) => RuleViolation[];
17
+ }, {
18
+ readonly id: "AF-02";
19
+ readonly name: "Trade Study freshness stamp present";
20
+ readonly severity: "warning";
21
+ readonly evaluate: (graph: OntologyGraph) => RuleViolation[];
22
+ }, {
23
+ readonly id: "AF-03";
24
+ readonly name: "Assumption Review freshness stamp present";
25
+ readonly severity: "warning";
26
+ readonly evaluate: (graph: OntologyGraph) => RuleViolation[];
27
+ }, {
28
+ readonly id: "AF-04";
29
+ readonly name: "FMEA freshness stamp present";
30
+ readonly severity: "warning";
31
+ readonly evaluate: (graph: OntologyGraph) => RuleViolation[];
32
+ }, {
33
+ readonly id: "AF-05";
34
+ readonly name: "Implementation Plan freshness stamp present";
35
+ readonly severity: "warning";
36
+ readonly evaluate: (graph: OntologyGraph) => RuleViolation[];
37
+ }];
38
+ /** Evaluate all analysis-freshness-presence rules. */
39
+ export declare function evaluateAFRules(graph: OntologyGraph): RuleViolation[];
@@ -0,0 +1,44 @@
1
+ import { AnalysisFreshnessStampSchema } from './ontology.js';
2
+ const ARTIFACT_LABEL = {
3
+ conops: 'Concept of Operations',
4
+ trade: 'Trade Study',
5
+ 'assumption-review': 'Assumption Review',
6
+ fmea: 'FMEA',
7
+ implplan: 'Implementation Plan',
8
+ };
9
+ /** Presence-check for one analysis artifact's freshness stamp, anchored on SYS (like R-28). */
10
+ function analysisFreshnessPresence(ruleId, artifactId) {
11
+ return (graph) => {
12
+ const sys = graph.elements.find(e => e.type === 'SYS');
13
+ if (!sys)
14
+ return []; // nothing to anchor on yet (same vacuous-complete exemption as R-28)
15
+ const stamp = sys.attributes?.analysisFreshness?.[artifactId];
16
+ if (AnalysisFreshnessStampSchema.safeParse(stamp).success)
17
+ return [];
18
+ const label = ARTIFACT_LABEL[artifactId];
19
+ return [{
20
+ rule_id: ruleId,
21
+ severity: 'warning',
22
+ 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`,
25
+ context: { element_type: sys.type, element_name: sys.name },
26
+ }];
27
+ };
28
+ }
29
+ const conopsFreshnessPresent = analysisFreshnessPresence('AF-01', 'conops');
30
+ const tradeFreshnessPresent = analysisFreshnessPresence('AF-02', 'trade');
31
+ const assumptionReviewFreshnessPresent = analysisFreshnessPresence('AF-03', 'assumption-review');
32
+ const fmeaFreshnessPresent = analysisFreshnessPresence('AF-04', 'fmea');
33
+ const implplanFreshnessPresent = analysisFreshnessPresence('AF-05', 'implplan');
34
+ export const AF_RULES = [
35
+ { id: 'AF-01', name: 'ConOps freshness stamp present', severity: 'warning', evaluate: conopsFreshnessPresent },
36
+ { id: 'AF-02', name: 'Trade Study freshness stamp present', severity: 'warning', evaluate: tradeFreshnessPresent },
37
+ { id: 'AF-03', name: 'Assumption Review freshness stamp present', severity: 'warning', evaluate: assumptionReviewFreshnessPresent },
38
+ { id: 'AF-04', name: 'FMEA freshness stamp present', severity: 'warning', evaluate: fmeaFreshnessPresent },
39
+ { id: 'AF-05', name: 'Implementation Plan freshness stamp present', severity: 'warning', evaluate: implplanFreshnessPresent },
40
+ ];
41
+ /** Evaluate all analysis-freshness-presence rules. */
42
+ export function evaluateAFRules(graph) {
43
+ return AF_RULES.flatMap(r => r.evaluate(graph));
44
+ }
@@ -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));
@@ -9,6 +9,7 @@ import { CR_RULES, evaluateCRRules } from './cr-quality-rules.js';
9
9
  import { ND_RULES, evaluateNDRules } from './near-duplicate-rules.js';
10
10
  import { AO_RULES, evaluateAORules } from './ao-rules.js';
11
11
  import { BQ_RULES, evaluateBQRules } from './quality-rules.js';
12
+ import { AF_RULES, evaluateAFRules } from './analysis-freshness-rules.js';
12
13
  /** All prescribed rule definitions (single source of truth for the catalog). */
13
14
  export const ALL_RULE_DEFS = [
14
15
  ...V3_RULES.map(r => ({ id: r.id, name: r.name, severity: r.severity })),
@@ -22,10 +23,11 @@ export const ALL_RULE_DEFS = [
22
23
  ...AO_RULES.map(r => ({ id: r.id, name: r.name, severity: r.severity })),
23
24
  ...FM_RULES.map(r => ({ id: r.id, name: r.name, severity: r.severity })),
24
25
  ...VIEW_RULES.map(r => ({ id: r.id, name: r.name, severity: r.severity })),
26
+ ...AF_RULES.map(r => ({ id: r.id, name: r.name, severity: r.severity })),
25
27
  ];
26
28
  // CR-SM-221: 'RD-' was missing — 'RD-01'.startsWith('R-') is false, so the
27
29
  // decomposition rules ran in `default` only and never in the `se` profile.
28
- const SE_PREFIXES = ['R-', 'RD-', 'UC-', 'FC-', 'SC-', 'MT-', 'CR-', 'AO-', 'FM-', 'NFR-', 'RT-', 'PH-', 'CA-', 'IO-', 'VR-', 'CL-'];
30
+ const SE_PREFIXES = ['R-', 'RD-', 'UC-', 'FC-', 'SC-', 'MT-', 'CR-', 'AO-', 'FM-', 'NFR-', 'RT-', 'PH-', 'CA-', 'IO-', 'VR-', 'CL-', 'AF-'];
29
31
  const CODING_PREFIXES = ['BQ-', 'ND-'];
30
32
  export function getRuleDefsForProfile(profile) {
31
33
  if (profile === 'se')
@@ -48,5 +50,6 @@ export function evaluateAllRules(graph) {
48
50
  ...evaluateAORules(graph),
49
51
  ...evaluateFMRules(graph),
50
52
  ...evaluateViewRules(graph),
53
+ ...evaluateAFRules(graph),
51
54
  ];
52
55
  }
@@ -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.21.0";
8
+ export declare const RULES_VERSION = "2.23.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';
@@ -21,6 +21,7 @@ export * from './cr-quality-rules.js';
21
21
  export * from './near-duplicate-rules.js';
22
22
  export * from './ao-rules.js';
23
23
  export * from './quality-rules.js';
24
+ export * from './analysis-freshness-rules.js';
24
25
  export * from './evaluate-all.js';
25
26
  export * from './readiness.js';
26
27
  export * from './meta-model.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.21.0'; // -MT-03 retired as a rule and reborn as the `allocationCohesion` measurement (CR-SM-223): the 80 % threshold fired on 6/7 graphcode, 4/4 gve and 10/11 family modules — in a flow-routed architecture cross-boundary interaction is the design, not a defect; the metric now reports internal/external per MOD, worst-first, and stays out of the violation stream so it cannot depress the readiness score; +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 REQ→MOD allocation rules deleted (CR-228 A: REQ→MOD allocate no longer a valid pattern, R-18 flags residual edges); NFR-01 budget target split physical→MOD / 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.23.0'; // CR-SM-227: +AF-01..05 Analysis-Freshness-Legs (analysis-freshness-rules.ts)the third leg-kind a review gate checks (rule legs, layer-presence legs [R-28], now analysis-freshness legs, docs/articles/07-the-scoring-landscape.md); presence-only rules (Presence/Resolution-Split like R-19/R-20/R-26/R-27 vs. RC-01..05) checking a `graphVersion` freshness stamp exists under SYS.attributes.analysisFreshness.<AnalysisArtifactId> for each of the 5 CR-GC-221 judgment artifacts (conops/trade/assumption-review/fmea/implplan) staleness (stamp vs live graphVersion) stays a consumer/I-O concern (`CreationCurrencyProvider`, @sigloch/graphcode-client), NOT evaluated here; Familie-Review 2026-08-05 gate assignment → RULE_TO_PHASE: ConOps/Trade/Assumption-Review PDR, FMEA CDR, Implementation Plan TRR (SRR gets none); +AnalysisArtifactId enum + AnalysisFreshnessStampSchema (ontology.ts) as the SSOT graphcode-client's ARTIFACT_CATALOG analysis ids must match; grober Graph-Versions-Stempel only, no scope-hash in this first cut (conscious CR-SM-227 limitation).
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';
@@ -21,6 +21,7 @@ export * from './cr-quality-rules.js';
21
21
  export * from './near-duplicate-rules.js';
22
22
  export * from './ao-rules.js';
23
23
  export * from './quality-rules.js';
24
+ export * from './analysis-freshness-rules.js';
24
25
  export * from './evaluate-all.js';
25
26
  export * from './readiness.js';
26
27
  export * from './meta-model.js';
@@ -131,6 +131,34 @@ export declare const RealRefSchema: z.ZodObject<{
131
131
  lang: z.ZodOptional<z.ZodString>;
132
132
  }, z.core.$strip>;
133
133
  export type RealRef = z.infer<typeof RealRefSchema>;
134
+ /**
135
+ * The five judgment-work artifact ids (CR-GC-221's creation keys) that graphcode's
136
+ * skills produce and that a phase gate checks for presence+freshness (CR-SM-227).
137
+ * SSOT — `@sigloch/graphcode-client`'s `ARTIFACT_CATALOG` (`kind:'analysis'` rows)
138
+ * must use exactly these ids.
139
+ */
140
+ export declare const AnalysisArtifactId: z.ZodEnum<{
141
+ conops: "conops";
142
+ fmea: "fmea";
143
+ trade: "trade";
144
+ implplan: "implplan";
145
+ "assumption-review": "assumption-review";
146
+ }>;
147
+ export type AnalysisArtifactId = z.infer<typeof AnalysisArtifactId>;
148
+ /**
149
+ * A "this analysis covers graph-stand V" stamp (CR-SM-227) — the grober
150
+ * Graph-Versions-Stempel Familie-Review decided (no scope-hash in the first
151
+ * cut, see CR-SM-227 "Achtung"). `graphVersion` is the harness's monotone
152
+ * counter (`ToolContext.graphVersion()`, graphcode) at the moment the
153
+ * artifact was written — I/O, so writing it stays a consumer (graphcode)
154
+ * concern; contracts only defines the shape.
155
+ * Stored under `OntologyElement.attributes.analysisFreshness.<AnalysisArtifactId>`
156
+ * on the SYS root element (same anchor convention as R-28).
157
+ */
158
+ export declare const AnalysisFreshnessStampSchema: z.ZodObject<{
159
+ graphVersion: z.ZodNumber;
160
+ }, z.core.$strip>;
161
+ export type AnalysisFreshnessStamp = z.infer<typeof AnalysisFreshnessStampSchema>;
134
162
  /**
135
163
  * An element (node) in the SE ontology graph.
136
164
  * `attributes` holds type-specific properties (e.g. FUNC.safety_relevant).
@@ -92,6 +92,26 @@ export const RealRefSchema = z.object({
92
92
  symbol: z.string().optional(),
93
93
  lang: z.string().optional(),
94
94
  });
95
+ /**
96
+ * The five judgment-work artifact ids (CR-GC-221's creation keys) that graphcode's
97
+ * skills produce and that a phase gate checks for presence+freshness (CR-SM-227).
98
+ * SSOT — `@sigloch/graphcode-client`'s `ARTIFACT_CATALOG` (`kind:'analysis'` rows)
99
+ * must use exactly these ids.
100
+ */
101
+ export const AnalysisArtifactId = z.enum(['conops', 'fmea', 'trade', 'implplan', 'assumption-review']);
102
+ /**
103
+ * A "this analysis covers graph-stand V" stamp (CR-SM-227) — the grober
104
+ * Graph-Versions-Stempel Familie-Review decided (no scope-hash in the first
105
+ * cut, see CR-SM-227 "Achtung"). `graphVersion` is the harness's monotone
106
+ * counter (`ToolContext.graphVersion()`, graphcode) at the moment the
107
+ * artifact was written — I/O, so writing it stays a consumer (graphcode)
108
+ * concern; contracts only defines the shape.
109
+ * Stored under `OntologyElement.attributes.analysisFreshness.<AnalysisArtifactId>`
110
+ * on the SYS root element (same anchor convention as R-28).
111
+ */
112
+ export const AnalysisFreshnessStampSchema = z.object({
113
+ graphVersion: z.number().int().nonnegative(),
114
+ });
95
115
  /**
96
116
  * An element (node) in the SE ontology graph.
97
117
  * `attributes` holds type-specific properties (e.g. FUNC.safety_relevant).
@@ -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,6 +61,8 @@ 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
@@ -73,6 +81,11 @@ export const RULE_TO_DIMENSION = {
73
81
  'MS-01': 'ms', 'MS-02': 'ms', 'MS-03': 'ms',
74
82
  // FMEA / risk
75
83
  'FM-01': 'req', 'FM-02': 'req', 'FM-03': 'ver',
84
+ // CR-SM-227: analysis-artifact freshness presence — closest topical fit per artifact.
85
+ 'AF-01': 'req', 'AF-03': 'req', // ConOps/Assumption-Review: requirements-adjacent judgment work
86
+ 'AF-02': 'arch', // Trade Study: architecture decision record
87
+ 'AF-04': 'ver', // FMEA: risk/verification, same bucket as FM-03
88
+ 'AF-05': 'ms', // Implementation Plan: milestone/planning
76
89
  // NFR budget
77
90
  'NFR-01': 'arch',
78
91
  // cross-module IO (CR-192)
@@ -81,3 +94,71 @@ export const RULE_TO_DIMENSION = {
81
94
  'VR-01': 'ver',
82
95
  'CL-01': 'uc',
83
96
  };
97
+ // ---------------------------------------------------------------------------
98
+ // Phase-gate readiness (CR-SM-226) — the INCOSE technical-review axis.
99
+ // Orthogonal to RULE_TO_DIMENSION (8 topic scores): RULE_TO_PHASE groups the
100
+ // SAME rule violations into the 4 lifecycle gates SRR/PDR/CDR/TRR. Both are
101
+ // projections of one rule stream — "three views over the same rule
102
+ // violations" (docs/articles/07-the-scoring-landscape.md), not three
103
+ // independent scoring systems. No advisory fall-through: every rule in
104
+ // ALL_RULE_DEFS must appear here (enforced by a completeness test analogous
105
+ // to the RULE_TO_DIMENSION one).
106
+ // ---------------------------------------------------------------------------
107
+ /** INCOSE technical-review gates, in lifecycle order. */
108
+ export const PhaseGate = z.enum(['SRR', 'PDR', 'CDR', 'TRR']);
109
+ /**
110
+ * Rule → phase-gate mapping (CR-SM-226). SRR = requirements/scope clarity,
111
+ * PDR = architecture/functional completeness, CDR = critical design/schema
112
+ * completeness, TRR = test readiness. Primary-gate assignment, same
113
+ * single-owner convention as RULE_TO_DIMENSION.
114
+ */
115
+ export const RULE_TO_PHASE = {
116
+ // CR-SM-227: Analysis-Freshness-Legs (AF-01..05) — Familie-Review 2026-08-05
117
+ // gate assignment, final: ConOps/Trade/Assumption-Review → PDR, FMEA → CDR,
118
+ // Implementation Plan → TRR. SRR gets no analysis-freshness leg.
119
+ 'AF-01': 'PDR', 'AF-02': 'PDR', 'AF-03': 'PDR', // ConOps, Trade Study, Assumption Review
120
+ 'AF-04': 'CDR', // FMEA
121
+ 'AF-05': 'TRR', // Implementation Plan
122
+ // SRR — requirements/scope clarity, system+UC boundary definition.
123
+ 'BQ-01': 'SRR', 'BQ-02': 'SRR', 'BQ-04': 'SRR', 'BQ-06': 'SRR', 'BQ-07': 'SRR',
124
+ 'RD-01': 'SRR', 'RD-02': 'SRR', 'RD-03': 'SRR',
125
+ 'UC-01': 'SRR', 'UC-02': 'SRR', 'UC-03': 'SRR', 'UC-04': 'SRR', 'UC-05': 'SRR', 'UC-06': 'SRR',
126
+ 'R-14': 'SRR', 'R-16': 'SRR', 'R-17': 'SRR',
127
+ 'FC-02': 'SRR',
128
+ 'CL-01': 'SRR',
129
+ 'FM-01': 'SRR', 'FM-02': 'SRR',
130
+ 'CR-R01': 'SRR', 'CR-R03': 'SRR',
131
+ 'MS-01': 'SRR', 'MS-02': 'SRR', 'MS-03': 'SRR',
132
+ // PDR — architecture/functional completeness.
133
+ 'RD-04': 'PDR',
134
+ 'R-02': 'PDR', 'R-03': 'PDR', 'R-08': 'PDR', 'R-10': 'PDR', 'R-12': 'PDR', 'R-18': 'PDR',
135
+ 'R-04': 'PDR', 'R-22': 'PDR', 'R-23': 'PDR',
136
+ 'R-15': 'PDR',
137
+ 'FC-01': 'PDR', 'FC-03': 'PDR', 'FC-04': 'PDR',
138
+ 'R-28': 'PDR', // Ebenen-Präsenz (CR-SM-226)
139
+ 'MT-01': 'PDR', 'MT-02': 'PDR',
140
+ 'ND-01': 'PDR',
141
+ 'AO-D01': 'PDR', 'AO-D03': 'PDR', 'CR-01': 'PDR', 'RT-01': 'PDR', 'PH-01': 'PDR', 'CA-01': 'PDR',
142
+ 'IO-01': 'PDR', // CR-SM-226: extended to all FCHAIN FUNC-pairs, added to the phase axis.
143
+ 'CR-R04': 'PDR',
144
+ // CDR — critical design/schema completeness.
145
+ 'R-26': 'CDR', 'R-27': 'CDR',
146
+ 'SC-02': 'CDR', 'SC-04': 'CDR', // SC-04: FLOW→SCHEMA sharp rule (CR-SM-226)
147
+ 'ND-02': 'CDR',
148
+ 'NFR-01': 'CDR',
149
+ // TRR — test readiness.
150
+ 'R-01': 'TRR', 'R-05': 'TRR', 'R-19': 'TRR', 'R-20': 'TRR', 'R-21': 'TRR',
151
+ 'VR-01': 'TRR',
152
+ 'FM-03': 'TRR',
153
+ 'CR-R02': 'TRR',
154
+ };
155
+ // ---------------------------------------------------------------------------
156
+ // Sprachregelung (CR-SM-226) — naming SSOT so graphcode/graph-view-edit/
157
+ // article render the same terms instead of re-inventing string literals.
158
+ // ---------------------------------------------------------------------------
159
+ /** The 8 RULE_TO_DIMENSION topic scores (req/uc/arch/alloc/ver/schema/cr/ms). */
160
+ export const DIMENSION_READINESS_NAME = 'dimension_readiness';
161
+ /** The 4 RULE_TO_PHASE gate scores (SRR/PDR/CDR/TRR). */
162
+ export const PHASE_READINESS_NAME = 'phase_readiness';
163
+ /** Best-of-N ranking KPI: candidate dimension_readiness minus baseline. */
164
+ 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": "2.0.0",
3
+ "version": "3.1.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",