@sigloch/contracts 3.3.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/harness/index.d.ts +2 -2
  2. package/dist/se/action-priority.d.ts +101 -0
  3. package/dist/se/action-priority.js +124 -0
  4. package/dist/se/analysis-freshness-rules.d.ts +5 -0
  5. package/dist/se/analysis-freshness-rules.js +5 -5
  6. package/dist/se/ao-rules.d.ts +5 -39
  7. package/dist/se/ao-rules.js +20 -13
  8. package/dist/se/conformance-rules.d.ts +2 -2
  9. package/dist/se/conformance-rules.js +36 -30
  10. package/dist/se/cr-quality-rules.d.ts +2 -1
  11. package/dist/se/cr-quality-rules.js +9 -7
  12. package/dist/se/evaluate-all.d.ts +18 -3
  13. package/dist/se/evaluate-all.js +29 -26
  14. package/dist/se/fchain-quality-rules.d.ts +2 -1
  15. package/dist/se/fchain-quality-rules.js +8 -6
  16. package/dist/se/fmea-rules.d.ts +3 -11
  17. package/dist/se/fmea-rules.js +53 -16
  18. package/dist/se/format-e-parser.d.ts +14 -1
  19. package/dist/se/format-e-parser.js +8 -3
  20. package/dist/se/index.d.ts +4 -2
  21. package/dist/se/index.js +4 -2
  22. package/dist/se/metric-rules.d.ts +13 -5
  23. package/dist/se/metric-rules.js +23 -13
  24. package/dist/se/near-duplicate-rules.d.ts +2 -0
  25. package/dist/se/near-duplicate-rules.js +2 -2
  26. package/dist/se/ontology.d.ts +55 -4
  27. package/dist/se/ontology.js +46 -6
  28. package/dist/se/policy.d.ts +65 -0
  29. package/dist/se/policy.js +100 -0
  30. package/dist/se/quality-rules.d.ts +2 -1
  31. package/dist/se/quality-rules.js +9 -7
  32. package/dist/se/readiness.d.ts +9 -1
  33. package/dist/se/readiness.js +18 -2
  34. package/dist/se/rules.d.ts +25 -5
  35. package/dist/se/rules.js +112 -47
  36. package/dist/se/schema-quality-rules.d.ts +2 -1
  37. package/dist/se/schema-quality-rules.js +6 -4
  38. package/dist/se/uc-quality-rules.d.ts +2 -1
  39. package/dist/se/uc-quality-rules.js +10 -8
  40. package/dist/se/view-rules.d.ts +2 -6
  41. package/dist/se/view-rules.js +40 -13
  42. package/package.json +2 -1
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, TestRefSchema, RealRefSchema } from './ontology.js';
7
+ import { ElementType, TraceType, TestRefsSchema, RealRefSchema } from './ontology.js';
8
8
  import { isValidTrace } from './meta-model.js';
9
9
  export const RuleSeverity = z.enum(['error', 'warning', 'info']);
10
10
  /** Candidate target for resolving a violation (e.g. a REQ to satisfy, a TEST to link). */
@@ -168,16 +168,26 @@ function asilIsolation(graph) {
168
168
  return violations;
169
169
  }
170
170
  // ---------------------------------------------------------------------------
171
- // R-04: Max module size
171
+ // R-04: Modulgroesse GEGEN Kreuzungen (nicht „max module size")
172
+ //
173
+ // CR-SM-236: die Regel waegt Groesse gegen kreuzende io-Flows ab — ein grosses Modul ohne
174
+ // Kreuzungen ist info („kohaesiv, nur gross"), dasselbe Modul mit Kreuzungen ist warning.
175
+ // Der alte Name gab das nicht her, und die drei Schwellen (8/12/2) standen inline: eine
176
+ // Urteilsschwelle, die sich weder per grep noch aus dem Regelnamen ablesen laesst, ist nicht
177
+ // ueberpruefbar. Jetzt sind es `policy.moduleSize.{coupled,large,crossings}`.
178
+ // `null` → messen, nicht urteilen: die Regel schweigt.
172
179
  // ---------------------------------------------------------------------------
