@orcalang/orca-lang 0.1.19 → 0.1.21

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 (40) hide show
  1. package/dist/compiler/dt-compiler.d.ts +3 -0
  2. package/dist/compiler/dt-compiler.d.ts.map +1 -1
  3. package/dist/compiler/dt-compiler.js +205 -1
  4. package/dist/compiler/dt-compiler.js.map +1 -1
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +5 -1
  7. package/dist/index.js.map +1 -1
  8. package/dist/parser/ast-to-markdown.d.ts.map +1 -1
  9. package/dist/parser/ast-to-markdown.js +3 -1
  10. package/dist/parser/ast-to-markdown.js.map +1 -1
  11. package/dist/parser/ast.d.ts +1 -0
  12. package/dist/parser/ast.d.ts.map +1 -1
  13. package/dist/parser/markdown-parser.d.ts.map +1 -1
  14. package/dist/parser/markdown-parser.js +14 -4
  15. package/dist/parser/markdown-parser.js.map +1 -1
  16. package/dist/skills.d.ts +1 -0
  17. package/dist/skills.d.ts.map +1 -1
  18. package/dist/skills.js +287 -23
  19. package/dist/skills.js.map +1 -1
  20. package/dist/verifier/dt-verifier.d.ts +28 -1
  21. package/dist/verifier/dt-verifier.d.ts.map +1 -1
  22. package/dist/verifier/dt-verifier.js +332 -1
  23. package/dist/verifier/dt-verifier.js.map +1 -1
  24. package/dist/verifier/properties.d.ts +4 -0
  25. package/dist/verifier/properties.d.ts.map +1 -1
  26. package/dist/verifier/properties.js +56 -20
  27. package/dist/verifier/properties.js.map +1 -1
  28. package/dist/verifier/structural.d.ts.map +1 -1
  29. package/dist/verifier/structural.js +6 -1
  30. package/dist/verifier/structural.js.map +1 -1
  31. package/package.json +1 -1
  32. package/src/compiler/dt-compiler.ts +223 -1
  33. package/src/index.ts +5 -1
  34. package/src/parser/ast-to-markdown.ts +2 -1
  35. package/src/parser/ast.ts +1 -0
  36. package/src/parser/markdown-parser.ts +11 -3
  37. package/src/skills.ts +319 -23
  38. package/src/verifier/dt-verifier.ts +367 -1
  39. package/src/verifier/properties.ts +78 -23
  40. package/src/verifier/structural.ts +5 -1
@@ -1,7 +1,9 @@
1
1
  // Decision Table Verifier
2
- // Checks: completeness, consistency, redundancy, and structural integrity
2
+ // Checks: completeness, consistency, redundancy, structural integrity, co-location alignment,
3
+ // and machine integration (coverage gap + dead guard detection).
3
4
 
4
5
  import { DecisionTableDef, ConditionDef, CellValue, Rule } from '../parser/dt-ast.js';
6
+ import { MachineDef, OrcaFile, ContextField, GuardExpression, ComparisonOp } from '../parser/ast.js';
5
7
  import { VerificationError, VerificationResult, Severity } from './types.js';
6
8
 
7
9
  // Helper to get all values for a condition
@@ -560,3 +562,367 @@ export function verifyDecisionTables(dts: DecisionTableDef[]): VerificationResul
560
562
  errors: allErrors,
561
563
  };
562
564
  }
