@sigloch/contracts 10.10.0 → 10.13.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.
package/dist/se/rules.js CHANGED
@@ -4,7 +4,7 @@
4
4
  * @sigloch/contracts/se — single source of truth for SE validation rules.
5
5
  */
6
6
  import { z } from 'zod/v4';
7
- import { ElementType, TraceType, TestRefsSchema, RealRefSchema } from './ontology.js';
7
+ import { ElementType, TraceType, readTestRefs, readRealRef } from './ontology.js';
8
8
  import { traceRejection, admittingAlternatives, isValidTrace, BOUNDED_PATTERNS, REQUIRED_PATTERNS, maxOccurs } from './meta-model.js';
9
9
  import { indexOf } from './graph-index.js';
10
10
  import { moduleCrossings, subtreeFuncs } from './module-crossings.js';
@@ -35,6 +35,17 @@ export const ViolationContext = z.object({
35
35
  * unterschlagen.
36
36
  */
37
37
  also_affects: z.array(z.string()).optional(),
38
+ /**
39
+ * CR-SM-363 (FC-05): die NEBENKOMPONENTEN einer zerfallenen FCHAIN, je mit Ursache —
40
+ * `missing-link` (Haupt- und Nebenteil sind ueber eine FUNC ausserhalb der Kette verbunden;
41
+ * `via` nennt sie) oder `bag` (nur eine gemeinsame Quelle, parallele Dienste). EIN Befund je
42
+ * Kette (CR-SM-242), die Komponenten hier statt als n Befunde.
43
+ */
44
+ side_components: z.array(z.object({
45
+ members: z.array(z.string()),
46
+ cause: z.enum(['missing-link', 'bag']),
47
+ via: z.array(z.string()),
48
+ })).optional(),
38
49
  /**
39
50
  * CR-SM-341: der Ablehnungsgrund von R-18, maschinenlesbar statt nur als Satz.
40
51
  *
@@ -242,24 +253,15 @@ function funcMustSatisfyReq(graph) {
242
253
  .map(st => idx.byId.get(st.target))
243
254
  .filter((e) => !!e)
244
255
  : [];
245
- // Also include constraints whose description mentions this func's name
246
- const namePattern = fn.name.toLowerCase();
247
- const matchingConstraints = allReqs.filter(r => r.kinds?.includes('non-functional') && r.description.toLowerCase().includes(namePattern));
248
256
  // CR-SM-346 (ITEM-2026-299): der Kandidatenkreis wird am META-MODELL gefiltert, nicht
249
- // an einer zweiten, hier abgeschriebenen kinds-Liste. `FUNC -satisfy-> REQ` laesst nur
250
- // BEHAVIOURAL_KINDS zu; bis hierher bot die Regel ALLE REQ an — gemessen an sigllm hatte
251
- // damit KEINER von 33 Befunden einen legalen Kandidaten, und die Vorlage aus CR-SM-342
252
- // gab korrekt `null` zurueck. 86 Befunde, 0 Empfehlungen: der Befund war unbedienbar.
257
+ // an einer zweiten, hier abgeschriebenen kinds-Liste — `isValidTrace` statt Eigenbau: eine
258
+ // Regel, die Kandidaten anbietet, die ihr eigenes Meta-Modell abweist, erzeugt Waste statt
259
+ // Fuehrung. `FUNC -satisfy-> REQ` laesst nur `functional` zu (CR-SM-366).
253
260
  //
254
- // Besonders der `matchingConstraints`-Zweig zog gezielt `non-functional` herein — genau
255
- // die Sorte, die das Muster abweist. Er bleibt stehen (ein REQ kann mehrere kinds tragen,
256
- // und die Namensnennung ist ein echtes Relevanzsignal), aber er entscheidet nicht mehr
257
- // ueber die Legalitaet.
258
- //
259
- // `isValidTrace` statt Eigenbau: eine Regel, die Kandidaten anbietet, die ihr eigenes
260
- // Meta-Modell abweist, erzeugt Waste statt Fuehrung — und zwei Wahrheiten ueber dieselbe
261
- // Grammatik sind genau der Defekt, den CR-SM-286/-302 beseitigt haben.
262
- const candidates = [...new Map([...siblingReqs, ...matchingConstraints, ...allReqs].map(e => [e.id, e])).values()]
261
+ // CR-SM-366: der fruehere `matchingConstraints`-Zweig (non-functional-REQ, deren Text den
262
+ // FUNC-Namen nennt) ist entfallen. Er zog genau die Sorte herein, die das Muster abweist;
263
+ // seit ein REQ genau EIN kind traegt, konnte kein Treffer den Filter je passieren.
264
+ const candidates = [...new Map([...siblingReqs, ...allReqs].map(e => [e.id, e])).values()]
263
265
  .filter(r => isValidTrace({ source: 'FUNC', target: 'REQ', type: 'satisfy', targetKinds: r.kinds }));
264
266
  return {
265
267
  rule_id: 'R-02',
@@ -663,35 +665,6 @@ function decompositionConsistency(graph) {
663
665
  context: { element_type: req.type, element_name: req.name },
664
666
  }));
665
667
  }
666
- // ---------------------------------------------------------------------------
667
- // RD-03: No premature decomposition (all children same satisfy target)
668
- // ---------------------------------------------------------------------------
669
- function noPrematureDecomposition(graph) {
670
- const idx = indexOf(graph);
671
- const reqs = idx.elementsOfType('REQ');
672
- const parentReqs = reqs.filter(req => idx.out(req.id, 'compose').some(t => idx.typeOf(t.target) === 'REQ'));
673
- return parentReqs
674
- .filter(parent => {
675
- const childIds = graph.traces
676
- .filter(t => t.source === parent.id && t.type === 'compose' &&
677
- idx.typeOf(t.target) === 'REQ')
678
- .map(t => t.target);
679
- if (childIds.length < 2)
680
- return false;
681
- const satisfyTargets = childIds.map(cid => idx.in(cid, 'satisfy').filter(t => t.source !== cid).map(t => t.source));
682
- // All children satisfied by the same single source
683
- const allSame = satisfyTargets.every(targets => targets.length === 1 && targets[0] === satisfyTargets[0]?.[0]);
684
- return allSame && satisfyTargets[0]?.length === 1;
685
- })
686
- .map(req => ({
687
- rule_id: 'RD-03',
688
- severity: 'info',
689
- element_id: req.id,
690
- message: `${req.id} children all have the same satisfy target — decomposition may be unnecessary`,
691
- fix_hint: 'Consider merging sub-REQs back into parent if they share the same verifiable level',
692
- context: { element_type: req.type, element_name: req.name },
693
- }));
694
- }
695
668
  function breadthCounts(graph) {
696
669
  const idx = indexOf(graph);
697
670
  // CR-SM-264: `typeOf` und `byId` baute diese Regel je Aufruf selbst — der Index hat beide.
@@ -856,7 +829,9 @@ function actorMustHaveTrace(graph) {
856
829
  severity: 'warning',
857
830
  element_id: a.id,
858
831
  message: `${a.id} has no io traces (disconnected actor)`,
859
- fix_hint: 'Link to a UC or FLOW via io trace',
832
+ // CR-SM-371: ein ACTOR hat genau zwei legale Kanten (ACTOR -io-> FLOW, FLOW -io-> ACTOR);
833
+ // der UC wird ueber den FLOW an einer FUNC seiner Kette erreicht, nie direkt (R-18).
834
+ fix_hint: `Wire the actor through a FLOW via graph_mutate Format-E: \`${a.id} -io-> FLOW-x\` (it sends) or \`FLOW-x -io-> ${a.id}\` (it receives), plus \`FLOW-x -io-> FUNC-y\` or \`FUNC-y -io-> FLOW-x\` for a FUNC in the use case's chain — never an io edge to the UC itself`,
860
835
  context: {
861
836
  element_type: a.type,
862
837
  element_name: a.name,
@@ -956,7 +931,6 @@ function validTracePattern(graph) {
956
931
  });
957
932
  if (!why)
958
933
  return [];
959
- const { detail, hint } = explainRejection(why, t, src, tgt);
960
934
  // CR-SM-341: derselbe Grund, den `detail` in Prosa giesst — hier zusaetzlich als Struktur.
961
935
  // Keine zweite Rechnung: `why` liegt vor, die Alternativen befragen `isValidTrace`.
962
936
  const shape = {
@@ -965,6 +939,7 @@ function validTracePattern(graph) {
965
939
  targetKinds: idx.byId.get(t.target)?.kinds,
966
940
  };
967
941
  const alt = admittingAlternatives(shape);
942
+ const { detail, hint } = explainRejection(why, t, src, tgt, alt.sources);
968
943
  const where = why.reason === 'no-pattern' ? undefined : why.pattern.where;
969
944
  return [{
970
945
  rule_id: 'R-18',
@@ -999,7 +974,7 @@ function validTracePattern(graph) {
999
974
  * `kinds` steht da und passt nicht (Kante heben oder Kind korrigieren). Wer das nicht
1000
975
  * unterscheiden kann, sucht am falschen Ende.
1001
976
  */
1002
- function explainRejection(why, t, src, tgt) {
977
+ function explainRejection(why, t, src, tgt, admittingSources) {
1003
978
  if (why.reason === 'no-pattern') {
1004
979
  return {
1005
980
  detail: `${src} → ${tgt} is not a valid ${t.type} pattern`,
@@ -1020,7 +995,11 @@ function explainRejection(why, t, src, tgt) {
1020
995
  }
1021
996
  : {
1022
997
  detail: `${admits}, but ${end} declares {${[...why.declared].sort().join(', ')}}`,
1023
- hint: `Correct '${where.field}' on ${end}, or use a source type whose ${t.type} pattern admits them (FCHAIN carries every kind)`,
998
+ // CR-SM-366: den Erfueller NENNEN statt "FCHAIN carries every kind" — das galt nie fuer
999
+ // die Code-Bindung und gilt seit dem FCHAIN-where gar nicht mehr.
1000
+ hint: admittingSources.length > 0
1001
+ ? `Correct '${where.field}' on ${end}, or ${t.type} it from ${admittingSources.join(' | ')} — the pattern admitting {${[...why.declared].sort().join(', ')}}`
1002
+ : `Correct '${where.field}' on ${end} to a subset of {${allowed}} — no source type admits {${[...why.declared].sort().join(', ')}} (retired kind? split a mixed REQ)`,
1024
1003
  };
1025
1004
  }
1026
1005
  /**
@@ -1162,7 +1141,7 @@ function testMustHaveRunnableBinding(graph) {
1162
1141
  .filter(e => e.attributes?.concept !== true) // concept-only TESTs are exempt
1163
1142
  // CR-SM-231: `testRefs` (Array, min 1) statt `testRef` (Objekt). Semantik unveraendert —
1164
1143
  // Praesenz und Form, keine Datei-Existenz; die pruefst RC-02.
1165
- .filter(e => !TestRefsSchema.safeParse(e.attributes?.testRefs).success)
1144
+ .filter(e => readTestRefs(e.attributes).state !== 'bound')
1166
1145
  .map(test => ({
1167
1146
  rule_id: 'R-19',
1168
1147
  severity: 'warning',
@@ -1194,12 +1173,12 @@ function testFileExclusivity(graph) {
1194
1173
  for (const el of graph.elements) {
1195
1174
  if (el.type !== 'TEST')
1196
1175
  continue;
1197
- const parsed = TestRefsSchema.safeParse(el.attributes?.testRefs);
1198
- if (!parsed.success)
1176
+ const parsed = readTestRefs(el.attributes);
1177
+ if (parsed.state !== 'bound')
1199
1178
  continue; // keine/ungueltige Bindung → R-19-Gebiet
1200
1179
  // Innerhalb EINES Knotens ist dieselbe Datei zweimal kein Konflikt, sondern eine
1201
1180
  // Redundanz — sie wuerde sonst als Selbstkollision gemeldet.
1202
- for (const file of new Set(parsed.data.map(r => r.file))) {
1181
+ for (const file of new Set(parsed.value.map(r => r.file))) {
1203
1182
  const owners = claimedBy.get(file) ?? [];
1204
1183
  owners.push(el.id);
1205
1184
  claimedBy.set(file, owners);
@@ -1244,7 +1223,7 @@ function funcMustHaveCodeBinding(graph) {
1244
1223
  const composeFuncChildren = (id) => graph.traces
1245
1224
  .filter(t => t.source === id && t.type === 'compose' && funcById.has(t.target))
1246
1225
  .map(t => t.target);
1247
- const hasCodeRef = (el) => RealRefSchema.safeParse(el.attributes?.realRef).success;
1226
+ const hasCodeRef = (el) => readRealRef(el.attributes).state === 'bound';
1248
1227
  const isExempt = (el) => el.attributes?.concept === true || el.attributes?.external === true;
1249
1228
  // A FUNC is bound iff it is exempt, has a codeRef, or is a parent whose
1250
1229
  // compose→FUNC children are all bound (recursive, cycle-safe).
@@ -1299,7 +1278,7 @@ function funcMustHaveCodeBinding(graph) {
1299
1278
  severity: 'warning',
1300
1279
  element_id: fn.id,
1301
1280
  message: `${fn.id} is not fully realized — unbound compose ${leafList.length === 1 ? 'child' : 'children'}: ${leafList.join(', ')}`,
1302
- fix_hint: `Realize the leaves ${leafList.join(', ')} (graph_realize) — the parent inherits their binding`,
1281
+ fix_hint: `Bind the leaves via graph_mutate Format-E (${leafList.map((id) => `\`~ ${id} @realRef {"file":…,"symbol":…}\``).join(', ')}) — the parent inherits their binding`,
1303
1282
  context: { element_type: fn.type, element_name: fn.name },
1304
1283
  });
1305
1284
  }
@@ -1329,6 +1308,10 @@ function funcMustHaveCodeBinding(graph) {
1329
1308
  // Co-adjacency at a shared FLOW is not an asserted interface — treating it
1330
1309
  // as one produced P·C findings per hub FLOW and made reuse the expensive
1331
1310
  // choice. The FCHAIN is the modelled claim; the test is owed on the claim.
1311
+ // CR-SM-376: second way to cover a chain. Since CR-SM-366 an FCHAIN carries
1312
+ // only NFRs; its effect is covered through its members when EVERY member
1313
+ // FUNC (FCHAIN ─compose→ FUNC) satisfies at least one REQ. Verifying those
1314
+ // member REQs is R-01's job, not R-21's.
1332
1315
  // ---------------------------------------------------------------------------
1333
1316
  function fchainMustHaveIntegrationTest(graph, policy) {
1334
1317
  const idx = indexOf(graph);
@@ -1362,6 +1345,22 @@ function fchainMustHaveIntegrationTest(graph, policy) {
1362
1345
  testedChains.add(t.source);
1363
1346
  }
1364
1347
  }
1348
+ // CR-SM-376: der zweite Weg — die Kette ist ueber ihre Glieder gedeckt, wenn JEDE Glied-FUNC
1349
+ // (direkte FCHAIN -compose-> FUNC, dieselbe Definition wie `chainsByFunc`) eine REQ erfuellt.
1350
+ // `unmetMembers` liefert die Glieder ohne REQ; leer = gedeckt. Eine Kette ohne Glied kann hier
1351
+ // nicht auftauchen: ZWEIG 2 hat mindestens die beiden Enden der Uebergabe als Glieder.
1352
+ const funcsWithReq = new Set(idx.tracesOfType('satisfy').filter(t => isFunc(t.source) && typeOf.get(t.target) === 'REQ').map(t => t.source));
1353
+ const membersOf = new Map();
1354
+ for (const [fn, chains] of chainsOf) {
1355
+ for (const ch of chains) {
1356
+ const list = membersOf.get(ch);
1357
+ if (list)
1358
+ list.push(fn);
1359
+ else
1360
+ membersOf.set(ch, [fn]);
1361
+ }
1362
+ }
1363
+ const unmetMembers = (ch) => (membersOf.get(ch) ?? []).filter(fn => !funcsWithReq.has(fn)).sort();
1365
1364
  // CR-SM-313: die Infrastruktur-Ausnahme. Die ZAHL kommt aus der Kennzahl (CR-SM-314), nicht
1366
1365
  // aus einer zweiten Rechnung — eine Uebergabe an eine Funktion, die quer durch alle Ketten
1367
1366
  // laeuft, ist kein fehlender Integrationstest, sondern ein Bus. `null` = keine Ausnahme.
@@ -1401,8 +1400,9 @@ function fchainMustHaveIntegrationTest(graph, policy) {
1401
1400
  });
1402
1401
  continue;
1403
1402
  }
1404
- // ZWEIG 2 — gemeinsame Kette, aber keine davon geprueft: Befund an der Kette. Unveraendert.
1405
- if (shared.some(ch => testedChains.has(ch)))
1403
+ // ZWEIG 2 — gemeinsame Kette, aber keine davon gedeckt: Befund an der Kette. Gedeckt heisst
1404
+ // (a) verifizierte Ketten-REQ oder (b, CR-SM-376) jede Glied-FUNC erfuellt eine REQ.
1405
+ if (shared.some(ch => testedChains.has(ch) || unmetMembers(ch).length === 0))
1406
1406
  continue;
1407
1407
  const anchor = shared[0];
1408
1408
  const key = `${anchor}|${p}>${c}`;
@@ -1414,8 +1414,8 @@ function fchainMustHaveIntegrationTest(graph, policy) {
1414
1414
  rule_id: 'R-21',
1415
1415
  severity: 'warning',
1416
1416
  element_id: anchor,
1417
- message: `${anchor} has no integration test covering connection ${p} → ${c}`,
1418
- fix_hint: 'Verify an FCHAIN-satisfied REQ with an integration TEST',
1417
+ message: `${anchor} has no integration test covering connection ${p} → ${c}; member FUNCs without a REQ: ${unmetMembers(anchor).join(', ')}`,
1418
+ fix_hint: 'Give each member FUNC of the chain a REQ it satisfies (FUNC -satisfy-> REQ), or attach an end-to-end non-functional REQ to the chain (FCHAIN -satisfy-> REQ) and verify it with an integration TEST',
1419
1419
  context: { element_type: anchorEl?.type, element_name: anchorEl?.name },
1420
1420
  });
1421
1421
  }
@@ -1501,7 +1501,7 @@ function schemaMustHaveSchemaRef(graph) {
1501
1501
  const idx = indexOf(graph);
1502
1502
  return idx.elementsOfType('SCHEMA')
1503
1503
  .filter(e => e.attributes?.concept !== true && e.attributes?.external !== true)
1504
- .filter(e => !RealRefSchema.safeParse(e.attributes?.realRef).success)
1504
+ .filter(e => readRealRef(e.attributes).state !== 'bound')
1505
1505
  .map(sc => ({
1506
1506
  rule_id: 'R-26',
1507
1507
  severity: 'warning',
@@ -1526,7 +1526,7 @@ function schemaMustHaveContractTest(graph) {
1526
1526
  const tests = idx.elementsOfType('TEST');
1527
1527
  return idx.elementsOfType('SCHEMA')
1528
1528
  .filter(sc => sc.attributes?.concept !== true)
1529
- .filter(sc => { const r = RealRefSchema.safeParse(sc.attributes?.realRef); return r.success && r.data.symbol !== undefined; })
1529
+ .filter(sc => { const r = readRealRef(sc.attributes); return r.state === 'bound' && r.value.symbol !== undefined; })
1530
1530
  .filter(sc => !graph.traces.some(t => t.type === 'verify' && t.target === sc.id && typeOf.get(t.source) === 'TEST'))
1531
1531
  .map(sc => ({
1532
1532
  rule_id: 'R-32',
@@ -1679,14 +1679,13 @@ export const V3_RULES = [
1679
1679
  { id: 'R-19', name: 'Runnable TEST binding', severity: 'warning', evaluate: testMustHaveRunnableBinding, domain: ['TEST'] },
1680
1680
  { id: 'R-29', name: 'Test file exclusivity', severity: 'error', evaluate: testFileExclusivity, domain: ['TEST'] },
1681
1681
  { id: 'R-20', name: 'FUNC realRef binding', severity: 'warning', evaluate: funcMustHaveCodeBinding, domain: ['FUNC'] },
1682
- { id: 'R-21', name: 'FUNC↔FUNC handover needs a shared chain and an integration test', severity: 'warning', evaluate: fchainMustHaveIntegrationTest, domain: ['FCHAIN', 'FUNC'] },
1682
+ { id: 'R-21', name: 'FUNC↔FUNC handover needs a shared chain covered by member REQs or an integration test', severity: 'warning', evaluate: fchainMustHaveIntegrationTest, domain: ['FCHAIN', 'FUNC'] },
1683
1683
  { id: 'R-22', name: 'FUNC must be allocated to MOD', severity: 'warning', evaluate: funcMustBeAllocated, domain: ['FUNC'] },
1684
1684
  { id: 'R-23', name: 'MOD must have allocated FUNC', severity: 'warning', evaluate: modMustHaveAllocatedFunc, domain: ['MOD'] },
1685
1685
  { id: 'R-26', name: 'SCHEMA must have realRef', severity: 'warning', evaluate: schemaMustHaveSchemaRef, domain: ['SCHEMA'] },
1686
1686
  { id: 'R-32', name: 'SCHEMA must have contract TEST', severity: 'warning', evaluate: schemaMustHaveContractTest, domain: ['SCHEMA'] },
1687
1687
  { id: 'RD-01', name: 'Unresolved requirement', severity: 'warning', evaluate: unresolvedRequirement, domain: ['REQ'] },
1688
1688
  { id: 'RD-02', name: 'Decomposition consistency', severity: 'warning', evaluate: decompositionConsistency, domain: ['REQ'] },
1689
- { id: 'RD-03', name: 'No premature decomposition', severity: 'info', evaluate: noPrematureDecomposition, domain: ['REQ'] },
1690
1689
  { id: 'RD-04', name: 'Decomposition breadth', severity: 'warning', evaluate: decompositionBreadth, domain: ['FUNC', 'MOD', 'SYS'] },
1691
1690
  { id: 'RD-05', name: 'Decomposition too narrow', severity: 'warning', evaluate: decompositionNarrow, domain: ['FUNC', 'MOD', 'SYS'] },
1692
1691
  { id: 'MS-01', name: 'Milestone empty scope', severity: 'warning', evaluate: msEmptyScope, domain: ['MS'] },
@@ -8,7 +8,5 @@ export declare function uc01HasRequirements(graph: OntologyGraph): RuleViolation
8
8
  export declare function uc02HasActor(graph: OntologyGraph): RuleViolation[];
9
9
  export declare function uc03HasScenario(graph: OntologyGraph): RuleViolation[];
10
10
  export declare function uc04GoalDefined(graph: OntologyGraph): RuleViolation[];
11
- export declare function uc05HasPostcondition(graph: OntologyGraph): RuleViolation[];
12
- export declare function uc06HasPrecondition(graph: OntologyGraph): RuleViolation[];
13
11
  export declare const UC_RULES: RuleDefinition[];
14
12
  export declare function evaluateUCRules(graph: OntologyGraph, policy: MetricPolicy): RuleViolation[];
@@ -112,53 +112,15 @@ export function uc04GoalDefined(graph) {
112
112
  }));
113
113
  }
114
114
  // ---------------------------------------------------------------------------
115
- // UC-05: UC should have postcondition REQ (CR-165: compose only, satisfy UC→REQ invalid)
116
- // ---------------------------------------------------------------------------
117
- export function uc05HasPostcondition(graph) {
118
- const idx = indexOf(graph);
119
- return idx.elementsOfType('UC')
120
- .filter(uc => !idx.out(uc.id, 'compose').some(t => {
121
- const target = idx.byId.get(t.target);
122
- return target?.type === 'REQ' && target.kinds?.includes('postcondition');
123
- }))
124
- .map(uc => ({
125
- rule_id: 'UC-05',
126
- severity: 'info',
127
- element_id: uc.id,
128
- message: `${uc.id} has no postcondition requirement`,
129
- fix_hint: 'Add a REQ with kinds=["postcondition"] via compose trace',
130
- context: { element_type: uc.type, element_name: uc.name },
131
- }));
132
- }
133
- // ---------------------------------------------------------------------------
134
- // UC-06: UC should have precondition REQ (CR-165: compose only, satisfy UC→REQ invalid)
135
- // ---------------------------------------------------------------------------
136
- export function uc06HasPrecondition(graph) {
137
- const idx = indexOf(graph);
138
- return idx.elementsOfType('UC')
139
- .filter(uc => !idx.out(uc.id, 'compose').some(t => {
140
- const target = idx.byId.get(t.target);
141
- return target?.type === 'REQ' && target.kinds?.includes('precondition');
142
- }))
143
- .map(uc => ({
144
- rule_id: 'UC-06',
145
- severity: 'info',
146
- element_id: uc.id,
147
- message: `${uc.id} has no precondition requirement`,
148
- fix_hint: 'Add a REQ with kinds=["precondition"] via compose trace',
149
- context: { element_type: uc.type, element_name: uc.name },
150
- }));
151
- }
152
- // ---------------------------------------------------------------------------
153
115
  // Aggregated
154
116
  // ---------------------------------------------------------------------------
117
+ // CR-SM-357: UC-05/UC-06 (Pre-/Postcondition-REQ) sind gestrichen — Schreibregel in se:author-uc,
118
+ // wie BQ in author-req (CR-GC-602): sie feuerten an fast jedem UC, und niemand handelte darauf.
155
119
  export const UC_RULES = [
156
120
  { id: 'UC-01', name: 'UC has requirements', severity: 'warning', evaluate: uc01HasRequirements, domain: ['UC'] },
157
121
  { id: 'UC-02', name: 'UC has actor', severity: 'warning', evaluate: uc02HasActor, domain: ['UC'] },
158
122
  { id: 'UC-03', name: 'UC has scenario', severity: 'warning', evaluate: uc03HasScenario, domain: ['UC'] },
159
123
  { id: 'UC-04', name: 'UC has goal', severity: 'warning', evaluate: uc04GoalDefined, domain: ['UC'] },
160
- { id: 'UC-05', name: 'UC has postcondition', severity: 'info', evaluate: uc05HasPostcondition, domain: ['UC'] },
161
- { id: 'UC-06', name: 'UC has precondition', severity: 'info', evaluate: uc06HasPrecondition, domain: ['UC'] },
162
124
  ];
163
125
  // CR-SM-236: `policy` wird durchgereicht, auch wo diese Familie heute keine Schwelle hat —
164
126
  // ein Sonderweg je Familie waere genau der zweite Pfad, den der Regelsatz verbietet.
@@ -3,7 +3,7 @@
3
3
  * VR-01: TEST with a testRefs entry that has no result (info).
4
4
  * CL-01: ACTOR without UCs in at least 2 distinct operatingModes (warning).
5
5
  */
6
- import { TestRefsSchema } from './ontology.js';
6
+ import { readTestRefs } from './ontology.js';
7
7
  // ---------------------------------------------------------------------------
8
8
  // VR-01: ein testRefs-Eintrag ohne Ergebnis
9
9
  //
@@ -20,17 +20,17 @@ export function vr01TestNoResult(graph) {
20
20
  for (const el of graph.elements) {
21
21
  if (el.type !== 'TEST')
22
22
  continue;
23
- const parsed = TestRefsSchema.safeParse(el.attributes?.testRefs);
24
- if (!parsed.success)
23
+ const parsed = readTestRefs(el.attributes);
24
+ if (parsed.state !== 'bound')
25
25
  continue; // keine/ungueltige Bindung → R-19-Gebiet
26
- const pending = parsed.data.filter(ref => ref.result === undefined);
26
+ const pending = parsed.value.filter(ref => ref.result === undefined);
27
27
  if (pending.length === 0)
28
28
  continue;
29
29
  violations.push({
30
30
  rule_id: 'VR-01',
31
31
  severity: 'info',
32
32
  element_id: el.id,
33
- message: `${el.id} has ${pending.length} of ${parsed.data.length} testRefs entries without a result — assumed pending: ${pending.map(r => r.file).join(', ')}`,
33
+ message: `${el.id} has ${pending.length} of ${parsed.value.length} testRefs entries without a result — assumed pending: ${pending.map(r => r.file).join(', ')}`,
34
34
  fix_hint: 'Record the run outcome on the entry (result, ranAt, evidence) — a binding without a result is not evidence',
35
35
  });
36
36
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sigloch/contracts",
3
- "version": "10.10.0",
3
+ "version": "10.13.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",