@synergenius/flow-weaver 0.8.2 → 0.8.3

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.
@@ -240,7 +240,7 @@ export type TNodeTypeAST = {
240
240
  /** Multiple scopes this node creates */
241
241
  scopes?: string[];
242
242
  /** Variant identifier (set by app layer) */
243
- variant?: 'FUNCTION' | 'WORKFLOW' | 'IMPORTED_WORKFLOW' | 'MAP_ITERATOR' | 'COERCION';
243
+ variant?: 'FUNCTION' | 'WORKFLOW' | 'IMPORTED_WORKFLOW' | 'MAP_ITERATOR' | 'COERCION' | 'STUB';
244
244
  /** File path for external node types */
245
245
  path?: string;
246
246
  /** Function reference for function-based node types */
@@ -32298,7 +32298,8 @@ function wrapFunctionDeclaration(fn) {
32298
32298
  getStartLineNumber: (inc) => fn.getStartLineNumber(inc),
32299
32299
  getText: (inc) => fn.getText(inc),
32300
32300
  getSourceFile: () => fn.getSourceFile(),
32301
- getTypeResolutionNode: () => fn
32301
+ getTypeResolutionNode: () => fn,
32302
+ isAmbient: () => !fn.hasBody()
32302
32303
  };
32303
32304
  }