565
+
566
+ // ============================================================
567
+ // Co-location Alignment Check
568
+ // ============================================================
569
+
570
+ /**
571
+ * Check that every condition name and output name in a co-located decision table
572
+ * exists as a context field in the machine. When a DT and machine are in the same
573
+ * file, this contract allows action generation to produce fully-wired code.
574
+ */
575
+ export function checkDTContextAlignment(
576
+ dt: DecisionTableDef,
577
+ machine: MachineDef
578
+ ): VerificationError[] {
579
+ const errors: VerificationError[] = [];
580
+ const contextNames = new Set(machine.context.map(f => f.name));
581
+
582
+ for (const cond of dt.conditions) {
583
+ if (!contextNames.has(cond.name)) {
584
+ errors.push({
585
+ code: 'DT_CONTEXT_MISMATCH',
586
+ message: `Decision table '${dt.name}' condition '${cond.name}' has no matching context field in machine '${machine.name}'`,
587
+ severity: 'error',
588
+ location: { decisionTable: dt.name, condition: cond.name },
589
+ suggestion: `Add '${cond.name}' to the ## context section, or rename the condition to match an existing context field`,
590
+ });
591
+ }
592
+ }
593
+
594
+ for (const action of dt.actions) {
595
+ if (!contextNames.has(action.name)) {
596
+ errors.push({
597
+ code: 'DT_CONTEXT_MISMATCH',
598
+ message: `Decision table '${dt.name}' output '${action.name}' has no matching context field in machine '${machine.name}'`,
599
+ severity: 'error',
600
+ location: { decisionTable: dt.name, action: action.name },
601
+ suggestion: `Add '${action.name}' to the ## context section, or rename the output to match an existing context field`,
602
+ });
603
+ }
604
+ }
605
+
606
+ return errors;
607
+ }
608
+
609
+ /**
610
+ * For a file with exactly one machine and one or more decision tables, verify
611
+ * that every DT condition and output name matches a machine context field.
612
+ * Multi-machine files are skipped (ambiguous ownership).
613
+ */
614
+ export function checkFileContextAlignment(file: OrcaFile): VerificationError[] {
615
+ if (file.machines.length !== 1 || file.decisionTables.length === 0) {
616
+ return [];
617
+ }
618
+ const machine = file.machines[0];
619
+ const errors: VerificationError[] = [];
620
+ for (const dt of file.decisionTables) {
621
+ errors.push(...checkDTContextAlignment(dt, machine));
622
+ }
623
+ return errors;
624
+ }
625
+
626
+ // ============================================================
627
+ // Machine Integration Checks
628
+ // ============================================================
629
+
630
+ /**
631
+ * Get the enumerable values for a machine context field.
632
+ * Returns null for types that cannot be exhaustively enumerated (string, int, decimal).
633
+ * For enum fields, values are stored as a comma-separated defaultValue string.
634
+ */
635
+ function getMachineFieldValues(field: ContextField): string[] | null {
636
+ if (field.type.kind === 'bool') return ['true', 'false'];
637
+ if (field.type.kind === 'custom' && field.type.name === 'enum') {
638
+ if (!field.defaultValue) return null;
639
+ const vals = field.defaultValue.split(',').map(v => v.trim()).filter(Boolean);
640
+ return vals.length > 0 ? vals : null;
641
+ }
642
+ return null;
643
+ }
644
+
645
+ /**
646
+ * Generate all combinations of condition values using machine context values as the
647
+ * input domain. Returns null if any condition cannot be enumerated or if the
648
+ * total combinations exceed the safety limit.
649
+ */
650
+ function generateMachineContextCombinations(
651
+ dt: DecisionTableDef,
652
+ contextMap: Map<string, ContextField>
653
+ ): Map<string, string>[] | null {
654
+ const domainPerCondition: string[][] = [];
655
+
656
+ for (const cond of dt.conditions) {
657
+ const field = contextMap.get(cond.name);
658
+ if (!field) return null; // Alignment not met — caller should have checked
659
+ const vals = getMachineFieldValues(field);
660
+ if (!vals) return null; // Non-enumerable type
661
+ domainPerCondition.push(vals);
662
+ }
663
+
664
+ // Safety limit
665
+ let total = 1;
666
+ for (const vals of domainPerCondition) total *= vals.length;
667
+ if (total > 4096) return null;
668
+
669
+ // Cartesian product
670
+ const combos: Map<string, string>[] = [];
671
+ function cartesian(idx: number, current: Map<string, string>): void {
672
+ if (idx === dt.conditions.length) {
673
+ combos.push(new Map(current));
674
+ return;
675
+ }
676
+ const condName = dt.conditions[idx].name;
677
+ for (const val of domainPerCondition[idx]) {
678
+ current.set(condName, val);
679
+ cartesian(idx + 1, current);
680
+ current.delete(condName);
681
+ }
682
+ }
683
+ cartesian(0, new Map());
684
+ return combos;
685
+ }
686
+
687
+ /**
688
+ * DT_COVERAGE_GAP: Decision table must cover all input combinations the machine
689
+ * context can actually produce. Uses machine enum/bool values as the authoritative
690
+ * domain — stricter than DT_INCOMPLETE which only checks DT-declared values.
691
+ */
692
+ function checkDTCoverageGap(
693
+ dt: DecisionTableDef,
694
+ contextMap: Map<string, ContextField>
695
+ ): VerificationError[] {
696
+ const errors: VerificationError[] = [];
697
+
698
+ const combos = generateMachineContextCombinations(dt, contextMap);
699
+ if (!combos) return errors; // Non-enumerable conditions or too many combinations
700
+
701
+ for (const combo of combos) {
702
+ const matched = dt.rules.some(rule => ruleMatchesInput(rule, dt.conditions, combo));
703
+ if (!matched) {
704
+ const comboDesc = [...combo.entries()].map(([k, v]) => `${k}=${v}`).join(', ');
705
+ errors.push({
706
+ code: 'DT_COVERAGE_GAP',
707
+ message: `Decision table '${dt.name}' has no rule for machine context combination: ${comboDesc}`,
708
+ severity: 'error',
709
+ location: { decisionTable: dt.name },
710
+ suggestion: `Add a rule covering this combination, or add a catch-all row using '-' wildcards`,
711
+ });
712
+ }
713
+ }
714
+
715
+ return errors;
716
+ }
717
+
718
+ /**
719
+ * Recursively collect all equality comparisons from a guard expression.
720
+ * Returns tuples of (fieldName, op, comparedValue) for any `ctx.X op Y` node.
721
+ */
722
+ function collectFieldComparisons(
723
+ expr: GuardExpression
724
+ ): Array<{ field: string; op: ComparisonOp; value: string }> {
725
+ if (expr.kind === 'compare') {
726
+ // Only handle ctx.fieldName comparisons
727
+ if (expr.left.path.length === 2 && expr.left.path[0] === 'ctx') {
728
+ return [{ field: expr.left.path[1], op: expr.op, value: String(expr.right.value) }];
729
+ }
730
+ return [];
731
+ }
732
+ if (expr.kind === 'not') return collectFieldComparisons(expr.expr);
733
+ if (expr.kind === 'and' || expr.kind === 'or') {
734
+ return [...collectFieldComparisons(expr.left), ...collectFieldComparisons(expr.right)];
735
+ }
736
+ return [];
737
+ }
738
+
739
+ /**
740
+ * Compute the set of guard names that test a DT output field against a value
741
+ * the DT never produces. These guards are always false after the DT action fires.
742
+ */
743
+ function computeDeadGuardNames(dt: DecisionTableDef, machine: MachineDef): Set<string> {
744
+ const outputDomain = new Map<string, Set<string>>();
745
+ for (const action of dt.actions) {
746
+ outputDomain.set(action.name, new Set<string>());
747
+ }
748
+ for (const rule of dt.rules) {
749
+ for (const [name, value] of rule.actions) {
750
+ outputDomain.get(name)?.add(value);
751
+ }
752
+ }
753
+
754
+ const outputFields = new Set(dt.actions.map(a => a.name));
755
+ const dead = new Set<string>();
756
+
757
+ for (const guardDef of machine.guards) {
758
+ const comparisons = collectFieldComparisons(guardDef.expression);
759
+ for (const { field, op, value } of comparisons) {
760
+ if (!outputFields.has(field)) continue;
761
+ if (op !== 'eq') continue;
762
+ const possible = outputDomain.get(field)!;
763
+ if (!possible.has(value)) {
764
+ dead.add(guardDef.name);
765
+ }
766
+ }
767
+ }
768
+
769
+ return dead;
770
+ }
771
+
772
+ /**
773
+ * DT_GUARD_DEAD: A guard that compares a DT output field against a value the DT
774
+ * never produces is a dead guard — it can never be true immediately after the
775
+ * DT action fires. Reported as a warning since another action might set the field.
776
+ */
777
+ function checkDTGuardDead(dt: DecisionTableDef, machine: MachineDef): VerificationError[] {
778
+ const errors: VerificationError[] = [];
779
+
780
+ // Build output domain: field → set of values the DT can produce
781
+ const outputDomain = new Map<string, Set<string>>();
782
+ for (const action of dt.actions) {
783
+ outputDomain.set(action.name, new Set<string>());
784
+ }
785
+ for (const rule of dt.rules) {
786
+ for (const [name, value] of rule.actions) {
787
+ outputDomain.get(name)?.add(value);
788
+ }
789
+ }
790
+
791
+ const outputFields = new Set(dt.actions.map(a => a.name));
792
+
793
+ for (const guardDef of machine.guards) {
794
+ const comparisons = collectFieldComparisons(guardDef.expression);
795
+ for (const { field, op, value } of comparisons) {
796
+ if (!outputFields.has(field)) continue; // Not a DT output field
797
+ if (op !== 'eq') continue; // Only equality checks are conclusive
798
+
799
+ const possible = outputDomain.get(field)!;
800
+ if (!possible.has(value)) {
801
+ const possibleList = [...possible].join(', ') || '(none)';
802
+ errors.push({
803
+ code: 'DT_GUARD_DEAD',
804
+ message: `Guard '${guardDef.name}' tests '${field} == ${value}' but '${dt.name}' never outputs '${value}' for '${field}' (possible: ${possibleList})`,
805
+ severity: 'warning',
806
+ location: { decisionTable: dt.name, condition: field },
807
+ suggestion: `Update '${dt.name}' to produce '${value}' for '${field}', or remove this guard`,
808
+ });
809
+ }
810
+ }
811
+ }
812
+
813
+ return errors;
814
+ }
815
+
816
+ /**
817
+ * BFS from initial state, optionally skipping transitions guarded by dead guards.
818
+ * A non-negated transition guarded by a name in `deadGuards` is skipped (never fires).
819
+ * A negated dead guard (!dead) is NOT skipped — negation of a dead guard is always true.
820
+ */
821
+ function bfsReachableWithDeadGuards(machine: MachineDef, deadGuards: Set<string>): Set<string> {
822
+ const initial = machine.states.find(s => s.isInitial);
823
+ if (!initial) return new Set();
824
+
825
+ const visited = new Set<string>();
826
+ const queue = [initial.name];
827
+
828
+ while (queue.length > 0) {
829
+ const state = queue.shift()!;
830
+ if (visited.has(state)) continue;
831
+ visited.add(state);
832
+
833
+ for (const t of machine.transitions) {
834
+ if (t.source !== state) continue;
835
+ // A non-negated dead guard means the transition can never fire
836
+ if (t.guard && !t.guard.negated && deadGuards.has(t.guard.name)) continue;
837
+ if (!visited.has(t.target)) queue.push(t.target);
838
+ }
839
+ }
840
+
841
+ return visited;
842
+ }
843
+
844
+ /**
845
+ * DT_UNREACHABLE_STATE: A state that is graph-reachable but only accessible via
846
+ * transitions guarded by dead guards — it can never be entered given DT outputs.
847
+ * Reported as a warning (structural reachability is preserved; the constraint is semantic).
848
+ */
849
+ function checkDTDeadGuardReachability(dt: DecisionTableDef, machine: MachineDef): VerificationError[] {
850
+ const deadGuards = computeDeadGuardNames(dt, machine);
851
+ if (deadGuards.size === 0) return [];
852
+
853
+ const plainReachable = bfsReachableWithDeadGuards(machine, new Set());
854
+ const dtReachable = bfsReachableWithDeadGuards(machine, deadGuards);
855
+
856
+ const errors: VerificationError[] = [];
857
+ const deadList = [...deadGuards].join(', ');
858
+
859
+ for (const state of machine.states) {
860
+ if (plainReachable.has(state.name) && !dtReachable.has(state.name)) {
861
+ errors.push({
862
+ code: 'DT_UNREACHABLE_STATE',
863
+ message: `State '${state.name}' is unreachable given '${dt.name}' output constraints — all entry paths are gated by dead guards (${deadList})`,
864
+ severity: 'warning',
865
+ location: { state: state.name, decisionTable: dt.name },
866
+ suggestion: `Update '${dt.name}' to produce values that satisfy the guards leading to '${state.name}', or revise the guard expressions`,
867
+ });
868
+ }
869
+ }
870
+
871
+ return errors;
872
+ }
873
+
874
+ /**
875
+ * Check DT integration with the machine: coverage gap, dead guards, and
876
+ * DT-constrained reachability. Only runs when exactly one machine is present
877
+ * and the DT is fully aligned. Multi-machine files are skipped (ambiguous ownership).
878
+ */
879
+ export function checkDTMachineIntegration(file: OrcaFile): VerificationError[] {
880
+ if (file.machines.length !== 1 || file.decisionTables.length === 0) {
881
+ return [];
882
+ }
883
+ const machine = file.machines[0];
884
+ const contextMap = new Map(machine.context.map(f => [f.name, f]));
885
+ const errors: VerificationError[] = [];
886
+
887
+ for (const dt of file.decisionTables) {
888
+ // Only verify DTs that are fully aligned with machine context
889
+ const allAligned = [...dt.conditions, ...dt.actions].every(item => contextMap.has(item.name));
890
+ if (!allAligned) continue;
891
+
892
+ errors.push(...checkDTCoverageGap(dt, contextMap));
893
+ errors.push(...checkDTGuardDead(dt, machine));
894
+ errors.push(...checkDTDeadGuardReachability(dt, machine));
895
+ }
896
+
897
+ return errors;
898
+ }
899
+
900
+ /**
901
+ * Compute the merged output domain across all aligned DTs in a single-machine file.
902
+ * Returns a map from DT output field name → set of values the DT(s) can produce.
903
+ * Used by the properties checker to prune guard-protected transitions that are
904
+ * semantically impossible given DT output constraints.
905
+ * Returns undefined if no aligned DTs are found.
906
+ */
907
+ export function computeAlignedDTOutputDomain(file: OrcaFile): Map<string, Set<string>> | undefined {
908
+ if (file.machines.length !== 1 || file.decisionTables.length === 0) return undefined;
909
+ const machine = file.machines[0];
910
+ const contextMap = new Map(machine.context.map(f => [f.name, f]));
911
+
912
+ const domain = new Map<string, Set<string>>();
913
+
914
+ for (const dt of file.decisionTables) {
915
+ const allAligned = [...dt.conditions, ...dt.actions].every(item => contextMap.has(item.name));
916
+ if (!allAligned) continue;
917
+
918
+ for (const actionDef of dt.actions) {
919
+ if (!domain.has(actionDef.name)) domain.set(actionDef.name, new Set());
920
+ for (const rule of dt.rules) {
921
+ const val = rule.actions.get(actionDef.name);
922
+ if (val !== undefined) domain.get(actionDef.name)!.add(val);
923
+ }
924
+ }
925
+ }
926
+
927
+ return domain.size > 0 ? domain : undefined;
928
+ }
@@ -1,29 +1,66 @@
1
- import { MachineDef, Property, ReachabilityProperty, PassesThroughProperty, RespondsProperty, InvariantProperty, GuardRef, GuardDef } from '../parser/ast.js';
1
+ import { MachineDef, Property, ReachabilityProperty, PassesThroughProperty, RespondsProperty, InvariantProperty, GuardRef, GuardDef, GuardExpression } from '../parser/ast.js';
2
2
  import { VerificationResult, VerificationError, MachineAnalysis, StateInfo } from './types.js';
