@orcalang/orca-lang 0.1.17 → 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.
Files changed (45) hide show
  1. package/dist/compiler/dt-compiler.d.ts +23 -0
  2. package/dist/compiler/dt-compiler.d.ts.map +1 -0
  3. package/dist/compiler/dt-compiler.js +183 -0
  4. package/dist/compiler/dt-compiler.js.map +1 -0
  5. package/dist/health-check.d.ts +3 -0
  6. package/dist/health-check.d.ts.map +1 -0
  7. package/dist/health-check.js +235 -0
  8. package/dist/health-check.js.map +1 -0
  9. package/dist/parser/ast.d.ts +2 -0
  10. package/dist/parser/ast.d.ts.map +1 -1
  11. package/dist/parser/dt-ast.d.ts +43 -0
  12. package/dist/parser/dt-ast.d.ts.map +1 -0
  13. package/dist/parser/dt-ast.js +3 -0
  14. package/dist/parser/dt-ast.js.map +1 -0
  15. package/dist/parser/dt-parser.d.ts +40 -0
  16. package/dist/parser/dt-parser.d.ts.map +1 -0
  17. package/dist/parser/dt-parser.js +240 -0
  18. package/dist/parser/dt-parser.js.map +1 -0
  19. package/dist/parser/markdown-parser.d.ts.map +1 -1
  20. package/dist/parser/markdown-parser.js +29 -4
  21. package/dist/parser/markdown-parser.js.map +1 -1
  22. package/dist/skills.d.ts +49 -1
  23. package/dist/skills.d.ts.map +1 -1
  24. package/dist/skills.js +223 -0
  25. package/dist/skills.js.map +1 -1
  26. package/dist/tools.d.ts.map +1 -1
  27. package/dist/tools.js +49 -0
  28. package/dist/tools.js.map +1 -1
  29. package/dist/verifier/dt-verifier.d.ts +5 -0
  30. package/dist/verifier/dt-verifier.d.ts.map +1 -0
  31. package/dist/verifier/dt-verifier.js +499 -0
  32. package/dist/verifier/dt-verifier.js.map +1 -0
  33. package/dist/verifier/types.d.ts +4 -0
  34. package/dist/verifier/types.d.ts.map +1 -1
  35. package/package.json +3 -2
  36. package/src/compiler/dt-compiler.ts +232 -0
  37. package/src/health-check.ts +273 -0
  38. package/src/parser/ast.ts +3 -0
  39. package/src/parser/dt-ast.ts +40 -0
  40. package/src/parser/dt-parser.ts +289 -0
  41. package/src/parser/markdown-parser.ts +32 -5
  42. package/src/skills.ts +274 -1
  43. package/src/tools.ts +53 -0
  44. package/src/verifier/dt-verifier.ts +562 -0
  45. 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 as a separate machine
813
- return chunks.map(chunk => parseMachineFromElements(chunk));
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
  ];