@orcalang/orca-lang 0.1.18 → 0.1.19
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 +23 -0
- package/dist/compiler/dt-compiler.d.ts.map +1 -0
- package/dist/compiler/dt-compiler.js +183 -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/parser/ast.d.ts +2 -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 +29 -4
- package/dist/parser/markdown-parser.js.map +1 -1
- package/dist/skills.d.ts +49 -1
- package/dist/skills.d.ts.map +1 -1
- package/dist/skills.js +223 -0
- 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 +5 -0
- package/dist/verifier/dt-verifier.d.ts.map +1 -0
- package/dist/verifier/dt-verifier.js +499 -0
- package/dist/verifier/dt-verifier.js.map +1 -0
- 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 +232 -0
- package/src/health-check.ts +273 -0
- package/src/parser/ast.ts +3 -0
- package/src/parser/dt-ast.ts +40 -0
- package/src/parser/dt-parser.ts +289 -0
- package/src/parser/markdown-parser.ts +32 -5
- package/src/skills.ts +274 -1
- package/src/tools.ts +53 -0
- package/src/verifier/dt-verifier.ts +562 -0
- package/src/verifier/types.ts +4 -0
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
// Decision Table Parser
|
|
2
|
+
// Parses ## conditions, ## actions, ## rules sections from markdown elements
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
ConditionDef, ConditionType, ActionOutputDef, ActionType,
|
|
6
|
+
CellValue, Rule, DecisionTableDef,
|
|
7
|
+
} from './dt-ast.js';
|
|
8
|
+
|
|
9
|
+
interface MdHeading { kind: 'heading'; level: number; text: string; line: number }
|
|
10
|
+
interface MdTable { kind: 'table'; headers: string[]; rows: string[][]; line: number }
|
|
11
|
+
interface MdBulletList { kind: 'bullets'; items: string[]; line: number }
|
|
12
|
+
interface MdBlockquote { kind: 'blockquote'; text: string; line: number }
|
|
13
|
+
interface MdParagraph { kind: 'paragraph'; text: string; line: number }
|
|
14
|
+
interface MdSeparator { kind: 'separator'; line: number }
|
|
15
|
+
|
|
16
|
+
type MdElement = MdHeading | MdTable | MdBulletList | MdBlockquote | MdParagraph | MdSeparator;
|
|
17
|
+
|
|
18
|
+
function findColumnIndex(headers: string[], name: string): number {
|
|
19
|
+
return headers.findIndex(h => h.toLowerCase() === name.toLowerCase());
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// --- Cell Value Parsing ---
|
|
23
|
+
|
|
24
|
+
function parseCellValue(text: string | undefined): CellValue {
|
|
25
|
+
if (!text || text.trim() === '' || text.trim() === '-') {
|
|
26
|
+
return { kind: 'any' };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const trimmed = text.trim();
|
|
30
|
+
|
|
31
|
+
// Negated: !value (bare '!' with no value falls through to exact match)
|
|
32
|
+
if (trimmed.startsWith('!')) {
|
|
33
|
+
const negatedValue = trimmed.slice(1);
|
|
34
|
+
if (negatedValue) {
|
|
35
|
+
return { kind: 'negated', value: negatedValue };
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Set: a,b,c
|
|
40
|
+
if (trimmed.includes(',')) {
|
|
41
|
+
return { kind: 'set', values: trimmed.split(',').map(v => v.trim()).filter(Boolean) };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Exact match
|
|
45
|
+
return { kind: 'exact', value: trimmed };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// --- Section Parsers ---
|
|
49
|
+
|
|
50
|
+
function parseConditionsTable(table: MdTable): ConditionDef[] {
|
|
51
|
+
const nameIdx = findColumnIndex(table.headers, 'name');
|
|
52
|
+
const typeIdx = findColumnIndex(table.headers, 'type');
|
|
53
|
+
const valuesIdx = findColumnIndex(table.headers, 'values');
|
|
54
|
+
|
|
55
|
+
return table.rows.map(row => {
|
|
56
|
+
const name = (nameIdx >= 0 ? row[nameIdx] : '') || '';
|
|
57
|
+
const typeStr = (typeIdx >= 0 ? row[typeIdx] : '') || 'string';
|
|
58
|
+
const valuesStr = (valuesIdx >= 0 ? row[valuesIdx] : '') || '';
|
|
59
|
+
|
|
60
|
+
const type: ConditionType = typeStr.trim() as ConditionType;
|
|
61
|
+
|
|
62
|
+
// Bool conditions auto-populate values ['true', 'false'] when Values column is empty
|
|
63
|
+
let values: string[];
|
|
64
|
+
let range: { min: number; max: number } | undefined;
|
|
65
|
+
|
|
66
|
+
if (type === 'bool') {
|
|
67
|
+
values = valuesStr.trim() ? valuesStr.split(',').map(v => v.trim()) : ['true', 'false'];
|
|
68
|
+
} else if (type === 'int_range') {
|
|
69
|
+
// Parse min..max format
|
|
70
|
+
const rangeMatch = valuesStr.match(/(\d+)\s*\.\.\s*(\d+)/);
|
|
71
|
+
if (rangeMatch) {
|
|
72
|
+
range = { min: parseInt(rangeMatch[1], 10), max: parseInt(rangeMatch[2], 10) };
|
|
73
|
+
values = [];
|
|
74
|
+
} else {
|
|
75
|
+
values = [];
|
|
76
|
+
}
|
|
77
|
+
} else {
|
|
78
|
+
// enum or string - comma-separated values
|
|
79
|
+
values = valuesStr ? valuesStr.split(',').map(v => v.trim()).filter(Boolean) : [];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const condition: ConditionDef = { name: name.trim(), type, values };
|
|
83
|
+
if (range) condition.range = range;
|
|
84
|
+
|
|
85
|
+
return condition;
|
|
86
|
+
}).filter(c => c.name !== '');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function parseActionsTable(table: MdTable): ActionOutputDef[] {
|
|
90
|
+
const nameIdx = findColumnIndex(table.headers, 'name');
|
|
91
|
+
const typeIdx = findColumnIndex(table.headers, 'type');
|
|
92
|
+
const descIdx = findColumnIndex(table.headers, 'description');
|
|
93
|
+
const valuesIdx = findColumnIndex(table.headers, 'values');
|
|
94
|
+
|
|
95
|
+
return table.rows.map(row => {
|
|
96
|
+
const name = (nameIdx >= 0 ? row[nameIdx] : '') || '';
|
|
97
|
+
const typeStr = (typeIdx >= 0 ? row[typeIdx] : '') || 'string';
|
|
98
|
+
const desc = descIdx >= 0 ? (row[descIdx] || '').trim() : '';
|
|
99
|
+
const valuesStr = valuesIdx >= 0 ? (row[valuesIdx] || '').trim() : '';
|
|
100
|
+
|
|
101
|
+
const type: ActionType = typeStr.trim() as ActionType;
|
|
102
|
+
const action: ActionOutputDef = {
|
|
103
|
+
name: name.trim(),
|
|
104
|
+
type,
|
|
105
|
+
description: desc || undefined,
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
if (valuesStr && type === 'enum') {
|
|
109
|
+
action.values = valuesStr.split(',').map(v => v.trim()).filter(Boolean);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return action;
|
|
113
|
+
}).filter(a => a.name !== '');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function parseRulesTable(
|
|
117
|
+
table: MdTable,
|
|
118
|
+
conditionNames: Set<string>,
|
|
119
|
+
actionNames: Set<string>
|
|
120
|
+
): { rules: Rule[]; warnings: string[] } {
|
|
121
|
+
const warnings: string[] = [];
|
|
122
|
+
const rules: Rule[] = [];
|
|
123
|
+
|
|
124
|
+
// Determine column types from headers
|
|
125
|
+
const columnTypes: Array<{ name: string; type: 'condition' | 'action' | 'skip' }> = [];
|
|
126
|
+
|
|
127
|
+
for (const header of table.headers) {
|
|
128
|
+
const trimmed = header.trim();
|
|
129
|
+
const lower = trimmed.toLowerCase();
|
|
130
|
+
|
|
131
|
+
if (lower === '#') {
|
|
132
|
+
columnTypes.push({ name: '#', type: 'skip' });
|
|
133
|
+
} else if (trimmed.startsWith('→ ') || trimmed.startsWith('-> ')) {
|
|
134
|
+
// Action column - strip prefix
|
|
135
|
+
const actionName = trimmed.replace(/^→\s*/, '').replace(/^->\s*/, '');
|
|
136
|
+
columnTypes.push({ name: actionName, type: 'action' });
|
|
137
|
+
|
|
138
|
+
if (!actionNames.has(actionName)) {
|
|
139
|
+
warnings.push(`Unknown action column: "${actionName}" (not declared in ## actions)`);
|
|
140
|
+
}
|
|
141
|
+
} else {
|
|
142
|
+
// Condition column
|
|
143
|
+
columnTypes.push({ name: trimmed, type: 'condition' });
|
|
144
|
+
|
|
145
|
+
if (!conditionNames.has(trimmed)) {
|
|
146
|
+
warnings.push(`Unknown condition column: "${trimmed}" (not declared in ## conditions)`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Parse each row
|
|
152
|
+
for (let rowIdx = 0; rowIdx < table.rows.length; rowIdx++) {
|
|
153
|
+
const row = table.rows[rowIdx];
|
|
154
|
+
const rule: Rule = {
|
|
155
|
+
conditions: new Map(),
|
|
156
|
+
actions: new Map(),
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
for (let colIdx = 0; colIdx < columnTypes.length && colIdx < row.length; colIdx++) {
|
|
160
|
+
const col = columnTypes[colIdx];
|
|
161
|
+
const cell = row[colIdx];
|
|
162
|
+
|
|
163
|
+
if (col.type === 'skip') {
|
|
164
|
+
// Rule numbering column - parse as optional number
|
|
165
|
+
const num = parseInt(cell?.trim() || '', 10);
|
|
166
|
+
if (!isNaN(num)) {
|
|
167
|
+
rule.number = num;
|
|
168
|
+
}
|
|
169
|
+
} else if (col.type === 'condition') {
|
|
170
|
+
const cellValue = parseCellValue(cell);
|
|
171
|
+
rule.conditions.set(col.name, cellValue);
|
|
172
|
+
} else if (col.type === 'action') {
|
|
173
|
+
const value = cell?.trim() || '';
|
|
174
|
+
if (value) {
|
|
175
|
+
rule.actions.set(col.name, value);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
rules.push(rule);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return { rules, warnings };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// --- Main Decision Table Parser ---
|
|
187
|
+
|
|
188
|
+
export function parseDecisionTable(elements: MdElement[]): { decisionTable: DecisionTableDef; warnings: string[] } {
|
|
189
|
+
let tableName = '';
|
|
190
|
+
let description: string | undefined;
|
|
191
|
+
let conditions: ConditionDef[] = [];
|
|
192
|
+
let actions: ActionOutputDef[] = [];
|
|
193
|
+
let rules: Rule[] = [];
|
|
194
|
+
let policy: 'first-match' | 'all-match' = 'first-match';
|
|
195
|
+
const warnings: string[] = [];
|
|
196
|
+
|
|
197
|
+
// Track current section
|
|
198
|
+
let currentSection: 'conditions' | 'actions' | 'rules' | null = null;
|
|
199
|
+
let currentTable: MdTable | null = null;
|
|
200
|
+
|
|
201
|
+
// Collect description from paragraphs before first ## heading
|
|
202
|
+
const descriptionParts: string[] = [];
|
|
203
|
+
|
|
204
|
+
for (let i = 0; i < elements.length; i++) {
|
|
205
|
+
const el = elements[i];
|
|
206
|
+
|
|
207
|
+
if (el.kind === 'heading') {
|
|
208
|
+
if (el.level === 1 && el.text.startsWith('decision_table ')) {
|
|
209
|
+
tableName = el.text.slice(15).trim();
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (el.level === 2) {
|
|
214
|
+
const sectionName = el.text.toLowerCase();
|
|
215
|
+
|
|
216
|
+
// Before processing first section, capture accumulated description
|
|
217
|
+
if (currentSection === null && descriptionParts.length > 0) {
|
|
218
|
+
description = descriptionParts.join(' ').trim();
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (sectionName === 'conditions') {
|
|
222
|
+
currentSection = 'conditions';
|
|
223
|
+
} else if (sectionName === 'actions') {
|
|
224
|
+
currentSection = 'actions';
|
|
225
|
+
} else if (sectionName === 'rules') {
|
|
226
|
+
currentSection = 'rules';
|
|
227
|
+
} else if (sectionName === 'metadata') {
|
|
228
|
+
currentSection = null; // metadata doesn't have a table
|
|
229
|
+
} else {
|
|
230
|
+
currentSection = null;
|
|
231
|
+
}
|
|
232
|
+
currentTable = null;
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Reset section on unknown headings
|
|
237
|
+
currentSection = null;
|
|
238
|
+
currentTable = null;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Accumulate description from paragraphs and blockquotes before first section
|
|
242
|
+
if (currentSection === null) {
|
|
243
|
+
if (el.kind === 'paragraph') {
|
|
244
|
+
descriptionParts.push(el.text);
|
|
245
|
+
} else if (el.kind === 'blockquote') {
|
|
246
|
+
descriptionParts.push(el.text);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Capture tables for current section
|
|
251
|
+
if (el.kind === 'table' && currentSection !== null) {
|
|
252
|
+
if (currentSection === 'conditions') {
|
|
253
|
+
conditions = parseConditionsTable(el);
|
|
254
|
+
} else if (currentSection === 'actions') {
|
|
255
|
+
actions = parseActionsTable(el);
|
|
256
|
+
} else if (currentSection === 'rules') {
|
|
257
|
+
const result = parseRulesTable(el, new Set(conditions.map(c => c.name)), new Set(actions.map(a => a.name)));
|
|
258
|
+
rules = result.rules;
|
|
259
|
+
warnings.push(...result.warnings);
|
|
260
|
+
}
|
|
261
|
+
currentTable = el;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const decisionTable: DecisionTableDef = {
|
|
266
|
+
name: tableName,
|
|
267
|
+
description,
|
|
268
|
+
conditions,
|
|
269
|
+
actions,
|
|
270
|
+
rules,
|
|
271
|
+
policy,
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
return { decisionTable, warnings };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Parse a chunk of markdown elements that represents a single decision table
|
|
278
|
+
// Returns null if the chunk doesn't start with # decision_table
|
|
279
|
+
export function parseDecisionTableChunk(chunk: MdElement[]): DecisionTableDef | null {
|
|
280
|
+
if (chunk.length === 0) return null;
|
|
281
|
+
|
|
282
|
+
const firstHeading = chunk.find(el => el.kind === 'heading' && el.level === 1) as MdHeading | undefined;
|
|
283
|
+
if (!firstHeading || !firstHeading.text.startsWith('decision_table ')) {
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const { decisionTable } = parseDecisionTable(chunk);
|
|
288
|
+
return decisionTable;
|
|
289
|
+
}
|
|
@@ -10,6 +10,8 @@ import {
|
|
|
10
10
|
ReachabilityProperty, PassesThroughProperty, RespondsProperty,
|
|
11
11
|
InvariantProperty, InvokeDef,
|
|
12
12
|
} from './ast.js';
|
|
13
|
+
import { DecisionTableDef } from './dt-ast.js';
|
|
14
|
+
import { parseDecisionTable } from './dt-parser.js';
|
|
13
15
|
|
|
14
16
|
// ============================================================
|
|
15
17
|
// Phase 1: Structural Markdown Parsing
|
|
@@ -790,7 +792,7 @@ function parseMachineFromElements(elements: MdElement[]): MachineDef {
|
|
|
790
792
|
return machine;
|
|
791
793
|
}
|
|
792
794
|
|
|
793
|
-
function parseMarkdownSemantic(elements: MdElement[]): MachineDef[] {
|
|
795
|
+
function parseMarkdownSemantic(elements: MdElement[]): { machines: MachineDef[]; decisionTables: DecisionTableDef[] } {
|
|
794
796
|
// Split elements by --- separators for multi-machine files
|
|
795
797
|
const chunks: MdElement[][] = [];
|
|
796
798
|
let currentChunk: MdElement[] = [];
|
|
@@ -809,8 +811,33 @@ function parseMarkdownSemantic(elements: MdElement[]): MachineDef[] {
|
|
|
809
811
|
chunks.push(currentChunk);
|
|
810
812
|
}
|
|
811
813
|
|
|
812
|
-
// Parse each chunk
|
|
813
|
-
|
|
814
|
+
// Parse each chunk based on its H1 heading type
|
|
815
|
+
const machines: MachineDef[] = [];
|
|
816
|
+
const decisionTables: DecisionTableDef[] = [];
|
|
817
|
+
|
|
818
|
+
for (const chunk of chunks) {
|
|
819
|
+
// Find ALL H1 headings in the chunk to determine what it contains
|
|
820
|
+
const headings = chunk.filter(el => el.kind === 'heading' && el.level === 1) as MdHeading[];
|
|
821
|
+
|
|
822
|
+
// Check if chunk contains a decision_table (must be first H1 to be recognized as DT chunk)
|
|
823
|
+
const firstHeading = headings[0];
|
|
824
|
+
if (firstHeading?.text.startsWith('decision_table ')) {
|
|
825
|
+
const { decisionTable } = parseDecisionTable(chunk);
|
|
826
|
+
decisionTables.push(decisionTable);
|
|
827
|
+
} else if (firstHeading?.text.startsWith('machine ')) {
|
|
828
|
+
// First heading is machine - parse entire chunk as machine
|
|
829
|
+
machines.push(parseMachineFromElements(chunk));
|
|
830
|
+
} else {
|
|
831
|
+
// First heading is not machine or decision_table - scan for machine heading
|
|
832
|
+
const machineHeading = headings.find(h => h.text.startsWith('machine '));
|
|
833
|
+
if (machineHeading) {
|
|
834
|
+
machines.push(parseMachineFromElements(chunk));
|
|
835
|
+
}
|
|
836
|
+
// Skip chunks without a recognized machine or decision_table heading
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
return { machines, decisionTables };
|
|
814
841
|
}
|
|
815
842
|
|
|
816
843
|
function parseEffectsTable(table: MdTable): EffectDef[] {
|
|
@@ -834,8 +861,8 @@ function parsePropertiesList(list: MdBulletList): Property[] {
|
|
|
834
861
|
|
|
835
862
|
export function parseMarkdown(source: string): ParseResult {
|
|
836
863
|
const elements = parseMarkdownStructure(source);
|
|
837
|
-
const machines = parseMarkdownSemantic(elements);
|
|
838
|
-
return { file: { machines }, tokens: [] };
|
|
864
|
+
const { machines, decisionTables } = parseMarkdownSemantic(elements);
|
|
865
|
+
return { file: { machines, decisionTables }, tokens: [] };
|
|
839
866
|
}
|
|
840
867
|
|
|
841
868
|
/**
|
package/src/skills.ts
CHANGED
|
@@ -7,6 +7,8 @@ import { checkProperties } from './verifier/properties.js';
|
|
|
7
7
|
import { compileToXState } from './compiler/xstate.js';
|
|
8
8
|
import { compileToMermaid } from './compiler/mermaid.js';
|
|
9
9
|
import { MachineDef, StateDef, GuardExpression, Type } from './parser/ast.js';
|
|
10
|
+
import { verifyDecisionTable, verifyDecisionTables } from './verifier/dt-verifier.js';
|
|
11
|
+
import { compileDecisionTableToTypeScript, compileDecisionTableToJSON } from './compiler/dt-compiler.js';
|
|
10
12
|
import { loadConfig, resolveConfigOverrides } from './config/index.js';
|
|
11
13
|
import { createProvider } from './llm/index.js';
|
|
12
14
|
import type { LLMProvider } from './llm/index.js';
|
|
@@ -45,7 +47,12 @@ export interface SkillError {
|
|
|
45
47
|
location?: {
|
|
46
48
|
state?: string;
|
|
47
49
|
event?: string;
|
|
48
|
-
transition?: string;
|
|
50
|
+
transition?: string | { source?: string; target?: string; event?: string }; // string for machine errors, object for DT errors
|
|
51
|
+
// Decision table specific
|
|
52
|
+
rule?: number;
|
|
53
|
+
condition?: string;
|
|
54
|
+
action?: string;
|
|
55
|
+
decisionTable?: string;
|
|
49
56
|
};
|
|
50
57
|
suggestion?: string;
|
|
51
58
|
}
|
|
@@ -1677,3 +1684,269 @@ function extractMachineNameFromSource(orca: string): string {
|
|
|
1677
1684
|
const match = orca.match(/^(?:#\s+)?machine\s+(\w+)/m);
|
|
1678
1685
|
return match ? match[1] : 'Unknown';
|
|
1679
1686
|
}
|
|
1687
|
+
|
|
1688
|
+
// ============================================================
|
|
1689
|
+
// Decision Table Skills
|
|
1690
|
+
// ============================================================
|
|
1691
|
+
|
|
1692
|
+
export interface VerifyDTSkillResult {
|
|
1693
|
+
status: 'valid' | 'invalid';
|
|
1694
|
+
decisionTable: string;
|
|
1695
|
+
conditions: number;
|
|
1696
|
+
actions: number;
|
|
1697
|
+
rules: number;
|
|
1698
|
+
errors: SkillError[];
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
export interface CompileDTSkillResult {
|
|
1702
|
+
status: 'success' | 'error';
|
|
1703
|
+
target: 'typescript' | 'json';
|
|
1704
|
+
output: string;
|
|
1705
|
+
warnings: SkillError[];
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
/**
|
|
1709
|
+
* Parse a decision table from source.
|
|
1710
|
+
*/
|
|
1711
|
+
export function parseDTSkill(input: SkillInput): { status: 'success' | 'error'; decisionTables?: object[]; error?: string } {
|
|
1712
|
+
try {
|
|
1713
|
+
const source = resolveSource(input);
|
|
1714
|
+
const { file } = parseMarkdown(source);
|
|
1715
|
+
|
|
1716
|
+
if (file.decisionTables.length === 0) {
|
|
1717
|
+
return { status: 'error', error: 'No decision table found in source' };
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
// Return all decision tables as plain objects
|
|
1721
|
+
const tables = file.decisionTables.map(dt => ({
|
|
1722
|
+
name: dt.name,
|
|
1723
|
+
description: dt.description,
|
|
1724
|
+
conditions: dt.conditions.map(c => ({
|
|
1725
|
+
name: c.name,
|
|
1726
|
+
type: c.type,
|
|
1727
|
+
values: c.values,
|
|
1728
|
+
range: c.range,
|
|
1729
|
+
})),
|
|
1730
|
+
actions: dt.actions.map(a => ({
|
|
1731
|
+
name: a.name,
|
|
1732
|
+
type: a.type,
|
|
1733
|
+
description: a.description,
|
|
1734
|
+
values: a.values,
|
|
1735
|
+
})),
|
|
1736
|
+
rules: dt.rules.map(r => ({
|
|
1737
|
+
number: r.number,
|
|
1738
|
+
conditions: Object.fromEntries(r.conditions),
|
|
1739
|
+
actions: Object.fromEntries(r.actions),
|
|
1740
|
+
})),
|
|
1741
|
+
policy: dt.policy,
|
|
1742
|
+
}));
|
|
1743
|
+
|
|
1744
|
+
return { status: 'success', decisionTables: tables };
|
|
1745
|
+
} catch (err) {
|
|
1746
|
+
return { status: 'error', error: err instanceof Error ? err.message : String(err) };
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
/**
|
|
1751
|
+
* Verify a decision table.
|
|
1752
|
+
*/
|
|
1753
|
+
export function verifyDTSkill(input: SkillInput): VerifyDTSkillResult {
|
|
1754
|
+
try {
|
|
1755
|
+
const source = resolveSource(input);
|
|
1756
|
+
const { file } = parseMarkdown(source);
|
|
1757
|
+
|
|
1758
|
+
if (file.decisionTables.length === 0) {
|
|
1759
|
+
return {
|
|
1760
|
+
status: 'invalid',
|
|
1761
|
+
decisionTable: '',
|
|
1762
|
+
conditions: 0,
|
|
1763
|
+
actions: 0,
|
|
1764
|
+
rules: 0,
|
|
1765
|
+
errors: [{ code: 'DT_NOT_FOUND', message: 'No decision table found in source', severity: 'error' }],
|
|
1766
|
+
};
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
// Verify all decision tables
|
|
1770
|
+
const verification = verifyDecisionTables(file.decisionTables);
|
|
1771
|
+
|
|
1772
|
+
// Return result for the first decision table
|
|
1773
|
+
const dt = file.decisionTables[0];
|
|
1774
|
+
const errors: SkillError[] = [];
|
|
1775
|
+
for (const e of verification.errors) {
|
|
1776
|
+
const loc = e.location;
|
|
1777
|
+
errors.push({
|
|
1778
|
+
code: e.code,
|
|
1779
|
+
message: e.message,
|
|
1780
|
+
severity: e.severity as 'error' | 'warning',
|
|
1781
|
+
location: loc ? {
|
|
1782
|
+
state: loc.state,
|
|
1783
|
+
event: loc.event,
|
|
1784
|
+
rule: loc.rule,
|
|
1785
|
+
condition: loc.condition,
|
|
1786
|
+
action: loc.action,
|
|
1787
|
+
decisionTable: loc.decisionTable,
|
|
1788
|
+
} : undefined,
|
|
1789
|
+
suggestion: e.suggestion,
|
|
1790
|
+
});
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
return {
|
|
1794
|
+
status: verification.valid ? 'valid' : 'invalid',
|
|
1795
|
+
decisionTable: dt.name,
|
|
1796
|
+
conditions: dt.conditions.length,
|
|
1797
|
+
actions: dt.actions.length,
|
|
1798
|
+
rules: dt.rules.length,
|
|
1799
|
+
errors,
|
|
1800
|
+
};
|
|
1801
|
+
} catch (err) {
|
|
1802
|
+
return {
|
|
1803
|
+
status: 'invalid',
|
|
1804
|
+
decisionTable: '',
|
|
1805
|
+
conditions: 0,
|
|
1806
|
+
actions: 0,
|
|
1807
|
+
rules: 0,
|
|
1808
|
+
errors: [{ code: 'PARSE_ERROR', message: err instanceof Error ? err.message : String(err), severity: 'error' }],
|
|
1809
|
+
};
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
/**
|
|
1814
|
+
* Compile a decision table to TypeScript or JSON.
|
|
1815
|
+
*/
|
|
1816
|
+
export function compileDTSkill(input: SkillInput, target: 'typescript' | 'json' = 'typescript'): CompileDTSkillResult {
|
|
1817
|
+
try {
|
|
1818
|
+
const source = resolveSource(input);
|
|
1819
|
+
const { file } = parseMarkdown(source);
|
|
1820
|
+
|
|
1821
|
+
if (file.decisionTables.length === 0) {
|
|
1822
|
+
return {
|
|
1823
|
+
status: 'error',
|
|
1824
|
+
target,
|
|
1825
|
+
output: '',
|
|
1826
|
+
warnings: [{ code: 'DT_NOT_FOUND', message: 'No decision table found in source', severity: 'error' }],
|
|
1827
|
+
};
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
const dt = file.decisionTables[0];
|
|
1831
|
+
const output = target === 'json'
|
|
1832
|
+
? compileDecisionTableToJSON(dt)
|
|
1833
|
+
: compileDecisionTableToTypeScript(dt);
|
|
1834
|
+
|
|
1835
|
+
return { status: 'success', target, output, warnings: [] };
|
|
1836
|
+
} catch (err) {
|
|
1837
|
+
return {
|
|
1838
|
+
status: 'error',
|
|
1839
|
+
target,
|
|
1840
|
+
output: '',
|
|
1841
|
+
warnings: [{ code: 'COMPILE_ERROR', message: err instanceof Error ? err.message : String(err), severity: 'error' }],
|
|
1842
|
+
};
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
// Decision table syntax reference for LLM generation
|
|
1847
|
+
const DT_SYNTAX_REFERENCE = `
|
|
1848
|
+
# decision_table Name
|
|
1849
|
+
|
|
1850
|
+
## conditions
|
|
1851
|
+
|
|
1852
|
+
| Name | Type | Values |
|
|
1853
|
+
|------|------|--------|
|
|
1854
|
+
| field_name | enum | value1, value2 |
|
|
1855
|
+
| is_active | bool | |
|
|
1856
|
+
| count | int_range | 1..100 |
|
|
1857
|
+
|
|
1858
|
+
## actions
|
|
1859
|
+
|
|
1860
|
+
| Name | Type | Description |
|
|
1861
|
+
|------|------|-------------|
|
|
1862
|
+
| result | enum | Result description |
|
|
1863
|
+
| flag | bool | Whether something |
|
|
1864
|
+
|
|
1865
|
+
## rules
|
|
1866
|
+
|
|
1867
|
+
| condition1 | condition2 | → action1 | → action2 |
|
|
1868
|
+
|------------|-----------|----------|-----------|
|
|
1869
|
+
| value1 | - | result1 | true |
|
|
1870
|
+
| value2 | !value3 | result2 | false |
|
|
1871
|
+
|
|
1872
|
+
Notes:
|
|
1873
|
+
- "-" in a condition cell means "any" (wildcard)
|
|
1874
|
+
- "!value" negates a value
|
|
1875
|
+
- "a,b" matches any of the values (OR semantics)
|
|
1876
|
+
- Rules are evaluated top-to-bottom; first match wins
|
|
1877
|
+
`;
|
|
1878
|
+
|
|
1879
|
+
/**
|
|
1880
|
+
* Generate a decision table from natural language spec.
|
|
1881
|
+
*/
|
|
1882
|
+
export async function generateDTSkill(spec: string, configPath?: string): Promise<{
|
|
1883
|
+
status: 'success' | 'error' | 'requires_refinement';
|
|
1884
|
+
decisionTable?: string;
|
|
1885
|
+
orca?: string;
|
|
1886
|
+
verification?: VerifyDTSkillResult;
|
|
1887
|
+
error?: string;
|
|
1888
|
+
}> {
|
|
1889
|
+
const config = loadConfig(configPath);
|
|
1890
|
+
const provider = createProvider(config.provider, {
|
|
1891
|
+
api_key: config.api_key,
|
|
1892
|
+
base_url: config.base_url,
|
|
1893
|
+
model: config.model,
|
|
1894
|
+
max_tokens: config.max_tokens,
|
|
1895
|
+
temperature: config.temperature,
|
|
1896
|
+
});
|
|
1897
|
+
|
|
1898
|
+
if (!provider) {
|
|
1899
|
+
return { status: 'error', error: 'No LLM provider configured' };
|
|
1900
|
+
}
|
|
1901
|
+
|
|
1902
|
+
const prompt = `Generate a decision table in .orca.md format based on the following specification.
|
|
1903
|
+
|
|
1904
|
+
${DT_SYNTAX_REFERENCE}
|
|
1905
|
+
|
|
1906
|
+
Specification:
|
|
1907
|
+
${spec}
|
|
1908
|
+
|
|
1909
|
+
Respond with the decision table only, no explanation.`;
|
|
1910
|
+
|
|
1911
|
+
try {
|
|
1912
|
+
const response = await provider.complete({
|
|
1913
|
+
messages: [{ role: 'user', content: prompt }],
|
|
1914
|
+
model: '',
|
|
1915
|
+
max_tokens: config.max_tokens || 2048,
|
|
1916
|
+
temperature: 0.7,
|
|
1917
|
+
});
|
|
1918
|
+
|
|
1919
|
+
const orca = stripCodeFence(response.content);
|
|
1920
|
+
|
|
1921
|
+
// Verify the generated decision table
|
|
1922
|
+
const { file } = parseMarkdown(orca);
|
|
1923
|
+
if (file.decisionTables.length === 0) {
|
|
1924
|
+
return { status: 'error', error: 'Generated content does not contain a valid decision table', orca };
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1927
|
+
const verification = verifyDecisionTables(file.decisionTables);
|
|
1928
|
+
|
|
1929
|
+
return {
|
|
1930
|
+
status: verification.valid ? 'success' : 'requires_refinement',
|
|
1931
|
+
decisionTable: file.decisionTables[0].name,
|
|
1932
|
+
orca,
|
|
1933
|
+
verification: verification.valid ? undefined : {
|
|
1934
|
+
status: 'invalid',
|
|
1935
|
+
decisionTable: file.decisionTables[0].name,
|
|
1936
|
+
conditions: file.decisionTables[0].conditions.length,
|
|
1937
|
+
actions: file.decisionTables[0].actions.length,
|
|
1938
|
+
rules: file.decisionTables[0].rules.length,
|
|
1939
|
+
errors: verification.errors.map(e => ({
|
|
1940
|
+
code: e.code,
|
|
1941
|
+
message: e.message,
|
|
1942
|
+
severity: e.severity,
|
|
1943
|
+
location: e.location,
|
|
1944
|
+
suggestion: e.suggestion,
|
|
1945
|
+
})),
|
|
1946
|
+
},
|
|
1947
|
+
};
|
|
1948
|
+
} catch (err) {
|
|
1949
|
+
return { status: 'error', error: err instanceof Error ? err.message : String(err) };
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1952
|
+
|
package/src/tools.ts
CHANGED
|
@@ -143,4 +143,57 @@ export const ORCA_TOOLS: ToolDef[] = [
|
|
|
143
143
|
required: [],
|
|
144
144
|
},
|
|
145
145
|
},
|
|
146
|
+
{
|
|
147
|
+
name: 'parse_decision_table',
|
|
148
|
+
description:
|
|
149
|
+
'Parse decision table from .orca.md source → JSON (conditions, actions, rules). Syntax: # decision_table Name, ## conditions table, ## actions table, ## rules table. Can be standalone or combined with machines in the same file.',
|
|
150
|
+
inputSchema: {
|
|
151
|
+
type: 'object',
|
|
152
|
+
properties: {
|
|
153
|
+
source: { type: 'string', description: 'Raw .orca.md content containing a # decision_table heading.' },
|
|
154
|
+
},
|
|
155
|
+
required: ['source'],
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
name: 'verify_decision_table',
|
|
160
|
+
description:
|
|
161
|
+
'Verify decision table: checks completeness (all condition combinations covered), consistency (no contradictory rules), redundancy. Returns structured errors with codes and suggestions.',
|
|
162
|
+
inputSchema: {
|
|
163
|
+
type: 'object',
|
|
164
|
+
properties: {
|
|
165
|
+
source: { type: 'string', description: 'Raw .orca.md content containing a # decision_table heading.' },
|
|
166
|
+
},
|
|
167
|
+
required: ['source'],
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
name: 'compile_decision_table',
|
|
172
|
+
description:
|
|
173
|
+
'Compile verified decision table to TypeScript evaluator function or portable JSON. Run verify_decision_table first. target: "typescript" (default) or "json".',
|
|
174
|
+
inputSchema: {
|
|
175
|
+
type: 'object',
|
|
176
|
+
properties: {
|
|
177
|
+
source: { type: 'string', description: 'Raw .orca.md content containing a # decision_table heading.' },
|
|
178
|
+
target: {
|
|
179
|
+
type: 'string',
|
|
180
|
+
enum: ['typescript', 'json'],
|
|
181
|
+
description: 'Compilation target (default: typescript)',
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
required: ['source'],
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
name: 'generate_decision_table',
|
|
189
|
+
description:
|
|
190
|
+
'Generate a decision table in .orca.md format from a natural language spec. Always verify_decision_table next. Requires LLM API key.',
|
|
191
|
+
inputSchema: {
|
|
192
|
+
type: 'object',
|
|
193
|
+
properties: {
|
|
194
|
+
spec: { type: 'string', description: 'Natural language description of the decision logic' },
|
|
195
|
+
},
|
|
196
|
+
required: ['spec'],
|
|
197
|
+
},
|
|
198
|
+
},
|
|
146
199
|
];
|