3
3
  import { analyzeMachine, flattenStates, FlattenedState } from './structural.js';
4
4
  import { resolveGuardExpression, isExpressionStaticallyFalse } from './determinism.js';
5
5
 
6
6
  const DEFAULT_MAX_STATES = 64;
7
7
 
8
+ /**
9
+ * Check if a guard expression is statically false given DT output domain constraints.
10
+ * A comparison `ctx.field == value` is false if the DT never outputs `value` for `field`.
11
+ * Handles AND (false if either branch is domain-blocked) and OR (false only if both are).
12
+ */
13
+ function isExpressionBlockedByDomain(
14
+ expr: GuardExpression,
15
+ domain: Map<string, Set<string>>
16
+ ): boolean {
17
+ if (expr.kind === 'compare' && expr.op === 'eq') {
18
+ if (expr.left.path.length === 2 && expr.left.path[0] === 'ctx') {
19
+ const field = expr.left.path[1];
20
+ const value = String(expr.right.value);
21
+ const possible = domain.get(field);
22
+ if (possible !== undefined && !possible.has(value)) return true;
23
+ }
24
+ }
25
+ if (expr.kind === 'and') {
26
+ return isExpressionBlockedByDomain(expr.left, domain) ||
27
+ isExpressionBlockedByDomain(expr.right, domain);
28
+ }
29
+ if (expr.kind === 'or') {
30
+ return isExpressionBlockedByDomain(expr.left, domain) &&
31
+ isExpressionBlockedByDomain(expr.right, domain);
32
+ }
33
+ return false;
34
+ }
35
+
8
36
  /**
9
37
  * Check if a transition's guard is statically false (can never fire).
10
38
  * Used in guard-aware BFS to prune impossible transitions.
39
+ * When dtOutputDomain is provided, also prunes guards that compare DT output
40
+ * fields against values the DT never produces.
11
41
  */