173
- function maxModuleSize(graph) {
174
- const modules = graph.elements.filter(e => e.type === 'MOD');
180
+ function maxModuleSize(graph, policy) {
175
181
  const violations = [];
182
+ const steps = policy.moduleSize;
183
+ if (steps === null)
184
+ return violations;
185
+ const modules = graph.elements.filter(e => e.type === 'MOD');
176
186
  for (const mod of modules) {
177
187
  const allocatedIds = graph.traces.filter(t => t.target === mod.id && t.type === 'allocate').map(t => t.source);
178
188
  const allocated = allocatedIds.map(id => graph.elements.find(e => e.id === id)).filter((e) => !!e);
179
189
  const funcCount = allocated.length;
180
- if (funcCount <= 8)
190
+ if (funcCount <= steps.coupled)
181
191
  continue;
182
192
  // Count crossing flows: io paths from FUNCs in this module to FUNCs in other modules
183
193
  const funcIds = new Set(allocatedIds);
@@ -188,27 +198,27 @@ function maxModuleSize(graph) {
188
198
  const tgtIn = funcIds.has(t.target);
189
199
  return (srcIn && !tgtIn) || (!srcIn && tgtIn);
190
200
  }).length;
191
- if (funcCount > 12 && crossings === 0) {
201
+ if (funcCount > steps.large && crossings === 0) {
192
202
  violations.push({
193
203
  rule_id: 'R-04',
194
204
  severity: 'info',
195
205
  element_id: mod.id,
196
206
  message: `${mod.id} has ${funcCount} functions but 0 crossings (cohesive, just large)`,
197
- fix_hint: 'Consider splitting if complexity grows',
207
+ fix_hint: `Size alone is not the finding: > ${steps.large} functions without crossing flows is cohesive. Split only if crossings appear`,
198
208
  context: { element_type: mod.type, element_name: mod.name, candidate_targets: toCandidates(allocated) },
199
209
  });
200
210
  }
201
- else if (funcCount > 12 && crossings > 0) {
211
+ else if (funcCount > steps.large && crossings > 0) {
202
212
  violations.push({
203
213
  rule_id: 'R-04',
204
214
  severity: 'warning',
205
215
  element_id: mod.id,
206
216
  message: `${mod.id} has ${funcCount} functions and ${crossings} crossing flows (split recommended)`,
207
- fix_hint: 'Split module into smaller units to reduce coupling',
217
+ fix_hint: `Split module into smaller units to reduce coupling (> ${steps.large} functions AND crossing flows)`,
208
218
  context: { element_type: mod.type, element_name: mod.name, candidate_targets: toCandidates(allocated) },
209
219
  });
210
220
  }
211
- else if (funcCount > 8 && crossings > 2) {
221
+ else if (funcCount > steps.coupled && crossings > steps.crossings) {
212
222
  violations.push({
213
223
  rule_id: 'R-04',
214
224
  severity: 'warning',
@@ -439,7 +449,7 @@ function decompositionBreadth(graph) {
439
449
  const byId = new Map(graph.elements.map(e => [e.id, e]));
440
450
  const counts = new Map();
441
451
  const bump = (parentId, kind) => {
442
- const key = `${parentId}${kind}`;
452
+ const key = `${parentId}\u0000${kind}`;
443
453
  const entry = counts.get(key);
444
454
  if (entry)
445
455
  entry.n++;
@@ -621,11 +631,11 @@ function validTracePattern(graph) {
621
631
  }
622
632
  // ---------------------------------------------------------------------------
623
633
  // R-19: Runnable TEST binding (CR-GC-205 Item 4) — a TEST that is not explicitly
624
- // concept-only (attributes.concept === true) should carry a valid testRef runnable
634
+ // concept-only (attributes.concept === true) should carry a valid testRefs runnable
625
635
  // binding so the deduced selective test set is trustworthy. WARNING (a completeness
626
636
  // signal like R-05, not a hard gate): a freshly-spec'd TEST is legitimately concept-
627
637
  // level until implemented, and the no-phantom-file guarantee is enforced separately
628
- // by the consumer's export materialization (every testRef.file is scaffolded). File
638
+ // by the consumer's export materialization (every testRefs entry file is scaffolded). File
629
639
  // EXISTENCE is out of scope here (rules are pure, no I/O); this guards presence/shape
630
640
  // only and surfaces an unbound runnable TEST in rules_evaluate / readiness.
631
641
  // ---------------------------------------------------------------------------
@@ -633,17 +643,71 @@ function testMustHaveRunnableBinding(graph) {
633
643
  return graph.elements
634
644
  .filter(e => e.type === 'TEST')
635
645
  .filter(e => e.attributes?.concept !== true) // concept-only TESTs are exempt
636
- .filter(e => !TestRefSchema.safeParse(e.attributes?.testRef).success)
646
+ // CR-SM-231: `testRefs` (Array, min 1) statt `testRef` (Objekt). Semantik unveraendert —
647
+ // Praesenz und Form, keine Datei-Existenz; die pruefst RC-02.
648
+ .filter(e => !TestRefsSchema.safeParse(e.attributes?.testRefs).success)
637
649
  .map(test => ({
638
650
  rule_id: 'R-19',
639
651
  severity: 'warning',
640
652
  element_id: test.id,
641
- message: `${test.id} is a runnable TEST without a valid testRef binding`,
642
- fix_hint: 'Add attributes.testRef {file, case?, tool, level?}, or set attributes.concept:true if it has no run artifact yet',
653
+ message: `${test.id} is a runnable TEST without a valid testRefs binding`,
654
+ fix_hint: 'Add attributes.testRefs [{file, case?, tool, level?}, …] with at least one entry, or set attributes.concept:true if it has no run artifact yet',
643
655
  context: { element_type: test.type, element_name: test.name },
644
656
  }));
645
657
  }
646
658
  // ---------------------------------------------------------------------------
659
+ // R-29: Testdatei-Exklusivitaet (CR-SM-231)
660
+ //
661
+ // Jede Testdatei erscheint in hoechstens einem `testRefs`. Das ist die Haelfte, die 1:n
662
+ // **erzwingt** statt es nur zu erlauben — ohne sie driftet das Attribut zurueck nach n:m,
663
+ // und genau das war der belegte Ist-Zustand: dieselbe Spec-Datei stand im `testRef` zweier
664
+ // TEST-Knoten. Folge: ein roter Lauf ist keiner Abnahme mehr eindeutig zuordenbar, und der
665
+ // TRR-Gate zaehlt dieselbe Evidenz doppelt.
666
+ //
667
+ // `error`, nicht `warning` (Entscheid 2026-08-13): eine doppelt beanspruchte Datei macht
668
+ // Gate-Zahlen nachweislich falsch. Das ist eine Fehlmessung, kein Vollstaendigkeits-Signal —
669
+ // und damit eine andere Klasse als R-19/R-20, die beide `warning` sind.
670
+ //
671
+ // Rein graph-strukturell, kein I/O: die Datei muss nicht existieren, um doppelt beansprucht
672
+ // zu sein.
673
+ // ---------------------------------------------------------------------------
674
+ function testFileExclusivity(graph) {
675
+ const claimedBy = new Map();
676
+ for (const el of graph.elements) {
677
+ if (el.type !== 'TEST')
678
+ continue;
679
+ const parsed = TestRefsSchema.safeParse(el.attributes?.testRefs);
680
+ if (!parsed.success)
681
+ continue; // keine/ungueltige Bindung → R-19-Gebiet
682
+ // Innerhalb EINES Knotens ist dieselbe Datei zweimal kein Konflikt, sondern eine
683
+ // Redundanz — sie wuerde sonst als Selbstkollision gemeldet.
684
+ for (const file of new Set(parsed.data.map(r => r.file))) {
685
+ const owners = claimedBy.get(file) ?? [];
686
+ owners.push(el.id);
687
+ claimedBy.set(file, owners);
688
+ }
689
+ }
690
+ const violations = [];
691
+ for (const [file, owners] of claimedBy) {
692
+ if (owners.length < 2)
693
+ continue;
694
+ const sorted = [...owners].sort();
695
+ // Einmal je beanspruchendem Knoten: jeder von ihnen muss handeln, und eine Meldung an
696
+ // nur einem waere fuer die anderen unsichtbar.
697
+ for (const owner of sorted) {
698
+ violations.push({
699
+ rule_id: 'R-29',
700
+ severity: 'error',
701
+ element_id: owner,
702
+ message: `${owner} claims test file '${file}', which is also claimed by ${sorted.filter(o => o !== owner).join(', ')}`,
703
+ fix_hint: 'A test file belongs to at most one TEST. Split the file, or drop the entry from all but the one acceptance it really evidences',
704
+ context: { element_type: 'TEST', element_name: graph.elements.find(e => e.id === owner)?.name },
705
+ });
706
+ }
707
+ }
708
+ return violations;
709
+ }
710
+ // ---------------------------------------------------------------------------
647
711
  // R-20: FUNC realRef binding (CR-GC-205 Item 5, extended by CR-210, unified CR-228)
648
712
  // — a FUNC that is not explicitly concept-only (attributes.concept === true) or
649
713
  // externally realized (attributes.external === true) counts as BOUND when it EITHER
@@ -876,7 +940,7 @@ function modMustHaveAllocatedFunc(graph) {
876
940
  // so its Zod definition is machine-resolvable (RC-03 then checks it resolves, RC-04
877
941
  // that it is parsed at the interface). WARNING, not error: currently-unbound SCHEMAs
878
942
  // on the reference model must not turn readiness red before any binding exists — the
879
- // presence signal mirrors R-20 (FUNC realRef) / R-19 (testRef). Symbol RESOLUTION is
943
+ // presence signal mirrors R-20 (FUNC realRef) / R-19 (testRefs). Symbol RESOLUTION is
880
944
  // out of scope here (pure, no I/O — that is RC-03's job).
881
945
  // ---------------------------------------------------------------------------
882
946
  function schemaMustHaveSchemaRef(graph) {
@@ -953,35 +1017,36 @@ function ebenenPraesenz(graph) {
953
1017
  }];
954
1018
  }
955
1019
  export const V3_RULES = [
956
- { id: 'R-01', name: 'REQ must have verification', severity: 'error', evaluate: reqMustHaveVerification },
957
- { id: 'R-02', name: 'FUNC must satisfy REQ', severity: 'warning', evaluate: funcMustSatisfyReq },
958
- { id: 'R-03', name: 'ASIL isolation', severity: 'error', evaluate: asilIsolation },
959
- { id: 'R-04', name: 'Max module size', severity: 'warning', evaluate: maxModuleSize },
960
- { id: 'R-05', name: 'TEST must verify REQ', severity: 'warning', evaluate: testMustVerifyReq },
961
- { id: 'R-14', name: 'UC must have compose', severity: 'warning', evaluate: ucMustHaveCompose },
962
- { id: 'R-15', name: 'FCHAIN must have compose', severity: 'warning', evaluate: fchainMustHaveCompose },
963
- { id: 'R-16', name: 'ACTOR must have io', severity: 'warning', evaluate: actorMustHaveTrace },
964
- { id: 'R-17', name: 'SYS must have compose', severity: 'warning', evaluate: sysMustHaveCompose },
965
- { id: 'R-08', name: 'Trace consistency', severity: 'error', evaluate: traceConsistency },
966
- { id: 'R-10', name: 'FLOW completeness', severity: 'warning', evaluate: flowCompleteness },
967
- { id: 'R-12', name: 'No circular dependencies', severity: 'warning', evaluate: noDirectCircular },
968
- { id: 'R-18', name: 'Valid trace pattern', severity: 'error', evaluate: validTracePattern },
969
- { id: 'R-19', name: 'Runnable TEST binding', severity: 'warning', evaluate: testMustHaveRunnableBinding },
970
- { id: 'R-20', name: 'FUNC realRef binding', severity: 'warning', evaluate: funcMustHaveCodeBinding },
971
- { id: 'R-21', name: 'FUNC↔FUNC connection needs integration test', severity: 'warning', evaluate: fchainMustHaveIntegrationTest },
972
- { id: 'R-22', name: 'FUNC must be allocated to MOD', severity: 'warning', evaluate: funcMustBeAllocated },
973
- { id: 'R-23', name: 'MOD must have allocated FUNC', severity: 'warning', evaluate: modMustHaveAllocatedFunc },
974
- { id: 'R-26', name: 'SCHEMA must have realRef', severity: 'warning', evaluate: schemaMustHaveSchemaRef },
975
- { id: 'R-27', name: 'Physical MOD must have realRef', severity: 'warning', evaluate: physicalModMustHaveRealRef },
976
- { id: 'RD-01', name: 'Unresolved requirement', severity: 'warning', evaluate: unresolvedRequirement },
977
- { id: 'RD-02', name: 'Decomposition consistency', severity: 'warning', evaluate: decompositionConsistency },
978
- { id: 'RD-03', name: 'No premature decomposition', severity: 'info', evaluate: noPrematureDecomposition },
979
- { id: 'RD-04', name: 'Decomposition breadth', severity: 'warning', evaluate: decompositionBreadth },
980
- { id: 'MS-01', name: 'Milestone empty scope', severity: 'warning', evaluate: msEmptyScope },
981
- { id: 'MS-02', name: 'Milestone dangling dependency', severity: 'error', evaluate: msDanglingDependency },
982
- { id: 'R-28', name: 'Ebenen-Präsenz (FLOW+SCHEMA when funcCount>1)', severity: 'warning', evaluate: ebenenPraesenz },
1020
+ { id: 'R-01', name: 'REQ must have verification', severity: 'error', evaluate: reqMustHaveVerification, domain: ['REQ'] },
1021
+ { id: 'R-02', name: 'FUNC must satisfy REQ', severity: 'warning', evaluate: funcMustSatisfyReq, domain: ['FUNC'] },
1022
+ { id: 'R-03', name: 'ASIL isolation', severity: 'error', evaluate: asilIsolation, domain: ['FUNC'] },
1023
+ { id: 'R-04', name: 'Module size relative to crossing flows', severity: 'warning', evaluate: maxModuleSize, domain: ['MOD'] },
1024
+ { id: 'R-05', name: 'TEST must verify REQ', severity: 'warning', evaluate: testMustVerifyReq, domain: ['TEST'] },
1025
+ { id: 'R-14', name: 'UC must have compose', severity: 'warning', evaluate: ucMustHaveCompose, domain: ['UC'] },
1026
+ { id: 'R-15', name: 'FCHAIN must have compose', severity: 'warning', evaluate: fchainMustHaveCompose, domain: ['FCHAIN'] },
1027
+ { id: 'R-16', name: 'ACTOR must have io', severity: 'warning', evaluate: actorMustHaveTrace, domain: ['ACTOR'] },
1028
+ { id: 'R-17', name: 'SYS must have compose', severity: 'warning', evaluate: sysMustHaveCompose, domain: ['SYS'] },
1029
+ { id: 'R-08', name: 'Trace consistency', severity: 'error', evaluate: traceConsistency, domain: ['all'] },
1030
+ { id: 'R-10', name: 'FLOW completeness', severity: 'warning', evaluate: flowCompleteness, domain: ['FUNC'] },
1031
+ { id: 'R-12', name: 'No circular dependencies', severity: 'warning', evaluate: noDirectCircular, domain: ['FUNC'] },
1032
+ { id: 'R-18', name: 'Valid trace pattern', severity: 'error', evaluate: validTracePattern, domain: ['all'] },
1033
+ { id: 'R-19', name: 'Runnable TEST binding', severity: 'warning', evaluate: testMustHaveRunnableBinding, domain: ['TEST'] },
1034
+ { id: 'R-29', name: 'Test file exclusivity', severity: 'error', evaluate: testFileExclusivity, domain: ['TEST'] },
1035
+ { id: 'R-20', name: 'FUNC realRef binding', severity: 'warning', evaluate: funcMustHaveCodeBinding, domain: ['FUNC'] },
1036
+ { id: 'R-21', name: 'FUNC↔FUNC connection needs integration test', severity: 'warning', evaluate: fchainMustHaveIntegrationTest, domain: ['FCHAIN'] },
1037
+ { id: 'R-22', name: 'FUNC must be allocated to MOD', severity: 'warning', evaluate: funcMustBeAllocated, domain: ['FUNC'] },
1038
+ { id: 'R-23', name: 'MOD must have allocated FUNC', severity: 'warning', evaluate: modMustHaveAllocatedFunc, domain: ['MOD'] },
1039
+ { id: 'R-26', name: 'SCHEMA must have realRef', severity: 'warning', evaluate: schemaMustHaveSchemaRef, domain: ['SCHEMA'] },
1040
+ { id: 'R-27', name: 'Physical MOD must have realRef', severity: 'warning', evaluate: physicalModMustHaveRealRef, domain: ['MOD'] },
1041
+ { id: 'RD-01', name: 'Unresolved requirement', severity: 'warning', evaluate: unresolvedRequirement, domain: ['REQ'] },
1042
+ { id: 'RD-02', name: 'Decomposition consistency', severity: 'warning', evaluate: decompositionConsistency, domain: ['REQ'] },
1043
+ { id: 'RD-03', name: 'No premature decomposition', severity: 'info', evaluate: noPrematureDecomposition, domain: ['REQ'] },
1044
+ { id: 'RD-04', name: 'Decomposition breadth', severity: 'warning', evaluate: decompositionBreadth, domain: ['FUNC', 'MOD', 'SYS'] },
1045
+ { id: 'MS-01', name: 'Milestone empty scope', severity: 'warning', evaluate: msEmptyScope, domain: ['MS'] },
1046
+ { id: 'MS-02', name: 'Milestone dangling dependency', severity: 'error', evaluate: msDanglingDependency, domain: ['MS'] },
1047
+ { id: 'R-28', name: 'Ebenen-Präsenz (FLOW+SCHEMA when funcCount>1)', severity: 'warning', evaluate: ebenenPraesenz, domain: ['SYS'] },
983
1048
  ];
984
- /** Run all rules against a graph */
985
- export function evaluateRules(graph) {
986
- return V3_RULES.flatMap(rule => rule.evaluate(graph));
1049
+ /** Run all rules against a graph. CR-SM-236: `policy` ist Pflicht — wie bei `evaluateAllRules`. */
1050
+ export function evaluateRules(graph, policy) {
1051
+ return V3_RULES.flatMap(rule => rule.evaluate(graph, policy));
987
1052
  }
@@ -14,7 +14,8 @@
14
14
  */
15
15
  import type { OntologyGraph } from './ontology.js';
16
16
  import type { RuleDefinition, RuleViolation } from './rules.js';
17
+ import type { MetricPolicy } from './policy.js';
17
18
  export declare function sc02IsReferenced(graph: OntologyGraph): RuleViolation[];
18
19
  export declare function sc04FlowHasSchema(graph: OntologyGraph): RuleViolation[];
19
20
  export declare const SC_RULES: RuleDefinition[];
20
- export declare function evaluateSCRules(graph: OntologyGraph): RuleViolation[];
21
+ export declare function evaluateSCRules(graph: OntologyGraph, policy: MetricPolicy): RuleViolation[];
@@ -49,9 +49,11 @@ export function sc04FlowHasSchema(graph) {
49
49
  // Aggregated array & convenience runner
50
50
  // ---------------------------------------------------------------------------
51
51
  export const SC_RULES = [
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 },
52
+ { id: 'SC-02', name: 'Schema referenced by FLOW', severity: 'warning', evaluate: sc02IsReferenced, domain: ['SCHEMA'] },
53
+ { id: 'SC-04', name: 'FLOW has SCHEMA binding', severity: 'warning', evaluate: sc04FlowHasSchema, domain: ['SCHEMA'] },
54
54
  ];
55
- export function evaluateSCRules(graph) {
56
- return SC_RULES.flatMap(rule => rule.evaluate(graph));
55
+ // CR-SM-236: `policy` wird durchgereicht, auch wo diese Familie heute keine Schwelle hat —
56
+ // ein Sonderweg je Familie waere genau der zweite Pfad, den der Regelsatz verbietet.
57
+ export function evaluateSCRules(graph, policy) {
58
+ return SC_RULES.flatMap(rule => rule.evaluate(graph, policy));
57
59
  }
@@ -3,6 +3,7 @@
3
3
  */
4
4
  import type { OntologyGraph } from './ontology.js';
5
5
  import type { RuleDefinition, RuleViolation } from './rules.js';
6
+ import type { MetricPolicy } from './policy.js';
6
7
  export declare function uc01HasRequirements(graph: OntologyGraph): RuleViolation[];
7
8
  export declare function uc02HasActor(graph: OntologyGraph): RuleViolation[];
8
9
  export declare function uc03HasScenario(graph: OntologyGraph): RuleViolation[];
@@ -10,4 +11,4 @@ export declare function uc04GoalDefined(graph: OntologyGraph): RuleViolation[];
10
11
  export declare function uc05HasPostcondition(graph: OntologyGraph): RuleViolation[];
11
12
  export declare function uc06HasPrecondition(graph: OntologyGraph): RuleViolation[];
12
13
  export declare const UC_RULES: RuleDefinition[];
13
- export declare function evaluateUCRules(graph: OntologyGraph): RuleViolation[];
14
+ export declare function evaluateUCRules(graph: OntologyGraph, policy: MetricPolicy): RuleViolation[];
@@ -111,13 +111,15 @@ export function uc06HasPrecondition(graph) {
111
111
  // Aggregated
112
112
  // ---------------------------------------------------------------------------
113
113
  export const UC_RULES = [
114
- { id: 'UC-01', name: 'UC has requirements', severity: 'error', evaluate: uc01HasRequirements },
115
- { id: 'UC-02', name: 'UC has actor', severity: 'error', evaluate: uc02HasActor },
116
- { id: 'UC-03', name: 'UC has scenario', severity: 'warning', evaluate: uc03HasScenario },
117
- { id: 'UC-04', name: 'UC has goal', severity: 'warning', evaluate: uc04GoalDefined },
118
- { id: 'UC-05', name: 'UC has postcondition', severity: 'info', evaluate: uc05HasPostcondition },
119
- { id: 'UC-06', name: 'UC has precondition', severity: 'info', evaluate: uc06HasPrecondition },
114
+ { id: 'UC-01', name: 'UC has requirements', severity: 'error', evaluate: uc01HasRequirements, domain: ['UC'] },
115
+ { id: 'UC-02', name: 'UC has actor', severity: 'error', evaluate: uc02HasActor, domain: ['UC'] },
116
+ { id: 'UC-03', name: 'UC has scenario', severity: 'warning', evaluate: uc03HasScenario, domain: ['UC'] },
117
+ { id: 'UC-04', name: 'UC has goal', severity: 'warning', evaluate: uc04GoalDefined, domain: ['UC'] },
118
+ { id: 'UC-05', name: 'UC has postcondition', severity: 'info', evaluate: uc05HasPostcondition, domain: ['UC'] },
119
+ { id: 'UC-06', name: 'UC has precondition', severity: 'info', evaluate: uc06HasPrecondition, domain: ['UC'] },
120
120
  ];
121
- export function evaluateUCRules(graph) {
122
- return UC_RULES.flatMap(rule => rule.evaluate(graph));
121
+ // CR-SM-236: `policy` wird durchgereicht, auch wo diese Familie heute keine Schwelle hat —
122
+ // ein Sonderweg je Familie waere genau der zweite Pfad, den der Regelsatz verbietet.
123
+ export function evaluateUCRules(graph, policy) {
124
+ return UC_RULES.flatMap(rule => rule.evaluate(graph, policy));
123
125
  }
@@ -1,11 +1,7 @@
1
- /**
2
- * CR-184: View-related quality rules.
3
- * VR-01: TEST without testResult attribute (info).
4
- * CL-01: ACTOR without UCs in at least 2 distinct operatingModes (warning).
5
- */
6
1
  import type { OntologyGraph } from './ontology.js';
7
2
  import type { RuleViolation, RuleDefinition } from './rules.js';
3
+ import type { MetricPolicy } from './policy.js';
8
4
  export declare function vr01TestNoResult(graph: OntologyGraph): RuleViolation[];
9
5
  export declare function cl01ConopsCompleteness(graph: OntologyGraph): RuleViolation[];
10
6
  export declare const VIEW_RULES: RuleDefinition[];
11
- export declare function evaluateViewRules(graph: OntologyGraph): RuleViolation[];
7
+ export declare function evaluateViewRules(graph: OntologyGraph, policy: MetricPolicy): RuleViolation[];
@@ -1,15 +1,40 @@
1
+ /**
2
+ * CR-184: View-related quality rules.
3
+ * VR-01: TEST with a testRefs entry that has no result (info).
4
+ * CL-01: ACTOR without UCs in at least 2 distinct operatingModes (warning).
5
+ */
6
+ import { TestRefsSchema } from './ontology.js';
1
7
  // ---------------------------------------------------------------------------
2
- // VR-01: TEST without testResult
8
+ // VR-01: ein testRefs-Eintrag ohne Ergebnis
9
+ //
10
+ // CR-SM-231b: liest das Ergebnis PRO EINTRAG statt am Knoten. Vorher hing `testResult` am
11
+ // TEST-Knoten — bei n Eintraegen mehrdeutig: laeuft eine Abnahme als vitest UND playwright,
12
+ // kann ein einzelnes Ergebnis nicht sagen, welcher Lauf gemeint ist. Die Meldung nennt jetzt
13
+ // die Dateien ohne Ergebnis, nicht nur den Knoten.
14
+ //
15
+ // Ein TEST ohne gueltige Bindung faellt R-19 zur Last, nicht hier: sonst meldeten beide
16
+ // Regeln denselben Sachverhalt (Presence/Resolution-Trennung).
3
17
  // ---------------------------------------------------------------------------
4
18
  export function vr01TestNoResult(graph) {
5
- return graph.elements
6
- .filter(e => e.type === 'TEST' && e.attributes?.['testResult'] == null)
7
- .map(e => ({
8
- rule_id: 'VR-01',
9
- severity: 'info',
10
- element_id: e.id,
11
- message: `${e.id} has no testResult — assumed pending`,
12
- }));
19
+ const violations = [];
20
+ for (const el of graph.elements) {
21
+ if (el.type !== 'TEST')
22
+ continue;
23
+ const parsed = TestRefsSchema.safeParse(el.attributes?.testRefs);
24
+ if (!parsed.success)
25
+ continue; // keine/ungueltige Bindung R-19-Gebiet
26
+ const pending = parsed.data.filter(ref => ref.result === undefined);
27
+ if (pending.length === 0)
28
+ continue;
29
+ violations.push({
30
+ rule_id: 'VR-01',
31
+ severity: 'info',
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(', ')}`,
34
+ fix_hint: 'Record the run outcome on the entry (result, ranAt, evidence) — a binding without a result is not evidence',
35
+ });
36
+ }
37
+ return violations;
13
38
  }
14
39
  // ---------------------------------------------------------------------------
15
40
  // CL-01: ACTOR without UCs in >= 2 distinct operatingModes
@@ -48,9 +73,11 @@ export function cl01ConopsCompleteness(graph) {
48
73
  // Aggregated array & convenience runner
49
74
  // ---------------------------------------------------------------------------
50
75
  export const VIEW_RULES = [
51
- { id: 'VR-01', name: 'TestNoResult', severity: 'info', evaluate: vr01TestNoResult },
52
- { id: 'CL-01', name: 'ConopsCompleteness', severity: 'warning', evaluate: cl01ConopsCompleteness },
76
+ { id: 'VR-01', name: 'TestNoResult', severity: 'info', evaluate: vr01TestNoResult, domain: ['TEST'] },
77
+ { id: 'CL-01', name: 'ConopsCompleteness', severity: 'warning', evaluate: cl01ConopsCompleteness, domain: ['ACTOR'] },
53
78
  ];
54
- export function evaluateViewRules(graph) {
55
- return VIEW_RULES.flatMap(rule => rule.evaluate(graph));
79
+ // CR-SM-236: `policy` wird durchgereicht, auch wo diese Familie heute keine Schwelle hat —
80
+ // ein Sonderweg je Familie waere genau der zweite Pfad, den der Regelsatz verbietet.
81
+ export function evaluateViewRules(graph, policy) {
82
+ return VIEW_RULES.flatMap(rule => rule.evaluate(graph, policy));
56
83
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sigloch/contracts",
3
- "version": "3.3.0",
3
+ "version": "4.0.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -28,6 +28,7 @@
28
28
  "prepublishOnly": "npm run build && npm run test"
29
29
  },
30
30
  "dependencies": {
31
+ "@sigloch/contracts": "^4.0.0",
31
32
  "zod": "^4.3.6"
32
33
  },
33
34
  "license": "MIT",