@lokascript/framework 2.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/aot/index.d.ts +1 -0
- package/dist/aot/index.d.ts.map +1 -1
- package/dist/aot/registry-bridge.d.ts +27 -0
- package/dist/aot/registry-bridge.d.ts.map +1 -0
- package/dist/api/create-dsl.d.ts.map +1 -1
- package/dist/api/dispatcher.d.ts +22 -0
- package/dist/api/dispatcher.d.ts.map +1 -1
- package/dist/api/domain-registry.d.ts +41 -0
- package/dist/api/domain-registry.d.ts.map +1 -1
- package/dist/api/index.js +1351 -21
- package/dist/api/index.js.map +1 -1
- package/dist/core/index.js +114 -10
- package/dist/core/index.js.map +1 -1
- package/dist/core/pattern-matching/index.js +74 -10
- package/dist/core/pattern-matching/index.js.map +1 -1
- package/dist/core/pattern-matching/pattern-matcher.d.ts.map +1 -1
- package/dist/core/types.d.ts +80 -2
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js +40 -0
- package/dist/core/types.js.map +1 -1
- package/dist/feedback/confidence-gate.d.ts +28 -0
- package/dist/feedback/confidence-gate.d.ts.map +1 -0
- package/dist/feedback/feedback-formatter.d.ts +21 -0
- package/dist/feedback/feedback-formatter.d.ts.map +1 -0
- package/dist/feedback/index.d.ts +11 -0
- package/dist/feedback/index.d.ts.map +1 -0
- package/dist/feedback/pattern-tracker.d.ts +47 -0
- package/dist/feedback/pattern-tracker.d.ts.map +1 -0
- package/dist/feedback/types.d.ts +113 -0
- package/dist/feedback/types.d.ts.map +1 -0
- package/dist/generation/index.js +2 -1
- package/dist/generation/index.js.map +1 -1
- package/dist/index.cjs +2685 -71
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +6 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2652 -71
- package/dist/index.js.map +1 -1
- package/dist/ir/explicit-parser.d.ts +31 -0
- package/dist/ir/explicit-parser.d.ts.map +1 -0
- package/dist/ir/explicit-renderer.d.ts +17 -0
- package/dist/ir/explicit-renderer.d.ts.map +1 -0
- package/dist/ir/from-interchange.d.ts +51 -0
- package/dist/ir/from-interchange.d.ts.map +1 -0
- package/dist/ir/index.d.ts +29 -0
- package/dist/ir/index.d.ts.map +1 -0
- package/dist/ir/index.js +1557 -0
- package/dist/ir/index.js.map +1 -0
- package/dist/ir/json-schema.d.ts +40 -0
- package/dist/ir/json-schema.d.ts.map +1 -0
- package/dist/ir/protocol-json.d.ts +73 -0
- package/dist/ir/protocol-json.d.ts.map +1 -0
- package/dist/ir/references.d.ts +19 -0
- package/dist/ir/references.d.ts.map +1 -0
- package/dist/ir/types.d.ts +141 -0
- package/dist/ir/types.d.ts.map +1 -0
- package/dist/parsing/index.js +74 -10
- package/dist/parsing/index.js.map +1 -1
- package/dist/prompts/index.d.ts +9 -0
- package/dist/prompts/index.d.ts.map +1 -0
- package/dist/prompts/prompt-generator.d.ts +49 -0
- package/dist/prompts/prompt-generator.d.ts.map +1 -0
- package/dist/prompts/types.d.ts +64 -0
- package/dist/prompts/types.d.ts.map +1 -0
- package/dist/schema/command-schema.d.ts +17 -0
- package/dist/schema/command-schema.d.ts.map +1 -1
- package/dist/schema/index.js.map +1 -1
- package/dist/testing/index.js +3 -0
- package/dist/testing/index.js.map +1 -1
- package/dist/training/index.d.ts +11 -0
- package/dist/training/index.d.ts.map +1 -0
- package/dist/training/jsonl-writer.d.ts +42 -0
- package/dist/training/jsonl-writer.d.ts.map +1 -0
- package/dist/training/schema-synthesizer.d.ts +20 -0
- package/dist/training/schema-synthesizer.d.ts.map +1 -0
- package/dist/training/types.d.ts +73 -0
- package/dist/training/types.d.ts.map +1 -0
- package/package.json +5 -1
- package/src/aot/index.ts +1 -0
- package/src/aot/registry-bridge.test.ts +104 -0
- package/src/aot/registry-bridge.ts +66 -0
- package/src/api/create-dsl.test.ts +306 -0
- package/src/api/create-dsl.ts +21 -0
- package/src/api/dispatcher.test.ts +66 -0
- package/src/api/dispatcher.ts +77 -9
- package/src/api/domain-registry.ts +120 -4
- package/src/core/pattern-matching/pattern-matcher.test.ts +131 -0
- package/src/core/pattern-matching/pattern-matcher.ts +102 -19
- package/src/core/types.ts +145 -2
- package/src/feedback/confidence-gate.test.ts +111 -0
- package/src/feedback/confidence-gate.ts +70 -0
- package/src/feedback/feedback-formatter.test.ts +198 -0
- package/src/feedback/feedback-formatter.ts +298 -0
- package/src/feedback/index.ts +24 -0
- package/src/feedback/pattern-tracker.test.ts +198 -0
- package/src/feedback/pattern-tracker.ts +140 -0
- package/src/feedback/types.ts +149 -0
- package/src/generation/pattern-generator.ts +2 -2
- package/src/index.ts +22 -0
- package/src/integration/llm-lse-integration.test.ts +457 -0
- package/src/ir/explicit-parser.test.ts +306 -0
- package/src/ir/explicit-parser.ts +419 -0
- package/src/ir/explicit-renderer.test.ts +144 -0
- package/src/ir/explicit-renderer.ts +146 -0
- package/src/ir/from-interchange.test.ts +853 -0
- package/src/ir/from-interchange.ts +594 -0
- package/src/ir/index.ts +66 -0
- package/src/ir/json-schema.test.ts +281 -0
- package/src/ir/json-schema.ts +299 -0
- package/src/ir/protocol-json.test.ts +1185 -0
- package/src/ir/protocol-json.ts +745 -0
- package/src/ir/references.test.ts +38 -0
- package/src/ir/references.ts +33 -0
- package/src/ir/round-trip.test.ts +557 -0
- package/src/ir/types.ts +181 -0
- package/src/prompts/index.ts +15 -0
- package/src/prompts/prompt-generator.test.ts +386 -0
- package/src/prompts/prompt-generator.ts +531 -0
- package/src/prompts/types.ts +89 -0
- package/src/schema/command-schema.ts +19 -0
- package/src/training/index.ts +13 -0
- package/src/training/jsonl-writer.test.ts +138 -0
- package/src/training/jsonl-writer.ts +67 -0
- package/src/training/schema-synthesizer.test.ts +250 -0
- package/src/training/schema-synthesizer.ts +306 -0
- package/src/training/types.ts +103 -0
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Explicit Syntax Parser
|
|
3
|
+
*
|
|
4
|
+
* Parses the universal [command role:value ...] bracket syntax.
|
|
5
|
+
* Domain-agnostic — schema validation is injectable via ParseExplicitOptions.
|
|
6
|
+
*
|
|
7
|
+
* Syntax:
|
|
8
|
+
* [command role1:value1 role2:value2 ...]
|
|
9
|
+
*
|
|
10
|
+
* Examples:
|
|
11
|
+
* [toggle patient:.active destination:#button]
|
|
12
|
+
* [put patient:"hello" destination:#output]
|
|
13
|
+
* [on event:click body:[toggle patient:.active]]
|
|
14
|
+
* [select patient:name source:users condition:age>25]
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type {
|
|
18
|
+
SemanticNode,
|
|
19
|
+
SemanticValue,
|
|
20
|
+
SemanticRole,
|
|
21
|
+
ActionType,
|
|
22
|
+
LoopVariant,
|
|
23
|
+
} from '../core/types';
|
|
24
|
+
import {
|
|
25
|
+
createCommandNode,
|
|
26
|
+
createEventHandlerNode,
|
|
27
|
+
createConditionalNode,
|
|
28
|
+
createLoopNode,
|
|
29
|
+
createSelector,
|
|
30
|
+
createLiteral,
|
|
31
|
+
createReference,
|
|
32
|
+
createFlag,
|
|
33
|
+
} from '../core/types';
|
|
34
|
+
import type { ParseExplicitOptions } from './types';
|
|
35
|
+
import { isValidReference, DEFAULT_REFERENCES } from './references';
|
|
36
|
+
|
|
37
|
+
/** Structural role names whose bracket-enclosed values may be nested commands. */
|
|
38
|
+
export const STRUCTURAL_ROLES = new Set([
|
|
39
|
+
'body',
|
|
40
|
+
'then',
|
|
41
|
+
'else',
|
|
42
|
+
'condition',
|
|
43
|
+
'loop-body',
|
|
44
|
+
'variable',
|
|
45
|
+
'catch', // v1.2: try/catch/finally
|
|
46
|
+
'finally', // v1.2: try/catch/finally
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Checks whether a bracket-enclosed value is a nested command (vs. an attribute selector).
|
|
51
|
+
* A value starting with `[` is a nested command if the inner content (after stripping
|
|
52
|
+
* outer `[]`) contains at least one ASCII space or `:` at bracket-depth 0.
|
|
53
|
+
*/
|
|
54
|
+
function isNestedCommand(value: string): boolean {
|
|
55
|
+
const inner = value.slice(1, -1);
|
|
56
|
+
let depth = 0;
|
|
57
|
+
for (const ch of inner) {
|
|
58
|
+
if (ch === '[') depth++;
|
|
59
|
+
else if (ch === ']') depth--;
|
|
60
|
+
else if (depth === 0 && (ch === ' ' || ch === ':')) return true;
|
|
61
|
+
}
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// =============================================================================
|
|
66
|
+
// Explicit Syntax Parser
|
|
67
|
+
// =============================================================================
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Parse explicit bracket syntax into a semantic node.
|
|
71
|
+
*
|
|
72
|
+
* When `options.schemaLookup` is provided, validates roles against the
|
|
73
|
+
* command's schema. When omitted, accepts any role names.
|
|
74
|
+
*/
|
|
75
|
+
export function parseExplicit(input: string, options: ParseExplicitOptions = {}): SemanticNode {
|
|
76
|
+
const trimmed = input.trim();
|
|
77
|
+
|
|
78
|
+
if (!trimmed.startsWith('[') || !trimmed.endsWith(']')) {
|
|
79
|
+
throw new Error('Explicit syntax must be wrapped in brackets: [command role:value ...]');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const content = trimmed.slice(1, -1).trim();
|
|
83
|
+
if (!content) {
|
|
84
|
+
throw new Error('Empty explicit statement');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const tokens = tokenizeExplicit(content);
|
|
88
|
+
if (tokens.length === 0) {
|
|
89
|
+
throw new Error('No command specified in explicit statement');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const command = tokens[0].toLowerCase() as ActionType;
|
|
93
|
+
const roles = new Map<SemanticRole, SemanticValue>();
|
|
94
|
+
const refSet = options.referenceSet ?? DEFAULT_REFERENCES;
|
|
95
|
+
|
|
96
|
+
// Look up schema for role validation (undefined = skip validation)
|
|
97
|
+
const schema = options.schemaLookup?.getSchema(command);
|
|
98
|
+
const validRoleNames = schema ? new Set(schema.roles.map(r => r.role)) : null;
|
|
99
|
+
|
|
100
|
+
// Parse role:value pairs and flags
|
|
101
|
+
for (let i = 1; i < tokens.length; i++) {
|
|
102
|
+
const token = tokens[i];
|
|
103
|
+
|
|
104
|
+
// Boolean flag: +flag-name or ~flag-name
|
|
105
|
+
if (token.startsWith('+') || token.startsWith('~')) {
|
|
106
|
+
const enabled = token.startsWith('+');
|
|
107
|
+
const flagName = token.slice(1);
|
|
108
|
+
if (!flagName) {
|
|
109
|
+
throw new Error(`Empty flag name: "${token}"`);
|
|
110
|
+
}
|
|
111
|
+
if (validRoleNames && !validRoleNames.has(flagName as SemanticRole)) {
|
|
112
|
+
const roleList = [...validRoleNames].join(', ');
|
|
113
|
+
throw new Error(
|
|
114
|
+
`Unknown flag "${flagName}" for command "${command}". Valid roles: ${roleList}`
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
roles.set(flagName as SemanticRole, createFlag(flagName, enabled));
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const colonIndex = token.indexOf(':');
|
|
122
|
+
|
|
123
|
+
if (colonIndex === -1) {
|
|
124
|
+
throw new Error(`Invalid role format: "${token}". Expected role:value or +flag`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const roleName = token.slice(0, colonIndex);
|
|
128
|
+
|
|
129
|
+
// Validate role name against schema (skip for structural roles)
|
|
130
|
+
if (
|
|
131
|
+
validRoleNames &&
|
|
132
|
+
!STRUCTURAL_ROLES.has(roleName) &&
|
|
133
|
+
!validRoleNames.has(roleName as SemanticRole)
|
|
134
|
+
) {
|
|
135
|
+
const roleList = [...validRoleNames].join(', ');
|
|
136
|
+
throw new Error(
|
|
137
|
+
`Unknown role "${roleName}" for command "${command}". Valid roles: ${roleList}`
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const role = roleName as SemanticRole;
|
|
142
|
+
const valueStr = token.slice(colonIndex + 1);
|
|
143
|
+
|
|
144
|
+
// Handle nested explicit syntax for structural roles (body, then, else, condition, loop-body, variable)
|
|
145
|
+
if (STRUCTURAL_ROLES.has(roleName) && valueStr.startsWith('[') && isNestedCommand(valueStr)) {
|
|
146
|
+
const nestedEnd = findMatchingBracket(token, colonIndex + 1);
|
|
147
|
+
const nestedSyntax = token.slice(colonIndex + 1, nestedEnd + 1);
|
|
148
|
+
roles.set(role, { type: 'expression', raw: nestedSyntax });
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const value = parseExplicitValue(valueStr, refSet);
|
|
153
|
+
roles.set(role, value);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Validate required roles are present (skip for event handlers — handled below)
|
|
157
|
+
if (schema && command !== 'on') {
|
|
158
|
+
for (const roleSpec of schema.roles) {
|
|
159
|
+
if (roleSpec.required && !roles.has(roleSpec.role as SemanticRole) && !roleSpec.default) {
|
|
160
|
+
throw new Error(
|
|
161
|
+
`Missing required role "${roleSpec.role}" for command "${command}": ${roleSpec.description}`
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Build appropriate node type based on action name
|
|
168
|
+
switch (command) {
|
|
169
|
+
case 'on': {
|
|
170
|
+
const eventValue = roles.get('event');
|
|
171
|
+
if (!eventValue) {
|
|
172
|
+
throw new Error('Event handler requires event role: [on event:click ...]');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Parse body if present
|
|
176
|
+
const body = extractStructuralBody(roles, 'body', options);
|
|
177
|
+
roles.delete('body' as SemanticRole);
|
|
178
|
+
|
|
179
|
+
return createEventHandlerNode('on', roles, body, {
|
|
180
|
+
sourceLanguage: 'explicit',
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
case 'if': {
|
|
185
|
+
// Extract structural roles for conditional
|
|
186
|
+
const thenBranch = extractStructuralBody(roles, 'then', options);
|
|
187
|
+
const elseBranch = extractStructuralBody(roles, 'else', options);
|
|
188
|
+
roles.delete('then' as SemanticRole);
|
|
189
|
+
roles.delete('else' as SemanticRole);
|
|
190
|
+
|
|
191
|
+
return createConditionalNode(
|
|
192
|
+
command,
|
|
193
|
+
roles,
|
|
194
|
+
thenBranch,
|
|
195
|
+
elseBranch.length > 0 ? elseBranch : undefined,
|
|
196
|
+
{
|
|
197
|
+
sourceLanguage: 'explicit',
|
|
198
|
+
}
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
case 'repeat': {
|
|
203
|
+
// Extract loop structural roles
|
|
204
|
+
const loopBody = extractStructuralBody(roles, 'loop-body', options);
|
|
205
|
+
const loopVariantValue = roles.get('loop-variant' as SemanticRole);
|
|
206
|
+
const variableValue = roles.get('variable' as SemanticRole);
|
|
207
|
+
const indexVariableValue = roles.get('index-variable' as SemanticRole);
|
|
208
|
+
|
|
209
|
+
roles.delete('loop-body' as SemanticRole);
|
|
210
|
+
roles.delete('loop-variant' as SemanticRole);
|
|
211
|
+
roles.delete('variable' as SemanticRole);
|
|
212
|
+
roles.delete('index-variable' as SemanticRole);
|
|
213
|
+
|
|
214
|
+
// Determine loop variant: explicit > inferred from roles
|
|
215
|
+
let variant: LoopVariant;
|
|
216
|
+
if (
|
|
217
|
+
loopVariantValue &&
|
|
218
|
+
loopVariantValue.type === 'literal' &&
|
|
219
|
+
typeof loopVariantValue.value === 'string'
|
|
220
|
+
) {
|
|
221
|
+
variant = loopVariantValue.value as LoopVariant;
|
|
222
|
+
} else if (roles.has('quantity' as SemanticRole)) {
|
|
223
|
+
variant = 'times';
|
|
224
|
+
} else if (roles.has('source' as SemanticRole)) {
|
|
225
|
+
variant = 'for';
|
|
226
|
+
} else if (roles.has('condition' as SemanticRole)) {
|
|
227
|
+
variant = 'while';
|
|
228
|
+
} else {
|
|
229
|
+
variant = 'forever';
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const loopVariable =
|
|
233
|
+
variableValue?.type === 'literal' && typeof variableValue.value === 'string'
|
|
234
|
+
? variableValue.value
|
|
235
|
+
: variableValue?.type === 'expression'
|
|
236
|
+
? variableValue.raw
|
|
237
|
+
: undefined;
|
|
238
|
+
const indexVariable =
|
|
239
|
+
indexVariableValue?.type === 'literal' && typeof indexVariableValue.value === 'string'
|
|
240
|
+
? indexVariableValue.value
|
|
241
|
+
: undefined;
|
|
242
|
+
|
|
243
|
+
return createLoopNode(command, roles, variant, loopBody, loopVariable, indexVariable, {
|
|
244
|
+
sourceLanguage: 'explicit',
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
default:
|
|
249
|
+
return createCommandNode(command, roles, {
|
|
250
|
+
sourceLanguage: 'explicit',
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Check if input is explicit bracket syntax.
|
|
257
|
+
*/
|
|
258
|
+
export function isExplicitSyntax(input: string): boolean {
|
|
259
|
+
const trimmed = input.trim();
|
|
260
|
+
return trimmed.startsWith('[') && trimmed.endsWith(']');
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// =============================================================================
|
|
264
|
+
// Internal Helpers
|
|
265
|
+
// =============================================================================
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Extract and parse a structural role (body, then, else, loop-body) into SemanticNode[].
|
|
269
|
+
* Returns an empty array if the role is not present.
|
|
270
|
+
*/
|
|
271
|
+
function extractStructuralBody(
|
|
272
|
+
roles: Map<SemanticRole, SemanticValue>,
|
|
273
|
+
roleName: string,
|
|
274
|
+
options: ParseExplicitOptions
|
|
275
|
+
): SemanticNode[] {
|
|
276
|
+
const value = roles.get(roleName as SemanticRole);
|
|
277
|
+
if (!value) return [];
|
|
278
|
+
if (value.type === 'expression') {
|
|
279
|
+
return [parseExplicit(value.raw, options)];
|
|
280
|
+
}
|
|
281
|
+
return [];
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Infer selectorKind from a CSS selector string.
|
|
286
|
+
*/
|
|
287
|
+
function inferSelectorKind(
|
|
288
|
+
value: string
|
|
289
|
+
): 'id' | 'class' | 'attribute' | 'element' | 'complex' | undefined {
|
|
290
|
+
if (!value) return undefined;
|
|
291
|
+
if (value.startsWith('#')) return 'id';
|
|
292
|
+
if (value.startsWith('.')) return 'class';
|
|
293
|
+
if (value.startsWith('[')) return 'attribute';
|
|
294
|
+
if (value.startsWith('<') || value.startsWith('*')) return 'element';
|
|
295
|
+
if (/[>+~ ]/.test(value) && value.length > 1) return 'complex';
|
|
296
|
+
return undefined;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Tokenize explicit syntax content (space-separated, respecting quotes and brackets).
|
|
301
|
+
*/
|
|
302
|
+
function tokenizeExplicit(content: string): string[] {
|
|
303
|
+
const tokens: string[] = [];
|
|
304
|
+
let current = '';
|
|
305
|
+
let inString = false;
|
|
306
|
+
let stringChar = '';
|
|
307
|
+
let bracketDepth = 0;
|
|
308
|
+
|
|
309
|
+
for (let i = 0; i < content.length; i++) {
|
|
310
|
+
const char = content[i];
|
|
311
|
+
|
|
312
|
+
if (inString) {
|
|
313
|
+
current += char;
|
|
314
|
+
if (char === stringChar && content[i - 1] !== '\\') {
|
|
315
|
+
inString = false;
|
|
316
|
+
}
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (char === '"' || char === "'") {
|
|
321
|
+
inString = true;
|
|
322
|
+
stringChar = char;
|
|
323
|
+
current += char;
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (char === '[') {
|
|
328
|
+
bracketDepth++;
|
|
329
|
+
current += char;
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (char === ']') {
|
|
334
|
+
bracketDepth--;
|
|
335
|
+
current += char;
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (char === ' ' && bracketDepth === 0) {
|
|
340
|
+
if (current) {
|
|
341
|
+
tokens.push(current);
|
|
342
|
+
current = '';
|
|
343
|
+
}
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
current += char;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (current) {
|
|
351
|
+
tokens.push(current);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return tokens;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Parse a value string into a SemanticValue.
|
|
359
|
+
*/
|
|
360
|
+
function parseExplicitValue(valueStr: string, referenceSet: ReadonlySet<string>): SemanticValue {
|
|
361
|
+
// CSS selector
|
|
362
|
+
if (
|
|
363
|
+
valueStr.startsWith('#') ||
|
|
364
|
+
valueStr.startsWith('.') ||
|
|
365
|
+
valueStr.startsWith('[') ||
|
|
366
|
+
valueStr.startsWith('@') ||
|
|
367
|
+
valueStr.startsWith('*')
|
|
368
|
+
) {
|
|
369
|
+
return createSelector(valueStr, inferSelectorKind(valueStr));
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// String literal
|
|
373
|
+
if (valueStr.startsWith('"') || valueStr.startsWith("'")) {
|
|
374
|
+
const inner = valueStr.slice(1, -1);
|
|
375
|
+
return createLiteral(inner, 'string');
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Boolean
|
|
379
|
+
if (valueStr === 'true') return createLiteral(true, 'boolean');
|
|
380
|
+
if (valueStr === 'false') return createLiteral(false, 'boolean');
|
|
381
|
+
|
|
382
|
+
// Reference
|
|
383
|
+
const lowerRef = valueStr.toLowerCase();
|
|
384
|
+
if (isValidReference(lowerRef, referenceSet)) {
|
|
385
|
+
return createReference(lowerRef);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Number (possibly with duration suffix)
|
|
389
|
+
const numMatch = valueStr.match(/^(-?\d+(?:\.\d+)?)(ms|s|m|h)?$/);
|
|
390
|
+
if (numMatch) {
|
|
391
|
+
const num = parseFloat(numMatch[1]);
|
|
392
|
+
const suffix = numMatch[2];
|
|
393
|
+
if (suffix) {
|
|
394
|
+
return createLiteral(valueStr, 'duration');
|
|
395
|
+
}
|
|
396
|
+
return createLiteral(num, 'number');
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Default to untyped string literal (no dataType — avoids spurious quoting on round-trip).
|
|
400
|
+
// Only explicitly-quoted values (handled above) get dataType: 'string'.
|
|
401
|
+
return createLiteral(valueStr);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Find the matching closing bracket.
|
|
406
|
+
*/
|
|
407
|
+
function findMatchingBracket(str: string, start: number): number {
|
|
408
|
+
let depth = 0;
|
|
409
|
+
|
|
410
|
+
for (let i = start; i < str.length; i++) {
|
|
411
|
+
if (str[i] === '[') depth++;
|
|
412
|
+
else if (str[i] === ']') {
|
|
413
|
+
depth--;
|
|
414
|
+
if (depth === 0) return i;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
return str.length - 1;
|
|
419
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { renderExplicit } from './explicit-renderer';
|
|
3
|
+
import { parseExplicit } from './explicit-parser';
|
|
4
|
+
import {
|
|
5
|
+
createCommandNode,
|
|
6
|
+
createSelector,
|
|
7
|
+
createLiteral,
|
|
8
|
+
createReference,
|
|
9
|
+
createFlag,
|
|
10
|
+
createEventHandlerNode,
|
|
11
|
+
createCompoundNode,
|
|
12
|
+
} from '../core/types';
|
|
13
|
+
|
|
14
|
+
describe('renderExplicit', () => {
|
|
15
|
+
it('renders a basic command', () => {
|
|
16
|
+
const node = createCommandNode('toggle', {
|
|
17
|
+
patient: createSelector('.active'),
|
|
18
|
+
});
|
|
19
|
+
expect(renderExplicit(node)).toBe('[toggle patient:.active]');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('renders multiple roles', () => {
|
|
23
|
+
const node = createCommandNode('put', {
|
|
24
|
+
patient: createLiteral('hello', 'string'),
|
|
25
|
+
destination: createSelector('#output', 'id'),
|
|
26
|
+
});
|
|
27
|
+
const result = renderExplicit(node);
|
|
28
|
+
expect(result).toContain('put');
|
|
29
|
+
expect(result).toContain('patient:"hello"');
|
|
30
|
+
expect(result).toContain('destination:#output');
|
|
31
|
+
expect(result.startsWith('[')).toBe(true);
|
|
32
|
+
expect(result.endsWith(']')).toBe(true);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('renders reference values', () => {
|
|
36
|
+
const node = createCommandNode('add', {
|
|
37
|
+
patient: createSelector('.clicked'),
|
|
38
|
+
destination: createReference('me'),
|
|
39
|
+
});
|
|
40
|
+
expect(renderExplicit(node)).toBe('[add patient:.clicked destination:me]');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('renders numeric values', () => {
|
|
44
|
+
const node = createCommandNode('increment', {
|
|
45
|
+
destination: createSelector('#count', 'id'),
|
|
46
|
+
quantity: createLiteral(5, 'number'),
|
|
47
|
+
});
|
|
48
|
+
expect(renderExplicit(node)).toBe('[increment destination:#count quantity:5]');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('renders boolean values', () => {
|
|
52
|
+
const node = createCommandNode('set', {
|
|
53
|
+
destination: createLiteral('myVar', 'string'),
|
|
54
|
+
goal: createLiteral(true, 'boolean'),
|
|
55
|
+
});
|
|
56
|
+
const result = renderExplicit(node);
|
|
57
|
+
expect(result).toContain('goal:true');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('renders event handler with body', () => {
|
|
61
|
+
const bodyNode = createCommandNode('toggle', {
|
|
62
|
+
patient: createSelector('.active'),
|
|
63
|
+
});
|
|
64
|
+
const node = createEventHandlerNode('on', { event: createLiteral('click', 'string') }, [
|
|
65
|
+
bodyNode,
|
|
66
|
+
]);
|
|
67
|
+
const result = renderExplicit(node);
|
|
68
|
+
expect(result).toContain('[on event:"click"');
|
|
69
|
+
expect(result).toContain('body:[toggle patient:.active]');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('renders compound nodes', () => {
|
|
73
|
+
const node1 = createCommandNode('add', {
|
|
74
|
+
patient: createSelector('.loading'),
|
|
75
|
+
});
|
|
76
|
+
const node2 = createCommandNode('fetch', {
|
|
77
|
+
source: createLiteral('/api/data', 'string'),
|
|
78
|
+
});
|
|
79
|
+
const compound = createCompoundNode([node1, node2], 'then');
|
|
80
|
+
const result = renderExplicit(compound);
|
|
81
|
+
expect(result).toBe('[add patient:.loading] then [fetch source:"/api/data"]');
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe('renderExplicit — flags', () => {
|
|
86
|
+
it('renders enabled flags as +name', () => {
|
|
87
|
+
const node = createCommandNode('column', {
|
|
88
|
+
name: createLiteral('id', 'string'),
|
|
89
|
+
'primary-key': createFlag('primary-key', true),
|
|
90
|
+
});
|
|
91
|
+
const result = renderExplicit(node);
|
|
92
|
+
expect(result).toContain('+primary-key');
|
|
93
|
+
expect(result).not.toContain('primary-key:');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('renders disabled flags as ~name', () => {
|
|
97
|
+
const node = createCommandNode('field', {
|
|
98
|
+
name: createLiteral('email', 'string'),
|
|
99
|
+
nullable: createFlag('nullable', false),
|
|
100
|
+
});
|
|
101
|
+
const result = renderExplicit(node);
|
|
102
|
+
expect(result).toContain('~nullable');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('renders flags alongside role:value pairs', () => {
|
|
106
|
+
const node = createCommandNode('column', {
|
|
107
|
+
name: createLiteral('id', 'string'),
|
|
108
|
+
type: createLiteral('uuid', 'string'),
|
|
109
|
+
'primary-key': createFlag('primary-key', true),
|
|
110
|
+
'not-null': createFlag('not-null', true),
|
|
111
|
+
});
|
|
112
|
+
const result = renderExplicit(node);
|
|
113
|
+
expect(result).toContain('name:"id"');
|
|
114
|
+
expect(result).toContain('type:"uuid"');
|
|
115
|
+
expect(result).toContain('+primary-key');
|
|
116
|
+
expect(result).toContain('+not-null');
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe('round-trip: parse → render', () => {
|
|
121
|
+
const cases = [
|
|
122
|
+
'[toggle patient:.active]',
|
|
123
|
+
'[add patient:.highlight destination:#output]',
|
|
124
|
+
'[increment destination:#count quantity:5]',
|
|
125
|
+
'[wait duration:500ms]',
|
|
126
|
+
'[fetch source:/api/data responseType:json]',
|
|
127
|
+
'[column name:id +primary-key +not-null]',
|
|
128
|
+
'[field name:email +required ~nullable]',
|
|
129
|
+
];
|
|
130
|
+
|
|
131
|
+
for (const input of cases) {
|
|
132
|
+
it(`round-trips: ${input}`, () => {
|
|
133
|
+
const node = parseExplicit(input);
|
|
134
|
+
const output = renderExplicit(node);
|
|
135
|
+
// Re-parse to verify semantic equivalence
|
|
136
|
+
const reparsed = parseExplicit(output);
|
|
137
|
+
expect(reparsed.action).toBe(node.action);
|
|
138
|
+
expect(reparsed.roles.size).toBe(node.roles.size);
|
|
139
|
+
for (const [role, value] of node.roles) {
|
|
140
|
+
expect(reparsed.roles.get(role)).toEqual(value);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
});
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Explicit Syntax Renderer
|
|
3
|
+
*
|
|
4
|
+
* Serializes SemanticNode to the universal [command role:value ...] bracket syntax.
|
|
5
|
+
* Zero dependencies beyond core types — no language-specific logic.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type {
|
|
9
|
+
SemanticNode,
|
|
10
|
+
SemanticValue,
|
|
11
|
+
CommandSemanticNode,
|
|
12
|
+
CompoundSemanticNode,
|
|
13
|
+
EventHandlerSemanticNode,
|
|
14
|
+
ConditionalSemanticNode,
|
|
15
|
+
LoopSemanticNode,
|
|
16
|
+
} from '../core/types';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Render a semantic node as explicit bracket syntax.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```typescript
|
|
23
|
+
* renderExplicit(node) // "[toggle patient:.active destination:#button]"
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export function renderExplicit(node: SemanticNode): string {
|
|
27
|
+
// Handle compound nodes
|
|
28
|
+
if (node.kind === 'compound') {
|
|
29
|
+
const compoundNode = node as CompoundSemanticNode;
|
|
30
|
+
const renderedStatements = compoundNode.statements.map(stmt => renderExplicit(stmt));
|
|
31
|
+
return renderedStatements.join(` ${compoundNode.chainType} `);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const parts: string[] = [node.action];
|
|
35
|
+
|
|
36
|
+
// Add roles
|
|
37
|
+
for (const [role, value] of node.roles) {
|
|
38
|
+
if (value.type === 'flag') {
|
|
39
|
+
parts.push(value.enabled ? `+${value.name}` : `~${value.name}`);
|
|
40
|
+
} else {
|
|
41
|
+
parts.push(`${role}:${valueToString(value)}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Handle event handler body
|
|
46
|
+
if (node.kind === 'event-handler') {
|
|
47
|
+
const eventNode = node as EventHandlerSemanticNode;
|
|
48
|
+
if (eventNode.body && eventNode.body.length > 0) {
|
|
49
|
+
const bodyParts = eventNode.body.map(n => renderExplicit(n));
|
|
50
|
+
parts.push(`body:${bodyParts.join(' ')}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Handle conditional branches (v1.1)
|
|
55
|
+
if (node.kind === 'conditional') {
|
|
56
|
+
const condNode = node as ConditionalSemanticNode;
|
|
57
|
+
if (condNode.thenBranch && condNode.thenBranch.length > 0) {
|
|
58
|
+
const thenParts = condNode.thenBranch.map(n => renderExplicit(n));
|
|
59
|
+
parts.push(`then:${thenParts.join(' ')}`);
|
|
60
|
+
}
|
|
61
|
+
if (condNode.elseBranch && condNode.elseBranch.length > 0) {
|
|
62
|
+
const elseParts = condNode.elseBranch.map(n => renderExplicit(n));
|
|
63
|
+
parts.push(`else:${elseParts.join(' ')}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Handle loop body and metadata (v1.1)
|
|
68
|
+
if (node.kind === 'loop') {
|
|
69
|
+
const loopNode = node as LoopSemanticNode;
|
|
70
|
+
if (loopNode.loopVariant) {
|
|
71
|
+
parts.push(`loop-variant:${loopNode.loopVariant}`);
|
|
72
|
+
}
|
|
73
|
+
if (loopNode.loopVariable) {
|
|
74
|
+
parts.push(`variable:${loopNode.loopVariable}`);
|
|
75
|
+
}
|
|
76
|
+
if (loopNode.indexVariable) {
|
|
77
|
+
parts.push(`index-variable:${loopNode.indexVariable}`);
|
|
78
|
+
}
|
|
79
|
+
if (loopNode.body && loopNode.body.length > 0) {
|
|
80
|
+
const bodyParts = loopNode.body.map(n => renderExplicit(n));
|
|
81
|
+
parts.push(`loop-body:${bodyParts.join(' ')}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Handle v1.2 command fields (try/catch/finally, async, match)
|
|
86
|
+
if (node.kind === 'command') {
|
|
87
|
+
const cmd = node as CommandSemanticNode;
|
|
88
|
+
if (cmd.body && cmd.body.length > 0) {
|
|
89
|
+
const bodyParts = cmd.body.map(n => renderExplicit(n));
|
|
90
|
+
parts.push(`body:${bodyParts.join(' ')}`);
|
|
91
|
+
}
|
|
92
|
+
if (cmd.catchBranch && cmd.catchBranch.length > 0) {
|
|
93
|
+
const catchParts = cmd.catchBranch.map(n => renderExplicit(n));
|
|
94
|
+
parts.push(`catch:${catchParts.join(' ')}`);
|
|
95
|
+
}
|
|
96
|
+
if (cmd.finallyBranch && cmd.finallyBranch.length > 0) {
|
|
97
|
+
const finallyParts = cmd.finallyBranch.map(n => renderExplicit(n));
|
|
98
|
+
parts.push(`finally:${finallyParts.join(' ')}`);
|
|
99
|
+
}
|
|
100
|
+
if (cmd.asyncVariant) {
|
|
101
|
+
parts.push(`async-variant:${cmd.asyncVariant}`);
|
|
102
|
+
}
|
|
103
|
+
if (cmd.asyncBody && cmd.asyncBody.length > 0) {
|
|
104
|
+
const asyncParts = cmd.asyncBody.map(n => renderExplicit(n));
|
|
105
|
+
parts.push(`async-body:${asyncParts.join(' ')}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return `[${parts.join(' ')}]`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// =============================================================================
|
|
113
|
+
// Internal Helpers
|
|
114
|
+
// =============================================================================
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Convert a semantic value to its explicit syntax string form.
|
|
118
|
+
*/
|
|
119
|
+
function valueToString(value: SemanticValue): string {
|
|
120
|
+
switch (value.type) {
|
|
121
|
+
case 'literal':
|
|
122
|
+
if (typeof value.value === 'string') {
|
|
123
|
+
// Quote strings that contain spaces or are explicitly typed as strings
|
|
124
|
+
if (value.dataType === 'string' || /\s/.test(value.value)) {
|
|
125
|
+
return `"${value.value}"`;
|
|
126
|
+
}
|
|
127
|
+
return value.value;
|
|
128
|
+
}
|
|
129
|
+
return String(value.value);
|
|
130
|
+
|
|
131
|
+
case 'selector':
|
|
132
|
+
return value.value;
|
|
133
|
+
|
|
134
|
+
case 'reference':
|
|
135
|
+
return value.value;
|
|
136
|
+
|
|
137
|
+
case 'property-path':
|
|
138
|
+
return `${valueToString(value.object)}'s ${value.property}`;
|
|
139
|
+
|
|
140
|
+
case 'expression':
|
|
141
|
+
return value.raw;
|
|
142
|
+
|
|
143
|
+
case 'flag':
|
|
144
|
+
return value.name;
|
|
145
|
+
}
|
|
146
|
+
}
|