32304
32305
  function extractFunctionLikes(sourceFile) {
@@ -43872,14 +43873,20 @@ var JSDocParser = class {
43872
43873
  if (jsdocs.length === 0) return null;
43873
43874
  let jsdoc = null;
43874
43875
  let flowWeaverTag = null;
43876
+ let isNodeShorthand = false;
43875
43877
  for (const doc of jsdocs) {
43876
43878
  const tags2 = doc.getTags();
43877
43879
  const tag = tags2.find(
43878
- (t) => t.getTagName() === "flowWeaver" && t.getCommentText()?.trim() === "nodeType"
43880
+ (t) => {
43881
+ if (t.getTagName() !== "flowWeaver") return false;
43882
+ const comment = t.getCommentText()?.trim();
43883
+ return comment === "nodeType" || comment === "node";
43884
+ }
43879
43885
  );
43880
43886
  if (tag) {
43881
43887
  jsdoc = doc;
43882
43888
  flowWeaverTag = tag;
43889
+ isNodeShorthand = flowWeaverTag.getCommentText()?.trim() === "node";
43883
43890
  break;
43884
43891
  }
43885
43892
  }
@@ -43889,6 +43896,9 @@ var JSDocParser = class {
43889
43896
  inputs: {},
43890
43897
  outputs: {}
43891
43898
  };
43899
+ if (isNodeShorthand) {
43900
+ config2.expression = true;
43901
+ }
43892
43902
  const descriptionText = jsdoc.getDescription();
43893
43903
  if (descriptionText && descriptionText.trim()) {
43894
43904
  config2.description = descriptionText.trim();
@@ -45475,6 +45485,10 @@ var AnnotationParser = class {
45475
45485
  };
45476
45486
  }
45477
45487
  }
45488
+ const isStub = fn.isAmbient?.() ?? false;
45489
+ if (isStub) {
45490
+ config2.expression = true;
45491
+ }
45478
45492
  if (config2.expression) {
45479
45493
  const hasExplicitDataInputs = Object.keys(inputs).some((k) => k !== "execute");
45480
45494
  const hasExplicitDataOutputs = Object.keys(outputs).some(
@@ -45534,7 +45548,7 @@ var AnnotationParser = class {
45534
45548
  assignImplicitPortOrders(outputs);
45535
45549
  const jsDocs = fn.getJsDocs();
45536
45550
  const jsDocText = jsDocs.map((doc) => doc.getText()).join("\n");
45537
- const functionText = jsDocText ? `${jsDocText}
45551
+ const functionText = isStub ? void 0 : jsDocText ? `${jsDocText}
45538
45552
  ${fn.getText()}` : fn.getText();
45539
45553
  const isAsync2 = fn.isAsync();
45540
45554
  let defaultConfig = void 0;
@@ -45557,7 +45571,7 @@ ${fn.getText()}` : fn.getText();
45557
45571
  type: "NodeType",
45558
45572
  name: nodeTypeName,
45559
45573
  functionName,
45560
- variant: "FUNCTION",
45574
+ variant: isStub ? "STUB" : "FUNCTION",
45561
45575
  inputs,
45562
45576
  outputs,
45563
45577
  hasSuccessPort: RESERVED_PORT_NAMES.ON_SUCCESS in outputs,
@@ -48562,7 +48576,7 @@ var configSchema = {
48562
48576
  };
48563
48577
  function generateDefaultTemplate(workflowName, asyncKeyword, returnType) {
48564
48578
  return `
48565
- // Use @expression for pure functions, normal mode for branching
48579
+ // Use @expression for most functions, normal mode only for error-with-data or void side-effects
48566
48580
 
48567
48581
  /**
48568
48582
  * Validates input data
@@ -50298,7 +50312,7 @@ var aggregatorTemplate = {
50298
50312
  const asyncKeyword = isAsync2 ? "async " : "";
50299
50313
  const returnType = isAsync2 ? "Promise<{ onSuccess: boolean; onFailure: boolean; aggregated: any }>" : "{ onSuccess: boolean; onFailure: boolean; aggregated: any }";
50300
50314
  return `
50301
- // Use @expression for pure functions, normal mode for branching
50315
+ // Use @expression for most functions, normal mode only for error-with-data or void side-effects
50302
50316
 
50303
50317
  /**
50304
50318
  * Fetches data from source A
@@ -95182,7 +95196,7 @@ function displayInstalledPackage(pkg) {
95182
95196
  }
95183
95197
 
95184
95198
  // src/cli/index.ts
95185
- var version2 = true ? "0.8.2" : "0.0.0-dev";
95199
+ var version2 = true ? "0.8.3" : "0.0.0-dev";
95186
95200
  var program2 = new Command();
95187
95201
  program2.name("flow-weaver").description("Flow Weaver Annotations - Compile and validate workflow files").version(version2, "-v, --version", "Output the current version");
95188
95202
  program2.configureOutput({
@@ -14,7 +14,7 @@ export const aggregatorTemplate = {
14
14
  ? "Promise<{ onSuccess: boolean; onFailure: boolean; aggregated: any }>"
15
15
  : "{ onSuccess: boolean; onFailure: boolean; aggregated: any }";
16
16
  return `
17
- // Use @expression for pure functions, normal mode for branching
17
+ // Use @expression for most functions, normal mode only for error-with-data or void side-effects
18
18
 
19
19
  /**
20
20
  * Fetches data from source A
@@ -26,7 +26,7 @@ const configSchema = {
26
26
  };
27
27
  function generateDefaultTemplate(workflowName, asyncKeyword, returnType) {
28
28
  return `
29
- // Use @expression for pure functions, normal mode for branching
29
+ // Use @expression for most functions, normal mode only for error-with-data or void side-effects
30
30
 
31
31
  /**
32
32
  * Validates input data
@@ -23,6 +23,8 @@ export interface FunctionLike {
23
23
  getDeclarationKind?(): 'const' | 'let' | 'var' | undefined;
24
24
  /** Returns the underlying ts-morph Node for type resolution (e.g., Symbol.getTypeAtLocation). */
25
25
  getTypeResolutionNode(): Node;
26
+ /** Whether this is an ambient declaration (declare function) with no implementation body. */
27
+ isAmbient?(): boolean;
26
28
  }
27
29
  /**
28
30
  * Scan a source file and return every function-like declaration that could
@@ -17,6 +17,7 @@ function wrapFunctionDeclaration(fn) {
17
17
  getText: (inc) => fn.getText(inc),
18
18
  getSourceFile: () => fn.getSourceFile(),
19
19
  getTypeResolutionNode: () => fn,
20
+ isAmbient: () => !fn.hasBody(),
20
21
  };
21
22
  }
22
23
  /**
@@ -104,15 +104,22 @@ export class JSDocParser {
104
104
  const jsdocs = func.getJsDocs();
105
105
  if (jsdocs.length === 0)
106
106
  return null;
107
- // Find the JSDoc block that contains @flowWeaver nodeType
107
+ // Find the JSDoc block that contains @flowWeaver nodeType (or @flowWeaver node shorthand)
108
108
  let jsdoc = null;
109
109
  let flowWeaverTag = null;
110
+ let isNodeShorthand = false;
110
111
  for (const doc of jsdocs) {
111
112
  const tags = doc.getTags();
112
- const tag = tags.find((t) => t.getTagName() === 'flowWeaver' && t.getCommentText()?.trim() === 'nodeType');
113
+ const tag = tags.find((t) => {
114
+ if (t.getTagName() !== 'flowWeaver')
115
+ return false;
116
+ const comment = t.getCommentText()?.trim();
117
+ return comment === 'nodeType' || comment === 'node';
118
+ });
113
119
  if (tag) {
114
120
  jsdoc = doc;
115
121
  flowWeaverTag = tag;
122
+ isNodeShorthand = flowWeaverTag.getCommentText()?.trim() === 'node';
116
123
  break;
117
124
  }
118
125
  }
@@ -123,6 +130,10 @@ export class JSDocParser {
123
130
  inputs: {},
124
131
  outputs: {},
125
132
  };
133
+ // @flowWeaver node implies expression mode (auto-detect from signature)
134
+ if (isNodeShorthand) {
135
+ config.expression = true;
136
+ }
126
137
  // Extract description from JSDoc comment text (before tags)
127
138
  const descriptionText = jsdoc.getDescription();
128
139
  if (descriptionText && descriptionText.trim()) {
package/dist/parser.js CHANGED
@@ -629,6 +629,12 @@ export class AnnotationParser {
629
629
  };
630
630
  }
631
631
  }
632
+ // Ambient declarations (declare function) are stub nodes — interface only, no implementation.
633
+ // Force expression mode so ports are inferred from the TypeScript signature.
634
+ const isStub = fn.isAmbient?.() ?? false;
635
+ if (isStub) {
636
+ config.expression = true;
637
+ }
632
638
  // Auto-infer ports for @expression nodes when @input/@output are missing.
633
639
  // If the function has @expression but no explicit port annotations, infer
634
640
  // data ports from the TypeScript function signature (same logic as unannotated functions).
@@ -679,11 +685,10 @@ export class AnnotationParser {
679
685
  // Assign implicit port orders with mandatory port precedence
680
686
  assignImplicitPortOrders(inputs);
681
687
  assignImplicitPortOrders(outputs);
682
- // Get function text (JSDoc comment + function)
683
- // getText() returns just the function, so we need to prepend the JSDoc
688
+ // Get function text (JSDoc comment + function). Stubs have no body to capture.
684
689
  const jsDocs = fn.getJsDocs();
685
690
  const jsDocText = jsDocs.map((doc) => doc.getText()).join('\n');
686
- const functionText = jsDocText ? `${jsDocText}\n${fn.getText()}` : fn.getText();
691
+ const functionText = isStub ? undefined : (jsDocText ? `${jsDocText}\n${fn.getText()}` : fn.getText());
687
692
  // Detect async keyword on function declaration
688
693
  const isAsync = fn.isAsync();
689
694
  // Convert defaultConfig
@@ -718,7 +723,7 @@ export class AnnotationParser {
718
723
  type: 'NodeType',
719
724
  name: nodeTypeName,
720
725
  functionName,
721
- variant: 'FUNCTION',
726
+ variant: isStub ? 'STUB' : 'FUNCTION',
722
727
  inputs,
723
728
  outputs,
724
729
  hasSuccessPort: RESERVED_PORT_NAMES.ON_SUCCESS in outputs,
@@ -137,7 +137,7 @@ Expression nodes are pure functions where:
137
137
  - Object return `{ a, b }` -> one port per property
138
138
  - Best for: transformers, math, utilities, data mapping, async fetchers, API calls
139
139
 
140
- > **Start with expression mode.** Only switch to normal mode when you need explicit branching (`onSuccess`/`onFailure` routing), custom error-with-data patterns, or void returns with side effects.
140
+ > **Start with expression mode.** Only switch to normal mode when you need to return data alongside a failure (error-with-data patterns) or for void side-effect functions. Expression nodes handle success/failure branching automatically — throw to trigger the `onFailure` path.
141
141
 
142
142
  #### Async Expression Example
143
143
 
@@ -157,7 +157,7 @@ async function fetchUser(userId: string): Promise<User> {
157
157
 
158
158
  ### Node Type (Normal Mode)
159
159
 
160
- Use normal mode when you need explicit `try/catch -> onFailure` error handling or `void` returns.
160
+ Use normal mode when you need to return error data alongside the failure signal, or for `void` side-effect functions.
161
161
 
162
162
  ```typescript
163
163
  /**
@@ -196,7 +196,7 @@ export function workflowName(
196
196
  }
197
197
  ```
198
198
 
199
- > STEP connections (`execute`, `onSuccess`, `onFailure`) are auto-wired for expression nodes in linear data flows. Add them explicitly only for branching or normal mode nodes.
199
+ > STEP connections (`execute`, `onSuccess`, `onFailure`) are auto-wired for expression nodes. Add explicit STEP connections only for normal mode nodes or to override the automatic wiring.
200
200
 
201
201
  ### Importing External Functions
202
202
 
@@ -55,16 +55,15 @@ flow-weaver validate <file>
55
55
 
56
56
  ### Phase 3: Create the Nodes
57
57
 
58
- **Start with `@expression` mode for all nodes.** Only switch to normal mode when you need explicit branching (quality gates, conditional routing, error-with-data patterns).
58
+ **Start with `@expression` mode for all nodes.** Only switch to normal mode when you need to return error data alongside the failure signal, or for void side-effects.
59
59
 
60
60
  Create nodes by adding `@flowWeaver nodeType` annotated functions.
61
61
 
62
62
  Create at most 3 nodes at a time, test each.
63
63
 
64
64
  Default to expression mode. Use normal mode only for:
65
- - **Quality gates** -- routing to different paths based on success/failure
66
- - **Conditional routing** -- explicit `onSuccess`/`onFailure` branching
67
- - **Error-with-data patterns** -- returning error details alongside the failure signal
65
+ - **Error-with-data patterns** -- returning structured error details alongside the failure signal
66
+ - **Void side-effects** -- functions with no return value that need explicit control flow
68
67
 
69
68
  **Expression mode (recommended for most nodes):**
70
69
 
@@ -82,7 +81,7 @@ function addNumbers(a: number, b: number): number {
82
81
  }
83
82
  ```
84
83
 
85
- > Use `@expression` for most nodes. Only use normal mode when you need custom failure handling or void returns.
84
+ > Use `@expression` for most nodes. Expression nodes support failure branching via throw. Only use normal mode for error-with-data patterns or void returns.
86
85
 
87
86
  **Normal mode (for custom error handling):**
88
87
 
@@ -147,7 +146,7 @@ flow-weaver describe <file> # Get workflow structure as JSON
147
146
 
148
147
  ### 1. Missing STEP Connections (Normal Mode) or Adding Unnecessary Ones (Expression Mode)
149
148
 
150
- **Normal mode nodes** need explicit STEP wiring: `@connect Start.execute -> firstNode.execute`. Without this, no node will run. **Expression mode** nodes auto-wire STEP connections from data flow -- you only need data connections for linear pipelines. Add explicit STEP connections only for branching (e.g., routing `onFailure` to a different node).
149
+ **Normal mode nodes** need explicit STEP wiring: `@connect Start.execute -> firstNode.execute`. Without this, no node will run. **Expression mode** nodes auto-wire STEP connections from data flow -- you only need data connections for linear pipelines. Add explicit STEP connections when you need to route `onFailure` to a specific node (both expression and normal mode nodes support this).
151
150
 
152
151
  ### 2. Wrapping Node Inputs in Object
153
152
 
@@ -173,13 +172,13 @@ Always run `flow-weaver validate` after adding nodes/connections. Don't batch 10
173
172
 
174
173
  ### 6. Using Normal Mode When Expression Mode Works
175
174
 
176
- If the function returns a value and doesn't need custom error handling, use `@expression`. It's simpler and less error-prone. Expression mode eliminates the `execute` parameter, the `if (!execute)` guard, and the `onSuccess`/`onFailure` boilerplate. The compiler handles all of it.
175
+ If the function returns a value, use `@expression`. It's simpler and less error-prone. Expression mode eliminates the `execute` parameter, the `if (!execute)` guard, and the `onSuccess`/`onFailure` boilerplate. The compiler wraps the call in try/catch, so throwing triggers the `onFailure` path automatically.
177
176
 
178
177
  **Rule of thumb:** If you are writing `if (!execute) return ...` and a `try/catch` that just returns `{ onSuccess: false, onFailure: true }`, you should be using expression mode instead.
179
178
 
180
179
  ### 7. Defaulting to Normal Mode
181
180
 
182
181
  Normal mode adds boilerplate that expression mode handles automatically. Default to `@expression` for every node. Only reach for normal mode when the function needs to:
183
- - Route to different downstream nodes on success vs. failure
184
- - Return error data (not just signal failure)
182
+ - Return error data alongside the failure signal (not just signal failure)
183
+ - Perform void side-effects with no return value
185
184
  - Perform void side-effects with explicit control flow
@@ -10,15 +10,12 @@ keywords: [conversion, expression mode, normal mode, function, transform, nodeTy
10
10
  Is it a pure function that returns a value?
11
11
  YES -> expression mode (@expression)
12
12
  NO |
13
- Does it need to route onSuccess/onFailure to different nodes?
14
- YES -> normal mode
15
- NO |
16
13
  Does it need to return error data alongside the failure signal?
17
14
  YES -> normal mode
18
15
  NO -> expression mode (@expression)
19
16
  ```
20
17
 
21
- **Default to expression mode** unless the function has explicit success/failure branching logic.
18
+ **Default to expression mode.** Expression nodes handle success/failure branching via throw. Only use normal mode when you need to return data on the failure path, or for void side-effects.
22
19
 
23
20
  ---
24
21
 
@@ -280,7 +277,7 @@ When no `--mode` is specified:
280
277
  - **Expression** (default): function returns a value (non-void), whether sync or async
281
278
  - **Normal**: void return, or user explicitly requests normal mode
282
279
 
283
- **Default to expression mode** unless the function has explicit success/failure branching logic. If the function simply takes inputs and returns outputs -- even async ones with `await` -- expression mode is the right choice.
280
+ **Default to expression mode.** Expression nodes support failure branching via throw. Only switch to normal mode for error-with-data patterns or void side-effects.
284
281
 
285
282
  The compiler fully supports async expression nodes -- `await`, async detection, try/catch wrapping, and onSuccess/onFailure are all handled automatically.
286
283
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@synergenius/flow-weaver",
3
- "version": "0.8.2",
3
+ "version": "0.8.3",
4
4
  "description": "Deterministic workflow compiler for AI agents. Compiles to standalone TypeScript, no runtime dependencies.",
5
5
  "private": false,
6
6
  "type": "module",