@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,129 @@
1
+ /**
2
+ * HTMX Attribute Generator
3
+ *
4
+ * Transforms a FlowSpec into HTMX-compatible HTML attributes.
5
+ * Maps fetch → hx-get, poll → hx-get + hx-trigger, submit → hx-post.
6
+ * Stream (SSE) has no direct HTMX equivalent — returns null with a note.
7
+ */
8
+
9
+ import type { FlowSpec } from '../types.js';
10
+
11
+ export interface HTMXAttributes {
12
+ /** Generated HTMX attribute map */
13
+ attrs: Record<string, string>;
14
+ /** Notes about limitations or warnings */
15
+ notes: string[];
16
+ }
17
+
18
+ /**
19
+ * Generate HTMX attributes from a FlowSpec.
20
+ *
21
+ * @returns HTMXAttributes with attr map and notes, or null if the command
22
+ * has no HTMX equivalent (e.g., transform)
23
+ */
24
+ export function generateHTMX(spec: FlowSpec): HTMXAttributes | null {
25
+ switch (spec.action) {
26
+ case 'fetch':
27
+ return generateFetchHTMX(spec);
28
+ case 'poll':
29
+ return generatePollHTMX(spec);
30
+ case 'stream':
31
+ return generateStreamHTMX(spec);
32
+ case 'submit':
33
+ return generateSubmitHTMX(spec);
34
+ case 'transform':
35
+ return null; // No HTMX equivalent for data transforms
36
+ default:
37
+ return null;
38
+ }
39
+ }
40
+
41
+ function generateFetchHTMX(spec: FlowSpec): HTMXAttributes {
42
+ const attrs: Record<string, string> = {};
43
+ const notes: string[] = [];
44
+
45
+ if (spec.url) {
46
+ attrs['hx-get'] = spec.url;
47
+ }
48
+
49
+ if (spec.target) {
50
+ attrs['hx-target'] = spec.target;
51
+ }
52
+
53
+ attrs['hx-swap'] = 'innerHTML';
54
+
55
+ if (spec.responseFormat === 'json') {
56
+ notes.push(
57
+ 'HTMX expects HTML responses by default. For JSON, use hx-ext="json-enc" or handle in a hyperscript handler.'
58
+ );
59
+ }
60
+
61
+ return { attrs, notes };
62
+ }
63
+
64
+ function generatePollHTMX(spec: FlowSpec): HTMXAttributes {
65
+ const attrs: Record<string, string> = {};
66
+ const notes: string[] = [];
67
+
68
+ if (spec.url) {
69
+ attrs['hx-get'] = spec.url;
70
+ }
71
+
72
+ if (spec.intervalMs) {
73
+ const seconds = spec.intervalMs / 1000;
74
+ attrs['hx-trigger'] = `every ${seconds}s`;
75
+ }
76
+
77
+ if (spec.target) {
78
+ attrs['hx-target'] = spec.target;
79
+ }
80
+
81
+ attrs['hx-swap'] = 'innerHTML';
82
+
83
+ if (spec.responseFormat === 'json') {
84
+ notes.push(
85
+ 'HTMX expects HTML responses by default. For JSON, use hx-ext="json-enc" or handle in a hyperscript handler.'
86
+ );
87
+ }
88
+
89
+ return { attrs, notes };
90
+ }
91
+
92
+ function generateStreamHTMX(spec: FlowSpec): HTMXAttributes {
93
+ const attrs: Record<string, string> = {};
94
+ const notes: string[] = [];
95
+
96
+ if (spec.url) {
97
+ attrs['hx-ext'] = 'sse';
98
+ attrs['sse-connect'] = spec.url;
99
+ attrs['sse-swap'] = 'message';
100
+ }
101
+
102
+ if (spec.target) {
103
+ attrs['hx-target'] = spec.target;
104
+ }
105
+
106
+ notes.push('SSE support requires the htmx sse extension (hx-ext="sse").');
107
+
108
+ return { attrs, notes };
109
+ }
110
+
111
+ function generateSubmitHTMX(spec: FlowSpec): HTMXAttributes {
112
+ const attrs: Record<string, string> = {};
113
+ const notes: string[] = [];
114
+
115
+ if (spec.url) {
116
+ attrs['hx-post'] = spec.url;
117
+ }
118
+
119
+ if (spec.responseFormat === 'json') {
120
+ attrs['hx-ext'] = 'json-enc';
121
+ attrs['hx-encoding'] = 'application/json';
122
+ }
123
+
124
+ if (spec.target) {
125
+ attrs['hx-target'] = spec.target;
126
+ }
127
+
128
+ return { attrs, notes };
129
+ }
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Route Extractor
3
+ *
4
+ * Extracts server route descriptors from parsed FlowScript commands.
5
+ * Each fetch/poll/stream/submit URL becomes a RouteDescriptor that can
6
+ * be fed into server-bridge for server-side code generation.
7
+ */
8
+
9
+ import type { FlowSpec } from '../types.js';
10
+
11
+ /**
12
+ * Lightweight route descriptor — compatible with server-bridge's RouteDescriptor
13
+ * but self-contained to avoid a hard dependency on the server-bridge package.
14
+ */
15
+ export interface FlowRouteDescriptor {
16
+ /** URL path (e.g., /api/users, /api/user/{id}) */
17
+ path: string;
18
+ /** HTTP method */
19
+ method: 'GET' | 'POST' | 'PUT' | 'DELETE';
20
+ /** Expected response format */
21
+ responseFormat: 'json' | 'html' | 'text' | 'sse';
22
+ /** Path parameters extracted from URL (e.g., ['id'] from /api/user/{id}) */
23
+ pathParams: string[];
24
+ /** Suggested handler function name */
25
+ handlerName: string;
26
+ /** Source command that produced this route */
27
+ sourceCommand: string;
28
+ }
29
+
30
+ /**
31
+ * Extract route descriptors from a FlowSpec.
32
+ * Returns null for commands without URLs (e.g., transform).
33
+ */
34
+ export function extractRoute(spec: FlowSpec): FlowRouteDescriptor | null {
35
+ if (!spec.url) return null;
36
+
37
+ const path = spec.url;
38
+ const pathParams = extractPathParams(path);
39
+ const handlerName = generateHandlerName(spec);
40
+
41
+ return {
42
+ path,
43
+ method: spec.method || 'GET',
44
+ responseFormat: spec.responseFormat || 'text',
45
+ pathParams,
46
+ handlerName,
47
+ sourceCommand: spec.action,
48
+ };
49
+ }
50
+
51
+ /**
52
+ * Extract multiple route descriptors from an array of FlowSpecs.
53
+ * Filters out nulls (commands without URLs).
54
+ */
55
+ export function extractRoutes(specs: FlowSpec[]): FlowRouteDescriptor[] {
56
+ return specs.map(extractRoute).filter((r): r is FlowRouteDescriptor => r !== null);
57
+ }
58
+
59
+ /**
60
+ * Extract path parameters from a URL.
61
+ * Supports both {param} and :param syntax.
62
+ */
63
+ function extractPathParams(url: string): string[] {
64
+ const params: string[] = [];
65
+
66
+ // {param} syntax
67
+ const braceMatches = url.matchAll(/\{(\w+)\}/g);
68
+ for (const match of braceMatches) {
69
+ params.push(match[1]);
70
+ }
71
+
72
+ // :param syntax
73
+ const colonMatches = url.matchAll(/:(\w+)/g);
74
+ for (const match of colonMatches) {
75
+ params.push(match[1]);
76
+ }
77
+
78
+ return params;
79
+ }
80
+
81
+ /**
82
+ * Generate a suggested handler function name from a FlowSpec.
83
+ */
84
+ function generateHandlerName(spec: FlowSpec): string {
85
+ if (!spec.url) return 'handler';
86
+
87
+ // Extract the last meaningful path segment
88
+ const segments = spec.url
89
+ .split('/')
90
+ .filter(s => s && !s.startsWith('{') && !s.startsWith(':') && !s.includes('?'));
91
+ const lastSegment = segments[segments.length - 1] || 'data';
92
+
93
+ const prefix =
94
+ spec.method === 'POST'
95
+ ? 'create'
96
+ : spec.method === 'PUT'
97
+ ? 'update'
98
+ : spec.method === 'DELETE'
99
+ ? 'delete'
100
+ : 'get';
101
+
102
+ // Capitalize first letter
103
+ const name = lastSegment.charAt(0).toUpperCase() + lastSegment.slice(1);
104
+ return `${prefix}${name}`;
105
+ }
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Workflow Generator — HATEOAS compilation target
3
+ *
4
+ * Converts a sequence of parsed FlowSpec objects into a WorkflowSpec
5
+ * compatible with siren-grail's compileWorkflow() step format.
6
+ *
7
+ * This is the bridge between FlowScript's natural-language parsing
8
+ * and siren-grail's workflow execution engine.
9
+ */
10
+
11
+ import type { FlowSpec, WorkflowSpec, WorkflowStep } from '../types.js';
12
+
13
+ /**
14
+ * Convert an ordered array of FlowSpecs (from parsed HATEOAS commands)
15
+ * into a WorkflowSpec ready for siren-grail execution.
16
+ *
17
+ * The first 'enter' command provides the entry point URL.
18
+ * Subsequent follow/perform/capture commands become workflow steps.
19
+ *
20
+ * @throws Error if no 'enter' command is found in the specs
21
+ */
22
+ export function toWorkflowSpec(specs: FlowSpec[]): WorkflowSpec {
23
+ let entryPoint: string | undefined;
24
+ const steps: WorkflowStep[] = [];
25
+
26
+ // Track the last step so capture can attach to it
27
+ let lastStep: WorkflowStep | undefined;
28
+
29
+ for (const spec of specs) {
30
+ switch (spec.action) {
31
+ case 'enter': {
32
+ if (!spec.url) throw new Error('enter command requires a URL');
33
+ entryPoint = spec.url;
34
+ break;
35
+ }
36
+
37
+ case 'follow': {
38
+ if (!spec.linkRel) throw new Error('follow command requires a link relation');
39
+ const step: WorkflowStep = { type: 'navigate', rel: spec.linkRel };
40
+ steps.push(step);
41
+ lastStep = step;
42
+ break;
43
+ }
44
+
45
+ case 'perform': {
46
+ if (!spec.actionName) throw new Error('perform command requires an action name');
47
+ const step: WorkflowStep = {
48
+ type: 'action',
49
+ action: spec.actionName,
50
+ };
51
+ if (spec.dataSource) {
52
+ step.dataSource = spec.dataSource;
53
+ }
54
+ steps.push(step);
55
+ lastStep = step;
56
+ break;
57
+ }
58
+
59
+ case 'capture': {
60
+ if (!spec.captureAs) throw new Error('capture command requires a variable name');
61
+
62
+ // Capture attaches to the previous step as a capture modifier
63
+ if (lastStep && (lastStep.type === 'navigate' || lastStep.type === 'action')) {
64
+ if (!lastStep.capture) lastStep.capture = {};
65
+ lastStep.capture[spec.captureAs] = spec.capturePath || 'properties';
66
+ } else {
67
+ // Standalone capture — attach to an implicit navigate to 'self'
68
+ const step: WorkflowStep = {
69
+ type: 'navigate',
70
+ rel: 'self',
71
+ capture: { [spec.captureAs]: spec.capturePath || 'properties' },
72
+ };
73
+ steps.push(step);
74
+ lastStep = step;
75
+ }
76
+ break;
77
+ }
78
+
79
+ default:
80
+ // Non-HATEOAS commands (fetch, poll, etc.) are not part of workflows
81
+ break;
82
+ }
83
+ }
84
+
85
+ if (!entryPoint) {
86
+ throw new Error('Workflow requires an "enter" command to specify the API entry point');
87
+ }
88
+
89
+ return { entryPoint, steps };
90
+ }
91
+
92
+ /**
93
+ * Convert a WorkflowSpec to siren-grail's compileWorkflow() step format.
94
+ *
95
+ * This produces a plain JSON array compatible with:
96
+ * import { compileWorkflow } from 'siren-agent/workflow';
97
+ * const decide = compileWorkflow(steps);
98
+ */
99
+ export function toSirenGrailSteps(spec: WorkflowSpec): Array<Record<string, unknown>> {
100
+ return spec.steps.map(step => {
101
+ switch (step.type) {
102
+ case 'navigate':
103
+ return {
104
+ type: 'navigate',
105
+ rel: step.rel,
106
+ ...(step.capture ? { capture: step.capture } : {}),
107
+ };
108
+
109
+ case 'action':
110
+ return {
111
+ type: 'action',
112
+ action: step.action,
113
+ ...(step.data ? { data: step.data } : {}),
114
+ ...(step.dataSource ? { dataSource: step.dataSource } : {}),
115
+ ...(step.capture ? { capture: step.capture } : {}),
116
+ };
117
+
118
+ case 'stop':
119
+ return {
120
+ type: 'stop',
121
+ ...(step.result ? { result: step.result } : {}),
122
+ ...(step.reason ? { reason: step.reason } : {}),
123
+ };
124
+
125
+ default:
126
+ return step as Record<string, unknown>;
127
+ }
128
+ });
129
+ }
package/src/index.ts ADDED
@@ -0,0 +1,210 @@
1
+ /**
2
+ * @lokascript/domain-flow — Declarative Reactive Data Flow DSL
3
+ *
4
+ * A multilingual data flow domain built on @lokascript/framework.
5
+ * Parses data flow commands written in 8 languages, compiling to
6
+ * vanilla JS (fetch, EventSource, setInterval) or HTMX attributes.
7
+ *
8
+ * @example
9
+ * ```typescript
10
+ * import { createFlowDSL } from '@lokascript/domain-flow';
11
+ *
12
+ * const flow = createFlowDSL();
13
+ *
14
+ * // English (SVO)
15
+ * flow.compile('fetch /api/users as json into #user-list', 'en');
16
+ * // → { ok: true, code: "fetch('/api/users').then(r => r.json())..." }
17
+ *
18
+ * // Spanish (SVO)
19
+ * flow.compile('obtener /api/users como json en #user-list', 'es');
20
+ *
21
+ * // Japanese (SOV)
22
+ * flow.compile('/api/users json で 取得', 'ja');
23
+ *
24
+ * // Arabic (VSO)
25
+ * flow.compile('جلب /api/users ك json في #user-list', 'ar');
26
+ *
27
+ * // Korean (SOV)
28
+ * flow.compile('/api/users json 로 가져오기', 'ko');
29
+ *
30
+ * // Chinese (SVO)
31
+ * flow.compile('获取 /api/users 以 json 到 #user-list', 'zh');
32
+ *
33
+ * // Turkish (SOV)
34
+ * flow.compile('/api/users json olarak getir', 'tr');
35
+ *
36
+ * // French (SVO)
37
+ * flow.compile('récupérer /api/users comme json dans #user-list', 'fr');
38
+ * ```
39
+ */
40
+
41
+ import { createMultilingualDSL, type MultilingualDSL } from '@lokascript/framework';
42
+ import {
43
+ allSchemas,
44
+ fetchSchema,
45
+ pollSchema,
46
+ streamSchema,
47
+ submitSchema,
48
+ transformSchema,
49
+ } from './schemas/index.js';
50
+ import {
51
+ englishProfile,
52
+ spanishProfile,
53
+ japaneseProfile,
54
+ arabicProfile,
55
+ koreanProfile,
56
+ chineseProfile,
57
+ turkishProfile,
58
+ frenchProfile,
59
+ } from './profiles/index.js';
60
+ import {
61
+ EnglishFlowTokenizer,
62
+ SpanishFlowTokenizer,
63
+ JapaneseFlowTokenizer,
64
+ ArabicFlowTokenizer,
65
+ KoreanFlowTokenizer,
66
+ ChineseFlowTokenizer,
67
+ TurkishFlowTokenizer,
68
+ FrenchFlowTokenizer,
69
+ } from './tokenizers/index.js';
70
+ import { flowCodeGenerator } from './generators/flow-generator.js';
71
+
72
+ /**
73
+ * Create a multilingual FlowScript DSL instance with all 8 supported languages.
74
+ */
75
+ export function createFlowDSL(): MultilingualDSL {
76
+ return /*#__PURE__*/ createMultilingualDSL({
77
+ name: 'FlowScript',
78
+ schemas: allSchemas,
79
+ languages: [
80
+ {
81
+ code: 'en',
82
+ name: 'English',
83
+ nativeName: 'English',
84
+ tokenizer: EnglishFlowTokenizer,
85
+ patternProfile: englishProfile,
86
+ },
87
+ {
88
+ code: 'es',
89
+ name: 'Spanish',
90
+ nativeName: 'Español',
91
+ tokenizer: SpanishFlowTokenizer,
92
+ patternProfile: spanishProfile,
93
+ },
94
+ {
95
+ code: 'ja',
96
+ name: 'Japanese',
97
+ nativeName: '日本語',
98
+ tokenizer: JapaneseFlowTokenizer,
99
+ patternProfile: japaneseProfile,
100
+ },
101
+ {
102
+ code: 'ar',
103
+ name: 'Arabic',
104
+ nativeName: 'العربية',
105
+ tokenizer: ArabicFlowTokenizer,
106
+ patternProfile: arabicProfile,
107
+ },
108
+ {
109
+ code: 'ko',
110
+ name: 'Korean',
111
+ nativeName: '한국어',
112
+ tokenizer: KoreanFlowTokenizer,
113
+ patternProfile: koreanProfile,
114
+ },
115
+ {
116
+ code: 'zh',
117
+ name: 'Chinese',
118
+ nativeName: '中文',
119
+ tokenizer: ChineseFlowTokenizer,
120
+ patternProfile: chineseProfile,
121
+ },
122
+ {
123
+ code: 'tr',
124
+ name: 'Turkish',
125
+ nativeName: 'Türkçe',
126
+ tokenizer: TurkishFlowTokenizer,
127
+ patternProfile: turkishProfile,
128
+ },
129
+ {
130
+ code: 'fr',
131
+ name: 'French',
132
+ nativeName: 'Français',
133
+ tokenizer: FrenchFlowTokenizer,
134
+ patternProfile: frenchProfile,
135
+ },
136
+ ],
137
+ codeGenerator: flowCodeGenerator,
138
+ });
139
+ }
140
+
141
+ // Re-export schemas for consumers who want to extend
142
+ export { allSchemas, fetchSchema, pollSchema, streamSchema, submitSchema, transformSchema };
143
+ export {
144
+ enterSchema,
145
+ followSchema,
146
+ performSchema,
147
+ captureSchema,
148
+ hateoasSchemas,
149
+ } from './schemas/hateoas-schemas.js';
150
+ export {
151
+ englishProfile,
152
+ spanishProfile,
153
+ japaneseProfile,
154
+ arabicProfile,
155
+ koreanProfile,
156
+ chineseProfile,
157
+ turkishProfile,
158
+ frenchProfile,
159
+ } from './profiles/index.js';
160
+ export { flowCodeGenerator, toFlowSpec, parseDuration } from './generators/flow-generator.js';
161
+ export { renderFlow } from './generators/flow-renderer.js';
162
+ export { generateHTMX } from './generators/htmx-generator.js';
163
+ export { extractRoute, extractRoutes } from './generators/route-extractor.js';
164
+ export { parseFlowPipeline, compilePipeline } from './parser/pipeline-parser.js';
165
+ export type { HTMXAttributes } from './generators/htmx-generator.js';
166
+ export type { FlowRouteDescriptor } from './generators/route-extractor.js';
167
+ export type { PipelineStep, PipelineParseResult } from './parser/pipeline-parser.js';
168
+ export {
169
+ EnglishFlowTokenizer,
170
+ SpanishFlowTokenizer,
171
+ JapaneseFlowTokenizer,
172
+ ArabicFlowTokenizer,
173
+ KoreanFlowTokenizer,
174
+ ChineseFlowTokenizer,
175
+ TurkishFlowTokenizer,
176
+ FrenchFlowTokenizer,
177
+ } from './tokenizers/index.js';
178
+ export type { FlowSpec, FlowAction, WorkflowSpec, WorkflowStep } from './types.js';
179
+ export { toWorkflowSpec, toSirenGrailSteps } from './generators/workflow-generator.js';
180
+
181
+ // =============================================================================
182
+ // Runtime: HATEOAS Workflow Execution + MCP Server
183
+ // =============================================================================
184
+
185
+ export {
186
+ executeWorkflow,
187
+ createWorkflowExecutor,
188
+ toSirenSteps,
189
+ } from './runtime/workflow-executor.js';
190
+ export type { WorkflowResult, ExecuteWorkflowOptions } from './runtime/workflow-executor.js';
191
+
192
+ export {
193
+ McpWorkflowServer,
194
+ createMcpWorkflowServer,
195
+ actionsToTools,
196
+ linksToTools,
197
+ entityToTools,
198
+ } from './runtime/mcp-workflow-server.js';
199
+ export type { McpWorkflowServerConfig } from './runtime/mcp-workflow-server.js';
200
+
201
+ // =============================================================================
202
+ // Domain Scan Config (for AOT / Vite plugin integration)
203
+ // =============================================================================
204
+
205
+ /** HTML attribute and script-type patterns for AOT scanning */
206
+ export const flowScanConfig = {
207
+ attributes: ['data-flow', '_flow'] as const,
208
+ scriptTypes: ['text/flowscript'] as const,
209
+ defaultLanguage: 'en',
210
+ };