12
42
  function isTransitionStaticallyBlocked(
13
43
  transition: { guard?: GuardRef },
14
- guardDefs: Map<string, GuardDef>
44
+ guardDefs: Map<string, GuardDef>,
45
+ dtOutputDomain?: Map<string, Set<string>>
15
46
  ): boolean {
16
47
  if (!transition.guard) return false; // No guard = always possible
17
48
 
18
49
  const resolved = resolveGuardExpression(transition.guard, guardDefs);
19
50
  if (!resolved) return false; // Can't resolve = assume possible
20
51
 
21
- return isExpressionStaticallyFalse(resolved);
52
+ if (isExpressionStaticallyFalse(resolved)) return true;
53
+
54
+ if (dtOutputDomain && isExpressionBlockedByDomain(resolved, dtOutputDomain)) return true;
55
+
56
+ return false;
22
57
  }
23
58
 
24
59
  /**
25
60
  * BFS from source state, returning reachable set and parent map for counterexample traces.
26
61
  * Supports guard-aware mode: skips transitions with statically false guards.
62
+ * When dtOutputDomain is provided, also skips transitions whose guards are impossible
63
+ * given the DT output domain constraints.
27
64
  */
28
65
  function bfs(
29
66
  stateMap: Map<string, StateInfo>,
@@ -31,7 +68,8 @@ function bfs(
31
68
  options?: {
32
69
  excludeState?: string;
33
70
  maxDepth?: number;
34
- guardDefs?: Map<string, GuardDef>; // Enable guard-aware pruning
71
+ guardDefs?: Map<string, GuardDef>;
72
+ dtOutputDomain?: Map<string, Set<string>>;
35
73
  }
36
74
  ): { reachable: Set<string>; parent: Map<string, { state: string; event: string; guard?: string }> } {
37
75
  const reachable = new Set<string>();
@@ -56,8 +94,8 @@ function bfs(
56
94
  // Skip excluded state
57
95
  if (options?.excludeState && target === options.excludeState) continue;
58
96
 
59
- // Guard-aware: skip transitions with statically false guards
60
- if (options?.guardDefs && isTransitionStaticallyBlocked(t, options.guardDefs)) continue;
97
+ // Guard-aware: skip transitions with statically false guards (including DT domain constraints)
98
+ if (options?.guardDefs && isTransitionStaticallyBlocked(t, options.guardDefs, options.dtOutputDomain)) continue;
61
99
 
62
100
  if (!reachable.has(target)) {
63
101
  reachable.add(target);
@@ -157,7 +195,8 @@ function checkReachable(
157
195
  prop: ReachabilityProperty,
158
196
  analysis: MachineAnalysis,
159
197
  flattenedStates: FlattenedState[],
160
- guardDefs: Map<string, GuardDef>
198
+ guardDefs: Map<string, GuardDef>,
199
+ dtOutputDomain?: Map<string, Set<string>>
161
200
  ): VerificationError[] {
162
201
  const errors: VerificationError[] = [];
163
202
 
@@ -166,7 +205,7 @@ function checkReachable(
166
205
  const toRes = resolveStateName(prop.to, flattenedStates);
167
206
  if (toRes.error) return [toRes.error];
168
207
 
169
- const { reachable } = bfs(analysis.stateMap, fromRes.resolved, { guardDefs });
208
+ const { reachable } = bfs(analysis.stateMap, fromRes.resolved, { guardDefs, dtOutputDomain });
170
209
 
171
210
  if (!reachable.has(toRes.resolved)) {
172
211
  errors.push({
@@ -185,7 +224,8 @@ function checkUnreachable(
185
224
  prop: ReachabilityProperty,
186
225
  analysis: MachineAnalysis,
187
226
  flattenedStates: FlattenedState[],
188
- guardDefs: Map<string, GuardDef>
227
+ guardDefs: Map<string, GuardDef>,
228
+ dtOutputDomain?: Map<string, Set<string>>
189
229
  ): VerificationError[] {
190
230
  const errors: VerificationError[] = [];
191
231
 
@@ -194,7 +234,7 @@ function checkUnreachable(
194
234
  const toRes = resolveStateName(prop.to, flattenedStates);
195
235
  if (toRes.error) return [toRes.error];
196
236
 
197
- const { reachable, parent } = bfs(analysis.stateMap, fromRes.resolved, { guardDefs });
237
+ const { reachable, parent } = bfs(analysis.stateMap, fromRes.resolved, { guardDefs, dtOutputDomain });
198
238
 
199
239
  if (reachable.has(toRes.resolved)) {
200
240
  const path = reconstructPath(parent, fromRes.resolved, toRes.resolved);
@@ -218,7 +258,8 @@ function checkPassesThrough(
218
258
  prop: PassesThroughProperty,
219
259
  analysis: MachineAnalysis,
220
260
  flattenedStates: FlattenedState[],
221
- guardDefs: Map<string, GuardDef>
261
+ guardDefs: Map<string, GuardDef>,
262
+ dtOutputDomain?: Map<string, Set<string>>
222
263
  ): VerificationError[] {
223
264
  const errors: VerificationError[] = [];
224
265
 
@@ -230,7 +271,7 @@ function checkPassesThrough(
230
271
  if (throughRes.error) return [throughRes.error];
231
272
 
232
273
  // First check: is target reachable from source at all?
233
- const { reachable: fullReachable } = bfs(analysis.stateMap, fromRes.resolved, { guardDefs });
274
+ const { reachable: fullReachable } = bfs(analysis.stateMap, fromRes.resolved, { guardDefs, dtOutputDomain });
234
275
  if (!fullReachable.has(toRes.resolved)) {
235
276
  errors.push({
236
277
  code: 'PROPERTY_PATH_FAIL',
@@ -246,6 +287,7 @@ function checkPassesThrough(
246
287
  const { reachable: withoutThrough, parent } = bfs(analysis.stateMap, fromRes.resolved, {
247
288
  excludeState: throughRes.resolved,
248
289
  guardDefs,
290
+ dtOutputDomain,
249
291
  });
250
292
 
251
293
  if (withoutThrough.has(toRes.resolved)) {
@@ -265,14 +307,15 @@ function checkPassesThrough(
265
307
  function checkLive(
266
308
  analysis: MachineAnalysis,
267
309
  flattenedStates: FlattenedState[],
268
- guardDefs: Map<string, GuardDef>
310
+ guardDefs: Map<string, GuardDef>,
311
+ dtOutputDomain?: Map<string, Set<string>>
269
312
  ): VerificationError[] {
270
313
  const errors: VerificationError[] = [];
271
314
 
272
315
  if (!analysis.initialState) return errors;
273
316
 
274
317
  // Find all reachable states from initial
275
- const { reachable: reachableFromInitial } = bfs(analysis.stateMap, analysis.initialState.name, { guardDefs });
318
+ const { reachable: reachableFromInitial } = bfs(analysis.stateMap, analysis.initialState.name, { guardDefs, dtOutputDomain });
276
319
 
277
320
  // Find all final state names
278
321
  const finalStateNames = new Set(analysis.finalStates.map(s => s.name));
@@ -285,7 +328,7 @@ function checkLive(
285
328
  const fs = flattenedStates.find(f => f.name === stateName);
286
329
  if (fs && (fs.isCompound || fs.isRegion)) continue;
287
330
 
288
- const { reachable: reachableFromState } = bfs(analysis.stateMap, stateName, { guardDefs });
331
+ const { reachable: reachableFromState } = bfs(analysis.stateMap, stateName, { guardDefs, dtOutputDomain });
289
332
 
290
333
  let canReachFinal = false;
291
334
  for (const finalName of finalStateNames) {
@@ -313,7 +356,8 @@ function checkResponds(
313
356
  prop: RespondsProperty,
314
357
  analysis: MachineAnalysis,
315
358
  flattenedStates: FlattenedState[],
316
- guardDefs: Map<string, GuardDef>
359
+ guardDefs: Map<string, GuardDef>,
360
+ dtOutputDomain?: Map<string, Set<string>>
317
361
  ): VerificationError[] {
318
362
  const errors: VerificationError[] = [];
319
363
 
@@ -325,11 +369,12 @@ function checkResponds(
325
369
  const { reachable } = bfs(analysis.stateMap, fromRes.resolved, {
326
370
  maxDepth: prop.within,
327
371
  guardDefs,
372
+ dtOutputDomain,
328
373
  });
329
374
 
330
375
  if (!reachable.has(toRes.resolved)) {
331
376
  // Check if it's reachable at all (just beyond the bound)
332
- const { reachable: unbounded } = bfs(analysis.stateMap, fromRes.resolved, { guardDefs });
377
+ const { reachable: unbounded } = bfs(analysis.stateMap, fromRes.resolved, { guardDefs, dtOutputDomain });
333
378
  const reachableButBeyondBound = unbounded.has(toRes.resolved);
334
379
 
335
380
  const suffix = reachableButBeyondBound
@@ -453,7 +498,16 @@ function checkMachineSize(
453
498
 
454
499
  // --- Main entry point ---
455
500
 
456
- export function checkProperties(machine: MachineDef, options?: { maxStates?: number }): VerificationResult {
501
+ export function checkProperties(
502
+ machine: MachineDef,
503
+ options?: {
504
+ maxStates?: number;
505
+ /** DT output domain from co-located aligned decision tables. When provided,
506
+ * the guard-aware BFS will prune transitions whose guards compare DT output
507
+ * fields against values the DT never produces, giving more precise results. */
508
+ dtOutputDomain?: Map<string, Set<string>>;
509
+ }
510
+ ): VerificationResult {
457
511
  const maxStates = options?.maxStates ?? DEFAULT_MAX_STATES;
458
512
  const flattenedStates = flattenStates(machine.states);
459
513
  const errors: VerificationError[] = [];
@@ -470,6 +524,7 @@ export function checkProperties(machine: MachineDef, options?: { maxStates?: num
470
524
  }
471
525
 
472
526
  const analysis = analyzeMachine(machine);
527
+ const dtOutputDomain = options?.dtOutputDomain;
473
528
 
474
529
  // Build guard definition map for guard-aware BFS
475
530
  const guardDefs = new Map<string, GuardDef>();
@@ -480,19 +535,19 @@ export function checkProperties(machine: MachineDef, options?: { maxStates?: num
480
535
  for (const prop of machine.properties) {
481
536
  switch (prop.kind) {
482
537
  case 'reachable':
483
- errors.push(...checkReachable(prop, analysis, flattenedStates, guardDefs));
538
+ errors.push(...checkReachable(prop, analysis, flattenedStates, guardDefs, dtOutputDomain));
484
539
  break;
485
540
  case 'unreachable':
486
- errors.push(...checkUnreachable(prop, analysis, flattenedStates, guardDefs));
541
+ errors.push(...checkUnreachable(prop, analysis, flattenedStates, guardDefs, dtOutputDomain));
487
542
  break;
488
543
  case 'passes_through':
489
- errors.push(...checkPassesThrough(prop, analysis, flattenedStates, guardDefs));
544
+ errors.push(...checkPassesThrough(prop, analysis, flattenedStates, guardDefs, dtOutputDomain));
490
545
  break;
491
546
  case 'live':
492
- errors.push(...checkLive(analysis, flattenedStates, guardDefs));
547
+ errors.push(...checkLive(analysis, flattenedStates, guardDefs, dtOutputDomain));
493
548
  break;
494
549
  case 'responds':
495
- errors.push(...checkResponds(prop, analysis, flattenedStates, guardDefs));
550
+ errors.push(...checkResponds(prop, analysis, flattenedStates, guardDefs, dtOutputDomain));
496
551
  break;
497
552
  case 'invariant':
498
553
  errors.push(...checkInvariant(prop, machine, flattenedStates));
@@ -183,7 +183,11 @@ export function analyzeMachine(machine: MachineDef): MachineAnalysis {
183
183
  if (fs) {
184
184
  // Find the original state definition
185
185
  const originalState = findOriginalState(machine.states, fs.simpleName, fs.parentName);
186
- if (originalState?.ignoredEvents) {
186
+ if (originalState?.ignoredAll) {
187
+ for (const event of machine.events) {
188
+ info.eventsIgnored.add(event.name);
189
+ }
190
+ } else if (originalState?.ignoredEvents) {
187
191
  for (const event of originalState.ignoredEvents) {
188
192
  info.eventsIgnored.add(event);
189
193
  }