@lokascript/domain-flow 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.
@@ -0,0 +1,592 @@
1
+ /**
2
+ * FlowScript Tokenizers
3
+ *
4
+ * Language-specific tokenizers for data flow commands (4 languages).
5
+ * Created via the framework's createSimpleTokenizer factory.
6
+ *
7
+ * Custom extractors handle:
8
+ * - CSS selectors (#id, .class)
9
+ * - URL paths (/api/users, /api/user/{id})
10
+ * - Duration literals (5s, 30s, 1m, 500ms)
11
+ * - Latin extended identifiers (diacritics in Spanish)
12
+ */
13
+
14
+ import { createSimpleTokenizer } from '@lokascript/framework';
15
+ import type { LanguageTokenizer, ValueExtractor, ExtractionResult } from '@lokascript/framework';
16
+
17
+ // =============================================================================
18
+ // CSS Selector Extractor (#id, .class)
19
+ // =============================================================================
20
+
21
+ class CSSSelectorExtractor implements ValueExtractor {
22
+ readonly name = 'css-selector';
23
+
24
+ canExtract(input: string, position: number): boolean {
25
+ const char = input[position];
26
+ if (char !== '#' && char !== '.') return false;
27
+ const next = input[position + 1];
28
+ return next !== undefined && /[a-zA-Z_-]/.test(next);
29
+ }
30
+
31
+ extract(input: string, position: number): ExtractionResult | null {
32
+ let end = position + 1;
33
+ while (end < input.length && /[a-zA-Z0-9_-]/.test(input[end])) {
34
+ end++;
35
+ }
36
+ if (end === position + 1) return null;
37
+ return { value: input.slice(position, end), length: end - position };
38
+ }
39
+ }
40
+
41
+ // =============================================================================
42
+ // URL Path Extractor (/api/users, /api/user/{id})
43
+ // =============================================================================
44
+
45
+ class URLPathExtractor implements ValueExtractor {
46
+ readonly name = 'url-path';
47
+
48
+ canExtract(input: string, position: number): boolean {
49
+ if (input[position] !== '/') return false;
50
+ const next = input[position + 1];
51
+ // Must be followed by a letter, digit, or path char — not a space or operator
52
+ return next !== undefined && /[a-zA-Z0-9_:{]/.test(next);
53
+ }
54
+
55
+ extract(input: string, position: number): ExtractionResult | null {
56
+ let end = position + 1;
57
+ // URL path characters: letters, digits, /, -, _, ., {, }, :, ?, =, &
58
+ while (end < input.length && /[a-zA-Z0-9/_\-.{}:?=&]/.test(input[end])) {
59
+ end++;
60
+ }
61
+ if (end <= position + 1) return null;
62
+ return { value: input.slice(position, end), length: end - position };
63
+ }
64
+ }
65
+
66
+ // =============================================================================
67
+ // Duration Extractor (5s, 30s, 1m, 500ms)
68
+ // =============================================================================
69
+
70
+ class DurationExtractor implements ValueExtractor {
71
+ readonly name = 'duration';
72
+
73
+ canExtract(input: string, position: number): boolean {
74
+ return /[0-9]/.test(input[position]);
75
+ }
76
+
77
+ extract(input: string, position: number): ExtractionResult | null {
78
+ let end = position;
79
+ // Consume digits
80
+ while (end < input.length && /[0-9]/.test(input[end])) {
81
+ end++;
82
+ }
83
+ if (end === position) return null;
84
+
85
+ // Check for duration suffix: ms, s, m, h
86
+ const remaining = input.slice(end);
87
+ if (remaining.startsWith('ms')) {
88
+ end += 2;
89
+ } else if (/^[smh](?![a-zA-Z])/.test(remaining)) {
90
+ end += 1;
91
+ } else {
92
+ // Plain number — still valid as a literal
93
+ return { value: input.slice(position, end), length: end - position };
94
+ }
95
+
96
+ return { value: input.slice(position, end), length: end - position };
97
+ }
98
+ }
99
+
100
+ // =============================================================================
101
+ // Latin Extended Identifier Extractor (diacritics for Spanish)
102
+ // =============================================================================
103
+
104
+ class LatinExtendedIdentifierExtractor implements ValueExtractor {
105
+ readonly name = 'latin-extended-identifier';
106
+
107
+ canExtract(input: string, position: number): boolean {
108
+ return /\p{L}/u.test(input[position]);
109
+ }
110
+
111
+ extract(input: string, position: number): ExtractionResult | null {
112
+ let end = position;
113
+ while (end < input.length && /[\p{L}\p{N}_-]/u.test(input[end])) {
114
+ end++;
115
+ }
116
+ if (end === position) return null;
117
+ return { value: input.slice(position, end), length: end - position };
118
+ }
119
+ }
120
+
121
+ // =============================================================================
122
+ // Shared custom extractors
123
+ // =============================================================================
124
+
125
+ const sharedExtractors = [
126
+ new CSSSelectorExtractor(),
127
+ new URLPathExtractor(),
128
+ new DurationExtractor(),
129
+ ];
130
+
131
+ // =============================================================================
132
+ // English FlowScript Tokenizer
133
+ // =============================================================================
134
+
135
+ export const EnglishFlowTokenizer: LanguageTokenizer = createSimpleTokenizer({
136
+ language: 'en',
137
+ customExtractors: sharedExtractors,
138
+ keywords: [
139
+ // Commands
140
+ 'fetch',
141
+ 'poll',
142
+ 'stream',
143
+ 'submit',
144
+ 'transform',
145
+ // HATEOAS commands
146
+ 'enter',
147
+ 'follow',
148
+ 'perform',
149
+ 'capture',
150
+ // Role markers
151
+ 'as',
152
+ 'into',
153
+ 'every',
154
+ 'to',
155
+ 'with',
156
+ 'from',
157
+ 'item',
158
+ ],
159
+ includeOperators: false,
160
+ caseInsensitive: true,
161
+ });
162
+
163
+ // =============================================================================
164
+ // Spanish FlowScript Tokenizer
165
+ // =============================================================================
166
+
167
+ export const SpanishFlowTokenizer: LanguageTokenizer = createSimpleTokenizer({
168
+ language: 'es',
169
+ customExtractors: [...sharedExtractors, new LatinExtendedIdentifierExtractor()],
170
+ keywords: [
171
+ // Commands
172
+ 'obtener',
173
+ 'sondear',
174
+ 'transmitir',
175
+ 'enviar',
176
+ 'transformar',
177
+ // HATEOAS commands
178
+ 'entrar',
179
+ 'seguir',
180
+ 'ejecutar',
181
+ 'capturar',
182
+ // Role markers
183
+ 'como',
184
+ 'en',
185
+ 'cada',
186
+ 'a',
187
+ 'con',
188
+ 'de',
189
+ 'elemento',
190
+ ],
191
+ keywordExtras: [
192
+ { native: 'obtener', normalized: 'fetch' },
193
+ { native: 'sondear', normalized: 'poll' },
194
+ { native: 'transmitir', normalized: 'stream' },
195
+ { native: 'enviar', normalized: 'submit' },
196
+ { native: 'transformar', normalized: 'transform' },
197
+ { native: 'entrar', normalized: 'enter' },
198
+ { native: 'seguir', normalized: 'follow' },
199
+ { native: 'ejecutar', normalized: 'perform' },
200
+ { native: 'capturar', normalized: 'capture' },
201
+ { native: 'como', normalized: 'as' },
202
+ { native: 'en', normalized: 'into' },
203
+ { native: 'cada', normalized: 'every' },
204
+ { native: 'a', normalized: 'to' },
205
+ { native: 'con', normalized: 'with' },
206
+ { native: 'elemento', normalized: 'item' },
207
+ ],
208
+ keywordProfile: {
209
+ keywords: {
210
+ fetch: { primary: 'obtener' },
211
+ poll: { primary: 'sondear' },
212
+ stream: { primary: 'transmitir' },
213
+ submit: { primary: 'enviar' },
214
+ transform: { primary: 'transformar' },
215
+ enter: { primary: 'entrar' },
216
+ follow: { primary: 'seguir' },
217
+ perform: { primary: 'ejecutar' },
218
+ capture: { primary: 'capturar' },
219
+ },
220
+ },
221
+ includeOperators: false,
222
+ caseInsensitive: true,
223
+ });
224
+
225
+ // =============================================================================
226
+ // Japanese FlowScript Tokenizer
227
+ // =============================================================================
228
+
229
+ export const JapaneseFlowTokenizer: LanguageTokenizer = createSimpleTokenizer({
230
+ language: 'ja',
231
+ customExtractors: sharedExtractors,
232
+ keywords: [
233
+ // Commands
234
+ '取得',
235
+ 'ポーリング',
236
+ 'ストリーム',
237
+ '送信',
238
+ '変換',
239
+ // HATEOAS commands
240
+ '入る',
241
+ '辿る',
242
+ '実行',
243
+ '取得変数',
244
+ // Role markers / particles
245
+ 'で',
246
+ 'に',
247
+ 'ごとに',
248
+ 'を',
249
+ 'から',
250
+ 'の',
251
+ 'として',
252
+ ],
253
+ keywordExtras: [
254
+ { native: '取得', normalized: 'fetch' },
255
+ { native: 'ポーリング', normalized: 'poll' },
256
+ { native: 'ストリーム', normalized: 'stream' },
257
+ { native: '送信', normalized: 'submit' },
258
+ { native: '変換', normalized: 'transform' },
259
+ { native: '入る', normalized: 'enter' },
260
+ { native: '辿る', normalized: 'follow' },
261
+ { native: '実行', normalized: 'perform' },
262
+ { native: '取得変数', normalized: 'capture' },
263
+ { native: 'で', normalized: 'as' },
264
+ { native: 'に', normalized: 'into' },
265
+ { native: 'ごとに', normalized: 'every' },
266
+ { native: 'を', normalized: 'patient' },
267
+ { native: 'の', normalized: 'item' },
268
+ { native: 'として', normalized: 'as' },
269
+ ],
270
+ keywordProfile: {
271
+ keywords: {
272
+ fetch: { primary: '取得' },
273
+ poll: { primary: 'ポーリング' },
274
+ stream: { primary: 'ストリーム' },
275
+ submit: { primary: '送信' },
276
+ transform: { primary: '変換' },
277
+ enter: { primary: '入る' },
278
+ follow: { primary: '辿る' },
279
+ perform: { primary: '実行' },
280
+ capture: { primary: '取得変数' },
281
+ },
282
+ },
283
+ includeOperators: false,
284
+ caseInsensitive: false,
285
+ });
286
+
287
+ // =============================================================================
288
+ // Arabic FlowScript Tokenizer
289
+ // =============================================================================
290
+
291
+ export const ArabicFlowTokenizer: LanguageTokenizer = createSimpleTokenizer({
292
+ language: 'ar',
293
+ direction: 'rtl',
294
+ customExtractors: sharedExtractors,
295
+ keywords: [
296
+ // Commands
297
+ 'جلب',
298
+ 'استطلع',
299
+ 'بث',
300
+ 'أرسل',
301
+ 'حوّل',
302
+ // HATEOAS commands
303
+ 'ادخل',
304
+ 'اتبع',
305
+ 'نفّذ',
306
+ 'التقط',
307
+ // Role markers
308
+ 'ك',
309
+ 'في',
310
+ 'كل',
311
+ 'إلى',
312
+ 'ب',
313
+ 'من',
314
+ 'عنصر',
315
+ ],
316
+ keywordExtras: [
317
+ { native: 'جلب', normalized: 'fetch' },
318
+ { native: 'استطلع', normalized: 'poll' },
319
+ { native: 'بث', normalized: 'stream' },
320
+ { native: 'أرسل', normalized: 'submit' },
321
+ { native: 'حوّل', normalized: 'transform' },
322
+ { native: 'ادخل', normalized: 'enter' },
323
+ { native: 'اتبع', normalized: 'follow' },
324
+ { native: 'نفّذ', normalized: 'perform' },
325
+ { native: 'التقط', normalized: 'capture' },
326
+ { native: 'ك', normalized: 'as' },
327
+ { native: 'في', normalized: 'into' },
328
+ { native: 'كل', normalized: 'every' },
329
+ { native: 'إلى', normalized: 'to' },
330
+ { native: 'ب', normalized: 'with' },
331
+ { native: 'عنصر', normalized: 'item' },
332
+ ],
333
+ keywordProfile: {
334
+ keywords: {
335
+ fetch: { primary: 'جلب' },
336
+ poll: { primary: 'استطلع' },
337
+ stream: { primary: 'بث' },
338
+ submit: { primary: 'أرسل' },
339
+ transform: { primary: 'حوّل' },
340
+ enter: { primary: 'ادخل' },
341
+ follow: { primary: 'اتبع' },
342
+ perform: { primary: 'نفّذ' },
343
+ capture: { primary: 'التقط' },
344
+ },
345
+ },
346
+ includeOperators: false,
347
+ caseInsensitive: false,
348
+ });
349
+
350
+ // =============================================================================
351
+ // Korean FlowScript Tokenizer
352
+ // =============================================================================
353
+
354
+ export const KoreanFlowTokenizer: LanguageTokenizer = createSimpleTokenizer({
355
+ language: 'ko',
356
+ customExtractors: sharedExtractors,
357
+ keywords: [
358
+ // Commands
359
+ '가져오기',
360
+ '폴링',
361
+ '스트리밍',
362
+ '제출',
363
+ '변환',
364
+ // HATEOAS commands
365
+ '진입',
366
+ '따라가기',
367
+ '실행',
368
+ '캡처',
369
+ // Role markers / particles
370
+ '로',
371
+ '에',
372
+ '마다',
373
+ '를',
374
+ '에서',
375
+ '항목',
376
+ ],
377
+ keywordExtras: [
378
+ { native: '가져오기', normalized: 'fetch' },
379
+ { native: '폴링', normalized: 'poll' },
380
+ { native: '스트리밍', normalized: 'stream' },
381
+ { native: '제출', normalized: 'submit' },
382
+ { native: '변환', normalized: 'transform' },
383
+ { native: '진입', normalized: 'enter' },
384
+ { native: '따라가기', normalized: 'follow' },
385
+ { native: '실행', normalized: 'perform' },
386
+ { native: '캡처', normalized: 'capture' },
387
+ { native: '로', normalized: 'as' },
388
+ { native: '에', normalized: 'into' },
389
+ { native: '마다', normalized: 'every' },
390
+ { native: '를', normalized: 'patient' },
391
+ { native: '항목', normalized: 'item' },
392
+ ],
393
+ keywordProfile: {
394
+ keywords: {
395
+ fetch: { primary: '가져오기' },
396
+ poll: { primary: '폴링' },
397
+ stream: { primary: '스트리밍' },
398
+ submit: { primary: '제출' },
399
+ transform: { primary: '변환' },
400
+ enter: { primary: '진입' },
401
+ follow: { primary: '따라가기' },
402
+ perform: { primary: '실행' },
403
+ capture: { primary: '캡처' },
404
+ },
405
+ },
406
+ includeOperators: false,
407
+ caseInsensitive: false,
408
+ });
409
+
410
+ // =============================================================================
411
+ // Chinese FlowScript Tokenizer
412
+ // =============================================================================
413
+
414
+ export const ChineseFlowTokenizer: LanguageTokenizer = createSimpleTokenizer({
415
+ language: 'zh',
416
+ customExtractors: sharedExtractors,
417
+ keywords: [
418
+ // Commands
419
+ '获取',
420
+ '轮询',
421
+ '流式',
422
+ '提交',
423
+ '转换',
424
+ // HATEOAS commands
425
+ '进入',
426
+ '跟随',
427
+ '执行',
428
+ '捕获',
429
+ // Role markers
430
+ '以',
431
+ '到',
432
+ '每',
433
+ '用',
434
+ '从',
435
+ '项',
436
+ '为',
437
+ ],
438
+ keywordExtras: [
439
+ { native: '获取', normalized: 'fetch' },
440
+ { native: '轮询', normalized: 'poll' },
441
+ { native: '流式', normalized: 'stream' },
442
+ { native: '提交', normalized: 'submit' },
443
+ { native: '转换', normalized: 'transform' },
444
+ { native: '进入', normalized: 'enter' },
445
+ { native: '跟随', normalized: 'follow' },
446
+ { native: '执行', normalized: 'perform' },
447
+ { native: '捕获', normalized: 'capture' },
448
+ { native: '以', normalized: 'as' },
449
+ { native: '到', normalized: 'into' },
450
+ { native: '每', normalized: 'every' },
451
+ { native: '用', normalized: 'with' },
452
+ { native: '项', normalized: 'item' },
453
+ { native: '为', normalized: 'as' },
454
+ ],
455
+ keywordProfile: {
456
+ keywords: {
457
+ fetch: { primary: '获取' },
458
+ poll: { primary: '轮询' },
459
+ stream: { primary: '流式' },
460
+ submit: { primary: '提交' },
461
+ transform: { primary: '转换' },
462
+ enter: { primary: '进入' },
463
+ follow: { primary: '跟随' },
464
+ perform: { primary: '执行' },
465
+ capture: { primary: '捕获' },
466
+ },
467
+ },
468
+ includeOperators: false,
469
+ caseInsensitive: false,
470
+ });
471
+
472
+ // =============================================================================
473
+ // Turkish FlowScript Tokenizer
474
+ // =============================================================================
475
+
476
+ export const TurkishFlowTokenizer: LanguageTokenizer = createSimpleTokenizer({
477
+ language: 'tr',
478
+ customExtractors: [...sharedExtractors, new LatinExtendedIdentifierExtractor()],
479
+ keywords: [
480
+ // Commands
481
+ 'getir',
482
+ 'yokla',
483
+ 'aktar',
484
+ 'gönder',
485
+ 'dönüştür',
486
+ // HATEOAS commands
487
+ 'gir',
488
+ 'izle',
489
+ 'yürüt',
490
+ 'yakala',
491
+ // Role markers
492
+ 'olarak',
493
+ 'e',
494
+ 'her',
495
+ 'ile',
496
+ 'dan',
497
+ 'öğe',
498
+ ],
499
+ keywordExtras: [
500
+ { native: 'getir', normalized: 'fetch' },
501
+ { native: 'yokla', normalized: 'poll' },
502
+ { native: 'aktar', normalized: 'stream' },
503
+ { native: 'gönder', normalized: 'submit' },
504
+ { native: 'dönüştür', normalized: 'transform' },
505
+ { native: 'gir', normalized: 'enter' },
506
+ { native: 'izle', normalized: 'follow' },
507
+ { native: 'yürüt', normalized: 'perform' },
508
+ { native: 'yakala', normalized: 'capture' },
509
+ { native: 'olarak', normalized: 'as' },
510
+ { native: 'e', normalized: 'into' },
511
+ { native: 'her', normalized: 'every' },
512
+ { native: 'ile', normalized: 'with' },
513
+ { native: 'öğe', normalized: 'item' },
514
+ ],
515
+ keywordProfile: {
516
+ keywords: {
517
+ fetch: { primary: 'getir' },
518
+ poll: { primary: 'yokla' },
519
+ stream: { primary: 'aktar' },
520
+ submit: { primary: 'gönder' },
521
+ transform: { primary: 'dönüştür' },
522
+ enter: { primary: 'gir' },
523
+ follow: { primary: 'izle' },
524
+ perform: { primary: 'yürüt' },
525
+ capture: { primary: 'yakala' },
526
+ },
527
+ },
528
+ includeOperators: false,
529
+ caseInsensitive: true,
530
+ });
531
+
532
+ // =============================================================================
533
+ // French FlowScript Tokenizer
534
+ // =============================================================================
535
+
536
+ export const FrenchFlowTokenizer: LanguageTokenizer = createSimpleTokenizer({
537
+ language: 'fr',
538
+ customExtractors: [...sharedExtractors, new LatinExtendedIdentifierExtractor()],
539
+ keywords: [
540
+ // Commands
541
+ 'récupérer',
542
+ 'interroger',
543
+ 'diffuser',
544
+ 'soumettre',
545
+ 'transformer',
546
+ // HATEOAS commands
547
+ 'entrer',
548
+ 'suivre',
549
+ 'exécuter',
550
+ 'capturer',
551
+ // Role markers
552
+ 'comme',
553
+ 'dans',
554
+ 'chaque',
555
+ 'vers',
556
+ 'avec',
557
+ 'de',
558
+ 'élément',
559
+ ],
560
+ keywordExtras: [
561
+ { native: 'récupérer', normalized: 'fetch' },
562
+ { native: 'interroger', normalized: 'poll' },
563
+ { native: 'diffuser', normalized: 'stream' },
564
+ { native: 'soumettre', normalized: 'submit' },
565
+ { native: 'transformer', normalized: 'transform' },
566
+ { native: 'entrer', normalized: 'enter' },
567
+ { native: 'suivre', normalized: 'follow' },
568
+ { native: 'exécuter', normalized: 'perform' },
569
+ { native: 'capturer', normalized: 'capture' },
570
+ { native: 'comme', normalized: 'as' },
571
+ { native: 'dans', normalized: 'into' },
572
+ { native: 'chaque', normalized: 'every' },
573
+ { native: 'vers', normalized: 'to' },
574
+ { native: 'avec', normalized: 'with' },
575
+ { native: 'élément', normalized: 'item' },
576
+ ],
577
+ keywordProfile: {
578
+ keywords: {
579
+ fetch: { primary: 'récupérer' },
580
+ poll: { primary: 'interroger' },
581
+ stream: { primary: 'diffuser' },
582
+ submit: { primary: 'soumettre' },
583
+ transform: { primary: 'transformer' },
584
+ enter: { primary: 'entrer' },
585
+ follow: { primary: 'suivre' },
586
+ perform: { primary: 'exécuter' },
587
+ capture: { primary: 'capturer' },
588
+ },
589
+ },
590
+ includeOperators: false,
591
+ caseInsensitive: true,
592
+ });
package/src/types.ts ADDED
@@ -0,0 +1,108 @@
1
+ /**
2
+ * FlowScript Domain Types
3
+ *
4
+ * Output types for the FlowScript code generator. FlowSpec captures the
5
+ * semantic intent of a data flow command — URL, method, response format,
6
+ * target element, polling interval — ready for compilation to vanilla JS,
7
+ * HTMX attributes, or route descriptors.
8
+ */
9
+
10
+ export type FlowAction =
11
+ | 'fetch'
12
+ | 'poll'
13
+ | 'stream'
14
+ | 'submit'
15
+ | 'transform'
16
+ | 'enter'
17
+ | 'follow'
18
+ | 'perform'
19
+ | 'capture';
20
+
21
+ /**
22
+ * Structured data flow specification.
23
+ *
24
+ * Output of domain-flow's code generator. Can be used to:
25
+ * - Generate vanilla JS (fetch, EventSource, setInterval)
26
+ * - Generate HTMX attributes (hx-get, hx-trigger, hx-target)
27
+ * - Extract route descriptors for server-bridge
28
+ */
29
+ export interface FlowSpec {
30
+ /** The command that produced this spec */
31
+ action: FlowAction;
32
+
33
+ /** URL for source commands (fetch, poll, stream, submit destination) */
34
+ url?: string;
35
+
36
+ /** HTTP method inferred from command type */
37
+ method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
38
+
39
+ /** Response format */
40
+ responseFormat?: 'json' | 'html' | 'text' | 'sse';
41
+
42
+ /** Target CSS selector for DOM insertion */
43
+ target?: string;
44
+
45
+ /** Polling interval in milliseconds (poll command) */
46
+ intervalMs?: number;
47
+
48
+ /** Form selector (submit command) */
49
+ formSelector?: string;
50
+
51
+ /** Transform function or format string (transform command) */
52
+ transformFn?: string;
53
+
54
+ // ----- HATEOAS-specific fields -----
55
+
56
+ /** Link relation name (follow command) */
57
+ linkRel?: string;
58
+
59
+ /** Action name (perform command) */
60
+ actionName?: string;
61
+
62
+ /** Data source selector or inline data (perform command) */
63
+ dataSource?: string;
64
+
65
+ /** Variable name for captured data (capture command) */
66
+ captureAs?: string;
67
+
68
+ /** Property path to capture (capture command) */
69
+ capturePath?: string;
70
+
71
+ /** Metadata for debugging */
72
+ metadata: {
73
+ /** Language the command was written in */
74
+ sourceLanguage: string;
75
+ /** Raw role values extracted from the parsed command */
76
+ roles: Record<string, string | undefined>;
77
+ };
78
+ }
79
+
80
+ // =============================================================================
81
+ // Workflow Spec — siren-grail compilation target
82
+ // =============================================================================
83
+
84
+ /** A single step in a HATEOAS workflow */
85
+ export type WorkflowStep =
86
+ | { type: 'navigate'; rel: string; capture?: Record<string, string> }
87
+ | {
88
+ type: 'action';
89
+ action: string;
90
+ data?: Record<string, unknown>;
91
+ dataSource?: string;
92
+ capture?: Record<string, string>;
93
+ }
94
+ | { type: 'stop'; result?: string; reason?: string };
95
+
96
+ /**
97
+ * A complete HATEOAS workflow specification.
98
+ *
99
+ * Compiles to siren-grail's compileWorkflow() step format.
100
+ * Can also drive an MCP server with dynamic tools.
101
+ */
102
+ export interface WorkflowSpec {
103
+ /** API entry point URL */
104
+ entryPoint: string;
105
+
106
+ /** Ordered workflow steps */
107
+ steps: WorkflowStep[];
108
+ }