@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,338 @@
1
+ /**
2
+ * FlowScript Code Generator
3
+ *
4
+ * Transforms semantic AST nodes into either:
5
+ * 1. Vanilla JavaScript strings (primary output via CodeGenerator interface)
6
+ * 2. Structured FlowSpec JSON (via toFlowSpec helper)
7
+ */
8
+
9
+ import type { SemanticNode, CodeGenerator } from '@lokascript/framework';
10
+ import { extractRoleValue } from '@lokascript/framework';
11
+ import type { FlowSpec } from '../types.js';
12
+
13
+ // =============================================================================
14
+ // Duration Parsing
15
+ // =============================================================================
16
+
17
+ /**
18
+ * Parse a duration string to milliseconds.
19
+ * Supports: 500ms, 5s, 1m, 1h, plain number (treated as ms)
20
+ */
21
+ export function parseDuration(duration: string): number {
22
+ const match = duration.match(/^(\d+)(ms|s|m|h)?$/);
23
+ if (!match) return 0;
24
+ const value = parseInt(match[1], 10);
25
+ switch (match[2]) {
26
+ case 'h':
27
+ return value * 3600000;
28
+ case 'm':
29
+ return value * 60000;
30
+ case 's':
31
+ return value * 1000;
32
+ case 'ms':
33
+ return value;
34
+ default:
35
+ return value; // plain number = ms
36
+ }
37
+ }
38
+
39
+ // =============================================================================
40
+ // FlowSpec Extraction
41
+ // =============================================================================
42
+
43
+ /**
44
+ * Extract a structured FlowSpec from a parsed SemanticNode.
45
+ */
46
+ export function toFlowSpec(node: SemanticNode, language: string): FlowSpec {
47
+ const action = node.action as FlowSpec['action'];
48
+ const roles: Record<string, string | undefined> = {};
49
+
50
+ for (const [key, val] of node.roles) {
51
+ roles[key] =
52
+ typeof val === 'string' ? val : ((val as { value?: string })?.value ?? String(val));
53
+ }
54
+
55
+ const base: FlowSpec = {
56
+ action,
57
+ metadata: { sourceLanguage: language, roles },
58
+ };
59
+
60
+ switch (action) {
61
+ case 'fetch': {
62
+ base.url = extractRoleValue(node, 'source') || undefined;
63
+ base.responseFormat = normalizeFormat(extractRoleValue(node, 'style'));
64
+ base.target = extractRoleValue(node, 'destination') || undefined;
65
+ base.method = 'GET';
66
+ break;
67
+ }
68
+ case 'poll': {
69
+ base.url = extractRoleValue(node, 'source') || undefined;
70
+ base.responseFormat = normalizeFormat(extractRoleValue(node, 'style'));
71
+ base.target = extractRoleValue(node, 'destination') || undefined;
72
+ base.method = 'GET';
73
+ const dur = extractRoleValue(node, 'duration');
74
+ if (dur) base.intervalMs = parseDuration(dur);
75
+ break;
76
+ }
77
+ case 'stream': {
78
+ base.url = extractRoleValue(node, 'source') || undefined;
79
+ base.responseFormat = 'sse';
80
+ base.target = extractRoleValue(node, 'destination') || undefined;
81
+ base.method = 'GET';
82
+ break;
83
+ }
84
+ case 'submit': {
85
+ base.formSelector = extractRoleValue(node, 'patient') || undefined;
86
+ base.url = extractRoleValue(node, 'destination') || undefined;
87
+ base.responseFormat = normalizeFormat(extractRoleValue(node, 'style'));
88
+ base.method = 'POST';
89
+ break;
90
+ }
91
+ case 'transform': {
92
+ base.transformFn = extractRoleValue(node, 'instrument') || undefined;
93
+ break;
94
+ }
95
+ case 'enter': {
96
+ base.url = extractRoleValue(node, 'source') || undefined;
97
+ break;
98
+ }
99
+ case 'follow': {
100
+ base.linkRel = extractRoleValue(node, 'patient') || undefined;
101
+ break;
102
+ }
103
+ case 'perform': {
104
+ base.actionName = extractRoleValue(node, 'patient') || undefined;
105
+ base.dataSource = extractRoleValue(node, 'source') || undefined;
106
+ break;
107
+ }
108
+ case 'capture': {
109
+ base.captureAs = extractRoleValue(node, 'destination') || undefined;
110
+ base.capturePath = extractRoleValue(node, 'patient') || undefined;
111
+ break;
112
+ }
113
+ }
114
+
115
+ return base;
116
+ }
117
+
118
+ function normalizeFormat(format: string | null): FlowSpec['responseFormat'] {
119
+ if (!format) return undefined;
120
+ const lower = format.toLowerCase();
121
+ if (lower === 'json') return 'json';
122
+ if (lower === 'html') return 'html';
123
+ if (lower === 'text') return 'text';
124
+ if (lower === 'sse') return 'sse';
125
+ return undefined;
126
+ }
127
+
128
+ // =============================================================================
129
+ // String Escaping
130
+ // =============================================================================
131
+
132
+ /** Escape a string for safe inclusion in single-quoted JS string literals. */
133
+ function escapeStr(s: string): string {
134
+ return s.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
135
+ }
136
+
137
+ // =============================================================================
138
+ // Per-Command JS Generators
139
+ // =============================================================================
140
+
141
+ function generateFetch(node: SemanticNode): string {
142
+ const url = extractRoleValue(node, 'source') || '/';
143
+ const format = extractRoleValue(node, 'style')?.toLowerCase() || 'text';
144
+ const target = extractRoleValue(node, 'destination');
145
+
146
+ const parseMethod = format === 'json' ? '.json()' : '.text()';
147
+ const lines: string[] = [`fetch('${escapeStr(url)}')`, ` .then(r => r${parseMethod})`];
148
+
149
+ if (target) {
150
+ lines.push(` .then(data => {`);
151
+ if (format === 'json') {
152
+ lines.push(
153
+ ` document.querySelector('${escapeStr(target)}').innerHTML = typeof data === 'string' ? data : JSON.stringify(data);`
154
+ );
155
+ } else {
156
+ lines.push(` document.querySelector('${escapeStr(target)}').innerHTML = data;`);
157
+ }
158
+ lines.push(` })`);
159
+ }
160
+
161
+ lines.push(` .catch(err => console.error('Fetch error:', err));`);
162
+ return lines.join('\n');
163
+ }
164
+
165
+ function generatePoll(node: SemanticNode): string {
166
+ const url = extractRoleValue(node, 'source') || '/';
167
+ const duration = extractRoleValue(node, 'duration') || '5s';
168
+ const format = extractRoleValue(node, 'style')?.toLowerCase() || 'text';
169
+ const target = extractRoleValue(node, 'destination');
170
+ const ms = parseDuration(duration);
171
+
172
+ const parseMethod = format === 'json' ? '.json()' : '.text()';
173
+ const lines: string[] = [
174
+ `setInterval(async () => {`,
175
+ ` try {`,
176
+ ` const r = await fetch('${escapeStr(url)}');`,
177
+ ` const data = await r${parseMethod};`,
178
+ ];
179
+
180
+ if (target) {
181
+ if (format === 'json') {
182
+ lines.push(
183
+ ` document.querySelector('${escapeStr(target)}').innerHTML = typeof data === 'string' ? data : JSON.stringify(data);`
184
+ );
185
+ } else {
186
+ lines.push(` document.querySelector('${escapeStr(target)}').innerHTML = data;`);
187
+ }
188
+ }
189
+
190
+ lines.push(` } catch (err) {`, ` console.error('Poll error:', err);`, ` }`, `}, ${ms});`);
191
+
192
+ return lines.join('\n');
193
+ }
194
+
195
+ function generateStream(node: SemanticNode): string {
196
+ const url = extractRoleValue(node, 'source') || '/';
197
+ const target = extractRoleValue(node, 'destination');
198
+
199
+ const lines: string[] = [`const es = new EventSource('${escapeStr(url)}');`];
200
+
201
+ if (target) {
202
+ lines.push(
203
+ `es.onmessage = (event) => {`,
204
+ ` document.querySelector('${escapeStr(target)}').insertAdjacentHTML('beforeend', event.data);`,
205
+ `};`
206
+ );
207
+ } else {
208
+ lines.push(`es.onmessage = (event) => {`, ` console.log('Stream data:', event.data);`, `};`);
209
+ }
210
+
211
+ lines.push(`es.onerror = () => {`, ` console.error('Stream error, reconnecting...');`, `};`);
212
+
213
+ return lines.join('\n');
214
+ }
215
+
216
+ function generateSubmit(node: SemanticNode): string {
217
+ const form = extractRoleValue(node, 'patient') || '#form';
218
+ const url = extractRoleValue(node, 'destination') || '/';
219
+ const format = extractRoleValue(node, 'style')?.toLowerCase();
220
+
221
+ const lines: string[] = [
222
+ `const form = document.querySelector('${escapeStr(form)}');`,
223
+ `const formData = new FormData(form);`,
224
+ ];
225
+
226
+ if (format === 'json') {
227
+ lines.push(
228
+ `fetch('${escapeStr(url)}', {`,
229
+ ` method: 'POST',`,
230
+ ` headers: { 'Content-Type': 'application/json' },`,
231
+ ` body: JSON.stringify(Object.fromEntries(formData)),`,
232
+ `})`,
233
+ ` .then(r => r.json())`,
234
+ ` .catch(err => console.error('Submit error:', err));`
235
+ );
236
+ } else {
237
+ lines.push(
238
+ `fetch('${escapeStr(url)}', {`,
239
+ ` method: 'POST',`,
240
+ ` body: formData,`,
241
+ `})`,
242
+ ` .then(r => r.text())`,
243
+ ` .catch(err => console.error('Submit error:', err));`
244
+ );
245
+ }
246
+
247
+ return lines.join('\n');
248
+ }
249
+
250
+ function generateTransform(node: SemanticNode): string {
251
+ const data = extractRoleValue(node, 'patient') || 'data';
252
+ const fn = extractRoleValue(node, 'instrument') || 'identity';
253
+
254
+ return `const result = ${fn}(${data});`;
255
+ }
256
+
257
+ // =============================================================================
258
+ // HATEOAS Command JS Generators
259
+ // =============================================================================
260
+
261
+ function generateEnter(node: SemanticNode): string {
262
+ const url = extractRoleValue(node, 'source') || '/';
263
+ return [
264
+ `// HATEOAS: Connect to entry point`,
265
+ `const agent = new SirenAgent('${escapeStr(url)}');`,
266
+ `await agent.start();`,
267
+ ].join('\n');
268
+ }
269
+
270
+ function generateFollow(node: SemanticNode): string {
271
+ const rel = extractRoleValue(node, 'patient') || 'self';
272
+ return [
273
+ `// HATEOAS: Follow link relation '${escapeStr(rel)}'`,
274
+ `await agent.followLink('${escapeStr(rel)}');`,
275
+ ].join('\n');
276
+ }
277
+
278
+ function generatePerform(node: SemanticNode): string {
279
+ const action = extractRoleValue(node, 'patient') || '';
280
+ const dataSource = extractRoleValue(node, 'source');
281
+ const lines = [`// HATEOAS: Execute action '${escapeStr(action)}'`];
282
+
283
+ if (dataSource) {
284
+ lines.push(
285
+ `const data = Object.fromEntries(new FormData(document.querySelector('${escapeStr(dataSource)}')));`
286
+ );
287
+ lines.push(`await agent.executeAction('${escapeStr(action)}', data);`);
288
+ } else {
289
+ lines.push(`await agent.executeAction('${escapeStr(action)}');`);
290
+ }
291
+
292
+ return lines.join('\n');
293
+ }
294
+
295
+ function generateCapture(node: SemanticNode): string {
296
+ const varName = extractRoleValue(node, 'destination') || 'captured';
297
+ const path = extractRoleValue(node, 'patient');
298
+
299
+ if (path) {
300
+ return `const ${varName} = agent.currentEntity.properties?.['${escapeStr(path)}'];`;
301
+ }
302
+ return `const ${varName} = agent.currentEntity.properties;`;
303
+ }
304
+
305
+ // =============================================================================
306
+ // Public Code Generator
307
+ // =============================================================================
308
+
309
+ /**
310
+ * FlowScript code generator implementation.
311
+ * Returns vanilla JavaScript — ready to execute in a browser.
312
+ */
313
+ export const flowCodeGenerator: CodeGenerator = {
314
+ generate(node: SemanticNode): string {
315
+ switch (node.action) {
316
+ case 'fetch':
317
+ return generateFetch(node);
318
+ case 'poll':
319
+ return generatePoll(node);
320
+ case 'stream':
321
+ return generateStream(node);
322
+ case 'submit':
323
+ return generateSubmit(node);
324
+ case 'transform':
325
+ return generateTransform(node);
326
+ case 'enter':
327
+ return generateEnter(node);
328
+ case 'follow':
329
+ return generateFollow(node);
330
+ case 'perform':
331
+ return generatePerform(node);
332
+ case 'capture':
333
+ return generateCapture(node);
334
+ default:
335
+ throw new Error(`Unknown FlowScript command: ${node.action}`);
336
+ }
337
+ },
338
+ };
@@ -0,0 +1,262 @@
1
+ /**
2
+ * FlowScript Natural Language Renderer
3
+ *
4
+ * Renders a SemanticNode back into natural-language FlowScript syntax
5
+ * for a target language. Inverse of the parser — used by translate().
6
+ */
7
+
8
+ import type { SemanticNode } from '@lokascript/framework';
9
+ import { extractRoleValue } from '@lokascript/framework';
10
+
11
+ // =============================================================================
12
+ // Keyword Tables
13
+ // =============================================================================
14
+
15
+ const COMMAND_KEYWORDS: Record<string, Record<string, string>> = {
16
+ fetch: {
17
+ en: 'fetch',
18
+ es: 'obtener',
19
+ ja: '取得',
20
+ ar: 'جلب',
21
+ ko: '가져오기',
22
+ zh: '获取',
23
+ tr: 'getir',
24
+ fr: 'récupérer',
25
+ },
26
+ poll: {
27
+ en: 'poll',
28
+ es: 'sondear',
29
+ ja: 'ポーリング',
30
+ ar: 'استطلع',
31
+ ko: '폴링',
32
+ zh: '轮询',
33
+ tr: 'yokla',
34
+ fr: 'interroger',
35
+ },
36
+ stream: {
37
+ en: 'stream',
38
+ es: 'transmitir',
39
+ ja: 'ストリーム',
40
+ ar: 'بث',
41
+ ko: '스트리밍',
42
+ zh: '流式',
43
+ tr: 'aktar',
44
+ fr: 'diffuser',
45
+ },
46
+ submit: {
47
+ en: 'submit',
48
+ es: 'enviar',
49
+ ja: '送信',
50
+ ar: 'أرسل',
51
+ ko: '제출',
52
+ zh: '提交',
53
+ tr: 'gönder',
54
+ fr: 'soumettre',
55
+ },
56
+ transform: {
57
+ en: 'transform',
58
+ es: 'transformar',
59
+ ja: '変換',
60
+ ar: 'حوّل',
61
+ ko: '변환',
62
+ zh: '转换',
63
+ tr: 'dönüştür',
64
+ fr: 'transformer',
65
+ },
66
+ };
67
+
68
+ const MARKERS: Record<string, Record<string, string>> = {
69
+ as: { en: 'as', es: 'como', ja: 'で', ar: 'ك', ko: '로', zh: '以', tr: 'olarak', fr: 'comme' },
70
+ into: { en: 'into', es: 'en', ja: 'に', ar: 'في', ko: '에', zh: '到', tr: 'e', fr: 'dans' },
71
+ every: {
72
+ en: 'every',
73
+ es: 'cada',
74
+ ja: 'ごとに',
75
+ ar: 'كل',
76
+ ko: '마다',
77
+ zh: '每',
78
+ tr: 'her',
79
+ fr: 'chaque',
80
+ },
81
+ to: { en: 'to', es: 'a', ja: 'に', ar: 'إلى', ko: '로', zh: '到', tr: 'e', fr: 'vers' },
82
+ with: { en: 'with', es: 'con', ja: 'で', ar: 'ب', ko: '로', zh: '用', tr: 'ile', fr: 'avec' },
83
+ };
84
+
85
+ // =============================================================================
86
+ // Word Order Helpers
87
+ // =============================================================================
88
+
89
+ const SOV_LANGUAGES = new Set(['ja', 'ko', 'tr']);
90
+ const VSO_LANGUAGES = new Set(['ar']);
91
+
92
+ function isSOV(lang: string): boolean {
93
+ return SOV_LANGUAGES.has(lang);
94
+ }
95
+
96
+ function isVSO(lang: string): boolean {
97
+ return VSO_LANGUAGES.has(lang);
98
+ }
99
+
100
+ function kw(command: string, lang: string): string {
101
+ return COMMAND_KEYWORDS[command]?.[lang] ?? command;
102
+ }
103
+
104
+ function mk(marker: string, lang: string): string {
105
+ return MARKERS[marker]?.[lang] ?? marker;
106
+ }
107
+
108
+ // =============================================================================
109
+ // Per-Command Renderers
110
+ // =============================================================================
111
+
112
+ function renderFetch(node: SemanticNode, lang: string): string {
113
+ const source = extractRoleValue(node, 'source') || '/';
114
+ const style = extractRoleValue(node, 'style');
115
+ const dest = extractRoleValue(node, 'destination');
116
+ const verb = kw('fetch', lang);
117
+ const parts: string[] = [];
118
+
119
+ if (isSOV(lang)) {
120
+ parts.push(source);
121
+ if (style) parts.push(style, mk('as', lang));
122
+ parts.push(verb);
123
+ if (dest) parts.push(dest, mk('into', lang));
124
+ } else if (isVSO(lang)) {
125
+ // VSO: verb first, then source, then modifiers
126
+ parts.push(verb, source);
127
+ if (style) parts.push(mk('as', lang), style);
128
+ if (dest) parts.push(mk('into', lang), dest);
129
+ } else {
130
+ // SVO (default)
131
+ parts.push(verb, source);
132
+ if (style) parts.push(mk('as', lang), style);
133
+ if (dest) parts.push(mk('into', lang), dest);
134
+ }
135
+
136
+ return parts.join(' ');
137
+ }
138
+
139
+ function renderPoll(node: SemanticNode, lang: string): string {
140
+ const source = extractRoleValue(node, 'source') || '/';
141
+ const duration = extractRoleValue(node, 'duration') || '5s';
142
+ const style = extractRoleValue(node, 'style');
143
+ const dest = extractRoleValue(node, 'destination');
144
+ const verb = kw('poll', lang);
145
+ const parts: string[] = [];
146
+
147
+ if (isSOV(lang)) {
148
+ parts.push(source);
149
+ parts.push(duration, mk('every', lang));
150
+ if (style) parts.push(style, mk('as', lang));
151
+ parts.push(verb);
152
+ if (dest) parts.push(dest, mk('into', lang));
153
+ } else if (isVSO(lang)) {
154
+ parts.push(verb, source);
155
+ parts.push(mk('every', lang), duration);
156
+ if (style) parts.push(mk('as', lang), style);
157
+ if (dest) parts.push(mk('into', lang), dest);
158
+ } else {
159
+ parts.push(verb, source);
160
+ parts.push(mk('every', lang), duration);
161
+ if (style) parts.push(mk('as', lang), style);
162
+ if (dest) parts.push(mk('into', lang), dest);
163
+ }
164
+
165
+ return parts.join(' ');
166
+ }
167
+
168
+ function renderStream(node: SemanticNode, lang: string): string {
169
+ const source = extractRoleValue(node, 'source') || '/';
170
+ const style = extractRoleValue(node, 'style');
171
+ const dest = extractRoleValue(node, 'destination');
172
+ const verb = kw('stream', lang);
173
+ const parts: string[] = [];
174
+
175
+ if (isSOV(lang)) {
176
+ parts.push(source);
177
+ if (style) parts.push(style, mk('as', lang));
178
+ parts.push(verb);
179
+ if (dest) parts.push(dest, mk('into', lang));
180
+ } else if (isVSO(lang)) {
181
+ parts.push(verb, source);
182
+ if (style) parts.push(mk('as', lang), style);
183
+ if (dest) parts.push(mk('into', lang), dest);
184
+ } else {
185
+ parts.push(verb, source);
186
+ if (style) parts.push(mk('as', lang), style);
187
+ if (dest) parts.push(mk('into', lang), dest);
188
+ }
189
+
190
+ return parts.join(' ');
191
+ }
192
+
193
+ function renderSubmit(node: SemanticNode, lang: string): string {
194
+ const patient = extractRoleValue(node, 'patient') || '#form';
195
+ const dest = extractRoleValue(node, 'destination') || '/';
196
+ const style = extractRoleValue(node, 'style');
197
+ const verb = kw('submit', lang);
198
+ const parts: string[] = [];
199
+
200
+ if (isSOV(lang)) {
201
+ parts.push(patient);
202
+ parts.push(dest, mk('to', lang));
203
+ if (style) parts.push(style, mk('as', lang));
204
+ parts.push(verb);
205
+ } else if (isVSO(lang)) {
206
+ // VSO: verb + patient + destination marker + destination + style
207
+ parts.push(verb, patient);
208
+ parts.push(mk('to', lang), dest);
209
+ if (style) parts.push(mk('as', lang), style);
210
+ } else {
211
+ parts.push(verb, patient);
212
+ parts.push(mk('to', lang), dest);
213
+ if (style) parts.push(mk('as', lang), style);
214
+ }
215
+
216
+ return parts.join(' ');
217
+ }
218
+
219
+ function renderTransform(node: SemanticNode, lang: string): string {
220
+ const patient = extractRoleValue(node, 'patient') || 'data';
221
+ const instrument = extractRoleValue(node, 'instrument') || 'identity';
222
+ const verb = kw('transform', lang);
223
+ const parts: string[] = [];
224
+
225
+ if (isSOV(lang)) {
226
+ parts.push(patient);
227
+ parts.push(instrument, mk('with', lang));
228
+ parts.push(verb);
229
+ } else if (isVSO(lang)) {
230
+ parts.push(verb, patient);
231
+ parts.push(mk('with', lang), instrument);
232
+ } else {
233
+ parts.push(verb, patient);
234
+ parts.push(mk('with', lang), instrument);
235
+ }
236
+
237
+ return parts.join(' ');
238
+ }
239
+
240
+ // =============================================================================
241
+ // Public API
242
+ // =============================================================================
243
+
244
+ /**
245
+ * Render a FlowScript SemanticNode to natural-language text in the target language.
246
+ */
247
+ export function renderFlow(node: SemanticNode, language: string): string {
248
+ switch (node.action) {
249
+ case 'fetch':
250
+ return renderFetch(node, language);
251
+ case 'poll':
252
+ return renderPoll(node, language);
253
+ case 'stream':
254
+ return renderStream(node, language);
255
+ case 'submit':
256
+ return renderSubmit(node, language);
257
+ case 'transform':
258
+ return renderTransform(node, language);
259
+ default:
260
+ return `-- Unknown: ${node.action}`;
261
+ }
262
+ }