@mmapp/player-core 0.1.0-alpha.1
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/index.d.mts +1436 -0
- package/dist/index.d.ts +1436 -0
- package/dist/index.js +4828 -0
- package/dist/index.mjs +4762 -0
- package/package.json +35 -0
- package/package.json.backup +35 -0
- package/src/__tests__/actions.test.ts +187 -0
- package/src/__tests__/blueprint-e2e.test.ts +706 -0
- package/src/__tests__/blueprint-test-runner.test.ts +680 -0
- package/src/__tests__/core-functions.test.ts +78 -0
- package/src/__tests__/dsl-compiler.test.ts +1382 -0
- package/src/__tests__/dsl-grammar.test.ts +1682 -0
- package/src/__tests__/events.test.ts +200 -0
- package/src/__tests__/expression.test.ts +296 -0
- package/src/__tests__/failure-policies.test.ts +110 -0
- package/src/__tests__/frontend-context.test.ts +182 -0
- package/src/__tests__/integration.test.ts +256 -0
- package/src/__tests__/security.test.ts +190 -0
- package/src/__tests__/state-machine.test.ts +450 -0
- package/src/__tests__/testing-engine.test.ts +671 -0
- package/src/actions/dispatcher.ts +80 -0
- package/src/actions/index.ts +7 -0
- package/src/actions/types.ts +25 -0
- package/src/dsl/compiler/component-mapper.ts +289 -0
- package/src/dsl/compiler/field-mapper.ts +187 -0
- package/src/dsl/compiler/index.ts +82 -0
- package/src/dsl/compiler/manifest-compiler.ts +76 -0
- package/src/dsl/compiler/symbol-table.ts +214 -0
- package/src/dsl/compiler/utils.ts +48 -0
- package/src/dsl/compiler/view-compiler.ts +286 -0
- package/src/dsl/compiler/workflow-compiler.ts +600 -0
- package/src/dsl/index.ts +66 -0
- package/src/dsl/ir-migration.ts +221 -0
- package/src/dsl/ir-types.ts +416 -0
- package/src/dsl/lexer.ts +579 -0
- package/src/dsl/parser.ts +115 -0
- package/src/dsl/types.ts +256 -0
- package/src/events/event-bus.ts +68 -0
- package/src/events/index.ts +9 -0
- package/src/events/pattern-matcher.ts +61 -0
- package/src/events/types.ts +27 -0
- package/src/expression/evaluator.ts +676 -0
- package/src/expression/functions.ts +214 -0
- package/src/expression/index.ts +13 -0
- package/src/expression/types.ts +64 -0
- package/src/index.ts +61 -0
- package/src/state-machine/index.ts +16 -0
- package/src/state-machine/interpreter.ts +319 -0
- package/src/state-machine/types.ts +89 -0
- package/src/testing/action-trace.ts +209 -0
- package/src/testing/blueprint-test-runner.ts +214 -0
- package/src/testing/graph-walker.ts +249 -0
- package/src/testing/index.ts +69 -0
- package/src/testing/nrt-comparator.ts +199 -0
- package/src/testing/nrt-types.ts +230 -0
- package/src/testing/test-actions.ts +645 -0
- package/src/testing/test-compiler.ts +278 -0
- package/src/testing/test-runner.ts +444 -0
- package/src/testing/types.ts +231 -0
- package/src/validation/definition-validator.ts +812 -0
- package/src/validation/index.ts +13 -0
- package/tsconfig.json +26 -0
- package/vitest.config.ts +8 -0
|
@@ -0,0 +1,600 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow Compiler — Pass 2 of compilation.
|
|
3
|
+
*
|
|
4
|
+
* Transforms ThingSymbol → IRWorkflowDefinition.
|
|
5
|
+
* Handles states, transitions, fields, roles, events, and expressions.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ASTNode } from '../types';
|
|
9
|
+
import type {
|
|
10
|
+
IRWorkflowDefinition,
|
|
11
|
+
IRStateDefinition,
|
|
12
|
+
IRTransitionDefinition,
|
|
13
|
+
IRFieldDefinition,
|
|
14
|
+
IRActionDefinition,
|
|
15
|
+
IRConditionDefinition,
|
|
16
|
+
IROnEventSubscription,
|
|
17
|
+
IROnEventAction,
|
|
18
|
+
IRRoleDefinition,
|
|
19
|
+
IRStateType,
|
|
20
|
+
CompilerError,
|
|
21
|
+
} from '../ir-types';
|
|
22
|
+
import type { SymbolTable, ThingSymbol } from './symbol-table';
|
|
23
|
+
import { mapField } from './field-mapper';
|
|
24
|
+
import { slugify, snakeCase, generateActionId } from './utils';
|
|
25
|
+
|
|
26
|
+
// =============================================================================
|
|
27
|
+
// Public API
|
|
28
|
+
// =============================================================================
|
|
29
|
+
|
|
30
|
+
export function compileWorkflows(
|
|
31
|
+
symbols: SymbolTable,
|
|
32
|
+
): { workflows: IRWorkflowDefinition[]; errors: CompilerError[] } {
|
|
33
|
+
const workflows: IRWorkflowDefinition[] = [];
|
|
34
|
+
const errors: CompilerError[] = [];
|
|
35
|
+
|
|
36
|
+
for (const [, thing] of symbols.things) {
|
|
37
|
+
const { workflow, errors: wfErrors } = compileThing(thing);
|
|
38
|
+
workflows.push(workflow);
|
|
39
|
+
errors.push(...wfErrors);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return { workflows, errors };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// =============================================================================
|
|
46
|
+
// Thing → Workflow
|
|
47
|
+
// =============================================================================
|
|
48
|
+
|
|
49
|
+
function compileThing(
|
|
50
|
+
thing: ThingSymbol,
|
|
51
|
+
): { workflow: IRWorkflowDefinition; errors: CompilerError[] } {
|
|
52
|
+
const errors: CompilerError[] = [];
|
|
53
|
+
const slug = slugify(thing.name);
|
|
54
|
+
|
|
55
|
+
// Collect field names for expression resolution
|
|
56
|
+
const fieldNames = new Set(
|
|
57
|
+
thing.fields.map(f => {
|
|
58
|
+
const fd = f.token.data;
|
|
59
|
+
return fd.type === 'field_def' ? fd.name : '';
|
|
60
|
+
}).filter(Boolean),
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
// Compile fields
|
|
64
|
+
const fields = compileFields(thing.fields);
|
|
65
|
+
|
|
66
|
+
// Compile states
|
|
67
|
+
const stateNames = new Set(
|
|
68
|
+
thing.states.map(s => {
|
|
69
|
+
const sd = s.token.data;
|
|
70
|
+
return sd.type === 'state_decl' ? sd.name : '';
|
|
71
|
+
}).filter(Boolean),
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const states = compileStates(thing, fieldNames);
|
|
75
|
+
|
|
76
|
+
// Compile transitions
|
|
77
|
+
const transitions = compileTransitions(thing, fieldNames, stateNames, errors);
|
|
78
|
+
|
|
79
|
+
// Collect roles from transition guards
|
|
80
|
+
const roles = collectRoles(thing);
|
|
81
|
+
|
|
82
|
+
// Validate starts_at
|
|
83
|
+
if (!thing.startsAt) {
|
|
84
|
+
errors.push({
|
|
85
|
+
code: 'MISSING_STARTS_AT',
|
|
86
|
+
message: `Workflow "${thing.name}" has no starts_at declaration`,
|
|
87
|
+
lineNumber: thing.node.token.lineNumber,
|
|
88
|
+
severity: 'error',
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Tags
|
|
93
|
+
const tags = thing.tags.length > 0
|
|
94
|
+
? thing.tags.map(t => ({ tag_name: t }))
|
|
95
|
+
: undefined;
|
|
96
|
+
|
|
97
|
+
// Metadata (levels)
|
|
98
|
+
let metadata: Record<string, unknown> | undefined;
|
|
99
|
+
if (thing.levels.length > 0) {
|
|
100
|
+
metadata = {
|
|
101
|
+
levels: thing.levels.map(l => {
|
|
102
|
+
const ld = l.token.data;
|
|
103
|
+
if (ld.type === 'level_def') {
|
|
104
|
+
return { level: ld.level, title: ld.title, fromXp: ld.fromXp };
|
|
105
|
+
}
|
|
106
|
+
return null;
|
|
107
|
+
}).filter(Boolean),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
workflow: {
|
|
113
|
+
slug,
|
|
114
|
+
name: thing.name,
|
|
115
|
+
version: thing.version,
|
|
116
|
+
category: 'blueprint',
|
|
117
|
+
states,
|
|
118
|
+
transitions,
|
|
119
|
+
fields,
|
|
120
|
+
roles,
|
|
121
|
+
tags,
|
|
122
|
+
metadata,
|
|
123
|
+
},
|
|
124
|
+
errors,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// =============================================================================
|
|
129
|
+
// Fields
|
|
130
|
+
// =============================================================================
|
|
131
|
+
|
|
132
|
+
function compileFields(fieldNodes: ASTNode[]): IRFieldDefinition[] {
|
|
133
|
+
return fieldNodes.map(node => {
|
|
134
|
+
const data = node.token.data;
|
|
135
|
+
if (data.type !== 'field_def') return null;
|
|
136
|
+
return mapField(data);
|
|
137
|
+
}).filter((f): f is IRFieldDefinition => f !== null);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// =============================================================================
|
|
141
|
+
// States
|
|
142
|
+
// =============================================================================
|
|
143
|
+
|
|
144
|
+
function compileStates(
|
|
145
|
+
thing: ThingSymbol,
|
|
146
|
+
fieldNames: Set<string>,
|
|
147
|
+
): IRStateDefinition[] {
|
|
148
|
+
return thing.states.map(stateNode => {
|
|
149
|
+
const data = stateNode.token.data;
|
|
150
|
+
if (data.type !== 'state_decl') {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const stateName = data.name;
|
|
155
|
+
const stateType = resolveStateType(stateName, data.isFinal, thing.startsAt);
|
|
156
|
+
|
|
157
|
+
// Collect on_enter actions from "when entered" children
|
|
158
|
+
const onEnterActions: IRActionDefinition[] = [];
|
|
159
|
+
const onEventSubs: IROnEventSubscription[] = [];
|
|
160
|
+
|
|
161
|
+
for (const child of stateNode.children) {
|
|
162
|
+
const cd = child.token.data;
|
|
163
|
+
if (cd.type === 'when') {
|
|
164
|
+
if (cd.condition === 'entered') {
|
|
165
|
+
// on_enter: collect set_action/do_action children
|
|
166
|
+
const actions = compileWhenEnteredActions(child, stateName, fieldNames);
|
|
167
|
+
onEnterActions.push(...actions);
|
|
168
|
+
} else if (cd.condition.startsWith('receives ')) {
|
|
169
|
+
// on_event subscription
|
|
170
|
+
const sub = compileOnEvent(child, stateName, fieldNames);
|
|
171
|
+
if (sub) onEventSubs.push(sub);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const state: IRStateDefinition = {
|
|
177
|
+
name: stateName,
|
|
178
|
+
type: stateType,
|
|
179
|
+
on_enter: onEnterActions,
|
|
180
|
+
during: [],
|
|
181
|
+
on_exit: [],
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
if (onEventSubs.length > 0) {
|
|
185
|
+
state.on_event = onEventSubs;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return state;
|
|
189
|
+
}).filter((s): s is IRStateDefinition => s !== null);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function resolveStateType(
|
|
193
|
+
name: string,
|
|
194
|
+
isFinal: boolean,
|
|
195
|
+
startsAt?: string,
|
|
196
|
+
): IRStateType {
|
|
197
|
+
if (startsAt && name === startsAt) return 'START';
|
|
198
|
+
if (isFinal && name.toLowerCase().includes('cancel')) return 'CANCELLED';
|
|
199
|
+
if (isFinal) return 'END';
|
|
200
|
+
return 'REGULAR';
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// =============================================================================
|
|
204
|
+
// When Entered → on_enter actions
|
|
205
|
+
// =============================================================================
|
|
206
|
+
|
|
207
|
+
function compileWhenEnteredActions(
|
|
208
|
+
whenNode: ASTNode,
|
|
209
|
+
stateName: string,
|
|
210
|
+
fieldNames: Set<string>,
|
|
211
|
+
): IRActionDefinition[] {
|
|
212
|
+
const context = `${slugify(stateName)}-on-enter`;
|
|
213
|
+
return whenNode.children.map((child, i) => {
|
|
214
|
+
const cd = child.token.data;
|
|
215
|
+
if (cd.type === 'set_action') {
|
|
216
|
+
return {
|
|
217
|
+
id: generateActionId(context, i),
|
|
218
|
+
type: 'set_field',
|
|
219
|
+
mode: 'auto' as const,
|
|
220
|
+
config: {
|
|
221
|
+
field: snakeCase(cd.field),
|
|
222
|
+
expression: transformExpression(cd.expression, fieldNames),
|
|
223
|
+
},
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
if (cd.type === 'do_action') {
|
|
227
|
+
return {
|
|
228
|
+
id: generateActionId(context, i),
|
|
229
|
+
type: cd.action,
|
|
230
|
+
mode: 'auto' as const,
|
|
231
|
+
config: {},
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
return null;
|
|
235
|
+
}).filter((a): a is IRActionDefinition => a !== null);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// =============================================================================
|
|
239
|
+
// When Receives → on_event subscription
|
|
240
|
+
// =============================================================================
|
|
241
|
+
|
|
242
|
+
function compileOnEvent(
|
|
243
|
+
whenNode: ASTNode,
|
|
244
|
+
stateName: string,
|
|
245
|
+
fieldNames: Set<string>,
|
|
246
|
+
): IROnEventSubscription | null {
|
|
247
|
+
const cd = whenNode.token.data;
|
|
248
|
+
if (cd.type !== 'when') return null;
|
|
249
|
+
|
|
250
|
+
// Parse: receives "event name" from source
|
|
251
|
+
const match = cd.condition.match(/^receives\s+"([^"]+)"\s+from\s+(\S+)$/);
|
|
252
|
+
if (!match) return null;
|
|
253
|
+
|
|
254
|
+
const eventName = match[1];
|
|
255
|
+
const sourceSlug = slugify(match[2]);
|
|
256
|
+
|
|
257
|
+
// Build topic pattern: *:{{ entity_id }}:<source-slug>:instance.<event-slug>
|
|
258
|
+
const eventSlug = eventName.replace(/\s+/g, '.');
|
|
259
|
+
const topicPattern = `*:{{ entity_id }}:${sourceSlug}:instance.${eventSlug}`;
|
|
260
|
+
|
|
261
|
+
// Compile child set_actions into on_event actions
|
|
262
|
+
const actions: IROnEventAction[] = whenNode.children.map(child => {
|
|
263
|
+
const acd = child.token.data;
|
|
264
|
+
if (acd.type === 'set_action') {
|
|
265
|
+
return {
|
|
266
|
+
type: 'set_field' as const,
|
|
267
|
+
field: snakeCase(acd.field),
|
|
268
|
+
expression: transformExpression(acd.expression, fieldNames),
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
return null;
|
|
272
|
+
}).filter((a): a is IROnEventAction => a !== null);
|
|
273
|
+
|
|
274
|
+
return {
|
|
275
|
+
match: topicPattern,
|
|
276
|
+
description: `On ${eventName} from ${match[2]}`,
|
|
277
|
+
actions,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// =============================================================================
|
|
282
|
+
// Transitions
|
|
283
|
+
// =============================================================================
|
|
284
|
+
|
|
285
|
+
function compileTransitions(
|
|
286
|
+
thing: ThingSymbol,
|
|
287
|
+
fieldNames: Set<string>,
|
|
288
|
+
stateNames: Set<string>,
|
|
289
|
+
errors: CompilerError[],
|
|
290
|
+
): IRTransitionDefinition[] {
|
|
291
|
+
const transitions: IRTransitionDefinition[] = [];
|
|
292
|
+
|
|
293
|
+
for (const stateNode of thing.states) {
|
|
294
|
+
const stateData = stateNode.token.data;
|
|
295
|
+
if (stateData.type !== 'state_decl') continue;
|
|
296
|
+
const fromState = stateData.name;
|
|
297
|
+
|
|
298
|
+
for (const child of stateNode.children) {
|
|
299
|
+
const cd = child.token.data;
|
|
300
|
+
if (cd.type !== 'transition') continue;
|
|
301
|
+
|
|
302
|
+
const isAuto = cd.verb.startsWith('auto ');
|
|
303
|
+
const transitionName = slugify(cd.verb);
|
|
304
|
+
|
|
305
|
+
// Validate target state
|
|
306
|
+
if (!stateNames.has(cd.target)) {
|
|
307
|
+
errors.push({
|
|
308
|
+
code: 'UNKNOWN_TARGET_STATE',
|
|
309
|
+
message: `Transition "${cd.verb}" targets unknown state "${cd.target}"`,
|
|
310
|
+
lineNumber: child.token.lineNumber,
|
|
311
|
+
severity: 'warning',
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Parse guard → roles
|
|
316
|
+
let roles: string[] | undefined;
|
|
317
|
+
if (cd.guard) {
|
|
318
|
+
roles = parseGuard(cd.guard);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Compile conditions from nested `when` children
|
|
322
|
+
const conditions = compileTransitionConditions(child, fieldNames);
|
|
323
|
+
|
|
324
|
+
// Compile actions from nested `set_action`/`do_action` children
|
|
325
|
+
const actions = compileTransitionActions(child, transitionName, fieldNames);
|
|
326
|
+
|
|
327
|
+
const transition: IRTransitionDefinition = {
|
|
328
|
+
name: transitionName,
|
|
329
|
+
from: [fromState],
|
|
330
|
+
to: cd.target,
|
|
331
|
+
actions,
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
if (isAuto) transition.auto = true;
|
|
335
|
+
if (roles && roles.length > 0) transition.roles = roles;
|
|
336
|
+
if (conditions && conditions.length > 0) transition.conditions = conditions;
|
|
337
|
+
|
|
338
|
+
transitions.push(transition);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return transitions;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function parseGuard(guard: string): string[] {
|
|
346
|
+
// "admin only" → ['admin']
|
|
347
|
+
// "owner only" → ['owner']
|
|
348
|
+
// "admin and owner" → ['admin', 'owner']
|
|
349
|
+
const cleaned = guard.replace(/\s+only$/, '');
|
|
350
|
+
return cleaned.split(/\s+and\s+/).map(r => r.trim());
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function compileTransitionConditions(
|
|
354
|
+
transitionNode: ASTNode,
|
|
355
|
+
fieldNames: Set<string>,
|
|
356
|
+
): IRConditionDefinition[] | undefined {
|
|
357
|
+
const conditions: IRConditionDefinition[] = [];
|
|
358
|
+
|
|
359
|
+
for (const child of transitionNode.children) {
|
|
360
|
+
const cd = child.token.data;
|
|
361
|
+
if (cd.type === 'when') {
|
|
362
|
+
const parsed = parseConditionExpression(cd.condition, fieldNames);
|
|
363
|
+
if (parsed) conditions.push(parsed);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return conditions.length > 0 ? conditions : undefined;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function compileTransitionActions(
|
|
371
|
+
transitionNode: ASTNode,
|
|
372
|
+
transitionName: string,
|
|
373
|
+
fieldNames: Set<string>,
|
|
374
|
+
): IRActionDefinition[] {
|
|
375
|
+
const actions: IRActionDefinition[] = [];
|
|
376
|
+
const context = `${transitionName}-action`;
|
|
377
|
+
|
|
378
|
+
for (const child of transitionNode.children) {
|
|
379
|
+
const cd = child.token.data;
|
|
380
|
+
if (cd.type === 'set_action') {
|
|
381
|
+
actions.push({
|
|
382
|
+
id: generateActionId(context, actions.length),
|
|
383
|
+
type: 'set_field',
|
|
384
|
+
mode: 'auto',
|
|
385
|
+
config: {
|
|
386
|
+
field: snakeCase(cd.field),
|
|
387
|
+
expression: transformExpression(cd.expression, fieldNames),
|
|
388
|
+
},
|
|
389
|
+
});
|
|
390
|
+
} else if (cd.type === 'do_action') {
|
|
391
|
+
actions.push({
|
|
392
|
+
id: generateActionId(context, actions.length),
|
|
393
|
+
type: cd.action,
|
|
394
|
+
mode: 'auto',
|
|
395
|
+
config: {},
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return actions;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// =============================================================================
|
|
404
|
+
// Condition Expression Parsing
|
|
405
|
+
// =============================================================================
|
|
406
|
+
|
|
407
|
+
function parseConditionExpression(
|
|
408
|
+
condition: string,
|
|
409
|
+
fieldNames: Set<string>,
|
|
410
|
+
): IRConditionDefinition | null {
|
|
411
|
+
// Handle compound "and" conditions
|
|
412
|
+
if (condition.includes(' and ')) {
|
|
413
|
+
const parts = condition.split(/\s+and\s+/);
|
|
414
|
+
const subConditions = parts
|
|
415
|
+
.map(p => parseSingleCondition(p.trim(), fieldNames))
|
|
416
|
+
.filter((c): c is IRConditionDefinition => c !== null);
|
|
417
|
+
|
|
418
|
+
if (subConditions.length > 1) {
|
|
419
|
+
return { AND: subConditions };
|
|
420
|
+
}
|
|
421
|
+
if (subConditions.length === 1) {
|
|
422
|
+
return subConditions[0];
|
|
423
|
+
}
|
|
424
|
+
return null;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
return parseSingleCondition(condition, fieldNames);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function parseSingleCondition(
|
|
431
|
+
expr: string,
|
|
432
|
+
fieldNames: Set<string>,
|
|
433
|
+
): IRConditionDefinition | null {
|
|
434
|
+
// Pattern: left operator right
|
|
435
|
+
const opMatch = expr.match(/^(.+?)\s*(>=|<=|>|<|==|!=)\s*(.+)$/);
|
|
436
|
+
if (!opMatch) return null;
|
|
437
|
+
|
|
438
|
+
const left = opMatch[1].trim();
|
|
439
|
+
const op = opMatch[2];
|
|
440
|
+
const right = opMatch[3].trim();
|
|
441
|
+
|
|
442
|
+
const operatorMap: Record<string, IRConditionDefinition['operator']> = {
|
|
443
|
+
'>=': 'gte',
|
|
444
|
+
'<=': 'lte',
|
|
445
|
+
'>': 'gt',
|
|
446
|
+
'<': 'lt',
|
|
447
|
+
'==': 'eq',
|
|
448
|
+
'!=': 'ne',
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
const operator = operatorMap[op];
|
|
452
|
+
if (!operator) return null;
|
|
453
|
+
|
|
454
|
+
const leftField = resolveFieldRef(left, fieldNames);
|
|
455
|
+
const rightValue = resolveConditionValue(right, fieldNames);
|
|
456
|
+
|
|
457
|
+
const condition: IRConditionDefinition = {
|
|
458
|
+
field: leftField,
|
|
459
|
+
operator,
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
if (typeof rightValue === 'number') {
|
|
463
|
+
condition.value = rightValue;
|
|
464
|
+
} else {
|
|
465
|
+
condition.expression = rightValue;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
return condition;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function resolveFieldRef(name: string, fieldNames: Set<string>): string {
|
|
472
|
+
// Check if name matches a known field
|
|
473
|
+
if (fieldNames.has(name)) {
|
|
474
|
+
return `state_data.${snakeCase(name)}`;
|
|
475
|
+
}
|
|
476
|
+
// Try to find by partial match
|
|
477
|
+
for (const fn of fieldNames) {
|
|
478
|
+
if (fn === name) return `state_data.${snakeCase(fn)}`;
|
|
479
|
+
}
|
|
480
|
+
return `state_data.${snakeCase(name)}`;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function resolveConditionValue(
|
|
484
|
+
value: string,
|
|
485
|
+
fieldNames: Set<string>,
|
|
486
|
+
): number | string {
|
|
487
|
+
// Numeric literal
|
|
488
|
+
const num = Number(value);
|
|
489
|
+
if (!isNaN(num)) return num;
|
|
490
|
+
|
|
491
|
+
// Field reference
|
|
492
|
+
if (fieldNames.has(value)) {
|
|
493
|
+
return `state_data.${snakeCase(value)}`;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
return `state_data.${snakeCase(value)}`;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// =============================================================================
|
|
500
|
+
// Expression Transformation
|
|
501
|
+
// =============================================================================
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Transform a DSL expression into an IR expression.
|
|
505
|
+
*
|
|
506
|
+
* Rules:
|
|
507
|
+
* 1. String literals (quoted) → passthrough
|
|
508
|
+
* 2. now() → passthrough
|
|
509
|
+
* 3. "the event's X" → $event.state_data.<snake_case_X>
|
|
510
|
+
* 4. Known field names → state_data.<snake_case>
|
|
511
|
+
* 5. A + B → add(resolved_A, resolved_B)
|
|
512
|
+
* 6. A - B → subtract(resolved_A, resolved_B)
|
|
513
|
+
* 7. Numeric literals → passthrough
|
|
514
|
+
*/
|
|
515
|
+
export function transformExpression(
|
|
516
|
+
expression: string,
|
|
517
|
+
fieldNames: Set<string>,
|
|
518
|
+
): string {
|
|
519
|
+
const trimmed = expression.trim();
|
|
520
|
+
|
|
521
|
+
// String literal: "value"
|
|
522
|
+
if (trimmed.startsWith('"') && trimmed.endsWith('"')) {
|
|
523
|
+
return trimmed;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// now() passthrough
|
|
527
|
+
if (trimmed === 'now()') return 'now()';
|
|
528
|
+
|
|
529
|
+
// Arithmetic: A + B or A - B
|
|
530
|
+
const addMatch = trimmed.match(/^(.+?)\s*\+\s*(.+)$/);
|
|
531
|
+
if (addMatch) {
|
|
532
|
+
const left = resolveExpressionPart(addMatch[1].trim(), fieldNames);
|
|
533
|
+
const right = resolveExpressionPart(addMatch[2].trim(), fieldNames);
|
|
534
|
+
return `add(${left}, ${right})`;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const subMatch = trimmed.match(/^(.+?)\s*-\s*(.+)$/);
|
|
538
|
+
if (subMatch) {
|
|
539
|
+
const left = resolveExpressionPart(subMatch[1].trim(), fieldNames);
|
|
540
|
+
const right = resolveExpressionPart(subMatch[2].trim(), fieldNames);
|
|
541
|
+
return `subtract(${left}, ${right})`;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// Single value
|
|
545
|
+
return resolveExpressionPart(trimmed, fieldNames);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function resolveExpressionPart(
|
|
549
|
+
part: string,
|
|
550
|
+
fieldNames: Set<string>,
|
|
551
|
+
): string {
|
|
552
|
+
const trimmed = part.trim();
|
|
553
|
+
|
|
554
|
+
// Numeric literal
|
|
555
|
+
const num = Number(trimmed);
|
|
556
|
+
if (!isNaN(num) && trimmed !== '') return String(num);
|
|
557
|
+
|
|
558
|
+
// String literal
|
|
559
|
+
if (trimmed.startsWith('"') && trimmed.endsWith('"')) return trimmed;
|
|
560
|
+
|
|
561
|
+
// "the event's X" → $event.state_data.<snake>
|
|
562
|
+
const eventMatch = trimmed.match(/^the event's\s+(.+)$/);
|
|
563
|
+
if (eventMatch) {
|
|
564
|
+
return `$event.state_data.${snakeCase(eventMatch[1])}`;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// now() passthrough
|
|
568
|
+
if (trimmed === 'now()') return 'now()';
|
|
569
|
+
|
|
570
|
+
// Known field name → state_data.<snake>
|
|
571
|
+
if (fieldNames.has(trimmed)) {
|
|
572
|
+
return `state_data.${snakeCase(trimmed)}`;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// Default: treat as field reference
|
|
576
|
+
return `state_data.${snakeCase(trimmed)}`;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// =============================================================================
|
|
580
|
+
// Role Collection
|
|
581
|
+
// =============================================================================
|
|
582
|
+
|
|
583
|
+
function collectRoles(thing: ThingSymbol): IRRoleDefinition[] {
|
|
584
|
+
const roleNames = new Set<string>();
|
|
585
|
+
|
|
586
|
+
for (const stateNode of thing.states) {
|
|
587
|
+
for (const child of stateNode.children) {
|
|
588
|
+
const cd = child.token.data;
|
|
589
|
+
if (cd.type === 'transition' && cd.guard) {
|
|
590
|
+
const roles = parseGuard(cd.guard);
|
|
591
|
+
for (const r of roles) roleNames.add(r);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
return Array.from(roleNames).map(name => ({
|
|
597
|
+
name,
|
|
598
|
+
permissions: [`transition:${name}`],
|
|
599
|
+
}));
|
|
600
|
+
}
|
package/src/dsl/index.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
export { tokenize, tokenizeLine } from './lexer';
|
|
2
|
+
export { parse, walkTree, findByType } from './parser';
|
|
3
|
+
export type {
|
|
4
|
+
LineType,
|
|
5
|
+
LineToken,
|
|
6
|
+
LineData,
|
|
7
|
+
ASTNode,
|
|
8
|
+
ParseResult,
|
|
9
|
+
ParseError,
|
|
10
|
+
Emphasis,
|
|
11
|
+
Constraint,
|
|
12
|
+
SpaceDeclData,
|
|
13
|
+
ThingDeclData,
|
|
14
|
+
ThingRefData,
|
|
15
|
+
FragmentDefData,
|
|
16
|
+
FieldDefData,
|
|
17
|
+
StateDeclData,
|
|
18
|
+
StartsAtData,
|
|
19
|
+
TransitionData,
|
|
20
|
+
WhenData,
|
|
21
|
+
SetActionData,
|
|
22
|
+
DoActionData,
|
|
23
|
+
GoActionData,
|
|
24
|
+
TellActionData,
|
|
25
|
+
ShowActionData,
|
|
26
|
+
DataSourceData,
|
|
27
|
+
IterationData,
|
|
28
|
+
GroupingData,
|
|
29
|
+
ContentData,
|
|
30
|
+
StringLiteralData,
|
|
31
|
+
SearchData,
|
|
32
|
+
QualifierData,
|
|
33
|
+
NavigationData,
|
|
34
|
+
PathMappingData,
|
|
35
|
+
SectionData,
|
|
36
|
+
TaggedData,
|
|
37
|
+
LevelDefData,
|
|
38
|
+
} from './types';
|
|
39
|
+
|
|
40
|
+
export { compile, compileAST } from './compiler';
|
|
41
|
+
export type {
|
|
42
|
+
CompilationResult,
|
|
43
|
+
CompilerError,
|
|
44
|
+
CompilerErrorCode,
|
|
45
|
+
IRWorkflowDefinition,
|
|
46
|
+
IRStateDefinition,
|
|
47
|
+
IRTransitionDefinition,
|
|
48
|
+
IRFieldDefinition,
|
|
49
|
+
IRFieldValidation,
|
|
50
|
+
IRActionDefinition,
|
|
51
|
+
IRConditionDefinition,
|
|
52
|
+
IRExperienceNode,
|
|
53
|
+
IRDataSource,
|
|
54
|
+
IRWorkflowDataSource,
|
|
55
|
+
IRExperienceDefinition,
|
|
56
|
+
IRBlueprintManifest,
|
|
57
|
+
IROnEventSubscription,
|
|
58
|
+
IROnEventAction,
|
|
59
|
+
IRRoleDefinition,
|
|
60
|
+
IRStateType,
|
|
61
|
+
IRActionMode,
|
|
62
|
+
IRWorkflowFieldType,
|
|
63
|
+
PureFormWorkflow,
|
|
64
|
+
CompiledOutput,
|
|
65
|
+
} from './ir-types';
|
|
66
|
+
export { normalizeCategory } from './ir-types';
|