@synergenius/flow-weaver 0.3.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/annotation-generator.js +36 -0
- package/dist/api/generate-in-place.js +39 -0
- package/dist/api/generate.js +11 -1
- package/dist/api/manipulation/nodes.js +22 -0
- package/dist/ast/types.d.ts +27 -1
- package/dist/built-in-nodes/index.d.ts +1 -0
- package/dist/built-in-nodes/index.js +1 -0
- package/dist/built-in-nodes/invoke-workflow.js +12 -1
- package/dist/built-in-nodes/mock-types.d.ts +2 -0
- package/dist/built-in-nodes/wait-for-agent.d.ts +13 -0
- package/dist/built-in-nodes/wait-for-agent.js +26 -0
- package/dist/chevrotain-parser/fan-parser.d.ts +38 -0
- package/dist/chevrotain-parser/fan-parser.js +149 -0
- package/dist/chevrotain-parser/grammar-diagrams.d.ts +1 -0
- package/dist/chevrotain-parser/grammar-diagrams.js +3 -0
- package/dist/chevrotain-parser/index.d.ts +3 -1
- package/dist/chevrotain-parser/index.js +3 -1
- package/dist/chevrotain-parser/tokens.d.ts +2 -0
- package/dist/chevrotain-parser/tokens.js +10 -0
- package/dist/cli/commands/diagram.d.ts +2 -1
- package/dist/cli/commands/diagram.js +9 -6
- package/dist/cli/commands/run.js +59 -1
- package/dist/cli/flow-weaver.mjs +1396 -77
- package/dist/cli/index.js +23 -36
- package/dist/diagram/geometry.js +47 -5
- package/dist/diagram/html-viewer.d.ts +12 -0
- package/dist/diagram/html-viewer.js +399 -0
- package/dist/diagram/index.d.ts +12 -0
- package/dist/diagram/index.js +22 -0
- package/dist/diagram/types.d.ts +1 -0
- package/dist/doc-metadata/extractors/annotations.js +282 -1
- package/dist/doc-metadata/types.d.ts +6 -0
- package/dist/generator/control-flow.d.ts +13 -0
- package/dist/generator/control-flow.js +74 -0
- package/dist/generator/inngest.js +23 -0
- package/dist/generator/unified.js +122 -2
- package/dist/jsdoc-parser.d.ts +24 -0
- package/dist/jsdoc-parser.js +41 -1
- package/dist/mcp/agent-channel.d.ts +35 -0
- package/dist/mcp/agent-channel.js +61 -0
- package/dist/mcp/run-registry.d.ts +29 -0
- package/dist/mcp/run-registry.js +24 -0
- package/dist/mcp/tools-diagram.d.ts +1 -1
- package/dist/mcp/tools-diagram.js +15 -7
- package/dist/mcp/tools-editor.js +75 -3
- package/dist/mcp/workflow-executor.d.ts +28 -0
- package/dist/mcp/workflow-executor.js +62 -1
- package/dist/parser.d.ts +8 -0
- package/dist/parser.js +100 -0
- package/dist/runtime/ExecutionContext.d.ts +2 -0
- package/dist/runtime/ExecutionContext.js +2 -0
- package/dist/runtime/events.d.ts +1 -1
- package/dist/sugar-optimizer.js +28 -3
- package/dist/validator.d.ts +8 -0
- package/dist/validator.js +92 -0
- package/docs/reference/advanced-annotations.md +431 -0
- package/docs/reference/built-in-nodes.md +225 -0
- package/docs/reference/cli-reference.md +882 -0
- package/docs/reference/compilation.md +351 -0
- package/docs/reference/concepts.md +400 -0
- package/docs/reference/debugging.md +255 -0
- package/docs/reference/deployment.md +207 -0
- package/docs/reference/error-codes.md +686 -0
- package/docs/reference/export-interface.md +229 -0
- package/docs/reference/iterative-development.md +186 -0
- package/docs/reference/jsdoc-grammar.md +471 -0
- package/docs/reference/marketplace.md +205 -0
- package/docs/reference/node-conversion.md +308 -0
- package/docs/reference/patterns.md +161 -0
- package/docs/reference/scaffold.md +160 -0
- package/docs/reference/tutorial.md +519 -0
- package/package.json +37 -1
package/README.md
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# @synergenius/flow-weaver
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/@synergenius/flow-weaver)
|
|
3
4
|
[](./LICENSE)
|
|
4
5
|
[](https://nodejs.org)
|
|
5
6
|
|
|
@@ -247,6 +247,16 @@ export class AnnotationGenerator {
|
|
|
247
247
|
const stepsStr = macro.steps.map(s => s.route ? `${s.node}:${s.route}` : s.node).join(' -> ');
|
|
248
248
|
lines.push(` * @path ${stepsStr}`);
|
|
249
249
|
}
|
|
250
|
+
else if (macro.type === 'fanOut') {
|
|
251
|
+
const src = `${macro.source.node}.${macro.source.port}`;
|
|
252
|
+
const tgts = macro.targets.map(t => t.port ? `${t.node}.${t.port}` : t.node).join(', ');
|
|
253
|
+
lines.push(` * @fanOut ${src} -> ${tgts}`);
|
|
254
|
+
}
|
|
255
|
+
else if (macro.type === 'fanIn') {
|
|
256
|
+
const srcs = macro.sources.map(s => s.port ? `${s.node}.${s.port}` : s.node).join(', ');
|
|
257
|
+
const tgt = `${macro.target.node}.${macro.target.port}`;
|
|
258
|
+
lines.push(` * @fanIn ${srcs} -> ${tgt}`);
|
|
259
|
+
}
|
|
250
260
|
}
|
|
251
261
|
}
|
|
252
262
|
// Add node positions (if they exist)
|
|
@@ -456,6 +466,32 @@ function isConnectionCoveredByMacroStatic(conn, macros) {
|
|
|
456
466
|
return true;
|
|
457
467
|
}
|
|
458
468
|
}
|
|
469
|
+
else if (macro.type === 'fanOut') {
|
|
470
|
+
if (conn.from.scope || conn.to.scope)
|
|
471
|
+
continue;
|
|
472
|
+
// Check if connection matches the fan-out pattern
|
|
473
|
+
if (conn.from.node === macro.source.node && conn.from.port === macro.source.port) {
|
|
474
|
+
for (const target of macro.targets) {
|
|
475
|
+
const targetPort = target.port ?? macro.source.port;
|
|
476
|
+
if (conn.to.node === target.node && conn.to.port === targetPort) {
|
|
477
|
+
return true;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
else if (macro.type === 'fanIn') {
|
|
483
|
+
if (conn.from.scope || conn.to.scope)
|
|
484
|
+
continue;
|
|
485
|
+
// Check if connection matches the fan-in pattern
|
|
486
|
+
if (conn.to.node === macro.target.node && conn.to.port === macro.target.port) {
|
|
487
|
+
for (const source of macro.sources) {
|
|
488
|
+
const sourcePort = source.port ?? macro.target.port;
|
|
489
|
+
if (conn.from.node === source.node && conn.from.port === sourcePort) {
|
|
490
|
+
return true;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
459
495
|
}
|
|
460
496
|
return false;
|
|
461
497
|
}
|
|
@@ -155,6 +155,11 @@ function generateRuntimeSection(functionName, production, moduleFormat = 'esm',
|
|
|
155
155
|
lines.push(`import { GeneratedExecutionContext, CancellationError } from '${externalRuntimePath}';`);
|
|
156
156
|
if (!production) {
|
|
157
157
|
lines.push(`import type { TDebugger } from '${externalRuntimePath}';`);
|
|
158
|
+
// Declare __flowWeaverDebugger__ so body code can reference it
|
|
159
|
+
lines.push('declare const __flowWeaverDebugger__: TDebugger | undefined;');
|
|
160
|
+
// Include inline debug client (createFlowWeaverDebugClient is not exported from runtime)
|
|
161
|
+
lines.push('');
|
|
162
|
+
lines.push(generateInlineDebugClient(moduleFormat));
|
|
158
163
|
}
|
|
159
164
|
}
|
|
160
165
|
else {
|
|
@@ -990,6 +995,30 @@ function isConnectionCoveredByMacro(conn, macros) {
|
|
|
990
995
|
return true;
|
|
991
996
|
}
|
|
992
997
|
}
|
|
998
|
+
else if (macro.type === 'fanOut') {
|
|
999
|
+
if (conn.from.scope || conn.to.scope)
|
|
1000
|
+
continue;
|
|
1001
|
+
if (conn.from.node === macro.source.node && conn.from.port === macro.source.port) {
|
|
1002
|
+
for (const target of macro.targets) {
|
|
1003
|
+
const targetPort = target.port ?? macro.source.port;
|
|
1004
|
+
if (conn.to.node === target.node && conn.to.port === targetPort) {
|
|
1005
|
+
return true;
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
else if (macro.type === 'fanIn') {
|
|
1011
|
+
if (conn.from.scope || conn.to.scope)
|
|
1012
|
+
continue;
|
|
1013
|
+
if (conn.to.node === macro.target.node && conn.to.port === macro.target.port) {
|
|
1014
|
+
for (const source of macro.sources) {
|
|
1015
|
+
const sourcePort = source.port ?? macro.target.port;
|
|
1016
|
+
if (conn.from.node === source.node && conn.from.port === sourcePort) {
|
|
1017
|
+
return true;
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
993
1022
|
}
|
|
994
1023
|
return false;
|
|
995
1024
|
}
|
|
@@ -1124,6 +1153,16 @@ function generateWorkflowJSDoc(ast, options = {}) {
|
|
|
1124
1153
|
const stepsStr = macro.steps.map(s => s.route ? `${s.node}:${s.route}` : s.node).join(' -> ');
|
|
1125
1154
|
lines.push(` * @path ${stepsStr}`);
|
|
1126
1155
|
}
|
|
1156
|
+
else if (macro.type === 'fanOut') {
|
|
1157
|
+
const src = `${macro.source.node}.${macro.source.port}`;
|
|
1158
|
+
const tgts = macro.targets.map(t => t.port ? `${t.node}.${t.port}` : t.node).join(', ');
|
|
1159
|
+
lines.push(` * @fanOut ${src} -> ${tgts}`);
|
|
1160
|
+
}
|
|
1161
|
+
else if (macro.type === 'fanIn') {
|
|
1162
|
+
const srcs = macro.sources.map(s => s.port ? `${s.node}.${s.port}` : s.node).join(', ');
|
|
1163
|
+
const tgt = `${macro.target.node}.${macro.target.port}`;
|
|
1164
|
+
lines.push(` * @fanIn ${srcs} -> ${tgt}`);
|
|
1165
|
+
}
|
|
1127
1166
|
}
|
|
1128
1167
|
}
|
|
1129
1168
|
// Auto-position: compute default positions for nodes without explicit positions.
|
package/dist/api/generate.js
CHANGED
|
@@ -88,8 +88,18 @@ export function generateCode(ast, options) {
|
|
|
88
88
|
lines.push(generateImportStatement(['GeneratedExecutionContext', 'CancellationError'], externalRuntimePath, moduleFormat));
|
|
89
89
|
addLine();
|
|
90
90
|
if (!production) {
|
|
91
|
-
|
|
91
|
+
// Import TDebugger type from external runtime
|
|
92
|
+
lines.push(moduleFormat === 'cjs'
|
|
93
|
+
? `const { TDebugger } = require('${externalRuntimePath}');`
|
|
94
|
+
: `import type { TDebugger } from '${externalRuntimePath}';`);
|
|
92
95
|
addLine();
|
|
96
|
+
// Include inline debug client (createFlowWeaverDebugClient is not exported from runtime)
|
|
97
|
+
const inlineDebugClient = generateInlineDebugClient(moduleFormat);
|
|
98
|
+
const debugClientLines = inlineDebugClient.split('\n');
|
|
99
|
+
debugClientLines.forEach((line) => {
|
|
100
|
+
lines.push(line);
|
|
101
|
+
addLine();
|
|
102
|
+
});
|
|
93
103
|
}
|
|
94
104
|
lines.push('');
|
|
95
105
|
addLine();
|
|
@@ -77,6 +77,12 @@ export function removeNode(ast, nodeId, options = {}) {
|
|
|
77
77
|
if (macro.type === 'path') {
|
|
78
78
|
return !macro.steps.some(s => s.node === nodeId);
|
|
79
79
|
}
|
|
80
|
+
if (macro.type === 'fanOut') {
|
|
81
|
+
return macro.source.node !== nodeId && !macro.targets.some(t => t.node === nodeId);
|
|
82
|
+
}
|
|
83
|
+
if (macro.type === 'fanIn') {
|
|
84
|
+
return macro.target.node !== nodeId && !macro.sources.some(s => s.node === nodeId);
|
|
85
|
+
}
|
|
80
86
|
return true;
|
|
81
87
|
});
|
|
82
88
|
if (draft.macros.length === 0) {
|
|
@@ -134,6 +140,22 @@ export function renameNode(ast, oldId, newId) {
|
|
|
134
140
|
else if (macro.type === 'path') {
|
|
135
141
|
macro.steps = macro.steps.map(s => s.node === oldId ? { ...s, node: newId } : s);
|
|
136
142
|
}
|
|
143
|
+
else if (macro.type === 'fanOut') {
|
|
144
|
+
if (macro.source.node === oldId)
|
|
145
|
+
macro.source.node = newId;
|
|
146
|
+
for (const t of macro.targets) {
|
|
147
|
+
if (t.node === oldId)
|
|
148
|
+
t.node = newId;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
else if (macro.type === 'fanIn') {
|
|
152
|
+
if (macro.target.node === oldId)
|
|
153
|
+
macro.target.node = newId;
|
|
154
|
+
for (const s of macro.sources) {
|
|
155
|
+
if (s.node === oldId)
|
|
156
|
+
s.node = newId;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
137
159
|
}
|
|
138
160
|
}
|
|
139
161
|
});
|
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;
|
|
114
|
+
export type TWorkflowMacro = TMapMacro | TPathMacro | TFanOutMacro | TFanInMacro;
|
|
115
115
|
export type TMapMacro = {
|
|
116
116
|
type: 'map';
|
|
117
117
|
/** Instance ID for the synthetic iterator node (e.g., "loop") */
|
|
@@ -133,6 +133,32 @@ export type TPathMacro = {
|
|
|
133
133
|
route?: 'ok' | 'fail';
|
|
134
134
|
}>;
|
|
135
135
|
};
|
|
136
|
+
export type TFanOutMacro = {
|
|
137
|
+
type: 'fanOut';
|
|
138
|
+
/** Single source port that fans out to multiple targets */
|
|
139
|
+
source: {
|
|
140
|
+
node: string;
|
|
141
|
+
port: string;
|
|
142
|
+
};
|
|
143
|
+
/** Target nodes/ports receiving the fanned-out data */
|
|
144
|
+
targets: Array<{
|
|
145
|
+
node: string;
|
|
146
|
+
port?: string;
|
|
147
|
+
}>;
|
|
148
|
+
};
|
|
149
|
+
export type TFanInMacro = {
|
|
150
|
+
type: 'fanIn';
|
|
151
|
+
/** Source nodes/ports that fan in to a single target */
|
|
152
|
+
sources: Array<{
|
|
153
|
+
node: string;
|
|
154
|
+
port?: string;
|
|
155
|
+
}>;
|
|
156
|
+
/** Single target port receiving all the fanned-in data */
|
|
157
|
+
target: {
|
|
158
|
+
node: string;
|
|
159
|
+
port: string;
|
|
160
|
+
};
|
|
161
|
+
};
|
|
136
162
|
/**
|
|
137
163
|
* Default configuration for a node type that can be overridden per instance.
|
|
138
164
|
*/
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { delay } from './delay.js';
|
|
2
2
|
export { waitForEvent } from './wait-for-event.js';
|
|
3
|
+
export { waitForAgent } from './wait-for-agent.js';
|
|
3
4
|
export { invokeWorkflow } from './invoke-workflow.js';
|
|
4
5
|
export type { FwMockConfig } from './mock-types.js';
|
|
5
6
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -19,7 +19,18 @@ export async function invokeWorkflow(execute, functionId, payload, timeout) {
|
|
|
19
19
|
// No mock data for this functionId — simulate failure
|
|
20
20
|
return { onSuccess: false, onFailure: true, result: {} };
|
|
21
21
|
}
|
|
22
|
-
//
|
|
22
|
+
// Check local workflow registry (populated by executeWorkflowFromFile)
|
|
23
|
+
const registry = globalThis.__fw_workflow_registry__;
|
|
24
|
+
if (registry?.[functionId]) {
|
|
25
|
+
try {
|
|
26
|
+
const result = await registry[functionId](true, payload);
|
|
27
|
+
return { onSuccess: true, onFailure: false, result: result ?? {} };
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return { onSuccess: false, onFailure: true, result: {} };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
// No mocks, no registry match: original no-op behavior (always succeeds)
|
|
23
34
|
return { onSuccess: true, onFailure: false, result: {} };
|
|
24
35
|
}
|
|
25
36
|
//# sourceMappingURL=invoke-workflow.js.map
|
|
@@ -8,6 +8,8 @@ export interface FwMockConfig {
|
|
|
8
8
|
events?: Record<string, object>;
|
|
9
9
|
/** Mock invocation results keyed by functionId. Used by invokeWorkflow. */
|
|
10
10
|
invocations?: Record<string, object>;
|
|
11
|
+
/** Mock agent results keyed by agentId. Used by waitForAgent. */
|
|
12
|
+
agents?: Record<string, object>;
|
|
11
13
|
/** When true, delay nodes skip the real sleep (1ms instead of full duration). */
|
|
12
14
|
fast?: boolean;
|
|
13
15
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @flowWeaver nodeType
|
|
3
|
+
* @input agentId - Agent/task identifier
|
|
4
|
+
* @input context - Context data to send to the agent
|
|
5
|
+
* @input [prompt] - Message to display when requesting input
|
|
6
|
+
* @output agentResult - Result returned by the agent
|
|
7
|
+
*/
|
|
8
|
+
export declare function waitForAgent(execute: boolean, agentId: string, context: object, prompt?: string): Promise<{
|
|
9
|
+
onSuccess: boolean;
|
|
10
|
+
onFailure: boolean;
|
|
11
|
+
agentResult: object;
|
|
12
|
+
}>;
|
|
13
|
+
//# sourceMappingURL=wait-for-agent.d.ts.map
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { getMockConfig } from './mock-types.js';
|
|
2
|
+
/**
|
|
3
|
+
* @flowWeaver nodeType
|
|
4
|
+
* @input agentId - Agent/task identifier
|
|
5
|
+
* @input context - Context data to send to the agent
|
|
6
|
+
* @input [prompt] - Message to display when requesting input
|
|
7
|
+
* @output agentResult - Result returned by the agent
|
|
8
|
+
*/
|
|
9
|
+
export async function waitForAgent(execute, agentId, context, prompt) {
|
|
10
|
+
if (!execute)
|
|
11
|
+
return { onSuccess: false, onFailure: false, agentResult: {} };
|
|
12
|
+
// 1. Check mocks first
|
|
13
|
+
const mocks = getMockConfig();
|
|
14
|
+
if (mocks?.agents?.[agentId]) {
|
|
15
|
+
return { onSuccess: true, onFailure: false, agentResult: mocks.agents[agentId] };
|
|
16
|
+
}
|
|
17
|
+
// 2. Check agent channel (set by executor for pause/resume)
|
|
18
|
+
const channel = globalThis.__fw_agent_channel__;
|
|
19
|
+
if (channel) {
|
|
20
|
+
const result = await channel.request({ agentId, context, prompt });
|
|
21
|
+
return { onSuccess: true, onFailure: false, agentResult: result };
|
|
22
|
+
}
|
|
23
|
+
// 3. No mocks, no channel: no-op
|
|
24
|
+
return { onSuccess: true, onFailure: false, agentResult: {} };
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=wait-for-agent.js.map
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module chevrotain-parser/fan-parser
|
|
3
|
+
*
|
|
4
|
+
* Parser for @fanOut and @fanIn sugar annotations using Chevrotain.
|
|
5
|
+
*
|
|
6
|
+
* Syntax:
|
|
7
|
+
* @fanOut Start.data -> a, b, c
|
|
8
|
+
* @fanOut Start.data -> a.input1, b.input2
|
|
9
|
+
* @fanIn a.result, b.result, c.result -> merge.inputs
|
|
10
|
+
* @fanIn a, b, c -> merge.data
|
|
11
|
+
*/
|
|
12
|
+
export interface PortRef {
|
|
13
|
+
node: string;
|
|
14
|
+
port?: string;
|
|
15
|
+
}
|
|
16
|
+
export interface FanOutParseResult {
|
|
17
|
+
source: PortRef;
|
|
18
|
+
targets: PortRef[];
|
|
19
|
+
}
|
|
20
|
+
export interface FanInParseResult {
|
|
21
|
+
sources: PortRef[];
|
|
22
|
+
target: PortRef;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Parse a @fanOut line and return structured result.
|
|
26
|
+
* Returns null if the line is not a valid @fanOut declaration.
|
|
27
|
+
*/
|
|
28
|
+
export declare function parseFanOutLine(input: string, warnings: string[]): FanOutParseResult | null;
|
|
29
|
+
/**
|
|
30
|
+
* Parse a @fanIn line and return structured result.
|
|
31
|
+
* Returns null if the line is not a valid @fanIn declaration.
|
|
32
|
+
*/
|
|
33
|
+
export declare function parseFanInLine(input: string, warnings: string[]): FanInParseResult | null;
|
|
34
|
+
/**
|
|
35
|
+
* Get serialized grammar for documentation/diagram generation.
|
|
36
|
+
*/
|
|
37
|
+
export declare function getFanGrammar(): import("chevrotain").ISerializedGast[];
|
|
38
|
+
//# sourceMappingURL=fan-parser.d.ts.map
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module chevrotain-parser/fan-parser
|
|
3
|
+
*
|
|
4
|
+
* Parser for @fanOut and @fanIn sugar annotations using Chevrotain.
|
|
5
|
+
*
|
|
6
|
+
* Syntax:
|
|
7
|
+
* @fanOut Start.data -> a, b, c
|
|
8
|
+
* @fanOut Start.data -> a.input1, b.input2
|
|
9
|
+
* @fanIn a.result, b.result, c.result -> merge.inputs
|
|
10
|
+
* @fanIn a, b, c -> merge.data
|
|
11
|
+
*/
|
|
12
|
+
import { CstParser } from 'chevrotain';
|
|
13
|
+
import { JSDocLexer, FanOutTag, FanInTag, Identifier, Arrow, Dot, Comma, allTokens, } from './tokens.js';
|
|
14
|
+
// =============================================================================
|
|
15
|
+
// Parser Definition
|
|
16
|
+
// =============================================================================
|
|
17
|
+
class FanParser extends CstParser {
|
|
18
|
+
constructor() {
|
|
19
|
+
super(allTokens);
|
|
20
|
+
this.performSelfAnalysis();
|
|
21
|
+
}
|
|
22
|
+
// @fanOut portRef Arrow portRef (Comma portRef)*
|
|
23
|
+
fanOutLine = this.RULE('fanOutLine', () => {
|
|
24
|
+
this.CONSUME(FanOutTag);
|
|
25
|
+
this.SUBRULE(this.portRef, { LABEL: 'source' });
|
|
26
|
+
this.CONSUME(Arrow);
|
|
27
|
+
this.SUBRULE2(this.portRef, { LABEL: 'firstTarget' });
|
|
28
|
+
this.MANY(() => {
|
|
29
|
+
this.CONSUME(Comma);
|
|
30
|
+
this.SUBRULE3(this.portRef, { LABEL: 'moreTargets' });
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
// @fanIn portRef (Comma portRef)* Arrow portRef
|
|
34
|
+
// Note: Because we can't look ahead past the comma-list to the arrow,
|
|
35
|
+
// we parse as: @fanIn portRefList Arrow portRef
|
|
36
|
+
// The last element after Arrow is the target; everything before is sources.
|
|
37
|
+
fanInLine = this.RULE('fanInLine', () => {
|
|
38
|
+
this.CONSUME(FanInTag);
|
|
39
|
+
this.SUBRULE(this.portRef, { LABEL: 'firstSource' });
|
|
40
|
+
this.MANY(() => {
|
|
41
|
+
this.CONSUME(Comma);
|
|
42
|
+
this.SUBRULE2(this.portRef, { LABEL: 'moreSources' });
|
|
43
|
+
});
|
|
44
|
+
this.CONSUME(Arrow);
|
|
45
|
+
this.SUBRULE3(this.portRef, { LABEL: 'target' });
|
|
46
|
+
});
|
|
47
|
+
// portRef: Identifier (Dot Identifier)?
|
|
48
|
+
portRef = this.RULE('portRef', () => {
|
|
49
|
+
this.CONSUME(Identifier, { LABEL: 'nodeName' });
|
|
50
|
+
this.OPTION(() => {
|
|
51
|
+
this.CONSUME(Dot);
|
|
52
|
+
this.CONSUME2(Identifier, { LABEL: 'portName' });
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
// =============================================================================
|
|
57
|
+
// Parser Instance (singleton)
|
|
58
|
+
// =============================================================================
|
|
59
|
+
const parserInstance = new FanParser();
|
|
60
|
+
// =============================================================================
|
|
61
|
+
// CST Visitor
|
|
62
|
+
// =============================================================================
|
|
63
|
+
const BaseVisitor = parserInstance.getBaseCstVisitorConstructor();
|
|
64
|
+
class FanVisitor extends BaseVisitor {
|
|
65
|
+
constructor() {
|
|
66
|
+
super();
|
|
67
|
+
this.validateVisitor();
|
|
68
|
+
}
|
|
69
|
+
fanOutLine(ctx) {
|
|
70
|
+
const source = this.portRef(ctx.source[0].children);
|
|
71
|
+
const targets = [];
|
|
72
|
+
targets.push(this.portRef(ctx.firstTarget[0].children));
|
|
73
|
+
if (ctx.moreTargets) {
|
|
74
|
+
for (const t of ctx.moreTargets) {
|
|
75
|
+
targets.push(this.portRef(t.children));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return { source, targets };
|
|
79
|
+
}
|
|
80
|
+
fanInLine(ctx) {
|
|
81
|
+
const sources = [];
|
|
82
|
+
sources.push(this.portRef(ctx.firstSource[0].children));
|
|
83
|
+
if (ctx.moreSources) {
|
|
84
|
+
for (const s of ctx.moreSources) {
|
|
85
|
+
sources.push(this.portRef(s.children));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const target = this.portRef(ctx.target[0].children);
|
|
89
|
+
return { sources, target };
|
|
90
|
+
}
|
|
91
|
+
portRef(ctx) {
|
|
92
|
+
const node = ctx.nodeName[0].image;
|
|
93
|
+
const port = ctx.portName?.[0]?.image;
|
|
94
|
+
return port ? { node, port } : { node };
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const visitorInstance = new FanVisitor();
|
|
98
|
+
// =============================================================================
|
|
99
|
+
// Public API
|
|
100
|
+
// =============================================================================
|
|
101
|
+
/**
|
|
102
|
+
* Parse a @fanOut line and return structured result.
|
|
103
|
+
* Returns null if the line is not a valid @fanOut declaration.
|
|
104
|
+
*/
|
|
105
|
+
export function parseFanOutLine(input, warnings) {
|
|
106
|
+
const lexResult = JSDocLexer.tokenize(input);
|
|
107
|
+
if (lexResult.errors.length > 0 || lexResult.tokens.length === 0)
|
|
108
|
+
return null;
|
|
109
|
+
if (lexResult.tokens[0].tokenType !== FanOutTag)
|
|
110
|
+
return null;
|
|
111
|
+
parserInstance.input = lexResult.tokens;
|
|
112
|
+
const cst = parserInstance.fanOutLine();
|
|
113
|
+
if (parserInstance.errors.length > 0) {
|
|
114
|
+
const truncatedInput = input.length > 80 ? input.substring(0, 80) + '...' : input;
|
|
115
|
+
warnings.push(`Failed to parse @fanOut line: "${truncatedInput}"\n` +
|
|
116
|
+
` Error: ${parserInstance.errors[0].message}\n` +
|
|
117
|
+
` Expected format: @fanOut source.port -> target1, target2, target3`);
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
return visitorInstance.visit(cst);
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Parse a @fanIn line and return structured result.
|
|
124
|
+
* Returns null if the line is not a valid @fanIn declaration.
|
|
125
|
+
*/
|
|
126
|
+
export function parseFanInLine(input, warnings) {
|
|
127
|
+
const lexResult = JSDocLexer.tokenize(input);
|
|
128
|
+
if (lexResult.errors.length > 0 || lexResult.tokens.length === 0)
|
|
129
|
+
return null;
|
|
130
|
+
if (lexResult.tokens[0].tokenType !== FanInTag)
|
|
131
|
+
return null;
|
|
132
|
+
parserInstance.input = lexResult.tokens;
|
|
133
|
+
const cst = parserInstance.fanInLine();
|
|
134
|
+
if (parserInstance.errors.length > 0) {
|
|
135
|
+
const truncatedInput = input.length > 80 ? input.substring(0, 80) + '...' : input;
|
|
136
|
+
warnings.push(`Failed to parse @fanIn line: "${truncatedInput}"\n` +
|
|
137
|
+
` Error: ${parserInstance.errors[0].message}\n` +
|
|
138
|
+
` Expected format: @fanIn source1.port, source2.port -> target.port`);
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
return visitorInstance.visit(cst);
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Get serialized grammar for documentation/diagram generation.
|
|
145
|
+
*/
|
|
146
|
+
export function getFanGrammar() {
|
|
147
|
+
return parserInstance.getSerializedGastProductions();
|
|
148
|
+
}
|
|
149
|
+
//# sourceMappingURL=fan-parser.js.map
|
|
@@ -12,6 +12,7 @@ import { getScopeGrammar } from './scope-parser.js';
|
|
|
12
12
|
import { getPathGrammar } from './path-parser.js';
|
|
13
13
|
import { getMapGrammar } from './map-parser.js';
|
|
14
14
|
import { getTriggerCancelGrammar } from './trigger-cancel-parser.js';
|
|
15
|
+
import { getFanGrammar } from './fan-parser.js';
|
|
15
16
|
export function serializedToEBNF(productions) {
|
|
16
17
|
const lines = [];
|
|
17
18
|
for (const prod of productions) {
|
|
@@ -129,6 +130,7 @@ export function getAllGrammars() {
|
|
|
129
130
|
scope: getScopeGrammar(),
|
|
130
131
|
path: getPathGrammar(),
|
|
131
132
|
map: getMapGrammar(),
|
|
133
|
+
fan: getFanGrammar(),
|
|
132
134
|
triggerCancel: getTriggerCancelGrammar(),
|
|
133
135
|
};
|
|
134
136
|
}
|
|
@@ -147,6 +149,7 @@ export function generateGrammarDiagrams() {
|
|
|
147
149
|
...grammars.scope,
|
|
148
150
|
...grammars.path,
|
|
149
151
|
...grammars.map,
|
|
152
|
+
...grammars.fan,
|
|
150
153
|
...grammars.triggerCancel,
|
|
151
154
|
];
|
|
152
155
|
// createSyntaxDiagramsCode returns a complete HTML document
|
|
@@ -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, TriggerTag, CancelOnTag, RetriesTag, TimeoutTag, ThrottleTag, } from './tokens.js';
|
|
7
|
+
export { JSDocLexer, allTokens, InputTag, OutputTag, NodeTag, ConnectTag, PositionTag, ScopeTag, MapTag, PathTag, FanOutTag, FanInTag, 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';
|
|
@@ -19,6 +19,8 @@ export { parseMapLine, getMapGrammar } from './map-parser.js';
|
|
|
19
19
|
export type { MapParseResult } from './map-parser.js';
|
|
20
20
|
export { parsePathLine, getPathGrammar } from './path-parser.js';
|
|
21
21
|
export type { PathParseResult, PathStep } from './path-parser.js';
|
|
22
|
+
export { parseFanOutLine, parseFanInLine } from './fan-parser.js';
|
|
23
|
+
export type { FanOutParseResult, FanInParseResult, PortRef } from './fan-parser.js';
|
|
22
24
|
export { parseTriggerLine, parseCancelOnLine, parseRetriesLine, parseTimeoutLine, parseThrottleLine } from './trigger-cancel-parser.js';
|
|
23
25
|
export type { TriggerParseResult, CancelOnParseResult, ThrottleParseResult } from './trigger-cancel-parser.js';
|
|
24
26
|
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, TriggerTag, CancelOnTag, RetriesTag, TimeoutTag, ThrottleTag, } from './tokens.js';
|
|
8
|
+
export { JSDocLexer, allTokens, InputTag, OutputTag, NodeTag, ConnectTag, PositionTag, ScopeTag, MapTag, PathTag, FanOutTag, FanInTag, 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
|
|
@@ -20,6 +20,8 @@ export { parseScopeLine, getScopeGrammar } from './scope-parser.js';
|
|
|
20
20
|
export { parseMapLine, getMapGrammar } from './map-parser.js';
|
|
21
21
|
// Path parser
|
|
22
22
|
export { parsePathLine, getPathGrammar } from './path-parser.js';
|
|
23
|
+
// Fan-out / Fan-in parser
|
|
24
|
+
export { parseFanOutLine, parseFanInLine } from './fan-parser.js';
|
|
23
25
|
// Trigger/cancel/retries/timeout/throttle parser
|
|
24
26
|
export { parseTriggerLine, parseCancelOnLine, parseRetriesLine, parseTimeoutLine, parseThrottleLine } from './trigger-cancel-parser.js';
|
|
25
27
|
// Grammar diagram generation
|
|
@@ -17,6 +17,8 @@ export declare const PositionTag: import("chevrotain").TokenType;
|
|
|
17
17
|
export declare const ScopeTag: import("chevrotain").TokenType;
|
|
18
18
|
export declare const MapTag: import("chevrotain").TokenType;
|
|
19
19
|
export declare const PathTag: import("chevrotain").TokenType;
|
|
20
|
+
export declare const FanOutTag: import("chevrotain").TokenType;
|
|
21
|
+
export declare const FanInTag: import("chevrotain").TokenType;
|
|
20
22
|
export declare const TriggerTag: import("chevrotain").TokenType;
|
|
21
23
|
export declare const CancelOnTag: import("chevrotain").TokenType;
|
|
22
24
|
export declare const RetriesTag: import("chevrotain").TokenType;
|
|
@@ -61,6 +61,14 @@ export const PathTag = createToken({
|
|
|
61
61
|
name: 'PathTag',
|
|
62
62
|
pattern: /@path\b/,
|
|
63
63
|
});
|
|
64
|
+
export const FanOutTag = createToken({
|
|
65
|
+
name: 'FanOutTag',
|
|
66
|
+
pattern: /@fanOut\b/,
|
|
67
|
+
});
|
|
68
|
+
export const FanInTag = createToken({
|
|
69
|
+
name: 'FanInTag',
|
|
70
|
+
pattern: /@fanIn\b/,
|
|
71
|
+
});
|
|
64
72
|
export const TriggerTag = createToken({
|
|
65
73
|
name: 'TriggerTag',
|
|
66
74
|
pattern: /@trigger\b/,
|
|
@@ -315,6 +323,8 @@ export const allTokens = [
|
|
|
315
323
|
ScopeTag,
|
|
316
324
|
MapTag,
|
|
317
325
|
PathTag,
|
|
326
|
+
FanOutTag,
|
|
327
|
+
FanInTag,
|
|
318
328
|
TriggerTag,
|
|
319
329
|
CancelOnTag,
|
|
320
330
|
RetriesTag,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Diagram command — generates SVG diagrams from workflow files.
|
|
2
|
+
* Diagram command — generates SVG or interactive HTML diagrams from workflow files.
|
|
3
3
|
*/
|
|
4
4
|
export interface DiagramCommandOptions {
|
|
5
5
|
theme?: 'dark' | 'light';
|
|
@@ -8,6 +8,7 @@ export interface DiagramCommandOptions {
|
|
|
8
8
|
showPortLabels?: boolean;
|
|
9
9
|
workflowName?: string;
|
|
10
10
|
output?: string;
|
|
11
|
+
format?: 'svg' | 'html';
|
|
11
12
|
}
|
|
12
13
|
export declare function diagramCommand(input: string, options?: DiagramCommandOptions): Promise<void>;
|
|
13
14
|
//# sourceMappingURL=diagram.d.ts.map
|
|
@@ -1,28 +1,31 @@
|
|
|
1
1
|
/* eslint-disable no-console */
|
|
2
2
|
/**
|
|
3
|
-
* Diagram command — generates SVG diagrams from workflow files.
|
|
3
|
+
* Diagram command — generates SVG or interactive HTML diagrams from workflow files.
|
|
4
4
|
*/
|
|
5
5
|
import * as fs from 'fs';
|
|
6
6
|
import * as path from 'path';
|
|
7
|
-
import { fileToSVG } from '../../diagram/index.js';
|
|
7
|
+
import { fileToSVG, fileToHTML } from '../../diagram/index.js';
|
|
8
8
|
import { logger } from '../utils/logger.js';
|
|
9
9
|
import { getErrorMessage } from '../../utils/error-utils.js';
|
|
10
10
|
export async function diagramCommand(input, options = {}) {
|
|
11
|
-
const { output, ...diagramOptions } = options;
|
|
11
|
+
const { output, format = 'svg', ...diagramOptions } = options;
|
|
12
12
|
const filePath = path.resolve(input);
|
|
13
13
|
if (!fs.existsSync(filePath)) {
|
|
14
14
|
logger.error(`File not found: ${filePath}`);
|
|
15
15
|
process.exit(1);
|
|
16
16
|
}
|
|
17
17
|
try {
|
|
18
|
-
const
|
|
18
|
+
const isHtml = format === 'html';
|
|
19
|
+
const result = isHtml
|
|
20
|
+
? fileToHTML(filePath, diagramOptions)
|
|
21
|
+
: fileToSVG(filePath, diagramOptions);
|
|
19
22
|
if (output) {
|
|
20
23
|
const outputPath = path.resolve(output);
|
|
21
|
-
fs.writeFileSync(outputPath,
|
|
24
|
+
fs.writeFileSync(outputPath, result, 'utf-8');
|
|
22
25
|
logger.success(`Diagram written to ${outputPath}`);
|
|
23
26
|
}
|
|
24
27
|
else {
|
|
25
|
-
process.stdout.write(
|
|
28
|
+
process.stdout.write(result);
|
|
26
29
|
}
|
|
27
30
|
}
|
|
28
31
|
catch (error) {
|