@synergenius/flow-weaver 0.3.0 → 0.4.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.
Files changed (73) hide show
  1. package/README.md +1 -0
  2. package/dist/annotation-generator.js +36 -0
  3. package/dist/api/generate-in-place.js +39 -0
  4. package/dist/api/generate.js +11 -1
  5. package/dist/api/manipulation/nodes.js +22 -0
  6. package/dist/ast/types.d.ts +27 -1
  7. package/dist/built-in-nodes/index.d.ts +1 -0
  8. package/dist/built-in-nodes/index.js +1 -0
  9. package/dist/built-in-nodes/invoke-workflow.js +12 -1
  10. package/dist/built-in-nodes/mock-types.d.ts +2 -0
  11. package/dist/built-in-nodes/wait-for-agent.d.ts +13 -0
  12. package/dist/built-in-nodes/wait-for-agent.js +26 -0
  13. package/dist/chevrotain-parser/fan-parser.d.ts +38 -0
  14. package/dist/chevrotain-parser/fan-parser.js +149 -0
  15. package/dist/chevrotain-parser/grammar-diagrams.d.ts +1 -0
  16. package/dist/chevrotain-parser/grammar-diagrams.js +3 -0
  17. package/dist/chevrotain-parser/index.d.ts +3 -1
  18. package/dist/chevrotain-parser/index.js +3 -1
  19. package/dist/chevrotain-parser/tokens.d.ts +2 -0
  20. package/dist/chevrotain-parser/tokens.js +10 -0
  21. package/dist/cli/commands/diagram.d.ts +2 -1
  22. package/dist/cli/commands/diagram.js +9 -6
  23. package/dist/cli/commands/run.js +59 -1
  24. package/dist/cli/flow-weaver.mjs +1396 -77
  25. package/dist/cli/index.js +23 -36
  26. package/dist/diagram/geometry.js +47 -5
  27. package/dist/diagram/html-viewer.d.ts +12 -0
  28. package/dist/diagram/html-viewer.js +399 -0
  29. package/dist/diagram/index.d.ts +12 -0
  30. package/dist/diagram/index.js +22 -0
  31. package/dist/diagram/types.d.ts +1 -0
  32. package/dist/doc-metadata/extractors/annotations.js +282 -1
  33. package/dist/doc-metadata/types.d.ts +6 -0
  34. package/dist/generator/control-flow.d.ts +13 -0
  35. package/dist/generator/control-flow.js +74 -0
  36. package/dist/generator/inngest.js +23 -0
  37. package/dist/generator/unified.js +122 -2
  38. package/dist/jsdoc-parser.d.ts +24 -0
  39. package/dist/jsdoc-parser.js +41 -1
  40. package/dist/mcp/agent-channel.d.ts +35 -0
  41. package/dist/mcp/agent-channel.js +61 -0
  42. package/dist/mcp/run-registry.d.ts +29 -0
  43. package/dist/mcp/run-registry.js +24 -0
  44. package/dist/mcp/tools-diagram.d.ts +1 -1
  45. package/dist/mcp/tools-diagram.js +15 -7
  46. package/dist/mcp/tools-editor.js +75 -3
  47. package/dist/mcp/workflow-executor.d.ts +28 -0
  48. package/dist/mcp/workflow-executor.js +62 -1
  49. package/dist/parser.d.ts +8 -0
  50. package/dist/parser.js +100 -0
  51. package/dist/runtime/ExecutionContext.d.ts +2 -0
  52. package/dist/runtime/ExecutionContext.js +2 -0
  53. package/dist/runtime/events.d.ts +1 -1
  54. package/dist/sugar-optimizer.js +28 -3
  55. package/dist/validator.d.ts +8 -0
  56. package/dist/validator.js +92 -0
  57. package/docs/reference/advanced-annotations.md +431 -0
  58. package/docs/reference/built-in-nodes.md +225 -0
  59. package/docs/reference/cli-reference.md +882 -0
  60. package/docs/reference/compilation.md +351 -0
  61. package/docs/reference/concepts.md +400 -0
  62. package/docs/reference/debugging.md +255 -0
  63. package/docs/reference/deployment.md +207 -0
  64. package/docs/reference/error-codes.md +686 -0
  65. package/docs/reference/export-interface.md +229 -0
  66. package/docs/reference/iterative-development.md +186 -0
  67. package/docs/reference/jsdoc-grammar.md +471 -0
  68. package/docs/reference/marketplace.md +205 -0
  69. package/docs/reference/node-conversion.md +308 -0
  70. package/docs/reference/patterns.md +161 -0
  71. package/docs/reference/scaffold.md +160 -0
  72. package/docs/reference/tutorial.md +519 -0
  73. package/package.json +1 -1
package/README.md CHANGED
@@ -1,5 +1,6 @@
1
1
  # @synergenius/flow-weaver
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/@synergenius/flow-weaver.svg)](https://www.npmjs.com/package/@synergenius/flow-weaver)
3
4
  [![License: ELv2-based](https://img.shields.io/badge/License-ELv2--based-blue.svg)](./LICENSE)
4
5
  [![Node.js](https://img.shields.io/badge/Node.js-%3E%3D18-green.svg)](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.
@@ -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
- lines.push(generateImportStatement(['TDebugger', 'createFlowWeaverDebugClient'], externalRuntimePath, moduleFormat));
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
  });
@@ -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
@@ -1,4 +1,5 @@
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
  //# sourceMappingURL=index.js.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
- // No mocks: original no-op behavior (always succeeds)
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
@@ -13,6 +13,7 @@ export interface GrammarCollection {
13
13
  scope: ISerializedGast[];
14
14
  path: ISerializedGast[];
15
15
  map: ISerializedGast[];
16
+ fan: ISerializedGast[];
16
17
  triggerCancel: ISerializedGast[];
17
18
  }
18
19
  /**
@@ -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 svg = fileToSVG(filePath, diagramOptions);
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, svg, 'utf-8');
24
+ fs.writeFileSync(outputPath, result, 'utf-8');
22
25
  logger.success(`Diagram written to ${outputPath}`);
23
26
  }
24
27
  else {
25
- process.stdout.write(svg);
28
+ process.stdout.write(result);
26
29
  }
27
30
  }
28
31
  catch (error) {