@synergenius/flow-weaver 0.9.0 → 0.9.2

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 CHANGED
@@ -4,36 +4,30 @@
4
4
  [![License: Flow Weaver Library License](https://img.shields.io/badge/License-Flow%20Weaver%20Library-blue.svg)](./LICENSE)
5
5
  [![Node.js](https://img.shields.io/badge/Node.js-%3E%3D18-green.svg)](https://nodejs.org)
6
6
 
7
- **Workflow compiler for AI agents.** LLMs create, validate, iterate, and test workflows programmatically. Humans review them visually. Compiled output is standalone TypeScript with no runtime dependencies.
7
+ **Build AI agent workflows visually. Ship them as your own code.**
8
8
 
9
- Flow Weaver turns standard TypeScript functions into executable workflow graphs using JSDoc annotations. No YAML. No JSON configs. No drag-and-drop. Just TypeScript with full type safety, IDE autocomplete, and compile-time validation, in a format LLMs can read and write directly. Compiled output runs anywhere with no dependency on Flow Weaver.
9
+ Design agent workflows in the Studio, in TypeScript, or let AI build them for you. Everything compiles to standalone functions you deploy anywhere, no runtime dependency on Flow Weaver.
10
10
 
11
- ## Why Flow Weaver?
11
+ ## Three Ways to Build
12
12
 
13
- **Agents generate code well. They don't generate reliable systems.**
13
+ **Studio.** Drag, drop, connect. The visual editor renders your workflow as an interactive graph with bidirectional sync: canvas changes write code, code changes update the canvas. 80+ plugins handle rendering, state, minimap, undo/redo, and more.
14
14
 
15
- With Flow Weaver, an agent builds a typed workflow graph instead of a monolithic script. Every connection is type-checked, every required input is enforced, every error path is explicit.
15
+ **TypeScript.** Annotate plain functions with JSDoc tags. The compiler turns them into executable workflow graphs with full type safety, IDE autocomplete, and compile-time validation. No YAML, no JSON configs.
16
16
 
17
- The development loop (steps 1-4 are fully autonomous):
17
+ **AI Agents.** Connect Claude Code, Cursor, or OpenClaw and they scaffold, validate, and ship workflows using 30+ MCP tools. The agent reads validation errors, fixes issues, and re-validates until the workflow compiles clean. The development loop (steps 1-4 are fully autonomous):
18
18
 
19
- 1. **Agent creates**: scaffolds from templates or builds from scratch via 35+ MCP tools
19
+ 1. **Agent creates**: scaffolds from templates, builds from a model, or writes from scratch
20
20
  2. **Compiler validates**: 15+ validation passes catch missing connections, type mismatches, unreachable paths
21
- 3. **Agent iterates**: validation errors include fix suggestions, the agent corrects and re-validates until clean
21
+ 3. **Agent iterates**: validation errors include fix suggestions, the agent corrects and re-validates
22
22
  4. **Agent tests**: deterministic mock providers for reproducible testing without real API calls
23
- 5. **Human reviews**: visual editor renders the workflow as an interactive graph for final approval
24
-
25
- The compiled code is yours. No runtime lock-in, no framework dependency.
23
+ 5. **Human reviews**: visual editor or SVG diagram for final approval
26
24
 
27
25
  ## Quick Start
28
26
 
29
- ### Install
30
-
31
27
  ```bash
32
28
  npm install @synergenius/flow-weaver
33
29
  ```
34
30
 
35
- ### Define a workflow
36
-
37
31
  Workflows are plain TypeScript. Annotations declare the graph structure:
38
32
 
39
33
  ```typescript
@@ -66,8 +60,6 @@ export async function dataPipeline(
66
60
  }
67
61
  ```
68
62
 
69
- ### Compile and run
70
-
71
63
  ```bash
72
64
  npx flow-weaver compile data-pipeline.ts # generates executable code in-place
73
65
  npx flow-weaver run data-pipeline.ts --params '{"rawData": "Hello World"}'
@@ -77,26 +69,36 @@ The compiler fills in the function body while preserving your code outside the g
77
69
 
78
70
  ## AI-Native Development with MCP
79
71
 
80
- Flow Weaver includes an MCP server with 35+ tools for Claude Code (or any MCP-compatible agent):
72
+ Flow Weaver includes an MCP server for Claude Code, Cursor, OpenClaw, or any MCP-compatible agent:
81
73
 
82
74
  ```bash
83
75
  npx flow-weaver mcp-server # auto-registers with Claude Code
84
76
  ```
85
77
 
86
- What an AI agent can do:
87
-
88
78
  | Capability | MCP Tools |
89
79
  |-----------|-----------|
90
80
  | **Build** | `fw_scaffold`, `fw_modify`, `fw_modify_batch`, `fw_add_node`, `fw_connect` |
81
+ | **Model** | `fw_create_model`, `fw_workflow_status`, `fw_implement_node` |
91
82
  | **Validate** | `fw_validate` (with friendly error hints), `fw_doctor` |
92
83
  | **Understand** | `fw_describe` (json/text/mermaid), `fw_query` (10 query types), `fw_diff` |
93
84
  | **Test** | `fw_execute_workflow` (with trace), `fw_compile` |
94
- | **Visualize** | `fw_diagram` (SVG), `fw_get_state`, `fw_focus_node` |
85
+ | **Visualize** | `fw_diagram` (SVG/HTML), `fw_get_state`, `fw_focus_node` |
95
86
  | **Deploy** | `fw_export` (Lambda, Vercel, Cloudflare, Inngest), `fw_compile --target inngest` |
96
87
  | **Reuse** | `fw_list_patterns`, `fw_apply_pattern`, `fw_extract_pattern` |
97
88
  | **Extend** | `fw_market_search`, `fw_market_install` |
98
89
 
99
- The agent reads validation errors, fixes issues, and re-validates until the workflow compiles clean.
90
+ ## Model-Driven Workflows
91
+
92
+ Design first, implement later. Describe the workflow shape (nodes, ports, execution path) and the compiler generates a valid skeleton with `declare function` stubs:
93
+
94
+ ```bash
95
+ # Via MCP: fw_create_model with nodes, inputs/outputs, and execution path
96
+ # Via CLI:
97
+ npx flow-weaver status my-workflow.ts # shows stub vs implemented progress
98
+ npx flow-weaver implement my-workflow.ts processData # scaffolds a node body
99
+ ```
100
+
101
+ The graph is valid before any node has a real implementation. Fill in node bodies incrementally, check `status` to track progress. Architecture and implementation stay separate: the architect defines the shape, developers fill in the logic.
100
102
 
101
103
  ## Agent Workflow Templates
102
104
 
@@ -140,14 +142,14 @@ npx flow-weaver create workflow ai-agent-durable durable-agent.ts
140
142
  The validator understands AI agent patterns and enforces safety rules:
141
143
 
142
144
  ```
143
- AGENT_LLM_MISSING_ERROR_HANDLER LLM node's onFailure is unconnected add error handling
144
- AGENT_UNGUARDED_TOOL_EXECUTOR Tool executor has no human-approval upstream add a gate
145
- AGENT_MISSING_MEMORY_IN_LOOP Agent loop has LLM but no memory conversations will be stateless
146
- AGENT_LLM_NO_FALLBACK LLM failure goes directly to Exit add retry or fallback logic
147
- AGENT_TOOL_NO_OUTPUT_HANDLING Tool executor outputs are all unconnected results are discarded
145
+ AGENT_LLM_MISSING_ERROR_HANDLER LLM node's onFailure is unconnected, add error handling
146
+ AGENT_UNGUARDED_TOOL_EXECUTOR Tool executor has no human-approval upstream, add a gate
147
+ AGENT_MISSING_MEMORY_IN_LOOP Agent loop has LLM but no memory, conversations will be stateless
148
+ AGENT_LLM_NO_FALLBACK LLM failure goes directly to Exit, add retry or fallback logic
149
+ AGENT_TOOL_NO_OUTPUT_HANDLING Tool executor outputs are all unconnected, results are discarded
148
150
  ```
149
151
 
150
- Not generic lint rules. The validator identifies LLM, tool-executor, human-approval, and memory nodes by port signatures, annotations, and naming patterns, then applies agent-specific checks.
152
+ These aren't generic lint rules. The validator identifies LLM, tool-executor, human-approval, and memory nodes by port signatures, annotations, and naming patterns, then applies agent-specific checks.
151
153
 
152
154
  ## Deterministic Agent Testing
153
155
 
@@ -190,13 +192,30 @@ Most workflow engines either ban loops (DAG-only) or allow arbitrary cycles (har
190
192
  */
191
193
  ```
192
194
 
193
- The scope's output ports become callback parameters, and input ports become return values. This enables:
194
- - Agent reasoning loops (LLM -> tools -> memory -> LLM)
195
- - ForEach over collections
196
- - Map/reduce patterns
197
- - Nested sub-workflows
195
+ The scope's output ports become callback parameters, and input ports become return values. Agent reasoning loops, ForEach over collections, map/reduce patterns, nested sub-workflows: all work while keeping the graph acyclic and statically analyzable.
198
196
 
199
- All while keeping the graph acyclic and statically analyzable.
197
+ ## Diagram Generation
198
+
199
+ Generate SVG or interactive HTML diagrams from any workflow:
200
+
201
+ ```bash
202
+ flow-weaver diagram workflow.ts -o workflow.svg --theme dark
203
+ flow-weaver diagram workflow.ts -o workflow.html --format html
204
+ ```
205
+
206
+ Customize node appearance with annotations:
207
+
208
+ ```typescript
209
+ /**
210
+ * @flowWeaver nodeType
211
+ * @color blue
212
+ * @icon database
213
+ */
214
+ ```
215
+
216
+ Named colors: `blue`, `purple`, `green`, `cyan`, `orange`, `pink`, `red`, `yellow`. Icons include `api`, `database`, `shield`, `brain`, `cloud`, `search`, `code`, and 60+ more from Material Design 3.
217
+
218
+ The interactive HTML viewer supports zoom/pan, click-to-inspect nodes, port-level hover with connection tracing, and works standalone or embedded in an iframe.
200
219
 
201
220
  ## Multi-Target Compilation
202
221
 
@@ -220,23 +239,6 @@ flow-weaver serve ./workflows --port 3000 --swagger
220
239
 
221
240
  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
241
 
223
- ## Visual Human-in-the-Loop
224
-
225
- Workflows compile from code, but humans review them visually:
226
-
227
- ```bash
228
- # Generate SVG diagram
229
- flow-weaver diagram workflow.ts -o workflow.svg --theme dark
230
-
231
- # Describe structure for quick review
232
- flow-weaver describe workflow.ts --format text
233
-
234
- # Semantic diff between versions
235
- flow-weaver diff workflow-v1.ts workflow-v2.ts
236
- ```
237
-
238
- Flow Weaver Studio is a visual editor with bidirectional sync: code changes update the canvas, canvas changes update the code. 80+ plugins handle rendering, state, minimap, undo/redo, and more.
239
-
240
242
  ## API
241
243
 
242
244
  ```typescript
@@ -263,8 +265,16 @@ const code = generateCode(ast);
263
265
  |-------------|---------|
264
266
  | `@synergenius/flow-weaver` | Parse, validate, compile, generate, AST types, builders, diff, patterns |
265
267
  | `@synergenius/flow-weaver/runtime` | Execution context, errors, function registry for generated code |
266
- | `@synergenius/flow-weaver/built-in-nodes` | delay, waitForEvent, invokeWorkflow |
267
- | `@synergenius/flow-weaver/diagram` | SVG diagram layout and rendering |
268
+ | `@synergenius/flow-weaver/testing` | Mock LLM/approval providers, recording/replay, assertions, token tracking |
269
+ | `@synergenius/flow-weaver/built-in-nodes` | delay, waitForEvent, waitForAgent, invokeWorkflow |
270
+ | `@synergenius/flow-weaver/diagram` | SVG/HTML diagram layout and rendering |
271
+ | `@synergenius/flow-weaver/ast` | AST types and utilities |
272
+ | `@synergenius/flow-weaver/api` | Programmatic workflow manipulation API |
273
+ | `@synergenius/flow-weaver/diff` | Semantic workflow diffing |
274
+ | `@synergenius/flow-weaver/deployment` | Multi-target deployment generators |
275
+ | `@synergenius/flow-weaver/marketplace` | Marketplace package utilities |
276
+ | `@synergenius/flow-weaver/editor` | Editor completions and suggestions |
277
+ | `@synergenius/flow-weaver/browser` | JSDoc port sync for browser environments |
268
278
  | `@synergenius/flow-weaver/describe` | Programmatic workflow description |
269
279
  | `@synergenius/flow-weaver/doc-metadata` | Documentation metadata extractors |
270
280
 
@@ -276,11 +286,16 @@ flow-weaver compile <file> # Compile to TypeScript or Inngest
276
286
  flow-weaver validate <file> # Validate without compiling
277
287
  flow-weaver run <file> # Execute a workflow
278
288
  flow-weaver dev <file> # Watch + compile + run
289
+ flow-weaver strip <file> # Remove generated code sections
279
290
  flow-weaver describe <file> # Structure output (json/text/mermaid)
280
- flow-weaver diagram <file> # Generate SVG diagram
291
+ flow-weaver diagram <file> # Generate SVG or interactive HTML diagram
281
292
  flow-weaver diff <f1> <f2> # Semantic workflow comparison
282
293
 
283
- # Setup
294
+ # Model-driven
295
+ flow-weaver status <file> # Show stub vs implemented progress
296
+ flow-weaver implement <file> <node> # Scaffold a node body from its stub
297
+
298
+ # Scaffolding
284
299
  flow-weaver init [directory] # Create new project
285
300
  flow-weaver create workflow <t> <f> # Scaffold from template
286
301
  flow-weaver create node <name> <f> # Scaffold node type
@@ -307,10 +322,16 @@ flow-weaver docs search <query> # Search documentation
307
322
  flow-weaver market search [query] # Search npm for packages
308
323
  flow-weaver market install <pkg> # Install a package
309
324
  flow-weaver market list # List installed packages
325
+ flow-weaver market init <name> # Scaffold a marketplace package
326
+ flow-weaver market pack # Validate and generate manifest
327
+ flow-weaver market publish # Publish to npm
310
328
 
311
- # IDE
329
+ # Editor / IDE
312
330
  flow-weaver mcp-server # Start MCP server for Claude Code
313
331
  flow-weaver listen # Stream editor events
332
+ flow-weaver changelog # Generate changelog from git history
333
+ flow-weaver migrate <glob> # Run migration transforms on workflow files
334
+ flow-weaver plugin init <name> # Scaffold an external plugin
314
335
  ```
315
336
 
316
337
  ## Built-in Nodes
@@ -339,17 +360,6 @@ function nodeName(
339
360
 
340
361
  Expression nodes (`@expression`) skip the control flow boilerplate. Inputs and outputs map directly to the TypeScript signature.
341
362
 
342
- ## Marketplace
343
-
344
- Distribute node types, workflows, and patterns as npm packages:
345
-
346
- ```bash
347
- flow-weaver market init my-nodes # Scaffold a package
348
- flow-weaver market pack # Validate and generate manifest
349
- flow-weaver market publish # Publish to npm
350
- flow-weaver market install flowweaver-pack-openai # Install
351
- ```
352
-
353
363
  ## Testing
354
364
 
355
365
  ```bash
@@ -44825,38 +44825,39 @@ function assignImplicitPortOrders(ports) {
44825
44825
  scopeGroups.get(scopeKey).push([portName, portDef]);
44826
44826
  }
44827
44827
  for (const [scope, portsInScope] of scopeGroups.entries()) {
44828
+ let nextSlot2 = function(from) {
44829
+ while (occupied.has(from)) from++;
44830
+ occupied.add(from);
44831
+ return from;
44832
+ };
44833
+ var nextSlot = nextSlot2;
44828
44834
  const isScoped = scope !== void 0;
44829
- const mandatoryPorts = portsInScope.filter(([name]) => isMandatoryPort(name, isScoped));
44830
- const regularPorts = portsInScope.filter(([name]) => !isMandatoryPort(name, isScoped));
44831
- let minRegularExplicitOrder = Infinity;
44832
- for (const [, portDef] of regularPorts) {
44835
+ const occupied = /* @__PURE__ */ new Set();
44836
+ for (const [, portDef] of portsInScope) {
44833
44837
  const order = portDef.metadata?.order;
44834
44838
  if (typeof order === "number") {
44835
- minRegularExplicitOrder = Math.min(minRegularExplicitOrder, order);
44839
+ occupied.add(order);
44836
44840
  }
44837
44841
  }
44838
- let mandatoryStartOrder = 0;
44839
- if (minRegularExplicitOrder !== Infinity && minRegularExplicitOrder === 0) {
44840
- const regularPortsWithOrder0 = regularPorts.filter(([, p]) => p.metadata?.order === 0);
44841
- mandatoryStartOrder = regularPortsWithOrder0.length;
44842
- }
44843
- let currentMandatoryOrder = mandatoryStartOrder;
44844
- for (const [, portDef] of mandatoryPorts) {
44845
- if (portDef.metadata?.order === void 0) {
44846
- if (!portDef.metadata) {
44847
- portDef.metadata = {};
44848
- }
44849
- portDef.metadata.order = currentMandatoryOrder++;
44850
- }
44842
+ const mandatoryNeedOrder = portsInScope.filter(
44843
+ ([name, def]) => isMandatoryPort(name, isScoped) && def.metadata?.order === void 0
44844
+ );
44845
+ const regularNeedOrder = portsInScope.filter(
44846
+ ([name, def]) => !isMandatoryPort(name, isScoped) && def.metadata?.order === void 0
44847
+ );
44848
+ let slot = -mandatoryNeedOrder.length;
44849
+ for (const [, portDef] of mandatoryNeedOrder) {
44850
+ if (!portDef.metadata) portDef.metadata = {};
44851
+ slot = nextSlot2(slot);
44852
+ portDef.metadata.order = slot;
44853
+ slot++;
44851
44854
  }
44852
- let currentRegularOrder = currentMandatoryOrder;
44853
- for (const [, portDef] of regularPorts) {
44854
- if (portDef.metadata?.order === void 0) {
44855
- if (!portDef.metadata) {
44856
- portDef.metadata = {};
44857
- }
44858
- portDef.metadata.order = currentRegularOrder++;
44859
- }
44855
+ slot = Math.max(slot, 0);
44856
+ for (const [, portDef] of regularNeedOrder) {
44857
+ if (!portDef.metadata) portDef.metadata = {};
44858
+ slot = nextSlot2(slot);
44859
+ portDef.metadata.order = slot;
44860
+ slot++;
44860
44861
  }
44861
44862
  }
44862
44863
  }
@@ -60592,7 +60593,7 @@ function renderSVG(graph, options = {}) {
60592
60593
  function renderConnection(parts2, conn, gradIndex) {
60593
60594
  const dashAttr = conn.isStepConnection ? "" : ' stroke-dasharray="8 4"';
60594
60595
  parts2.push(
60595
- ` <path d="${conn.path}" fill="none" stroke="url(#conn-grad-${gradIndex})" stroke-width="3"${dashAttr} stroke-linecap="round" data-source="${escapeXml(conn.fromNode)}.${escapeXml(conn.fromPort)}" data-target="${escapeXml(conn.toNode)}.${escapeXml(conn.toPort)}"/>`
60596
+ ` <path d="${conn.path}" fill="none" stroke="url(#conn-grad-${gradIndex})" stroke-width="3"${dashAttr} stroke-linecap="round" data-source="${escapeXml(conn.fromNode)}.${escapeXml(conn.fromPort)}:output" data-target="${escapeXml(conn.toNode)}.${escapeXml(conn.toPort)}:input"/>`
60596
60597
  );
60597
60598
  }
60598
60599
  function renderScopeConnection(parts2, conn, allConnections) {
@@ -60600,7 +60601,7 @@ function renderScopeConnection(parts2, conn, allConnections) {
60600
60601
  if (gradIndex < 0) return;
60601
60602
  const dashAttr = conn.isStepConnection ? "" : ' stroke-dasharray="8 4"';
60602
60603
  parts2.push(
60603
- ` <path d="${conn.path}" fill="none" stroke="url(#conn-grad-${gradIndex})" stroke-width="2.5"${dashAttr} stroke-linecap="round" data-source="${escapeXml(conn.fromNode)}.${escapeXml(conn.fromPort)}" data-target="${escapeXml(conn.toNode)}.${escapeXml(conn.toPort)}"/>`
60604
+ ` <path d="${conn.path}" fill="none" stroke="url(#conn-grad-${gradIndex})" stroke-width="2.5"${dashAttr} stroke-linecap="round" data-source="${escapeXml(conn.fromNode)}.${escapeXml(conn.fromPort)}:output" data-target="${escapeXml(conn.toNode)}.${escapeXml(conn.toPort)}:input"/>`
60604
60605
  );
60605
60606
  }
60606
60607
  function renderNodeBody(parts2, node, theme, indent) {
@@ -60681,7 +60682,7 @@ function renderPortDots(parts2, nodeId, inputs, outputs, themeName) {
60681
60682
  const color = getPortColor(port.dataType, port.isFailure, themeName);
60682
60683
  const ringColor = getPortRingColor(port.dataType, port.isFailure, themeName);
60683
60684
  const dir = port.direction === "INPUT" ? "input" : "output";
60684
- parts2.push(` <circle cx="${port.cx}" cy="${port.cy}" r="${PORT_RADIUS}" fill="${color}" stroke="${ringColor}" stroke-width="2" data-port-id="${escapeXml(nodeId)}.${escapeXml(port.name)}" data-direction="${dir}"/>`);
60685
+ parts2.push(` <circle cx="${port.cx}" cy="${port.cy}" r="${PORT_RADIUS}" fill="${color}" stroke="${ringColor}" stroke-width="2" data-port-id="${escapeXml(nodeId)}.${escapeXml(port.name)}:${dir}" data-direction="${dir}"/>`);
60685
60686
  }
60686
60687
  }
60687
60688
  function renderPortLabels(parts2, nodeId, inputs, outputs, theme, themeName) {
@@ -60689,7 +60690,8 @@ function renderPortLabels(parts2, nodeId, inputs, outputs, theme, themeName) {
60689
60690
  const color = getPortColor(port.dataType, port.isFailure, themeName);
60690
60691
  const isInput = port.direction === "INPUT";
60691
60692
  const abbrev = TYPE_ABBREVIATIONS[port.dataType] ?? port.dataType;
60692
- const portId = `${escapeXml(nodeId)}.${escapeXml(port.name)}`;
60693
+ const dir = isInput ? "input" : "output";
60694
+ const portId = `${escapeXml(nodeId)}.${escapeXml(port.name)}:${dir}`;
60693
60695
  const portLabel = port.label;
60694
60696
  const typeWidth = measureText(abbrev);
60695
60697
  const labelWidth = measureText(portLabel);
@@ -60983,43 +60985,79 @@ body.node-active .connections path.dimmed { opacity: 0.15; }
60983
60985
  else if (e.key === 'Escape') deselectNode();
60984
60986
  });
60985
60987
 
60986
- // ---- Port label visibility via JS (since CSS sibling selectors can't reach .labels group) ----
60987
- var labelEls = content.querySelectorAll('.labels g[data-port-label]');
60988
- var nodeEls = content.querySelectorAll('.nodes g[data-node-id]');
60988
+ // ---- Port label visibility ----
60989
+ var labelMap = {};
60990
+ content.querySelectorAll('.labels g[data-port-label]').forEach(function(lbl) {
60991
+ labelMap[lbl.getAttribute('data-port-label')] = lbl;
60992
+ });
60989
60993
 
60990
- function showLabelsFor(id) {
60991
- labelEls.forEach(function(lbl) {
60992
- var portId = lbl.getAttribute('data-port-label') || '';
60993
- if (portId.indexOf(id + '.') === 0) {
60994
- lbl.style.opacity = '1';
60995
- lbl.style.pointerEvents = 'auto';
60996
- }
60994
+ // Build adjacency: portId \u2192 array of connected portIds
60995
+ var portConnections = {};
60996
+ content.querySelectorAll('.connections path').forEach(function(p) {
60997
+ var src = p.getAttribute('data-source');
60998
+ var tgt = p.getAttribute('data-target');
60999
+ if (!src || !tgt) return;
61000
+ if (!portConnections[src]) portConnections[src] = [];
61001
+ if (!portConnections[tgt]) portConnections[tgt] = [];
61002
+ portConnections[src].push(tgt);
61003
+ portConnections[tgt].push(src);
61004
+ });
61005
+
61006
+ var allLabelIds = Object.keys(labelMap);
61007
+ var hoveredPort = null;
61008
+
61009
+ function showLabel(id) { var l = labelMap[id]; if (l) { l.style.opacity = '1'; l.style.pointerEvents = 'auto'; } }
61010
+ function hideLabel(id) { var l = labelMap[id]; if (l) { l.style.opacity = '0'; l.style.pointerEvents = 'none'; } }
61011
+
61012
+ function showLabelsFor(nodeId) {
61013
+ allLabelIds.forEach(function(id) {
61014
+ if (id.indexOf(nodeId + '.') === 0) showLabel(id);
60997
61015
  });
60998
61016
  }
60999
- function hideLabelsFor(id) {
61000
- labelEls.forEach(function(lbl) {
61001
- var portId = lbl.getAttribute('data-port-label') || '';
61002
- if (portId.indexOf(id + '.') === 0) {
61003
- lbl.style.opacity = '0';
61004
- lbl.style.pointerEvents = 'none';
61005
- }
61017
+ function hideLabelsFor(nodeId) {
61018
+ allLabelIds.forEach(function(id) {
61019
+ if (id.indexOf(nodeId + '.') === 0) hideLabel(id);
61006
61020
  });
61007
61021
  }
61008
61022
 
61023
+ // Node hover: show all port labels for the hovered node
61024
+ var nodeEls = content.querySelectorAll('.nodes g[data-node-id]');
61009
61025
  nodeEls.forEach(function(nodeG) {
61010
61026
  var nodeId = nodeG.getAttribute('data-node-id');
61011
61027
  var parentNodeG = nodeG.parentElement ? nodeG.parentElement.closest('g[data-node-id]') : null;
61012
61028
  var parentId = parentNodeG ? parentNodeG.getAttribute('data-node-id') : null;
61013
61029
  nodeG.addEventListener('mouseenter', function() {
61030
+ if (hoveredPort) return; // port hover takes priority
61014
61031
  if (parentId) hideLabelsFor(parentId);
61015
61032
  showLabelsFor(nodeId);
61016
61033
  });
61017
61034
  nodeG.addEventListener('mouseleave', function() {
61035
+ if (hoveredPort) return;
61018
61036
  hideLabelsFor(nodeId);
61019
61037
  if (parentId) showLabelsFor(parentId);
61020
61038
  });
61021
61039
  });
61022
61040
 
61041
+ // Port hover: show this port's label + all connected port labels
61042
+ content.querySelectorAll('[data-port-id]').forEach(function(portEl) {
61043
+ var portId = portEl.getAttribute('data-port-id');
61044
+ var nodeId = portId.split('.')[0];
61045
+ var peers = (portConnections[portId] || []).concat(portId);
61046
+
61047
+ portEl.addEventListener('mouseenter', function() {
61048
+ hoveredPort = portId;
61049
+ // Hide all labels for this node first, then show only the relevant ones
61050
+ hideLabelsFor(nodeId);
61051
+ peers.forEach(showLabel);
61052
+ });
61053
+ portEl.addEventListener('mouseleave', function() {
61054
+ hoveredPort = null;
61055
+ peers.forEach(hideLabel);
61056
+ // Restore all labels for the node since we're still inside it
61057
+ showLabelsFor(nodeId);
61058
+ });
61059
+ });
61060
+
61023
61061
  // ---- Click to inspect node ----
61024
61062
  function deselectNode() {
61025
61063
  selectedNodeId = null;
@@ -61046,7 +61084,7 @@ body.node-active .connections path.dimmed { opacity: 0.15; }
61046
61084
  ports.forEach(function(p) {
61047
61085
  var id = p.getAttribute('data-port-id');
61048
61086
  var dir = p.getAttribute('data-direction');
61049
- var name = id.split('.').slice(1).join('.');
61087
+ var name = id.split('.').slice(1).join('.').replace(/:(?:input|output)$/, '');
61050
61088
  if (dir === 'input') inputs.push(name);
61051
61089
  else outputs.push(name);
61052
61090
  });
@@ -95710,7 +95748,7 @@ function displayInstalledPackage(pkg) {
95710
95748
  }
95711
95749
 
95712
95750
  // src/cli/index.ts
95713
- var version2 = true ? "0.9.0" : "0.0.0-dev";
95751
+ var version2 = true ? "0.9.2" : "0.0.0-dev";
95714
95752
  var program2 = new Command();
95715
95753
  program2.name("flow-weaver").description("Flow Weaver Annotations - Compile and validate workflow files").version(version2, "-v, --version", "Output the current version");
95716
95754
  program2.configureOutput({
@@ -266,43 +266,79 @@ body.node-active .connections path.dimmed { opacity: 0.15; }
266
266
  else if (e.key === 'Escape') deselectNode();
267
267
  });
268
268
 
269
- // ---- Port label visibility via JS (since CSS sibling selectors can't reach .labels group) ----
270
- var labelEls = content.querySelectorAll('.labels g[data-port-label]');
271
- var nodeEls = content.querySelectorAll('.nodes g[data-node-id]');
269
+ // ---- Port label visibility ----
270
+ var labelMap = {};
271
+ content.querySelectorAll('.labels g[data-port-label]').forEach(function(lbl) {
272
+ labelMap[lbl.getAttribute('data-port-label')] = lbl;
273
+ });
272
274
 
273
- function showLabelsFor(id) {
274
- labelEls.forEach(function(lbl) {
275
- var portId = lbl.getAttribute('data-port-label') || '';
276
- if (portId.indexOf(id + '.') === 0) {
277
- lbl.style.opacity = '1';
278
- lbl.style.pointerEvents = 'auto';
279
- }
275
+ // Build adjacency: portId → array of connected portIds
276
+ var portConnections = {};
277
+ content.querySelectorAll('.connections path').forEach(function(p) {
278
+ var src = p.getAttribute('data-source');
279
+ var tgt = p.getAttribute('data-target');
280
+ if (!src || !tgt) return;
281
+ if (!portConnections[src]) portConnections[src] = [];
282
+ if (!portConnections[tgt]) portConnections[tgt] = [];
283
+ portConnections[src].push(tgt);
284
+ portConnections[tgt].push(src);
285
+ });
286
+
287
+ var allLabelIds = Object.keys(labelMap);
288
+ var hoveredPort = null;
289
+
290
+ function showLabel(id) { var l = labelMap[id]; if (l) { l.style.opacity = '1'; l.style.pointerEvents = 'auto'; } }
291
+ function hideLabel(id) { var l = labelMap[id]; if (l) { l.style.opacity = '0'; l.style.pointerEvents = 'none'; } }
292
+
293
+ function showLabelsFor(nodeId) {
294
+ allLabelIds.forEach(function(id) {
295
+ if (id.indexOf(nodeId + '.') === 0) showLabel(id);
280
296
  });
281
297
  }
282
- function hideLabelsFor(id) {
283
- labelEls.forEach(function(lbl) {
284
- var portId = lbl.getAttribute('data-port-label') || '';
285
- if (portId.indexOf(id + '.') === 0) {
286
- lbl.style.opacity = '0';
287
- lbl.style.pointerEvents = 'none';
288
- }
298
+ function hideLabelsFor(nodeId) {
299
+ allLabelIds.forEach(function(id) {
300
+ if (id.indexOf(nodeId + '.') === 0) hideLabel(id);
289
301
  });
290
302
  }
291
303
 
304
+ // Node hover: show all port labels for the hovered node
305
+ var nodeEls = content.querySelectorAll('.nodes g[data-node-id]');
292
306
  nodeEls.forEach(function(nodeG) {
293
307
  var nodeId = nodeG.getAttribute('data-node-id');
294
308
  var parentNodeG = nodeG.parentElement ? nodeG.parentElement.closest('g[data-node-id]') : null;
295
309
  var parentId = parentNodeG ? parentNodeG.getAttribute('data-node-id') : null;
296
310
  nodeG.addEventListener('mouseenter', function() {
311
+ if (hoveredPort) return; // port hover takes priority
297
312
  if (parentId) hideLabelsFor(parentId);
298
313
  showLabelsFor(nodeId);
299
314
  });
300
315
  nodeG.addEventListener('mouseleave', function() {
316
+ if (hoveredPort) return;
301
317
  hideLabelsFor(nodeId);
302
318
  if (parentId) showLabelsFor(parentId);
303
319
  });
304
320
  });
305
321
 
322
+ // Port hover: show this port's label + all connected port labels
323
+ content.querySelectorAll('[data-port-id]').forEach(function(portEl) {
324
+ var portId = portEl.getAttribute('data-port-id');
325
+ var nodeId = portId.split('.')[0];
326
+ var peers = (portConnections[portId] || []).concat(portId);
327
+
328
+ portEl.addEventListener('mouseenter', function() {
329
+ hoveredPort = portId;
330
+ // Hide all labels for this node first, then show only the relevant ones
331
+ hideLabelsFor(nodeId);
332
+ peers.forEach(showLabel);
333
+ });
334
+ portEl.addEventListener('mouseleave', function() {
335
+ hoveredPort = null;
336
+ peers.forEach(hideLabel);
337
+ // Restore all labels for the node since we're still inside it
338
+ showLabelsFor(nodeId);
339
+ });
340
+ });
341
+
306
342
  // ---- Click to inspect node ----
307
343
  function deselectNode() {
308
344
  selectedNodeId = null;
@@ -329,7 +365,7 @@ body.node-active .connections path.dimmed { opacity: 0.15; }
329
365
  ports.forEach(function(p) {
330
366
  var id = p.getAttribute('data-port-id');
331
367
  var dir = p.getAttribute('data-direction');
332
- var name = id.split('.').slice(1).join('.');
368
+ var name = id.split('.').slice(1).join('.').replace(/:(?:input|output)$/, '');
333
369
  if (dir === 'input') inputs.push(name);
334
370
  else outputs.push(name);
335
371
  });
@@ -90,14 +90,14 @@ export function renderSVG(graph, options = {}) {
90
90
  // ---- Connection rendering ----
91
91
  function renderConnection(parts, conn, gradIndex) {
92
92
  const dashAttr = conn.isStepConnection ? '' : ' stroke-dasharray="8 4"';
93
- parts.push(` <path d="${conn.path}" fill="none" stroke="url(#conn-grad-${gradIndex})" stroke-width="3"${dashAttr} stroke-linecap="round" data-source="${escapeXml(conn.fromNode)}.${escapeXml(conn.fromPort)}" data-target="${escapeXml(conn.toNode)}.${escapeXml(conn.toPort)}"/>`);
93
+ parts.push(` <path d="${conn.path}" fill="none" stroke="url(#conn-grad-${gradIndex})" stroke-width="3"${dashAttr} stroke-linecap="round" data-source="${escapeXml(conn.fromNode)}.${escapeXml(conn.fromPort)}:output" data-target="${escapeXml(conn.toNode)}.${escapeXml(conn.toPort)}:input"/>`);
94
94
  }
95
95
  function renderScopeConnection(parts, conn, allConnections) {
96
96
  const gradIndex = allConnections.indexOf(conn);
97
97
  if (gradIndex < 0)
98
98
  return;
99
99
  const dashAttr = conn.isStepConnection ? '' : ' stroke-dasharray="8 4"';
100
- parts.push(` <path d="${conn.path}" fill="none" stroke="url(#conn-grad-${gradIndex})" stroke-width="2.5"${dashAttr} stroke-linecap="round" data-source="${escapeXml(conn.fromNode)}.${escapeXml(conn.fromPort)}" data-target="${escapeXml(conn.toNode)}.${escapeXml(conn.toPort)}"/>`);
100
+ parts.push(` <path d="${conn.path}" fill="none" stroke="url(#conn-grad-${gradIndex})" stroke-width="2.5"${dashAttr} stroke-linecap="round" data-source="${escapeXml(conn.fromNode)}.${escapeXml(conn.fromPort)}:output" data-target="${escapeXml(conn.toNode)}.${escapeXml(conn.toPort)}:input"/>`);
101
101
  }
102
102
  // ---- Node rendering ----
103
103
  /** Render node body rect + icon */
@@ -183,7 +183,7 @@ function renderPortDots(parts, nodeId, inputs, outputs, themeName) {
183
183
  const color = getPortColor(port.dataType, port.isFailure, themeName);
184
184
  const ringColor = getPortRingColor(port.dataType, port.isFailure, themeName);
185
185
  const dir = port.direction === 'INPUT' ? 'input' : 'output';
186
- parts.push(` <circle cx="${port.cx}" cy="${port.cy}" r="${PORT_RADIUS}" fill="${color}" stroke="${ringColor}" stroke-width="2" data-port-id="${escapeXml(nodeId)}.${escapeXml(port.name)}" data-direction="${dir}"/>`);
186
+ parts.push(` <circle cx="${port.cx}" cy="${port.cy}" r="${PORT_RADIUS}" fill="${color}" stroke="${ringColor}" stroke-width="2" data-port-id="${escapeXml(nodeId)}.${escapeXml(port.name)}:${dir}" data-direction="${dir}"/>`);
187
187
  }
188
188
  }
189
189
  /** Render only port label badges (no dots) */
@@ -192,7 +192,8 @@ function renderPortLabels(parts, nodeId, inputs, outputs, theme, themeName) {
192
192
  const color = getPortColor(port.dataType, port.isFailure, themeName);
193
193
  const isInput = port.direction === 'INPUT';
194
194
  const abbrev = TYPE_ABBREVIATIONS[port.dataType] ?? port.dataType;
195
- const portId = `${escapeXml(nodeId)}.${escapeXml(port.name)}`;
195
+ const dir = isInput ? 'input' : 'output';
196
+ const portId = `${escapeXml(nodeId)}.${escapeXml(port.name)}:${dir}`;
196
197
  const portLabel = port.label;
197
198
  const typeWidth = measureText(abbrev);
198
199
  const labelWidth = measureText(portLabel);
@@ -14,11 +14,11 @@ export declare function isMandatoryPort(portName: string, isScoped: boolean): bo
14
14
  *
15
15
  * Rules:
16
16
  * 1. Ports are grouped by scope (undefined = external, string = scoped)
17
- * 2. Within each scope group:
18
- * - Mandatory ports (execute, onSuccess, onFailure) get lower order values
19
- * - Regular ports get higher order values
20
- * 3. Explicit order metadata is always preserved
21
- * 4. If a regular port has explicit order 0, mandatory ports are pushed to order >= 1
17
+ * 2. Explicit order metadata is always preserved
18
+ * 3. Mandatory ports without explicit orders get negative slots (-N, ..., -1)
19
+ * so they always sort before any user-specified [order:0] data port
20
+ * 4. Regular ports without explicit orders fill non-negative slots (0+),
21
+ * skipping any slots already occupied by explicit orders
22
22
  *
23
23
  * @param ports - Record of port definitions to process (mutated in place)
24
24
  */
@@ -19,11 +19,11 @@ export function isMandatoryPort(portName, isScoped) {
19
19
  *
20
20
  * Rules:
21
21
  * 1. Ports are grouped by scope (undefined = external, string = scoped)
22
- * 2. Within each scope group:
23
- * - Mandatory ports (execute, onSuccess, onFailure) get lower order values
24
- * - Regular ports get higher order values
25
- * 3. Explicit order metadata is always preserved
26
- * 4. If a regular port has explicit order 0, mandatory ports are pushed to order >= 1
22
+ * 2. Explicit order metadata is always preserved
23
+ * 3. Mandatory ports without explicit orders get negative slots (-N, ..., -1)
24
+ * so they always sort before any user-specified [order:0] data port
25
+ * 4. Regular ports without explicit orders fill non-negative slots (0+),
26
+ * skipping any slots already occupied by explicit orders
27
27
  *
28
28
  * @param ports - Record of port definitions to process (mutated in place)
29
29
  */
@@ -40,48 +40,41 @@ export function assignImplicitPortOrders(ports) {
40
40
  // Process each scope group independently
41
41
  for (const [scope, portsInScope] of scopeGroups.entries()) {
42
42
  const isScoped = scope !== undefined;
43
- // Separate mandatory from regular ports
44
- const mandatoryPorts = portsInScope.filter(([name]) => isMandatoryPort(name, isScoped));
45
- const regularPorts = portsInScope.filter(([name]) => !isMandatoryPort(name, isScoped));
46
- // Find minimum explicit order among regular ports (if any)
47
- let minRegularExplicitOrder = Infinity;
48
- for (const [, portDef] of regularPorts) {
43
+ // Collect all explicitly occupied order slots
44
+ const occupied = new Set();
45
+ for (const [, portDef] of portsInScope) {
49
46
  const order = portDef.metadata?.order;
50
47
  if (typeof order === "number") {
51
- minRegularExplicitOrder = Math.min(minRegularExplicitOrder, order);
48
+ occupied.add(order);
52
49
  }
53
50
  }
54
- // Determine starting order for mandatory ports
55
- let mandatoryStartOrder = 0;
56
- // If a regular port has explicit order 0 (or any low value),
57
- // mandatory ports should be pushed after it
58
- if (minRegularExplicitOrder !== Infinity && minRegularExplicitOrder === 0) {
59
- // Count how many regular ports have explicit order 0
60
- const regularPortsWithOrder0 = regularPorts.filter(([, p]) => p.metadata?.order === 0);
61
- mandatoryStartOrder = regularPortsWithOrder0.length;
51
+ // Helper: find next available slot starting from `from`
52
+ function nextSlot(from) {
53
+ while (occupied.has(from))
54
+ from++;
55
+ occupied.add(from);
56
+ return from;
62
57
  }
63
- // Assign orders to mandatory ports (if they don't have explicit order)
64
- let currentMandatoryOrder = mandatoryStartOrder;
65
- for (const [, portDef] of mandatoryPorts) {
66
- if (portDef.metadata?.order === undefined) {
67
- // Assign implicit order
68
- if (!portDef.metadata) {
69
- portDef.metadata = {};
70
- }
71
- portDef.metadata.order = currentMandatoryOrder++;
72
- }
58
+ // Separate mandatory from regular ports (only those needing implicit orders)
59
+ const mandatoryNeedOrder = portsInScope.filter(([name, def]) => isMandatoryPort(name, isScoped) && def.metadata?.order === undefined);
60
+ const regularNeedOrder = portsInScope.filter(([name, def]) => !isMandatoryPort(name, isScoped) && def.metadata?.order === undefined);
61
+ // Mandatory ports fill negative slots so they always sort before [order:0] data ports
62
+ let slot = -mandatoryNeedOrder.length;
63
+ for (const [, portDef] of mandatoryNeedOrder) {
64
+ if (!portDef.metadata)
65
+ portDef.metadata = {};
66
+ slot = nextSlot(slot);
67
+ portDef.metadata.order = slot;
68
+ slot++;
73
69
  }
74
- // Assign orders to regular ports (if they don't have explicit order)
75
- // Regular ports start after mandatory ports
76
- let currentRegularOrder = currentMandatoryOrder;
77
- for (const [, portDef] of regularPorts) {
78
- if (portDef.metadata?.order === undefined) {
79
- // Assign implicit order
80
- if (!portDef.metadata) {
81
- portDef.metadata = {};
82
- }
83
- portDef.metadata.order = currentRegularOrder++;
84
- }
70
+ // Regular ports fill non-negative slots, skipping occupied ones
71
+ slot = Math.max(slot, 0);
72
+ for (const [, portDef] of regularNeedOrder) {
73
+ if (!portDef.metadata)
74
+ portDef.metadata = {};
75
+ slot = nextSlot(slot);
76
+ portDef.metadata.order = slot;
77
+ slot++;
85
78
  }
86
79
  }
87
80
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@synergenius/flow-weaver",
3
- "version": "0.9.0",
3
+ "version": "0.9.2",
4
4
  "description": "Deterministic workflow compiler for AI agents. Compiles to standalone TypeScript, no runtime dependencies.",
5
5
  "private": false,
6
6
  "type": "module",
@@ -66,6 +66,10 @@
66
66
  "./marketplace": {
67
67
  "types": "./dist/marketplace/index.d.ts",
68
68
  "default": "./dist/marketplace/index.js"
69
+ },
70
+ "./testing": {
71
+ "types": "./dist/testing/index.d.ts",
72
+ "default": "./dist/testing/index.js"
69
73
  }
70
74
  },
71
75
  "bin": {