@sigloch/contracts 3.0.0 → 3.2.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.
@@ -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
+ }
@@ -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
  }
@@ -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.22.0";
8
+ export declare const RULES_VERSION = "2.24.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.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 rulecloses 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 (ACTOR→FLOW→FUNC∈chain entry AND FUNC∈chain→FLOW→ACTOR exit, both required stricter than FC-01's any-direction/UC-bypass check) PDR; +SC-04 FLOW→SCHEMA 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.
8
+ export const RULES_VERSION = '2.24.0'; // CR-GC-315: R-12 + R-21 no longer tax FLOW reuse. R-12 restricted to dependency-carrying traces (compose/allocate/relation) — on `io` the only reachable shape was FUNC ─ioFLOW ─io→ the SAME FUNC, i.e. read-modify-write on a shared FLOW, so the check was pure false-positive there (real io-cycles span ≥2 FUNCs and a 2-cycle test cannot see them); satisfy/verify/produces 2-cycles are pattern-illegal and belong to R-18. Its dedup key is now direction-independent (`type|sorted pair`) keying on the message never collapsed anything, so every cycle was reported twice. R-21 no longer derives FUNC↔FUNC connections from FLOW co-adjacency alone: a producer/consumer pair with NO shared FCHAIN is silent. One hub FLOW with P producers and C consumers manufactured P·C findings, each demanding its own FCHAIN+integration test including pairs that never interact reuse was penalised quadratically. The FCHAIN is the declared integration scope; the test is owed on that declared claim only. Rule intent (CR-GC-240 gap: unit/UC tests do not cover FUNC↔FUNC wiring) unchanged. Prior: 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).
@@ -81,6 +81,11 @@ export const RULE_TO_DIMENSION = {
81
81
  'MS-01': 'ms', 'MS-02': 'ms', 'MS-03': 'ms',
82
82
  // FMEA / risk
83
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
84
89
  // NFR budget
85
90
  'NFR-01': 'arch',
86
91
  // cross-module IO (CR-192)
@@ -108,6 +113,12 @@ export const PhaseGate = z.enum(['SRR', 'PDR', 'CDR', 'TRR']);
108
113
  * single-owner convention as RULE_TO_DIMENSION.
109
114
  */
110
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
111
122
  // SRR — requirements/scope clarity, system+UC boundary definition.
112
123
  'BQ-01': 'SRR', 'BQ-02': 'SRR', 'BQ-04': 'SRR', 'BQ-06': 'SRR', 'BQ-07': 'SRR',
113
124
  'RD-01': 'SRR', 'RD-02': 'SRR', 'RD-03': 'SRR',
package/dist/se/rules.js CHANGED
@@ -312,28 +312,39 @@ function flowCompleteness(graph) {
312
312
  // R-11: REMOVED — superseded by SC-02 (identical check, better severity).
313
313
  // ---------------------------------------------------------------------------
314
314
  // ---------------------------------------------------------------------------
315
- // R-12: Circular dependency detection
316
- // ---------------------------------------------------------------------------
315
+ // R-12: Circular dependency detection — direct 2-cycles over dependency-carrying
316
+ // traces only (CR-GC-315). `io` is excluded: the only shape a 2-cycle test can
317
+ // hit on `io` is FUNC ─io→ FLOW ─io→ the SAME FUNC, i.e. read-modify-write on a
318
+ // shared FLOW — the reuse pattern, not a dependency cycle. Real io-cycles span
319
+ // ≥2 FUNCs and are invisible to a 2-cycle test anyway, so on `io` the rule was
320
+ // pure false-positive and taxed FLOW reuse. satisfy/verify/produces 2-cycles are
321
+ // already pattern-illegal and belong to R-18.
322
+ // ---------------------------------------------------------------------------
323
+ /** Trace types on which a direct 2-cycle is a real finding (CR-GC-315). */
324
+ const CIRCULAR_TRACE_TYPES = new Set(['compose', 'allocate', 'relation']);
317
325
  function noDirectCircular(graph) {
318
326
  const violations = [];
319
- for (const t of graph.traces) {
320
- if (graph.traces.some(other => other.source === t.target && other.target === t.source && other.type === t.type)) {
321
- violations.push({
322
- rule_id: 'R-12',
323
- severity: 'warning',
324
- element_id: t.source,
325
- message: `Circular ${t.type} between ${t.source} and ${t.target}`,
326
- });
327
- }
328
- }
327
+ // Direction-independent key: A↔B is ONE cycle, reported once. Keying on the
328
+ // message (CR-GC-315 predecessor) never collapsed anything the two
329
+ // directions render different messages, so every cycle was reported twice.
329
330
  const seen = new Set();
330
- return violations.filter(v => {
331
- const key = v.message;
331
+ for (const t of graph.traces) {
332
+ if (!CIRCULAR_TRACE_TYPES.has(t.type))
333
+ continue;
334
+ if (!graph.traces.some(other => other.source === t.target && other.target === t.source && other.type === t.type))
335
+ continue;
336
+ const key = `${t.type}|${[t.source, t.target].sort().join('|')}`;
332
337
  if (seen.has(key))
333
- return false;
338
+ continue;
334
339
  seen.add(key);
335
- return true;
336
- });
340
+ violations.push({
341
+ rule_id: 'R-12',
342
+ severity: 'warning',
343
+ element_id: t.source,
344
+ message: `Circular ${t.type} between ${t.source} and ${t.target}`,
345
+ });
346
+ }
347
+ return violations;
337
348
  }
338
349
  // ---------------------------------------------------------------------------
339
350
  // R-13: DELETED — superseded by RD-01 (CR-180)
@@ -724,13 +735,17 @@ function funcMustHaveCodeBinding(graph) {
724
735
  }
725
736
  /** All rules — see RULES_VERSION in ./index.ts (R-06/R-07/R-09/R-11/R-13/R-24/R-25 removed, R-14..R-23/R-26/RD-01..03/MS-01..02 added) */
726
737
  // ---------------------------------------------------------------------------
727
- // R-21: Every FUNC↔FUNC connection must be covered by an integration test.
728
- // A connection is FUNC ─io→ FLOW ─io→ FUNC. It is covered iff both
729
- // endpoints share an FCHAIN that owns a verifying integration test
730
- // (TEST ─verify→ REQ ←satisfy─ FCHAIN). Unit tests (FUNC→REQ) and UC
731
- // acceptance tests do NOT cover the interface between two functions —
732
- // CR-GC-240 folded "integration" onto the UC level, leaving FUNC↔FUNC
733
- // wiring unverified; this rule closes that gap edge-granularly.
738
+ // R-21: Every FUNC↔FUNC connection *declared as an FCHAIN* must be covered by an
739
+ // integration test. A connection is FUNC ─io→ FLOW ─io→ FUNC. It is
740
+ // covered iff both endpoints share an FCHAIN that owns a verifying
741
+ // integration test (TEST ─verify→ REQ ←satisfy─ FCHAIN). Unit tests
742
+ // (FUNC→REQ) and UC acceptance tests do NOT cover the interface between
743
+ // two functions — CR-GC-240 folded "integration" onto the UC level,
744
+ // leaving FUNC↔FUNC wiring unverified; this rule closes that gap.
745
+ // CR-GC-315: a producer/consumer pair with NO shared FCHAIN is silent.
746
+ // Co-adjacency at a shared FLOW is not an asserted interface — treating it
747
+ // as one produced P·C findings per hub FLOW and made reuse the expensive
748
+ // choice. The FCHAIN is the modelled claim; the test is owed on the claim.
734
749
  // ---------------------------------------------------------------------------
735
750
  function fchainMustHaveIntegrationTest(graph) {
736
751
  const typeOf = new Map(graph.elements.map(e => [e.id, e.type]));
@@ -769,9 +784,15 @@ function fchainMustHaveIntegrationTest(graph) {
769
784
  const seen = new Set();
770
785
  for (const [p, c] of connections) {
771
786
  const shared = [...(chainsOfFunc.get(p) ?? [])].filter(ch => chainsOfFunc.get(c)?.has(ch));
787
+ // No shared FCHAIN = no asserted integration (CR-GC-315). Sharing one FLOW
788
+ // between P producers and C consumers does not mean P·C interfaces exist —
789
+ // deriving connections from FLOW adjacency alone taxed reuse quadratically.
790
+ // The FCHAIN is the declared integration scope; only that is held to a test.
791
+ if (shared.length === 0)
792
+ continue;
772
793
  if (shared.some(ch => testedChains.has(ch)))
773
794
  continue;
774
- const anchor = shared[0] ?? p;
795
+ const anchor = shared[0];
775
796
  const key = `${anchor}|${p}>${c}`;
776
797
  if (seen.has(key))
777
798
  continue;
@@ -781,10 +802,8 @@ function fchainMustHaveIntegrationTest(graph) {
781
802
  rule_id: 'R-21',
782
803
  severity: 'warning',
783
804
  element_id: anchor,
784
- message: shared.length > 0
785
- ? `${anchor} has no integration test covering connection ${p} → ${c}`
786
- : `connection ${p} → ${c} is in no FCHAIN with an integration test`,
787
- fix_hint: 'Group both FUNCs in an FCHAIN and verify an FCHAIN-satisfied REQ with an integration TEST',
805
+ message: `${anchor} has no integration test covering connection ${p} → ${c}`,
806
+ fix_hint: 'Verify an FCHAIN-satisfied REQ with an integration TEST',
788
807
  context: { element_type: anchorEl?.type, element_name: anchorEl?.name },
789
808
  });
790
809
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sigloch/contracts",
3
- "version": "3.0.0",
3
+ "version": "3.2.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",