@orcalang/orca-lang 0.1.18 → 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.
- package/dist/compiler/dt-compiler.d.ts +26 -0
- package/dist/compiler/dt-compiler.d.ts.map +1 -0
- package/dist/compiler/dt-compiler.js +387 -0
- package/dist/compiler/dt-compiler.js.map +1 -0
- package/dist/health-check.d.ts +3 -0
- package/dist/health-check.d.ts.map +1 -0
- package/dist/health-check.js +235 -0
- package/dist/health-check.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/parser/ast-to-markdown.d.ts.map +1 -1
- package/dist/parser/ast-to-markdown.js +3 -1
- package/dist/parser/ast-to-markdown.js.map +1 -1
- package/dist/parser/ast.d.ts +3 -0
- package/dist/parser/ast.d.ts.map +1 -1
- package/dist/parser/dt-ast.d.ts +43 -0
- package/dist/parser/dt-ast.d.ts.map +1 -0
- package/dist/parser/dt-ast.js +3 -0
- package/dist/parser/dt-ast.js.map +1 -0
- package/dist/parser/dt-parser.d.ts +40 -0
- package/dist/parser/dt-parser.d.ts.map +1 -0
- package/dist/parser/dt-parser.js +240 -0
- package/dist/parser/dt-parser.js.map +1 -0
- package/dist/parser/markdown-parser.d.ts.map +1 -1
- package/dist/parser/markdown-parser.js +43 -8
- package/dist/parser/markdown-parser.js.map +1 -1
- package/dist/skills.d.ts +50 -1
- package/dist/skills.d.ts.map +1 -1
- package/dist/skills.js +508 -21
- package/dist/skills.js.map +1 -1
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +49 -0
- package/dist/tools.js.map +1 -1
- package/dist/verifier/dt-verifier.d.ts +32 -0
- package/dist/verifier/dt-verifier.d.ts.map +1 -0
- package/dist/verifier/dt-verifier.js +830 -0
- package/dist/verifier/dt-verifier.js.map +1 -0
- package/dist/verifier/properties.d.ts +4 -0
- package/dist/verifier/properties.d.ts.map +1 -1
- package/dist/verifier/properties.js +56 -20
- package/dist/verifier/properties.js.map +1 -1
- package/dist/verifier/structural.d.ts.map +1 -1
- package/dist/verifier/structural.js +6 -1
- package/dist/verifier/structural.js.map +1 -1
- package/dist/verifier/types.d.ts +4 -0
- package/dist/verifier/types.d.ts.map +1 -1
- package/package.json +3 -2
- package/src/compiler/dt-compiler.ts +454 -0
- package/src/health-check.ts +273 -0
- package/src/index.ts +5 -1
- package/src/parser/ast-to-markdown.ts +2 -1
- package/src/parser/ast.ts +4 -0
- package/src/parser/dt-ast.ts +40 -0
- package/src/parser/dt-parser.ts +289 -0
- package/src/parser/markdown-parser.ts +43 -8
- package/src/skills.ts +591 -22
- package/src/tools.ts +53 -0
- package/src/verifier/dt-verifier.ts +928 -0
- package/src/verifier/properties.ts +78 -23
- package/src/verifier/structural.ts +5 -1
- package/src/verifier/types.ts +4 -0
|
@@ -0,0 +1,830 @@
|
|
|
1
|
+
// Decision Table Verifier
|
|
2
|
+
// Checks: completeness, consistency, redundancy, structural integrity, co-location alignment,
|
|
3
|
+
// and machine integration (coverage gap + dead guard detection).
|
|
4
|
+
// Helper to get all values for a condition
|
|
5
|
+
function getConditionValues(condition) {
|
|
6
|
+
if (condition.type === 'bool') {
|
|
7
|
+
return condition.values.length > 0 ? condition.values : ['true', 'false'];
|
|
8
|
+
}
|
|
9
|
+
return condition.values;
|
|
10
|
+
}
|
|
11
|
+
// Check if a cell matches a given value
|
|
12
|
+
function cellMatches(cell, value) {
|
|
13
|
+
switch (cell.kind) {
|
|
14
|
+
case 'any':
|
|
15
|
+
return true;
|
|
16
|
+
case 'exact':
|
|
17
|
+
return cell.value === value;
|
|
18
|
+
case 'negated':
|
|
19
|
+
return cell.value !== value;
|
|
20
|
+
case 'set':
|
|
21
|
+
return cell.values.includes(value);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
// Check if a rule's conditions cover a specific input combination
|
|
25
|
+
function ruleMatchesInput(rule, conditionDefs, input) {
|
|
26
|
+
for (const cond of conditionDefs) {
|
|
27
|
+
const cell = rule.conditions.get(cond.name);
|
|
28
|
+
if (!cell)
|
|
29
|
+
continue; // No condition for this column means "any"
|
|
30
|
+
const expectedValue = input.get(cond.name);
|
|
31
|
+
if (!cellMatches(cell, expectedValue || '')) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
// Check if two rules overlap (can match the same input)
|
|
38
|
+
function rulesOverlap(rule1, rule2, conditionDefs) {
|
|
39
|
+
for (const cond of conditionDefs) {
|
|
40
|
+
const cell1 = rule1.conditions.get(cond.name);
|
|
41
|
+
const cell2 = rule2.conditions.get(cond.name);
|
|
42
|
+
// If either rule doesn't constrain this condition, they overlap on it
|
|
43
|
+
if (!cell1 || cell1.kind === 'any')
|
|
44
|
+
continue;
|
|
45
|
+
if (!cell2 || cell2.kind === 'any')
|
|
46
|
+
continue;
|
|
47
|
+
// Check if cells intersect
|
|
48
|
+
if (!cellsIntersect(cell1, cell2)) {
|
|
49
|
+
return false; // No overlap on this condition means no overall overlap
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return true; // All constrained conditions intersect
|
|
53
|
+
}
|
|
54
|
+
// Check if two cells intersect (can match the same value)
|
|
55
|
+
function cellsIntersect(cell1, cell2) {
|
|
56
|
+
// Any intersects with everything
|
|
57
|
+
if (cell1.kind === 'any' || cell2.kind === 'any')
|
|
58
|
+
return true;
|
|
59
|
+
// Exact vs Exact
|
|
60
|
+
if (cell1.kind === 'exact' && cell2.kind === 'exact') {
|
|
61
|
+
return cell1.value === cell2.value;
|
|
62
|
+
}
|
|
63
|
+
// Exact vs Negated
|
|
64
|
+
if (cell1.kind === 'exact' && cell2.kind === 'negated') {
|
|
65
|
+
return cell2.value !== cell1.value;
|
|
66
|
+
}
|
|
67
|
+
if (cell1.kind === 'negated' && cell2.kind === 'exact') {
|
|
68
|
+
return cell1.value !== cell2.value;
|
|
69
|
+
}
|
|
70
|
+
// Exact vs Set
|
|
71
|
+
if (cell1.kind === 'exact' && cell2.kind === 'set') {
|
|
72
|
+
return cell2.values.includes(cell1.value);
|
|
73
|
+
}
|
|
74
|
+
if (cell1.kind === 'set' && cell2.kind === 'exact') {
|
|
75
|
+
return cell1.values.includes(cell2.value);
|
|
76
|
+
}
|
|
77
|
+
// Negated vs Negated
|
|
78
|
+
if (cell1.kind === 'negated' && cell2.kind === 'negated') {
|
|
79
|
+
return cell1.value !== cell2.value;
|
|
80
|
+
}
|
|
81
|
+
// Negated vs Set
|
|
82
|
+
if (cell1.kind === 'negated' && cell2.kind === 'set') {
|
|
83
|
+
return !cell2.values.includes(cell1.value);
|
|
84
|
+
}
|
|
85
|
+
if (cell1.kind === 'set' && cell2.kind === 'negated') {
|
|
86
|
+
return !cell1.values.includes(cell2.value);
|
|
87
|
+
}
|
|
88
|
+
// Set vs Set
|
|
89
|
+
if (cell1.kind === 'set' && cell2.kind === 'set') {
|
|
90
|
+
return cell1.values.some(v => cell2.values.includes(v));
|
|
91
|
+
}
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
// Generate all possible combinations of condition values
|
|
95
|
+
function generateCombinations(conditionDefs) {
|
|
96
|
+
const combinations = [];
|
|
97
|
+
const values = conditionDefs.map(c => getConditionValues(c));
|
|
98
|
+
function* cartesian(index, current) {
|
|
99
|
+
if (index === conditionDefs.length) {
|
|
100
|
+
yield new Map(current);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const condName = conditionDefs[index].name;
|
|
104
|
+
for (const val of values[index]) {
|
|
105
|
+
current.set(condName, val);
|
|
106
|
+
yield* cartesian(index + 1, current);
|
|
107
|
+
current.delete(condName);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return [...cartesian(0, new Map())];
|
|
111
|
+
}
|
|
112
|
+
// Find which rules match a given input
|
|
113
|
+
function findMatchingRules(dt, input) {
|
|
114
|
+
return dt.rules.filter(rule => ruleMatchesInput(rule, dt.conditions, input));
|
|
115
|
+
}
|
|
116
|
+
// Check if two rules produce the same action outputs
|
|
117
|
+
function actionsMatch(rule1, rule2, actionNames) {
|
|
118
|
+
for (const actionName of actionNames) {
|
|
119
|
+
const val1 = rule1.actions.get(actionName);
|
|
120
|
+
const val2 = rule2.actions.get(actionName);
|
|
121
|
+
if (val1 !== val2)
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
// ============================================================
|
|
127
|
+
// Structural Checks
|
|
128
|
+
// ============================================================
|
|
129
|
+
function checkStructural(dt) {
|
|
130
|
+
const errors = [];
|
|
131
|
+
// DT_NO_CONDITIONS
|
|
132
|
+
if (dt.conditions.length === 0) {
|
|
133
|
+
errors.push({
|
|
134
|
+
code: 'DT_NO_CONDITIONS',
|
|
135
|
+
message: 'Decision table has no conditions declared',
|
|
136
|
+
severity: 'error',
|
|
137
|
+
location: { decisionTable: dt.name },
|
|
138
|
+
suggestion: 'Add a ## conditions section with at least one condition',
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
// DT_NO_ACTIONS
|
|
142
|
+
if (dt.actions.length === 0) {
|
|
143
|
+
errors.push({
|
|
144
|
+
code: 'DT_NO_ACTIONS',
|
|
145
|
+
message: 'Decision table has no actions declared',
|
|
146
|
+
severity: 'error',
|
|
147
|
+
location: { decisionTable: dt.name },
|
|
148
|
+
suggestion: 'Add an ## actions section with at least one action',
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
// DT_EMPTY_RULES
|
|
152
|
+
if (dt.rules.length === 0) {
|
|
153
|
+
errors.push({
|
|
154
|
+
code: 'DT_EMPTY_RULES',
|
|
155
|
+
message: 'Decision table has no rules',
|
|
156
|
+
severity: 'warning',
|
|
157
|
+
location: { decisionTable: dt.name },
|
|
158
|
+
suggestion: 'Add rules to the ## rules section',
|
|
159
|
+
});
|
|
160
|
+
return errors; // No point checking rule content if there are no rules
|
|
161
|
+
}
|
|
162
|
+
const conditionNames = new Set(dt.conditions.map(c => c.name));
|
|
163
|
+
const actionNames = new Set(dt.actions.map(a => a.name));
|
|
164
|
+
const ruleConditionNames = new Set();
|
|
165
|
+
const ruleActionNames = new Set();
|
|
166
|
+
// Check each rule
|
|
167
|
+
for (let ruleIdx = 0; ruleIdx < dt.rules.length; ruleIdx++) {
|
|
168
|
+
const rule = dt.rules[ruleIdx];
|
|
169
|
+
const ruleNum = rule.number ?? ruleIdx + 1;
|
|
170
|
+
// Check condition columns
|
|
171
|
+
for (const [condName] of rule.conditions) {
|
|
172
|
+
ruleConditionNames.add(condName);
|
|
173
|
+
// DT_UNKNOWN_CONDITION_COLUMN
|
|
174
|
+
if (!conditionNames.has(condName)) {
|
|
175
|
+
errors.push({
|
|
176
|
+
code: 'DT_UNKNOWN_CONDITION_COLUMN',
|
|
177
|
+
message: `Rule ${ruleNum} has unknown condition column "${condName}"`,
|
|
178
|
+
severity: 'warning',
|
|
179
|
+
location: { decisionTable: dt.name, rule: ruleNum, condition: condName },
|
|
180
|
+
suggestion: `Remove or rename the column to match a declared condition`,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
// DT_UNKNOWN_CONDITION_VALUE
|
|
185
|
+
const cond = dt.conditions.find(c => c.name === condName);
|
|
186
|
+
const cell = rule.conditions.get(condName);
|
|
187
|
+
// Skip value validation for int_range — values are ranges, not enum values
|
|
188
|
+
if (cell.kind !== 'any' && cond.type !== 'string' && cond.type !== 'int_range') {
|
|
189
|
+
const validValues = getConditionValues(cond);
|
|
190
|
+
if (cell.kind === 'exact' && !validValues.includes(cell.value)) {
|
|
191
|
+
errors.push({
|
|
192
|
+
code: 'DT_UNKNOWN_CONDITION_VALUE',
|
|
193
|
+
message: `Rule ${ruleNum} condition "${condName}" has value "${cell.value}" not in declared values`,
|
|
194
|
+
severity: 'error',
|
|
195
|
+
location: { decisionTable: dt.name, rule: ruleNum, condition: condName },
|
|
196
|
+
suggestion: `Change to one of: ${validValues.join(', ')}`,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
else if (cell.kind === 'negated' && !validValues.includes(cell.value)) {
|
|
200
|
+
errors.push({
|
|
201
|
+
code: 'DT_UNKNOWN_CONDITION_VALUE',
|
|
202
|
+
message: `Rule ${ruleNum} condition "${condName}" negates "${cell.value}" which is not in declared values`,
|
|
203
|
+
severity: 'error',
|
|
204
|
+
location: { decisionTable: dt.name, rule: ruleNum, condition: condName },
|
|
205
|
+
suggestion: `Change to negate one of: ${validValues.join(', ')}`,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
else if (cell.kind === 'set') {
|
|
209
|
+
for (const v of cell.values) {
|
|
210
|
+
if (!validValues.includes(v)) {
|
|
211
|
+
errors.push({
|
|
212
|
+
code: 'DT_UNKNOWN_CONDITION_VALUE',
|
|
213
|
+
message: `Rule ${ruleNum} condition "${condName}" has value "${v}" not in declared values`,
|
|
214
|
+
severity: 'error',
|
|
215
|
+
location: { decisionTable: dt.name, rule: ruleNum, condition: condName },
|
|
216
|
+
suggestion: `Change to one of: ${validValues.join(', ')}`,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
// Check action columns
|
|
225
|
+
for (const [actionName] of rule.actions) {
|
|
226
|
+
ruleActionNames.add(actionName);
|
|
227
|
+
// DT_UNKNOWN_ACTION_COLUMN
|
|
228
|
+
if (!actionNames.has(actionName)) {
|
|
229
|
+
errors.push({
|
|
230
|
+
code: 'DT_UNKNOWN_ACTION_COLUMN',
|
|
231
|
+
message: `Rule ${ruleNum} has unknown action column "${actionName}"`,
|
|
232
|
+
severity: 'warning',
|
|
233
|
+
location: { decisionTable: dt.name, rule: ruleNum, action: actionName },
|
|
234
|
+
suggestion: `Remove or rename the column to match a declared action`,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
// DT_UNKNOWN_ACTION_VALUE
|
|
239
|
+
const action = dt.actions.find(a => a.name === actionName);
|
|
240
|
+
const value = rule.actions.get(actionName);
|
|
241
|
+
if (action.type === 'enum' && action.values) {
|
|
242
|
+
if (!action.values.includes(value)) {
|
|
243
|
+
errors.push({
|
|
244
|
+
code: 'DT_UNKNOWN_ACTION_VALUE',
|
|
245
|
+
message: `Rule ${ruleNum} action "${actionName}" has value "${value}" not in declared values`,
|
|
246
|
+
severity: 'error',
|
|
247
|
+
location: { decisionTable: dt.name, rule: ruleNum, action: actionName },
|
|
248
|
+
suggestion: `Change to one of: ${action.values.join(', ')}`,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
// DT_MISSING_CONDITION_COLUMN
|
|
256
|
+
for (const condName of conditionNames) {
|
|
257
|
+
if (!ruleConditionNames.has(condName)) {
|
|
258
|
+
errors.push({
|
|
259
|
+
code: 'DT_MISSING_CONDITION_COLUMN',
|
|
260
|
+
message: `Condition "${condName}" is declared but has no column in the rules table`,
|
|
261
|
+
severity: 'warning',
|
|
262
|
+
location: { decisionTable: dt.name, condition: condName },
|
|
263
|
+
suggestion: `Add a "${condName}" column to the rules table`,
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
// DT_MISSING_ACTION_COLUMN
|
|
268
|
+
for (const actionName of actionNames) {
|
|
269
|
+
if (!ruleActionNames.has(actionName)) {
|
|
270
|
+
errors.push({
|
|
271
|
+
code: 'DT_MISSING_ACTION_COLUMN',
|
|
272
|
+
message: `Action "${actionName}" is declared but has no corresponding column in the rules table`,
|
|
273
|
+
severity: 'warning',
|
|
274
|
+
location: { decisionTable: dt.name, action: actionName },
|
|
275
|
+
suggestion: `Add a "→ ${actionName}" column to the rules table`,
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
// DT_DUPLICATE_RULE
|
|
280
|
+
for (let i = 0; i < dt.rules.length; i++) {
|
|
281
|
+
for (let j = i + 1; j < dt.rules.length; j++) {
|
|
282
|
+
const rule1 = dt.rules[i];
|
|
283
|
+
const rule2 = dt.rules[j];
|
|
284
|
+
// Check if conditions are identical
|
|
285
|
+
let identical = true;
|
|
286
|
+
if (rule1.conditions.size !== rule2.conditions.size) {
|
|
287
|
+
identical = false;
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
for (const [name, cell1] of rule1.conditions) {
|
|
291
|
+
const cell2 = rule2.conditions.get(name);
|
|
292
|
+
if (!cell2 || !cellsEqual(cell1, cell2)) {
|
|
293
|
+
identical = false;
|
|
294
|
+
break;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
if (identical) {
|
|
299
|
+
const rule1Num = rule1.number ?? i + 1;
|
|
300
|
+
const rule2Num = rule2.number ?? j + 1;
|
|
301
|
+
errors.push({
|
|
302
|
+
code: 'DT_DUPLICATE_RULE',
|
|
303
|
+
message: `Rule ${rule1Num} and Rule ${rule2Num} have identical condition patterns`,
|
|
304
|
+
severity: 'warning',
|
|
305
|
+
location: { decisionTable: dt.name, rule: rule2Num },
|
|
306
|
+
suggestion: `Remove or modify one of the duplicate rules`,
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return errors;
|
|
312
|
+
}
|
|
313
|
+
function cellsEqual(cell1, cell2) {
|
|
314
|
+
if (cell1.kind !== cell2.kind)
|
|
315
|
+
return false;
|
|
316
|
+
// Now cell1 and cell2 have the same kind, narrow both
|
|
317
|
+
const kind = cell1.kind;
|
|
318
|
+
if (kind === 'any')
|
|
319
|
+
return true;
|
|
320
|
+
if (kind === 'exact') {
|
|
321
|
+
return cell1.value === cell2.value;
|
|
322
|
+
}
|
|
323
|
+
if (kind === 'negated') {
|
|
324
|
+
return cell1.value === cell2.value;
|
|
325
|
+
}
|
|
326
|
+
if (kind === 'set') {
|
|
327
|
+
const s1 = cell1;
|
|
328
|
+
const s2 = cell2;
|
|
329
|
+
return s1.values.length === s2.values.length && s1.values.every(v => s2.values.includes(v));
|
|
330
|
+
}
|
|
331
|
+
return true;
|
|
332
|
+
}
|
|
333
|
+
// ============================================================
|
|
334
|
+
// Completeness Check
|
|
335
|
+
// ============================================================
|
|
336
|
+
function checkCompleteness(dt) {
|
|
337
|
+
const errors = [];
|
|
338
|
+
if (dt.conditions.length === 0)
|
|
339
|
+
return errors; // Already reported in structural
|
|
340
|
+
// int_range conditions cannot be exhaustively enumerated — skip completeness check
|
|
341
|
+
if (dt.conditions.some(c => c.type === 'int_range')) {
|
|
342
|
+
errors.push({
|
|
343
|
+
code: 'DT_COMPLETENESS_SKIPPED',
|
|
344
|
+
message: 'Completeness check skipped: int_range conditions cannot be exhaustively enumerated',
|
|
345
|
+
severity: 'warning',
|
|
346
|
+
location: { decisionTable: dt.name },
|
|
347
|
+
suggestion: 'Manually verify that all numeric ranges are covered without gaps',
|
|
348
|
+
});
|
|
349
|
+
return errors;
|
|
350
|
+
}
|
|
351
|
+
// Calculate total combinations for enum/bool conditions
|
|
352
|
+
let totalCombinations = 1;
|
|
353
|
+
for (const cond of dt.conditions) {
|
|
354
|
+
const values = getConditionValues(cond);
|
|
355
|
+
if (values.length === 0) {
|
|
356
|
+
totalCombinations = Infinity;
|
|
357
|
+
break;
|
|
358
|
+
}
|
|
359
|
+
totalCombinations *= values.length;
|
|
360
|
+
if (totalCombinations > 4096)
|
|
361
|
+
break;
|
|
362
|
+
}
|
|
363
|
+
if (totalCombinations > 4096) {
|
|
364
|
+
errors.push({
|
|
365
|
+
code: 'DT_COMPLETENESS_SKIPPED',
|
|
366
|
+
message: `Completeness check skipped: ${totalCombinations} combinations exceed limit of 4096`,
|
|
367
|
+
severity: 'warning',
|
|
368
|
+
location: { decisionTable: dt.name },
|
|
369
|
+
suggestion: 'Consider simplifying conditions or using wildcards to reduce combination count',
|
|
370
|
+
});
|
|
371
|
+
return errors;
|
|
372
|
+
}
|
|
373
|
+
// Generate all combinations and check coverage
|
|
374
|
+
const combinations = generateCombinations(dt.conditions);
|
|
375
|
+
const actionNames = dt.actions.map(a => a.name);
|
|
376
|
+
for (const combo of combinations) {
|
|
377
|
+
const matchingRules = findMatchingRules(dt, combo);
|
|
378
|
+
if (matchingRules.length === 0) {
|
|
379
|
+
const comboDesc = [...combo.entries()].map(([k, v]) => `${k}=${v}`).join(', ');
|
|
380
|
+
errors.push({
|
|
381
|
+
code: 'DT_INCOMPLETE',
|
|
382
|
+
message: `Missing coverage for: ${comboDesc}`,
|
|
383
|
+
severity: 'error',
|
|
384
|
+
location: { decisionTable: dt.name },
|
|
385
|
+
suggestion: `Add a rule to cover this condition combination`,
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
return errors;
|
|
390
|
+
}
|
|
391
|
+
// ============================================================
|
|
392
|
+
// Consistency Check
|
|
393
|
+
// ============================================================
|
|
394
|
+
function checkConsistency(dt) {
|
|
395
|
+
const errors = [];
|
|
396
|
+
const isAllMatch = dt.policy === 'all-match';
|
|
397
|
+
for (let i = 0; i < dt.rules.length; i++) {
|
|
398
|
+
for (let j = i + 1; j < dt.rules.length; j++) {
|
|
399
|
+
const rule1 = dt.rules[i];
|
|
400
|
+
const rule2 = dt.rules[j];
|
|
401
|
+
// Check if rules overlap
|
|
402
|
+
if (!rulesOverlap(rule1, rule2, dt.conditions))
|
|
403
|
+
continue;
|
|
404
|
+
// Rules overlap - check if actions agree
|
|
405
|
+
const actionNames = dt.actions.map(a => a.name);
|
|
406
|
+
const actionsAgree = actionsMatch(rule1, rule2, actionNames);
|
|
407
|
+
if (!actionsAgree) {
|
|
408
|
+
const rule1Num = rule1.number ?? i + 1;
|
|
409
|
+
const rule2Num = rule2.number ?? j + 1;
|
|
410
|
+
const severity = isAllMatch ? 'error' : 'warning';
|
|
411
|
+
const code = isAllMatch ? 'DT_INCONSISTENT' : 'DT_INCONSISTENT';
|
|
412
|
+
const overlappingConditions = [];
|
|
413
|
+
for (const cond of dt.conditions) {
|
|
414
|
+
const cell1 = rule1.conditions.get(cond.name);
|
|
415
|
+
const cell2 = rule2.conditions.get(cond.name);
|
|
416
|
+
if (cell1 && cell2 && cellsIntersect(cell1, cell2)) {
|
|
417
|
+
overlappingConditions.push(cond.name);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
errors.push({
|
|
421
|
+
code,
|
|
422
|
+
message: `Rules ${rule1Num} and ${rule2Num} can match the same input but produce different results`,
|
|
423
|
+
severity,
|
|
424
|
+
location: { decisionTable: dt.name, rule: rule2Num },
|
|
425
|
+
suggestion: `For ${isAllMatch ? 'all-match' : 'first-match'} policy: review overlapping rules on conditions: ${overlappingConditions.join(', ')}`,
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
return errors;
|
|
431
|
+
}
|
|
432
|
+
// ============================================================
|
|
433
|
+
// Redundancy Check
|
|
434
|
+
// ============================================================
|
|
435
|
+
function checkRedundancy(dt) {
|
|
436
|
+
const errors = [];
|
|
437
|
+
// For each rule, check if it's fully covered by earlier rules with same actions
|
|
438
|
+
for (let ruleIdx = 1; ruleIdx < dt.rules.length; ruleIdx++) {
|
|
439
|
+
const rule = dt.rules[ruleIdx];
|
|
440
|
+
const ruleNum = rule.number ?? ruleIdx + 1;
|
|
441
|
+
const actionNames = dt.actions.map(a => a.name);
|
|
442
|
+
// A rule is redundant if at least one earlier rule overlaps it AND all overlapping
|
|
443
|
+
// earlier rules produce the same actions (meaning first-match would always hit them first
|
|
444
|
+
// with an identical result, so this rule can never change the outcome).
|
|
445
|
+
let hasOverlappingPredecessor = false;
|
|
446
|
+
let allOverlappingHaveSameActions = true;
|
|
447
|
+
for (let prevIdx = 0; prevIdx < ruleIdx; prevIdx++) {
|
|
448
|
+
const prevRule = dt.rules[prevIdx];
|
|
449
|
+
if (!rulesOverlap(prevRule, rule, dt.conditions))
|
|
450
|
+
continue;
|
|
451
|
+
hasOverlappingPredecessor = true;
|
|
452
|
+
if (!actionsMatch(prevRule, rule, actionNames)) {
|
|
453
|
+
allOverlappingHaveSameActions = false;
|
|
454
|
+
break;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
if (hasOverlappingPredecessor && allOverlappingHaveSameActions) {
|
|
458
|
+
errors.push({
|
|
459
|
+
code: 'DT_REDUNDANT',
|
|
460
|
+
message: `Rule ${ruleNum} is redundant — earlier rules cover all its cases with the same actions`,
|
|
461
|
+
severity: 'warning',
|
|
462
|
+
location: { decisionTable: dt.name, rule: ruleNum },
|
|
463
|
+
suggestion: `Remove this rule or make it more specific`,
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
return errors;
|
|
468
|
+
}
|
|
469
|
+
// ============================================================
|
|
470
|
+
// Main Verifier
|
|
471
|
+
// ============================================================
|
|
472
|
+
export function verifyDecisionTable(dt) {
|
|
473
|
+
const errors = [];
|
|
474
|
+
// Run structural checks first
|
|
475
|
+
errors.push(...checkStructural(dt));
|
|
476
|
+
// Run semantic checks (only if basic structure is valid)
|
|
477
|
+
const hasNoConditions = dt.conditions.length === 0;
|
|
478
|
+
const hasNoActions = dt.actions.length === 0;
|
|
479
|
+
if (!hasNoConditions && !hasNoActions) {
|
|
480
|
+
errors.push(...checkCompleteness(dt));
|
|
481
|
+
errors.push(...checkConsistency(dt));
|
|
482
|
+
errors.push(...checkRedundancy(dt));
|
|
483
|
+
}
|
|
484
|
+
return {
|
|
485
|
+
valid: !errors.some(e => e.severity === 'error'),
|
|
486
|
+
errors,
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
export function verifyDecisionTables(dts) {
|
|
490
|
+
const allErrors = [];
|
|
491
|
+
for (const dt of dts) {
|
|
492
|
+
const result = verifyDecisionTable(dt);
|
|
493
|
+
allErrors.push(...result.errors);
|
|
494
|
+
}
|
|
495
|
+
return {
|
|
496
|
+
valid: !allErrors.some(e => e.severity === 'error'),
|
|
497
|
+
errors: allErrors,
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
// ============================================================
|
|
501
|
+
// Co-location Alignment Check
|
|
502
|
+
// ============================================================
|
|
503
|
+
/**
|
|
504
|
+
* Check that every condition name and output name in a co-located decision table
|
|
505
|
+
* exists as a context field in the machine. When a DT and machine are in the same
|
|
506
|
+
* file, this contract allows action generation to produce fully-wired code.
|
|
507
|
+
*/
|
|
508
|
+
export function checkDTContextAlignment(dt, machine) {
|
|
509
|
+
const errors = [];
|
|
510
|
+
const contextNames = new Set(machine.context.map(f => f.name));
|
|
511
|
+
for (const cond of dt.conditions) {
|
|
512
|
+
if (!contextNames.has(cond.name)) {
|
|
513
|
+
errors.push({
|
|
514
|
+
code: 'DT_CONTEXT_MISMATCH',
|
|
515
|
+
message: `Decision table '${dt.name}' condition '${cond.name}' has no matching context field in machine '${machine.name}'`,
|
|
516
|
+
severity: 'error',
|
|
517
|
+
location: { decisionTable: dt.name, condition: cond.name },
|
|
518
|
+
suggestion: `Add '${cond.name}' to the ## context section, or rename the condition to match an existing context field`,
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
for (const action of dt.actions) {
|
|
523
|
+
if (!contextNames.has(action.name)) {
|
|
524
|
+
errors.push({
|
|
525
|
+
code: 'DT_CONTEXT_MISMATCH',
|
|
526
|
+
message: `Decision table '${dt.name}' output '${action.name}' has no matching context field in machine '${machine.name}'`,
|
|
527
|
+
severity: 'error',
|
|
528
|
+
location: { decisionTable: dt.name, action: action.name },
|
|
529
|
+
suggestion: `Add '${action.name}' to the ## context section, or rename the output to match an existing context field`,
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
return errors;
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* For a file with exactly one machine and one or more decision tables, verify
|
|
537
|
+
* that every DT condition and output name matches a machine context field.
|
|
538
|
+
* Multi-machine files are skipped (ambiguous ownership).
|
|
539
|
+
*/
|
|
540
|
+
export function checkFileContextAlignment(file) {
|
|
541
|
+
if (file.machines.length !== 1 || file.decisionTables.length === 0) {
|
|
542
|
+
return [];
|
|
543
|
+
}
|
|
544
|
+
const machine = file.machines[0];
|
|
545
|
+
const errors = [];
|
|
546
|
+
for (const dt of file.decisionTables) {
|
|
547
|
+
errors.push(...checkDTContextAlignment(dt, machine));
|
|
548
|
+
}
|
|
549
|
+
return errors;
|
|
550
|
+
}
|
|
551
|
+
// ============================================================
|
|
552
|
+
// Machine Integration Checks
|
|
553
|
+
// ============================================================
|
|
554
|
+
/**
|
|
555
|
+
* Get the enumerable values for a machine context field.
|
|
556
|
+
* Returns null for types that cannot be exhaustively enumerated (string, int, decimal).
|
|
557
|
+
* For enum fields, values are stored as a comma-separated defaultValue string.
|
|
558
|
+
*/
|
|
559
|
+
function getMachineFieldValues(field) {
|
|
560
|
+
if (field.type.kind === 'bool')
|
|
561
|
+
return ['true', 'false'];
|
|
562
|
+
if (field.type.kind === 'custom' && field.type.name === 'enum') {
|
|
563
|
+
if (!field.defaultValue)
|
|
564
|
+
return null;
|
|
565
|
+
const vals = field.defaultValue.split(',').map(v => v.trim()).filter(Boolean);
|
|
566
|
+
return vals.length > 0 ? vals : null;
|
|
567
|
+
}
|
|
568
|
+
return null;
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* Generate all combinations of condition values using machine context values as the
|
|
572
|
+
* input domain. Returns null if any condition cannot be enumerated or if the
|
|
573
|
+
* total combinations exceed the safety limit.
|
|
574
|
+
*/
|
|
575
|
+
function generateMachineContextCombinations(dt, contextMap) {
|
|
576
|
+
const domainPerCondition = [];
|
|
577
|
+
for (const cond of dt.conditions) {
|
|
578
|
+
const field = contextMap.get(cond.name);
|
|
579
|
+
if (!field)
|
|
580
|
+
return null; // Alignment not met — caller should have checked
|
|
581
|
+
const vals = getMachineFieldValues(field);
|
|
582
|
+
if (!vals)
|
|
583
|
+
return null; // Non-enumerable type
|
|
584
|
+
domainPerCondition.push(vals);
|
|
585
|
+
}
|
|
586
|
+
// Safety limit
|
|
587
|
+
let total = 1;
|
|
588
|
+
for (const vals of domainPerCondition)
|
|
589
|
+
total *= vals.length;
|
|
590
|
+
if (total > 4096)
|
|
591
|
+
return null;
|
|
592
|
+
// Cartesian product
|
|
593
|
+
const combos = [];
|
|
594
|
+
function cartesian(idx, current) {
|
|
595
|
+
if (idx === dt.conditions.length) {
|
|
596
|
+
combos.push(new Map(current));
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
const condName = dt.conditions[idx].name;
|
|
600
|
+
for (const val of domainPerCondition[idx]) {
|
|
601
|
+
current.set(condName, val);
|
|
602
|
+
cartesian(idx + 1, current);
|
|
603
|
+
current.delete(condName);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
cartesian(0, new Map());
|
|
607
|
+
return combos;
|
|
608
|
+
}
|
|
609
|
+
/**
|
|
610
|
+
* DT_COVERAGE_GAP: Decision table must cover all input combinations the machine
|
|
611
|
+
* context can actually produce. Uses machine enum/bool values as the authoritative
|
|
612
|
+
* domain — stricter than DT_INCOMPLETE which only checks DT-declared values.
|
|
613
|
+
*/
|
|
614
|
+
function checkDTCoverageGap(dt, contextMap) {
|
|
615
|
+
const errors = [];
|
|
616
|
+
const combos = generateMachineContextCombinations(dt, contextMap);
|
|
617
|
+
if (!combos)
|
|
618
|
+
return errors; // Non-enumerable conditions or too many combinations
|
|
619
|
+
for (const combo of combos) {
|
|
620
|
+
const matched = dt.rules.some(rule => ruleMatchesInput(rule, dt.conditions, combo));
|
|
621
|
+
if (!matched) {
|
|
622
|
+
const comboDesc = [...combo.entries()].map(([k, v]) => `${k}=${v}`).join(', ');
|
|
623
|
+
errors.push({
|
|
624
|
+
code: 'DT_COVERAGE_GAP',
|
|
625
|
+
message: `Decision table '${dt.name}' has no rule for machine context combination: ${comboDesc}`,
|
|
626
|
+
severity: 'error',
|
|
627
|
+
location: { decisionTable: dt.name },
|
|
628
|
+
suggestion: `Add a rule covering this combination, or add a catch-all row using '-' wildcards`,
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
return errors;
|
|
633
|
+
}
|
|
634
|
+
/**
|
|
635
|
+
* Recursively collect all equality comparisons from a guard expression.
|
|
636
|
+
* Returns tuples of (fieldName, op, comparedValue) for any `ctx.X op Y` node.
|
|
637
|
+
*/
|
|
638
|
+
function collectFieldComparisons(expr) {
|
|
639
|
+
if (expr.kind === 'compare') {
|
|
640
|
+
// Only handle ctx.fieldName comparisons
|
|
641
|
+
if (expr.left.path.length === 2 && expr.left.path[0] === 'ctx') {
|
|
642
|
+
return [{ field: expr.left.path[1], op: expr.op, value: String(expr.right.value) }];
|
|
643
|
+
}
|
|
644
|
+
return [];
|
|
645
|
+
}
|
|
646
|
+
if (expr.kind === 'not')
|
|
647
|
+
return collectFieldComparisons(expr.expr);
|
|
648
|
+
if (expr.kind === 'and' || expr.kind === 'or') {
|
|
649
|
+
return [...collectFieldComparisons(expr.left), ...collectFieldComparisons(expr.right)];
|
|
650
|
+
}
|
|
651
|
+
return [];
|
|
652
|
+
}
|
|
653
|
+
/**
|
|
654
|
+
* Compute the set of guard names that test a DT output field against a value
|
|
655
|
+
* the DT never produces. These guards are always false after the DT action fires.
|
|
656
|
+
*/
|
|
657
|
+
function computeDeadGuardNames(dt, machine) {
|
|
658
|
+
const outputDomain = new Map();
|
|
659
|
+
for (const action of dt.actions) {
|
|
660
|
+
outputDomain.set(action.name, new Set());
|
|
661
|
+
}
|
|
662
|
+
for (const rule of dt.rules) {
|
|
663
|
+
for (const [name, value] of rule.actions) {
|
|
664
|
+
outputDomain.get(name)?.add(value);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
const outputFields = new Set(dt.actions.map(a => a.name));
|
|
668
|
+
const dead = new Set();
|
|
669
|
+
for (const guardDef of machine.guards) {
|
|
670
|
+
const comparisons = collectFieldComparisons(guardDef.expression);
|
|
671
|
+
for (const { field, op, value } of comparisons) {
|
|
672
|
+
if (!outputFields.has(field))
|
|
673
|
+
continue;
|
|
674
|
+
if (op !== 'eq')
|
|
675
|
+
continue;
|
|
676
|
+
const possible = outputDomain.get(field);
|
|
677
|
+
if (!possible.has(value)) {
|
|
678
|
+
dead.add(guardDef.name);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
return dead;
|
|
683
|
+
}
|
|
684
|
+
/**
|
|
685
|
+
* DT_GUARD_DEAD: A guard that compares a DT output field against a value the DT
|
|
686
|
+
* never produces is a dead guard — it can never be true immediately after the
|
|
687
|
+
* DT action fires. Reported as a warning since another action might set the field.
|
|
688
|
+
*/
|
|
689
|
+
function checkDTGuardDead(dt, machine) {
|
|
690
|
+
const errors = [];
|
|
691
|
+
// Build output domain: field → set of values the DT can produce
|
|
692
|
+
const outputDomain = new Map();
|
|
693
|
+
for (const action of dt.actions) {
|
|
694
|
+
outputDomain.set(action.name, new Set());
|
|
695
|
+
}
|
|
696
|
+
for (const rule of dt.rules) {
|
|
697
|
+
for (const [name, value] of rule.actions) {
|
|
698
|
+
outputDomain.get(name)?.add(value);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
const outputFields = new Set(dt.actions.map(a => a.name));
|
|
702
|
+
for (const guardDef of machine.guards) {
|
|
703
|
+
const comparisons = collectFieldComparisons(guardDef.expression);
|
|
704
|
+
for (const { field, op, value } of comparisons) {
|
|
705
|
+
if (!outputFields.has(field))
|
|
706
|
+
continue; // Not a DT output field
|
|
707
|
+
if (op !== 'eq')
|
|
708
|
+
continue; // Only equality checks are conclusive
|
|
709
|
+
const possible = outputDomain.get(field);
|
|
710
|
+
if (!possible.has(value)) {
|
|
711
|
+
const possibleList = [...possible].join(', ') || '(none)';
|
|
712
|
+
errors.push({
|
|
713
|
+
code: 'DT_GUARD_DEAD',
|
|
714
|
+
message: `Guard '${guardDef.name}' tests '${field} == ${value}' but '${dt.name}' never outputs '${value}' for '${field}' (possible: ${possibleList})`,
|
|
715
|
+
severity: 'warning',
|
|
716
|
+
location: { decisionTable: dt.name, condition: field },
|
|
717
|
+
suggestion: `Update '${dt.name}' to produce '${value}' for '${field}', or remove this guard`,
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
return errors;
|
|
723
|
+
}
|
|
724
|
+
/**
|
|
725
|
+
* BFS from initial state, optionally skipping transitions guarded by dead guards.
|
|
726
|
+
* A non-negated transition guarded by a name in `deadGuards` is skipped (never fires).
|
|
727
|
+
* A negated dead guard (!dead) is NOT skipped — negation of a dead guard is always true.
|
|
728
|
+
*/
|
|
729
|
+
function bfsReachableWithDeadGuards(machine, deadGuards) {
|
|
730
|
+
const initial = machine.states.find(s => s.isInitial);
|
|
731
|
+
if (!initial)
|
|
732
|
+
return new Set();
|
|
733
|
+
const visited = new Set();
|
|
734
|
+
const queue = [initial.name];
|
|
735
|
+
while (queue.length > 0) {
|
|
736
|
+
const state = queue.shift();
|
|
737
|
+
if (visited.has(state))
|
|
738
|
+
continue;
|
|
739
|
+
visited.add(state);
|
|
740
|
+
for (const t of machine.transitions) {
|
|
741
|
+
if (t.source !== state)
|
|
742
|
+
continue;
|
|
743
|
+
// A non-negated dead guard means the transition can never fire
|
|
744
|
+
if (t.guard && !t.guard.negated && deadGuards.has(t.guard.name))
|
|
745
|
+
continue;
|
|
746
|
+
if (!visited.has(t.target))
|
|
747
|
+
queue.push(t.target);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
return visited;
|
|
751
|
+
}
|
|
752
|
+
/**
|
|
753
|
+
* DT_UNREACHABLE_STATE: A state that is graph-reachable but only accessible via
|
|
754
|
+
* transitions guarded by dead guards — it can never be entered given DT outputs.
|
|
755
|
+
* Reported as a warning (structural reachability is preserved; the constraint is semantic).
|
|
756
|
+
*/
|
|
757
|
+
function checkDTDeadGuardReachability(dt, machine) {
|
|
758
|
+
const deadGuards = computeDeadGuardNames(dt, machine);
|
|
759
|
+
if (deadGuards.size === 0)
|
|
760
|
+
return [];
|
|
761
|
+
const plainReachable = bfsReachableWithDeadGuards(machine, new Set());
|
|
762
|
+
const dtReachable = bfsReachableWithDeadGuards(machine, deadGuards);
|
|
763
|
+
const errors = [];
|
|
764
|
+
const deadList = [...deadGuards].join(', ');
|
|
765
|
+
for (const state of machine.states) {
|
|
766
|
+
if (plainReachable.has(state.name) && !dtReachable.has(state.name)) {
|
|
767
|
+
errors.push({
|
|
768
|
+
code: 'DT_UNREACHABLE_STATE',
|
|
769
|
+
message: `State '${state.name}' is unreachable given '${dt.name}' output constraints — all entry paths are gated by dead guards (${deadList})`,
|
|
770
|
+
severity: 'warning',
|
|
771
|
+
location: { state: state.name, decisionTable: dt.name },
|
|
772
|
+
suggestion: `Update '${dt.name}' to produce values that satisfy the guards leading to '${state.name}', or revise the guard expressions`,
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
return errors;
|
|
777
|
+
}
|
|
778
|
+
/**
|
|
779
|
+
* Check DT integration with the machine: coverage gap, dead guards, and
|
|
780
|
+
* DT-constrained reachability. Only runs when exactly one machine is present
|
|
781
|
+
* and the DT is fully aligned. Multi-machine files are skipped (ambiguous ownership).
|
|
782
|
+
*/
|
|
783
|
+
export function checkDTMachineIntegration(file) {
|
|
784
|
+
if (file.machines.length !== 1 || file.decisionTables.length === 0) {
|
|
785
|
+
return [];
|
|
786
|
+
}
|
|
787
|
+
const machine = file.machines[0];
|
|
788
|
+
const contextMap = new Map(machine.context.map(f => [f.name, f]));
|
|
789
|
+
const errors = [];
|
|
790
|
+
for (const dt of file.decisionTables) {
|
|
791
|
+
// Only verify DTs that are fully aligned with machine context
|
|
792
|
+
const allAligned = [...dt.conditions, ...dt.actions].every(item => contextMap.has(item.name));
|
|
793
|
+
if (!allAligned)
|
|
794
|
+
continue;
|
|
795
|
+
errors.push(...checkDTCoverageGap(dt, contextMap));
|
|
796
|
+
errors.push(...checkDTGuardDead(dt, machine));
|
|
797
|
+
errors.push(...checkDTDeadGuardReachability(dt, machine));
|
|
798
|
+
}
|
|
799
|
+
return errors;
|
|
800
|
+
}
|
|
801
|
+
/**
|
|
802
|
+
* Compute the merged output domain across all aligned DTs in a single-machine file.
|
|
803
|
+
* Returns a map from DT output field name → set of values the DT(s) can produce.
|
|
804
|
+
* Used by the properties checker to prune guard-protected transitions that are
|
|
805
|
+
* semantically impossible given DT output constraints.
|
|
806
|
+
* Returns undefined if no aligned DTs are found.
|
|
807
|
+
*/
|
|
808
|
+
export function computeAlignedDTOutputDomain(file) {
|
|
809
|
+
if (file.machines.length !== 1 || file.decisionTables.length === 0)
|
|
810
|
+
return undefined;
|
|
811
|
+
const machine = file.machines[0];
|
|
812
|
+
const contextMap = new Map(machine.context.map(f => [f.name, f]));
|
|
813
|
+
const domain = new Map();
|
|
814
|
+
for (const dt of file.decisionTables) {
|
|
815
|
+
const allAligned = [...dt.conditions, ...dt.actions].every(item => contextMap.has(item.name));
|
|
816
|
+
if (!allAligned)
|
|
817
|
+
continue;
|
|
818
|
+
for (const actionDef of dt.actions) {
|
|
819
|
+
if (!domain.has(actionDef.name))
|
|
820
|
+
domain.set(actionDef.name, new Set());
|
|
821
|
+
for (const rule of dt.rules) {
|
|
822
|
+
const val = rule.actions.get(actionDef.name);
|
|
823
|
+
if (val !== undefined)
|
|
824
|
+
domain.get(actionDef.name).add(val);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
return domain.size > 0 ? domain : undefined;
|
|
829
|
+
}
|
|
830
|
+
//# sourceMappingURL=dt-verifier.js.map
|