@synergenius/flow-weaver 0.5.1 → 0.7.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/README.md +2 -1
- package/dist/annotation-generator.js +29 -4
- package/dist/api/generate-in-place.js +3 -0
- package/dist/ast/types.d.ts +22 -2
- package/dist/built-in-nodes/coercion-types.d.ts +15 -0
- package/dist/built-in-nodes/coercion-types.js +61 -0
- package/dist/built-in-nodes/invoke-workflow.js +3 -3
- package/dist/built-in-nodes/mock-types.d.ts +20 -0
- package/dist/built-in-nodes/mock-types.js +30 -0
- package/dist/built-in-nodes/wait-for-agent.js +5 -4
- package/dist/built-in-nodes/wait-for-event.js +3 -3
- package/dist/chevrotain-parser/coerce-parser.d.ts +33 -0
- package/dist/chevrotain-parser/coerce-parser.js +103 -0
- package/dist/chevrotain-parser/index.d.ts +3 -1
- package/dist/chevrotain-parser/index.js +3 -1
- package/dist/chevrotain-parser/port-parser.js +2 -1
- package/dist/chevrotain-parser/tokens.d.ts +2 -0
- package/dist/chevrotain-parser/tokens.js +10 -0
- package/dist/cli/commands/compile.js +6 -0
- package/dist/cli/commands/init.js +2 -0
- package/dist/cli/commands/run.d.ts +4 -0
- package/dist/cli/commands/run.js +68 -1
- package/dist/cli/commands/validate.js +12 -0
- package/dist/cli/flow-weaver.mjs +764 -65
- package/dist/cli/index.js +1 -0
- package/dist/doc-metadata/extractors/annotations.js +17 -0
- package/dist/doc-metadata/extractors/error-codes.js +50 -0
- package/dist/friendly-errors.js +131 -5
- package/dist/generator/inngest.js +27 -0
- package/dist/generator/unified.d.ts +7 -2
- package/dist/generator/unified.js +31 -3
- package/dist/jsdoc-parser.d.ts +14 -0
- package/dist/jsdoc-parser.js +19 -1
- package/dist/mcp/tools-editor.js +20 -1
- package/dist/mcp/workflow-executor.d.ts +1 -0
- package/dist/mcp/workflow-executor.js +11 -4
- package/dist/parser.d.ts +4 -0
- package/dist/parser.js +69 -0
- package/dist/validator.d.ts +5 -0
- package/dist/validator.js +207 -14
- package/docs/reference/advanced-annotations.md +71 -2
- package/docs/reference/concepts.md +44 -4
- package/docs/reference/debugging.md +33 -0
- package/docs/reference/error-codes.md +7 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -218,7 +218,7 @@ flow-weaver export workflow.ts --target cloudflare --output deploy/
|
|
|
218
218
|
flow-weaver serve ./workflows --port 3000 --swagger
|
|
219
219
|
```
|
|
220
220
|
|
|
221
|
-
Inngest
|
|
221
|
+
Both the default TypeScript target and Inngest target parallelize independent nodes with `Promise.all()`. Inngest additionally wraps each node in `step.run()` for individual durability and generates typed Zod event schemas.
|
|
222
222
|
|
|
223
223
|
## Visual Human-in-the-Loop
|
|
224
224
|
|
|
@@ -319,6 +319,7 @@ flow-weaver listen # Stream editor events
|
|
|
319
319
|
|------|---------|
|
|
320
320
|
| `delay` | Sleep for a duration (ms, s, m, h, d). Mockable for fast testing. |
|
|
321
321
|
| `waitForEvent` | Wait for an external event with optional field matching and timeout. Maps to Inngest `step.waitForEvent()` for zero-cost durable pauses. |
|
|
322
|
+
| `waitForAgent` | Pause execution and wait for an external agent to provide a result. Supports multi-step human-in-the-loop and agent delegation patterns. |
|
|
322
323
|
| `invokeWorkflow` | Invoke another workflow by ID with payload and timeout. Maps to Inngest `step.invoke()`. |
|
|
323
324
|
|
|
324
325
|
## STEP Port Architecture
|
|
@@ -20,6 +20,10 @@ export class AnnotationGenerator {
|
|
|
20
20
|
if (nodeType.variant === 'MAP_ITERATOR') {
|
|
21
21
|
return;
|
|
22
22
|
}
|
|
23
|
+
// Skip synthetic COERCION node types — they're generated from @coerce macros
|
|
24
|
+
if (nodeType.variant === 'COERCION') {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
23
27
|
lines.push(...this.generateNodeTypeAnnotation(nodeType, indent, includeComments, includeMetadata));
|
|
24
28
|
lines.push("");
|
|
25
29
|
});
|
|
@@ -36,9 +40,11 @@ export class AnnotationGenerator {
|
|
|
36
40
|
}
|
|
37
41
|
// Generate JSDoc comment block (only when no functionText)
|
|
38
42
|
lines.push("/**");
|
|
39
|
-
// Add description if present
|
|
43
|
+
// Add description if present (handle multi-line descriptions)
|
|
40
44
|
if (includeComments && nodeType.description) {
|
|
41
|
-
|
|
45
|
+
for (const descLine of nodeType.description.split('\n')) {
|
|
46
|
+
lines.push(` * ${descLine}`);
|
|
47
|
+
}
|
|
42
48
|
lines.push(` *`);
|
|
43
49
|
}
|
|
44
50
|
// @flowWeaver nodeType marker
|
|
@@ -136,10 +142,11 @@ export class AnnotationGenerator {
|
|
|
136
142
|
}
|
|
137
143
|
generateWorkflowAnnotation(workflow, indent, includeComments, skipParamReturns = false) {
|
|
138
144
|
const lines = [];
|
|
139
|
-
// Build macro coverage sets for filtering (@map
|
|
145
|
+
// Build macro coverage sets for filtering (@map and @coerce)
|
|
140
146
|
const macroInstanceIds = new Set();
|
|
141
147
|
const macroChildIds = new Set();
|
|
142
148
|
const macroScopeNames = new Set();
|
|
149
|
+
const coerceInstanceIds = new Set();
|
|
143
150
|
if (workflow.macros && workflow.macros.length > 0) {
|
|
144
151
|
for (const macro of workflow.macros) {
|
|
145
152
|
if (macro.type === 'map') {
|
|
@@ -147,6 +154,9 @@ export class AnnotationGenerator {
|
|
|
147
154
|
macroChildIds.add(macro.childId);
|
|
148
155
|
macroScopeNames.add(`${macro.instanceId}.iterate`);
|
|
149
156
|
}
|
|
157
|
+
else if (macro.type === 'coerce') {
|
|
158
|
+
coerceInstanceIds.add(macro.instanceId);
|
|
159
|
+
}
|
|
150
160
|
}
|
|
151
161
|
}
|
|
152
162
|
// Generate JSDoc comment block
|
|
@@ -211,10 +221,12 @@ export class AnnotationGenerator {
|
|
|
211
221
|
if (workflow.description && includeComments) {
|
|
212
222
|
lines.push(` * @description ${workflow.description}`);
|
|
213
223
|
}
|
|
214
|
-
// Add node instances — skip synthetic MAP_ITERATOR instances, strip parent from macro children
|
|
224
|
+
// Add node instances — skip synthetic MAP_ITERATOR/COERCION instances, strip parent from macro children
|
|
215
225
|
workflow.instances.forEach((instance) => {
|
|
216
226
|
if (macroInstanceIds.has(instance.id))
|
|
217
227
|
return;
|
|
228
|
+
if (coerceInstanceIds.has(instance.id))
|
|
229
|
+
return;
|
|
218
230
|
if (macroChildIds.has(instance.id) && instance.parent) {
|
|
219
231
|
const stripped = { ...instance, parent: undefined };
|
|
220
232
|
lines.push(generateNodeInstanceTag(stripped));
|
|
@@ -257,6 +269,11 @@ export class AnnotationGenerator {
|
|
|
257
269
|
const tgt = `${macro.target.node}.${macro.target.port}`;
|
|
258
270
|
lines.push(` * @fanIn ${srcs} -> ${tgt}`);
|
|
259
271
|
}
|
|
272
|
+
else if (macro.type === 'coerce') {
|
|
273
|
+
const src = `${macro.source.node}.${macro.source.port}`;
|
|
274
|
+
const tgt = `${macro.target.node}.${macro.target.port}`;
|
|
275
|
+
lines.push(` * @coerce ${macro.instanceId} ${src} -> ${tgt} as ${macro.targetType}`);
|
|
276
|
+
}
|
|
260
277
|
}
|
|
261
278
|
}
|
|
262
279
|
// Add node positions (if they exist)
|
|
@@ -492,6 +509,14 @@ function isConnectionCoveredByMacroStatic(conn, macros) {
|
|
|
492
509
|
}
|
|
493
510
|
}
|
|
494
511
|
}
|
|
512
|
+
else if (macro.type === 'coerce') {
|
|
513
|
+
if (conn.from.scope || conn.to.scope)
|
|
514
|
+
continue;
|
|
515
|
+
// Connections to/from the synthetic coercion instance are covered
|
|
516
|
+
if (conn.to.node === macro.instanceId || conn.from.node === macro.instanceId) {
|
|
517
|
+
return true;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
495
520
|
}
|
|
496
521
|
return false;
|
|
497
522
|
}
|
|
@@ -408,6 +408,9 @@ function replaceWorkflowFunctionBody(source, functionName, newBody) {
|
|
|
408
408
|
}
|
|
409
409
|
// Find the closing brace
|
|
410
410
|
const closeBraceIdx = functionNode.body.end - 1;
|
|
411
|
+
if (closeBraceIdx <= openBraceIdx) {
|
|
412
|
+
return { code: source, changed: false };
|
|
413
|
+
}
|
|
411
414
|
const before = source.slice(0, openBraceIdx + 1);
|
|
412
415
|
const after = source.slice(closeBraceIdx);
|
|
413
416
|
const newBodyWithMarkers = [
|
package/dist/ast/types.d.ts
CHANGED
|
@@ -111,7 +111,7 @@ export type TWorkflowAST = {
|
|
|
111
111
|
* A sugar macro that expands to a full scope pattern or connection set.
|
|
112
112
|
* Stored on the workflow AST for round-trip preservation in generateInPlace.
|
|
113
113
|
*/
|
|
114
|
-
export type TWorkflowMacro = TMapMacro | TPathMacro | TFanOutMacro | TFanInMacro;
|
|
114
|
+
export type TWorkflowMacro = TMapMacro | TPathMacro | TFanOutMacro | TFanInMacro | TCoerceMacro;
|
|
115
115
|
export type TMapMacro = {
|
|
116
116
|
type: 'map';
|
|
117
117
|
/** Instance ID for the synthetic iterator node (e.g., "loop") */
|
|
@@ -159,6 +159,24 @@ export type TFanInMacro = {
|
|
|
159
159
|
port: string;
|
|
160
160
|
};
|
|
161
161
|
};
|
|
162
|
+
export type TCoerceTargetType = 'string' | 'number' | 'boolean' | 'json' | 'object';
|
|
163
|
+
export type TCoerceMacro = {
|
|
164
|
+
type: 'coerce';
|
|
165
|
+
/** Instance ID for the synthetic coercion node */
|
|
166
|
+
instanceId: string;
|
|
167
|
+
/** Source port in "node.port" format */
|
|
168
|
+
source: {
|
|
169
|
+
node: string;
|
|
170
|
+
port: string;
|
|
171
|
+
};
|
|
172
|
+
/** Target port in "node.port" format */
|
|
173
|
+
target: {
|
|
174
|
+
node: string;
|
|
175
|
+
port: string;
|
|
176
|
+
};
|
|
177
|
+
/** Coercion target type */
|
|
178
|
+
targetType: TCoerceTargetType;
|
|
179
|
+
};
|
|
162
180
|
/**
|
|
163
181
|
* Default configuration for a node type that can be overridden per instance.
|
|
164
182
|
*/
|
|
@@ -222,7 +240,7 @@ export type TNodeTypeAST = {
|
|
|
222
240
|
/** Multiple scopes this node creates */
|
|
223
241
|
scopes?: string[];
|
|
224
242
|
/** Variant identifier (set by app layer) */
|
|
225
|
-
variant?: 'FUNCTION' | 'WORKFLOW' | 'IMPORTED_WORKFLOW' | 'MAP_ITERATOR';
|
|
243
|
+
variant?: 'FUNCTION' | 'WORKFLOW' | 'IMPORTED_WORKFLOW' | 'MAP_ITERATOR' | 'COERCION';
|
|
226
244
|
/** File path for external node types */
|
|
227
245
|
path?: string;
|
|
228
246
|
/** Function reference for function-based node types */
|
|
@@ -619,6 +637,8 @@ export type TValidationError = {
|
|
|
619
637
|
node?: string;
|
|
620
638
|
connection?: TConnectionAST;
|
|
621
639
|
location?: TSourceLocation;
|
|
640
|
+
/** Reference to documentation explaining this error and how to fix it. */
|
|
641
|
+
docUrl?: string;
|
|
622
642
|
};
|
|
623
643
|
export type TAnalysisResult = {
|
|
624
644
|
controlFlowGraph: TControlFlowGraph;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Synthetic node type definitions for explicit type coercion.
|
|
3
|
+
*
|
|
4
|
+
* These are compiler-internal node types created by @coerce macros.
|
|
5
|
+
* They generate inline JavaScript coercion expressions instead of function calls.
|
|
6
|
+
*/
|
|
7
|
+
import type { TNodeTypeAST, TCoerceTargetType } from '../ast/types.js';
|
|
8
|
+
/** Maps coercion target type keyword to the synthetic node type name */
|
|
9
|
+
export declare const COERCE_TYPE_MAP: Record<TCoerceTargetType, string>;
|
|
10
|
+
/** Maps synthetic node type name to the inline JS expression */
|
|
11
|
+
export declare const COERCE_EXPRESSIONS: Record<string, string>;
|
|
12
|
+
export declare const COERCION_NODE_TYPES: Record<string, TNodeTypeAST>;
|
|
13
|
+
/** Check if a function name is a coercion node type */
|
|
14
|
+
export declare function isCoercionNode(functionName: string): boolean;
|
|
15
|
+
//# sourceMappingURL=coercion-types.d.ts.map
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Synthetic node type definitions for explicit type coercion.
|
|
3
|
+
*
|
|
4
|
+
* These are compiler-internal node types created by @coerce macros.
|
|
5
|
+
* They generate inline JavaScript coercion expressions instead of function calls.
|
|
6
|
+
*/
|
|
7
|
+
/** Maps coercion target type keyword to the synthetic node type name */
|
|
8
|
+
export const COERCE_TYPE_MAP = {
|
|
9
|
+
string: '__fw_toString',
|
|
10
|
+
number: '__fw_toNumber',
|
|
11
|
+
boolean: '__fw_toBoolean',
|
|
12
|
+
json: '__fw_toJSON',
|
|
13
|
+
object: '__fw_parseJSON',
|
|
14
|
+
};
|
|
15
|
+
/** Maps synthetic node type name to the inline JS expression */
|
|
16
|
+
export const COERCE_EXPRESSIONS = {
|
|
17
|
+
__fw_toString: 'String',
|
|
18
|
+
__fw_toNumber: 'Number',
|
|
19
|
+
__fw_toBoolean: 'Boolean',
|
|
20
|
+
__fw_toJSON: 'JSON.stringify',
|
|
21
|
+
__fw_parseJSON: 'JSON.parse',
|
|
22
|
+
};
|
|
23
|
+
function makeCoercionNodeType(name, outputType, outputTsType) {
|
|
24
|
+
return {
|
|
25
|
+
type: 'NodeType',
|
|
26
|
+
name,
|
|
27
|
+
functionName: name,
|
|
28
|
+
expression: true,
|
|
29
|
+
isAsync: false,
|
|
30
|
+
hasSuccessPort: true,
|
|
31
|
+
hasFailurePort: true,
|
|
32
|
+
executeWhen: 'CONJUNCTION',
|
|
33
|
+
variant: 'COERCION',
|
|
34
|
+
inputs: {
|
|
35
|
+
execute: { dataType: 'STEP', label: 'Execute' },
|
|
36
|
+
value: { dataType: 'ANY', label: 'Value', tsType: 'unknown' },
|
|
37
|
+
},
|
|
38
|
+
outputs: {
|
|
39
|
+
onSuccess: { dataType: 'STEP', label: 'On Success', isControlFlow: true },
|
|
40
|
+
onFailure: { dataType: 'STEP', label: 'On Failure', failure: true, isControlFlow: true },
|
|
41
|
+
result: { dataType: outputType, label: 'Result', tsType: outputTsType },
|
|
42
|
+
},
|
|
43
|
+
visuals: {
|
|
44
|
+
color: '#0d9488',
|
|
45
|
+
icon: 'transform',
|
|
46
|
+
tags: [{ label: 'coerce' }],
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
export const COERCION_NODE_TYPES = {
|
|
51
|
+
__fw_toString: makeCoercionNodeType('__fw_toString', 'STRING', 'string'),
|
|
52
|
+
__fw_toNumber: makeCoercionNodeType('__fw_toNumber', 'NUMBER', 'number'),
|
|
53
|
+
__fw_toBoolean: makeCoercionNodeType('__fw_toBoolean', 'BOOLEAN', 'boolean'),
|
|
54
|
+
__fw_toJSON: makeCoercionNodeType('__fw_toJSON', 'STRING', 'string'),
|
|
55
|
+
__fw_parseJSON: makeCoercionNodeType('__fw_parseJSON', 'OBJECT', 'object'),
|
|
56
|
+
};
|
|
57
|
+
/** Check if a function name is a coercion node type */
|
|
58
|
+
export function isCoercionNode(functionName) {
|
|
59
|
+
return functionName in COERCION_NODE_TYPES;
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=coercion-types.js.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getMockConfig } from './mock-types.js';
|
|
1
|
+
import { getMockConfig, lookupMock } from './mock-types.js';
|
|
2
2
|
/**
|
|
3
3
|
* @flowWeaver nodeType
|
|
4
4
|
* @input functionId - Inngest function ID (e.g. "my-service/sub-workflow")
|
|
@@ -11,8 +11,8 @@ export async function invokeWorkflow(execute, functionId, payload, timeout) {
|
|
|
11
11
|
return { onSuccess: false, onFailure: false, result: {} };
|
|
12
12
|
const mocks = getMockConfig();
|
|
13
13
|
if (mocks) {
|
|
14
|
-
// Mock mode
|
|
15
|
-
const mockResult = mocks.invocations
|
|
14
|
+
// Mock mode — look up result by functionId (supports instance-qualified keys)
|
|
15
|
+
const mockResult = lookupMock(mocks.invocations, functionId);
|
|
16
16
|
if (mockResult !== undefined) {
|
|
17
17
|
return { onSuccess: true, onFailure: false, result: mockResult };
|
|
18
18
|
}
|
|
@@ -17,4 +17,24 @@ export interface FwMockConfig {
|
|
|
17
17
|
* Read the mock config from globalThis, returning undefined if not set.
|
|
18
18
|
*/
|
|
19
19
|
export declare function getMockConfig(): FwMockConfig | undefined;
|
|
20
|
+
/**
|
|
21
|
+
* Look up a mock value from a section, supporting instance-qualified keys.
|
|
22
|
+
*
|
|
23
|
+
* Checks "instanceId:key" first (for per-node targeting), then falls back
|
|
24
|
+
* to plain "key". The instance ID comes from __fw_current_node_id__ which
|
|
25
|
+
* the generated code sets before each node invocation.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```json
|
|
29
|
+
* {
|
|
30
|
+
* "invocations": {
|
|
31
|
+
* "retryCall:api/process": { "status": "ok" },
|
|
32
|
+
* "api/process": { "status": "default" }
|
|
33
|
+
* }
|
|
34
|
+
* }
|
|
35
|
+
* ```
|
|
36
|
+
* When the node "retryCall" invokes "api/process", it gets `{ status: "ok" }`.
|
|
37
|
+
* Any other node invoking "api/process" gets `{ status: "default" }`.
|
|
38
|
+
*/
|
|
39
|
+
export declare function lookupMock<T>(section: Record<string, T> | undefined, key: string): T | undefined;
|
|
20
40
|
//# sourceMappingURL=mock-types.d.ts.map
|
|
@@ -9,4 +9,34 @@
|
|
|
9
9
|
export function getMockConfig() {
|
|
10
10
|
return globalThis.__fw_mocks__;
|
|
11
11
|
}
|
|
12
|
+
/**
|
|
13
|
+
* Look up a mock value from a section, supporting instance-qualified keys.
|
|
14
|
+
*
|
|
15
|
+
* Checks "instanceId:key" first (for per-node targeting), then falls back
|
|
16
|
+
* to plain "key". The instance ID comes from __fw_current_node_id__ which
|
|
17
|
+
* the generated code sets before each node invocation.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```json
|
|
21
|
+
* {
|
|
22
|
+
* "invocations": {
|
|
23
|
+
* "retryCall:api/process": { "status": "ok" },
|
|
24
|
+
* "api/process": { "status": "default" }
|
|
25
|
+
* }
|
|
26
|
+
* }
|
|
27
|
+
* ```
|
|
28
|
+
* When the node "retryCall" invokes "api/process", it gets `{ status: "ok" }`.
|
|
29
|
+
* Any other node invoking "api/process" gets `{ status: "default" }`.
|
|
30
|
+
*/
|
|
31
|
+
export function lookupMock(section, key) {
|
|
32
|
+
if (!section)
|
|
33
|
+
return undefined;
|
|
34
|
+
const nodeId = globalThis.__fw_current_node_id__;
|
|
35
|
+
if (nodeId) {
|
|
36
|
+
const qualified = section[`${nodeId}:${key}`];
|
|
37
|
+
if (qualified !== undefined)
|
|
38
|
+
return qualified;
|
|
39
|
+
}
|
|
40
|
+
return section[key];
|
|
41
|
+
}
|
|
12
42
|
//# sourceMappingURL=mock-types.js.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getMockConfig } from './mock-types.js';
|
|
1
|
+
import { getMockConfig, lookupMock } from './mock-types.js';
|
|
2
2
|
/**
|
|
3
3
|
* @flowWeaver nodeType
|
|
4
4
|
* @input agentId - Agent/task identifier
|
|
@@ -9,10 +9,11 @@ import { getMockConfig } from './mock-types.js';
|
|
|
9
9
|
export async function waitForAgent(execute, agentId, context, prompt) {
|
|
10
10
|
if (!execute)
|
|
11
11
|
return { onSuccess: false, onFailure: false, agentResult: {} };
|
|
12
|
-
// 1. Check mocks first
|
|
12
|
+
// 1. Check mocks first (supports instance-qualified keys)
|
|
13
13
|
const mocks = getMockConfig();
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
const mockResult = lookupMock(mocks?.agents, agentId);
|
|
15
|
+
if (mockResult !== undefined) {
|
|
16
|
+
return { onSuccess: true, onFailure: false, agentResult: mockResult };
|
|
16
17
|
}
|
|
17
18
|
// 2. Check agent channel (set by executor for pause/resume)
|
|
18
19
|
const channel = globalThis.__fw_agent_channel__;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getMockConfig } from './mock-types.js';
|
|
1
|
+
import { getMockConfig, lookupMock } from './mock-types.js';
|
|
2
2
|
/**
|
|
3
3
|
* @flowWeaver nodeType
|
|
4
4
|
* @input eventName - Event name to wait for (e.g. "app/approval.received")
|
|
@@ -11,8 +11,8 @@ export async function waitForEvent(execute, eventName, match, timeout) {
|
|
|
11
11
|
return { onSuccess: false, onFailure: false, eventData: {} };
|
|
12
12
|
const mocks = getMockConfig();
|
|
13
13
|
if (mocks) {
|
|
14
|
-
// Mock mode
|
|
15
|
-
const mockData = mocks.events
|
|
14
|
+
// Mock mode — look up event data by name (supports instance-qualified keys)
|
|
15
|
+
const mockData = lookupMock(mocks.events, eventName);
|
|
16
16
|
if (mockData !== undefined) {
|
|
17
17
|
return { onSuccess: true, onFailure: false, eventData: mockData };
|
|
18
18
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module chevrotain-parser/coerce-parser
|
|
3
|
+
*
|
|
4
|
+
* Parser for @coerce sugar annotation using Chevrotain.
|
|
5
|
+
*
|
|
6
|
+
* Syntax:
|
|
7
|
+
* @coerce instanceId sourceNode.port -> targetNode.port as targetType
|
|
8
|
+
*
|
|
9
|
+
* Where targetType is one of: string, number, boolean, json, object
|
|
10
|
+
*/
|
|
11
|
+
import type { TCoerceTargetType } from '../ast/types.js';
|
|
12
|
+
export interface CoerceParseResult {
|
|
13
|
+
instanceId: string;
|
|
14
|
+
source: {
|
|
15
|
+
node: string;
|
|
16
|
+
port: string;
|
|
17
|
+
};
|
|
18
|
+
target: {
|
|
19
|
+
node: string;
|
|
20
|
+
port: string;
|
|
21
|
+
};
|
|
22
|
+
targetType: TCoerceTargetType;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Parse a @coerce line and return structured result.
|
|
26
|
+
* Returns null if the line is not a valid @coerce declaration.
|
|
27
|
+
*/
|
|
28
|
+
export declare function parseCoerceLine(input: string, warnings: string[]): CoerceParseResult | null;
|
|
29
|
+
/**
|
|
30
|
+
* Get serialized grammar for documentation/diagram generation.
|
|
31
|
+
*/
|
|
32
|
+
export declare function getCoerceGrammar(): import("chevrotain").ISerializedGast[];
|
|
33
|
+
//# sourceMappingURL=coerce-parser.d.ts.map
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module chevrotain-parser/coerce-parser
|
|
3
|
+
*
|
|
4
|
+
* Parser for @coerce sugar annotation using Chevrotain.
|
|
5
|
+
*
|
|
6
|
+
* Syntax:
|
|
7
|
+
* @coerce instanceId sourceNode.port -> targetNode.port as targetType
|
|
8
|
+
*
|
|
9
|
+
* Where targetType is one of: string, number, boolean, json, object
|
|
10
|
+
*/
|
|
11
|
+
import { CstParser } from 'chevrotain';
|
|
12
|
+
import { JSDocLexer, CoerceTag, Identifier, Arrow, Dot, AsKeyword, allTokens, } from './tokens.js';
|
|
13
|
+
const VALID_COERCE_TYPES = new Set(['string', 'number', 'boolean', 'json', 'object']);
|
|
14
|
+
// =============================================================================
|
|
15
|
+
// Parser Definition
|
|
16
|
+
// =============================================================================
|
|
17
|
+
class CoerceParser extends CstParser {
|
|
18
|
+
constructor() {
|
|
19
|
+
super(allTokens);
|
|
20
|
+
this.performSelfAnalysis();
|
|
21
|
+
}
|
|
22
|
+
// @coerce Identifier portRef Arrow portRef AsKeyword Identifier
|
|
23
|
+
coerceLine = this.RULE('coerceLine', () => {
|
|
24
|
+
this.CONSUME(CoerceTag);
|
|
25
|
+
this.CONSUME(Identifier, { LABEL: 'instanceId' });
|
|
26
|
+
this.SUBRULE(this.portRef, { LABEL: 'source' });
|
|
27
|
+
this.CONSUME(Arrow);
|
|
28
|
+
this.SUBRULE2(this.portRef, { LABEL: 'target' });
|
|
29
|
+
this.CONSUME(AsKeyword);
|
|
30
|
+
this.CONSUME2(Identifier, { LABEL: 'typeName' });
|
|
31
|
+
});
|
|
32
|
+
// portRef: Identifier Dot Identifier
|
|
33
|
+
portRef = this.RULE('portRef', () => {
|
|
34
|
+
this.CONSUME(Identifier, { LABEL: 'nodeName' });
|
|
35
|
+
this.CONSUME(Dot);
|
|
36
|
+
this.CONSUME2(Identifier, { LABEL: 'portName' });
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
// =============================================================================
|
|
40
|
+
// Parser Instance (singleton)
|
|
41
|
+
// =============================================================================
|
|
42
|
+
const parserInstance = new CoerceParser();
|
|
43
|
+
// =============================================================================
|
|
44
|
+
// CST Visitor
|
|
45
|
+
// =============================================================================
|
|
46
|
+
const BaseVisitor = parserInstance.getBaseCstVisitorConstructor();
|
|
47
|
+
class CoerceVisitor extends BaseVisitor {
|
|
48
|
+
constructor() {
|
|
49
|
+
super();
|
|
50
|
+
this.validateVisitor();
|
|
51
|
+
}
|
|
52
|
+
coerceLine(ctx) {
|
|
53
|
+
const instanceId = ctx.instanceId[0].image;
|
|
54
|
+
const source = this.portRef(ctx.source[0].children);
|
|
55
|
+
const target = this.portRef(ctx.target[0].children);
|
|
56
|
+
const targetType = ctx.typeName[0].image;
|
|
57
|
+
return { instanceId, source, target, targetType };
|
|
58
|
+
}
|
|
59
|
+
portRef(ctx) {
|
|
60
|
+
return {
|
|
61
|
+
node: ctx.nodeName[0].image,
|
|
62
|
+
port: ctx.portName[0].image,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const visitorInstance = new CoerceVisitor();
|
|
67
|
+
// =============================================================================
|
|
68
|
+
// Public API
|
|
69
|
+
// =============================================================================
|
|
70
|
+
/**
|
|
71
|
+
* Parse a @coerce line and return structured result.
|
|
72
|
+
* Returns null if the line is not a valid @coerce declaration.
|
|
73
|
+
*/
|
|
74
|
+
export function parseCoerceLine(input, warnings) {
|
|
75
|
+
const lexResult = JSDocLexer.tokenize(input);
|
|
76
|
+
if (lexResult.errors.length > 0 || lexResult.tokens.length === 0)
|
|
77
|
+
return null;
|
|
78
|
+
if (lexResult.tokens[0].tokenType !== CoerceTag)
|
|
79
|
+
return null;
|
|
80
|
+
parserInstance.input = lexResult.tokens;
|
|
81
|
+
const cst = parserInstance.coerceLine();
|
|
82
|
+
if (parserInstance.errors.length > 0) {
|
|
83
|
+
const truncatedInput = input.length > 80 ? input.substring(0, 80) + '...' : input;
|
|
84
|
+
warnings.push(`Failed to parse @coerce line: "${truncatedInput}"\n` +
|
|
85
|
+
` Error: ${parserInstance.errors[0].message}\n` +
|
|
86
|
+
` Expected format: @coerce instanceId source.port -> target.port as type`);
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
const result = visitorInstance.visit(cst);
|
|
90
|
+
// Validate the target type
|
|
91
|
+
if (!VALID_COERCE_TYPES.has(result.targetType)) {
|
|
92
|
+
warnings.push(`@coerce: invalid target type "${result.targetType}". Valid types: ${[...VALID_COERCE_TYPES].join(', ')}`);
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
return result;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Get serialized grammar for documentation/diagram generation.
|
|
99
|
+
*/
|
|
100
|
+
export function getCoerceGrammar() {
|
|
101
|
+
return parserInstance.getSerializedGastProductions();
|
|
102
|
+
}
|
|
103
|
+
//# sourceMappingURL=coerce-parser.js.map
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Chevrotain-based parsers for JSDoc annotations.
|
|
5
5
|
* Provides structured parsing for @input, @output, @node, @connect, and @position.
|
|
6
6
|
*/
|
|
7
|
-
export { JSDocLexer, allTokens, InputTag, OutputTag, NodeTag, ConnectTag, PositionTag, ScopeTag, MapTag, PathTag, FanOutTag, FanInTag, TriggerTag, CancelOnTag, RetriesTag, TimeoutTag, ThrottleTag, } from './tokens.js';
|
|
7
|
+
export { JSDocLexer, allTokens, InputTag, OutputTag, NodeTag, ConnectTag, PositionTag, ScopeTag, MapTag, PathTag, FanOutTag, FanInTag, CoerceTag, TriggerTag, CancelOnTag, RetriesTag, TimeoutTag, ThrottleTag, } from './tokens.js';
|
|
8
8
|
export { parsePortLine, getPortGrammar } from './port-parser.js';
|
|
9
9
|
export type { PortParseResult } from './port-parser.js';
|
|
10
10
|
export { parseNodeLine, getNodeGrammar } from './node-parser.js';
|
|
@@ -21,6 +21,8 @@ export { parsePathLine, getPathGrammar } from './path-parser.js';
|
|
|
21
21
|
export type { PathParseResult, PathStep } from './path-parser.js';
|
|
22
22
|
export { parseFanOutLine, parseFanInLine } from './fan-parser.js';
|
|
23
23
|
export type { FanOutParseResult, FanInParseResult, PortRef } from './fan-parser.js';
|
|
24
|
+
export { parseCoerceLine } from './coerce-parser.js';
|
|
25
|
+
export type { CoerceParseResult } from './coerce-parser.js';
|
|
24
26
|
export { parseTriggerLine, parseCancelOnLine, parseRetriesLine, parseTimeoutLine, parseThrottleLine } from './trigger-cancel-parser.js';
|
|
25
27
|
export type { TriggerParseResult, CancelOnParseResult, ThrottleParseResult } from './trigger-cancel-parser.js';
|
|
26
28
|
export { generateGrammarDiagrams, getAllGrammars, serializedToEBNF } from './grammar-diagrams.js';
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Provides structured parsing for @input, @output, @node, @connect, and @position.
|
|
6
6
|
*/
|
|
7
7
|
// Token exports (for advanced use cases)
|
|
8
|
-
export { JSDocLexer, allTokens, InputTag, OutputTag, NodeTag, ConnectTag, PositionTag, ScopeTag, MapTag, PathTag, FanOutTag, FanInTag, TriggerTag, CancelOnTag, RetriesTag, TimeoutTag, ThrottleTag, } from './tokens.js';
|
|
8
|
+
export { JSDocLexer, allTokens, InputTag, OutputTag, NodeTag, ConnectTag, PositionTag, ScopeTag, MapTag, PathTag, FanOutTag, FanInTag, CoerceTag, TriggerTag, CancelOnTag, RetriesTag, TimeoutTag, ThrottleTag, } from './tokens.js';
|
|
9
9
|
// Port parser
|
|
10
10
|
export { parsePortLine, getPortGrammar } from './port-parser.js';
|
|
11
11
|
// Node parser
|
|
@@ -22,6 +22,8 @@ export { parseMapLine, getMapGrammar } from './map-parser.js';
|
|
|
22
22
|
export { parsePathLine, getPathGrammar } from './path-parser.js';
|
|
23
23
|
// Fan-out / Fan-in parser
|
|
24
24
|
export { parseFanOutLine, parseFanInLine } from './fan-parser.js';
|
|
25
|
+
// Coerce parser
|
|
26
|
+
export { parseCoerceLine } from './coerce-parser.js';
|
|
25
27
|
// Trigger/cancel/retries/timeout/throttle parser
|
|
26
28
|
export { parseTriggerLine, parseCancelOnLine, parseRetriesLine, parseTimeoutLine, parseThrottleLine } from './trigger-cancel-parser.js';
|
|
27
29
|
// Grammar diagram generation
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Parser for @input/@output/@step port declarations using Chevrotain.
|
|
5
5
|
*/
|
|
6
6
|
import { CstParser } from 'chevrotain';
|
|
7
|
-
import { JSDocLexer, InputTag, OutputTag, StepTag, Identifier, ScopePrefix, OrderPrefix, PlacementPrefix, TypePrefix, MergeStrategyPrefix, TopKeyword, BottomKeyword, TrueKeyword, FalseKeyword, OverKeyword, MinimizedKeyword, Integer, StringLiteral, DescriptionText, LBracket, RBracket, LParen, RParen, Comma, Colon, Dot, Dash, Equals, GreaterThan, LessThan, Pipe, Ampersand, LBrace, RBrace, Asterisk, allTokens, } from './tokens.js';
|
|
7
|
+
import { JSDocLexer, InputTag, OutputTag, StepTag, Identifier, ScopePrefix, OrderPrefix, PlacementPrefix, TypePrefix, MergeStrategyPrefix, TopKeyword, BottomKeyword, TrueKeyword, FalseKeyword, OverKeyword, AsKeyword, MinimizedKeyword, Integer, StringLiteral, DescriptionText, LBracket, RBracket, LParen, RParen, Comma, Colon, Dot, Dash, Equals, GreaterThan, LessThan, Pipe, Ampersand, LBrace, RBrace, Asterisk, allTokens, } from './tokens.js';
|
|
8
8
|
// =============================================================================
|
|
9
9
|
// Parser Definition
|
|
10
10
|
// =============================================================================
|
|
@@ -160,6 +160,7 @@ class PortParser extends CstParser {
|
|
|
160
160
|
{ ALT: () => this.CONSUME(Asterisk) },
|
|
161
161
|
// Keywords that can appear in description text
|
|
162
162
|
{ ALT: () => this.CONSUME(OverKeyword) },
|
|
163
|
+
{ ALT: () => this.CONSUME(AsKeyword) },
|
|
163
164
|
{ ALT: () => this.CONSUME(TopKeyword) },
|
|
164
165
|
{ ALT: () => this.CONSUME(BottomKeyword) },
|
|
165
166
|
{ ALT: () => this.CONSUME(TrueKeyword) },
|
|
@@ -19,12 +19,14 @@ export declare const MapTag: import("chevrotain").TokenType;
|
|
|
19
19
|
export declare const PathTag: import("chevrotain").TokenType;
|
|
20
20
|
export declare const FanOutTag: import("chevrotain").TokenType;
|
|
21
21
|
export declare const FanInTag: import("chevrotain").TokenType;
|
|
22
|
+
export declare const CoerceTag: import("chevrotain").TokenType;
|
|
22
23
|
export declare const TriggerTag: import("chevrotain").TokenType;
|
|
23
24
|
export declare const CancelOnTag: import("chevrotain").TokenType;
|
|
24
25
|
export declare const RetriesTag: import("chevrotain").TokenType;
|
|
25
26
|
export declare const TimeoutTag: import("chevrotain").TokenType;
|
|
26
27
|
export declare const ThrottleTag: import("chevrotain").TokenType;
|
|
27
28
|
export declare const OverKeyword: import("chevrotain").TokenType;
|
|
29
|
+
export declare const AsKeyword: import("chevrotain").TokenType;
|
|
28
30
|
export declare const ParamTag: import("chevrotain").TokenType;
|
|
29
31
|
export declare const ReturnsTag: import("chevrotain").TokenType;
|
|
30
32
|
export declare const FlowWeaverTag: import("chevrotain").TokenType;
|
|
@@ -69,6 +69,10 @@ export const FanInTag = createToken({
|
|
|
69
69
|
name: 'FanInTag',
|
|
70
70
|
pattern: /@fanIn\b/,
|
|
71
71
|
});
|
|
72
|
+
export const CoerceTag = createToken({
|
|
73
|
+
name: 'CoerceTag',
|
|
74
|
+
pattern: /@coerce\b/,
|
|
75
|
+
});
|
|
72
76
|
export const TriggerTag = createToken({
|
|
73
77
|
name: 'TriggerTag',
|
|
74
78
|
pattern: /@trigger\b/,
|
|
@@ -93,6 +97,10 @@ export const OverKeyword = createToken({
|
|
|
93
97
|
name: 'OverKeyword',
|
|
94
98
|
pattern: /over\b/,
|
|
95
99
|
});
|
|
100
|
+
export const AsKeyword = createToken({
|
|
101
|
+
name: 'AsKeyword',
|
|
102
|
+
pattern: /as\b/,
|
|
103
|
+
});
|
|
96
104
|
export const ParamTag = createToken({
|
|
97
105
|
name: 'ParamTag',
|
|
98
106
|
pattern: /@param\b/,
|
|
@@ -325,6 +333,7 @@ export const allTokens = [
|
|
|
325
333
|
PathTag,
|
|
326
334
|
FanOutTag,
|
|
327
335
|
FanInTag,
|
|
336
|
+
CoerceTag,
|
|
328
337
|
TriggerTag,
|
|
329
338
|
CancelOnTag,
|
|
330
339
|
RetriesTag,
|
|
@@ -358,6 +367,7 @@ export const allTokens = [
|
|
|
358
367
|
PeriodEq,
|
|
359
368
|
// Keywords (before Identifier)
|
|
360
369
|
OverKeyword,
|
|
370
|
+
AsKeyword,
|
|
361
371
|
MinimizedKeyword,
|
|
362
372
|
TopKeyword,
|
|
363
373
|
BottomKeyword,
|
|
@@ -116,6 +116,9 @@ export async function compileCommand(input, options = {}) {
|
|
|
116
116
|
const loc = err.location ? `[line ${err.location.line}] ` : '';
|
|
117
117
|
logger.error(` ${loc}${friendly.title}: ${friendly.explanation}`);
|
|
118
118
|
logger.info(` How to fix: ${friendly.fix}`);
|
|
119
|
+
if (err.docUrl) {
|
|
120
|
+
logger.info(` See: ${err.docUrl}`);
|
|
121
|
+
}
|
|
119
122
|
}
|
|
120
123
|
else {
|
|
121
124
|
let msg = ` - ${err.message}`;
|
|
@@ -123,6 +126,9 @@ export async function compileCommand(input, options = {}) {
|
|
|
123
126
|
msg += ` (node: ${err.node})`;
|
|
124
127
|
}
|
|
125
128
|
logger.error(msg);
|
|
129
|
+
if (err.docUrl) {
|
|
130
|
+
logger.info(` See: ${err.docUrl}`);
|
|
131
|
+
}
|
|
126
132
|
}
|
|
127
133
|
});
|
|
128
134
|
errorCount++;
|
|
@@ -273,12 +273,14 @@ export function generateProjectFiles(projectName, template, format = 'esm') {
|
|
|
273
273
|
].join('\n');
|
|
274
274
|
}
|
|
275
275
|
const gitignore = `node_modules/\ndist/\n.tsbuildinfo\n`;
|
|
276
|
+
const configYaml = `defaultFileType: ts\n`;
|
|
276
277
|
return {
|
|
277
278
|
'package.json': packageJson,
|
|
278
279
|
'tsconfig.json': tsconfigJson,
|
|
279
280
|
[`src/${workflowFile}`]: workflowCode,
|
|
280
281
|
'src/main.ts': mainTs,
|
|
281
282
|
'.gitignore': gitignore,
|
|
283
|
+
'.flowweaver/config.yaml': configYaml,
|
|
282
284
|
};
|
|
283
285
|
}
|
|
284
286
|
// ── Filesystem writer ────────────────────────────────────────────────────────
|