@synergenius/flow-weaver 0.21.13 → 0.21.15

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.
@@ -225,7 +225,18 @@ export class AnnotationGenerator {
225
225
  }
226
226
  });
227
227
  // Filter stale macros (e.g. paths whose connections were deleted)
228
- const existingMacros = filterStaleMacros(workflow.macros || [], workflow.connections, workflow.instances);
228
+ const existingMacros = filterStaleMacros(workflow.macros || [], workflow.connections, workflow.instances, workflow.nodeTypes, workflow.startPorts, workflow.exitPorts);
229
+ // Compute dropped coerce instance IDs — their connections must be excluded
230
+ const survivingCoerceIds = new Set();
231
+ for (const macro of existingMacros) {
232
+ if (macro.type === 'coerce')
233
+ survivingCoerceIds.add(macro.instanceId);
234
+ }
235
+ const droppedCoerceIds = new Set();
236
+ for (const id of coerceInstanceIds) {
237
+ if (!survivingCoerceIds.has(id))
238
+ droppedCoerceIds.add(id);
239
+ }
229
240
  // Auto-detect @path sugar patterns from connections
230
241
  const detected = detectSugarPatterns(workflow.connections, workflow.instances, existingMacros, workflow.nodeTypes, workflow.startPorts, workflow.exitPorts);
231
242
  // Merge detected macros with existing ones
@@ -275,11 +286,13 @@ export class AnnotationGenerator {
275
286
  if (workflow.ui?.exitNode?.x !== undefined && workflow.ui?.exitNode?.y !== undefined) {
276
287
  lines.push(` * @position Exit ${Math.round(workflow.ui.exitNode.x)} ${Math.round(workflow.ui.exitNode.y)}`);
277
288
  }
278
- // Add connections — skip connections covered by macros
289
+ // Add connections — skip connections covered by macros and dropped coerce connections
279
290
  if (!workflow.options?.autoConnect) {
280
291
  workflow.connections.forEach((conn) => {
281
292
  if (allMacros.length > 0 && isConnectionCoveredByMacroStatic(conn, allMacros))
282
293
  return;
294
+ if (droppedCoerceIds.has(conn.from.node) || droppedCoerceIds.has(conn.to.node))
295
+ return;
283
296
  const fromScope = conn.from.scope ? `:${conn.from.scope}` : '';
284
297
  const toScope = conn.to.scope ? `:${conn.to.scope}` : '';
285
298
  lines.push(` * @connect ${conn.from.node}.${conn.from.port}${fromScope} -> ${conn.to.node}.${conn.to.port}${toScope}`);
@@ -1035,10 +1035,11 @@ function isConnectionCoveredByMacro(conn, macros) {
1035
1035
  */
1036
1036
  function generateWorkflowJSDoc(ast, options = {}) {
1037
1037
  const lines = [];
1038
- // Build macro coverage sets for filtering (@map-specific)
1038
+ // Build macro coverage sets for filtering (@map and @coerce)
1039
1039
  const macroInstanceIds = new Set();
1040
1040
  const macroChildIds = new Set();
1041
1041
  const macroScopeNames = new Set();
1042
+ const allCoerceInstanceIds = new Set();
1042
1043
  if (ast.macros && ast.macros.length > 0) {
1043
1044
  for (const macro of ast.macros) {
1044
1045
  if (macro.type === 'map') {
@@ -1046,6 +1047,9 @@ function generateWorkflowJSDoc(ast, options = {}) {
1046
1047
  macroChildIds.add(macro.childId);
1047
1048
  macroScopeNames.add(`${macro.instanceId}.iterate`);
1048
1049
  }
1050
+ else if (macro.type === 'coerce') {
1051
+ allCoerceInstanceIds.add(macro.instanceId);
1052
+ }
1049
1053
  }
1050
1054
  }
1051
1055
  lines.push('/**');
@@ -1127,11 +1131,13 @@ function generateWorkflowJSDoc(ast, options = {}) {
1127
1131
  // Auto-position: compute default positions for nodes without explicit positions.
1128
1132
  // Must happen before instance tags are generated so [position:] can be emitted.
1129
1133
  const autoPositions = computeAutoPositions(ast);
1130
- // Add node instances — skip synthetic MAP_ITERATOR instances, strip parent from macro children.
1134
+ // Add node instances — skip synthetic MAP_ITERATOR/COERCION instances, strip parent from macro children.
1131
1135
  // Merge auto-computed positions into instance config (without mutating the AST).
1132
1136
  for (const instance of ast.instances) {
1133
1137
  if (macroInstanceIds.has(instance.id))
1134
1138
  continue;
1139
+ if (allCoerceInstanceIds.has(instance.id))
1140
+ continue;
1135
1141
  // Merge auto-position into config if not already set
1136
1142
  let inst = instance;
1137
1143
  if (inst.config?.x === undefined || inst.config?.y === undefined) {
@@ -1157,7 +1163,18 @@ function generateWorkflowJSDoc(ast, options = {}) {
1157
1163
  }
1158
1164
  }
1159
1165
  // Filter stale macros (e.g. paths whose connections were deleted)
1160
- const existingMacros = filterStaleMacros(ast.macros || [], ast.connections, ast.instances);
1166
+ const existingMacros = filterStaleMacros(ast.macros || [], ast.connections, ast.instances, ast.nodeTypes, ast.startPorts, ast.exitPorts);
1167
+ // Compute dropped coerce instance IDs — their synthetic instances and connections must be excluded
1168
+ const survivingCoerceIds = new Set();
1169
+ for (const macro of existingMacros) {
1170
+ if (macro.type === 'coerce')
1171
+ survivingCoerceIds.add(macro.instanceId);
1172
+ }
1173
+ const droppedCoerceIds = new Set();
1174
+ for (const id of allCoerceInstanceIds) {
1175
+ if (!survivingCoerceIds.has(id))
1176
+ droppedCoerceIds.add(id);
1177
+ }
1161
1178
  // Auto-detect @path sugar patterns from connections
1162
1179
  const detected = detectSugarPatterns(ast.connections, ast.instances, existingMacros, ast.nodeTypes, ast.startPorts, ast.exitPorts);
1163
1180
  // Merge detected macros with existing ones
@@ -1205,11 +1222,13 @@ function generateWorkflowJSDoc(ast, options = {}) {
1205
1222
  lines.push(` * @position Exit ${Math.round(exitX)} ${Math.round(exitY)}`);
1206
1223
  }
1207
1224
  // Add connections (with scope suffix when present)
1208
- // Skip connections covered by @map macros and autoConnect-generated connections
1225
+ // Skip connections covered by macros, autoConnect-generated connections, and dropped coerce connections
1209
1226
  if (!ast.options?.autoConnect) {
1210
1227
  for (const conn of ast.connections) {
1211
1228
  if (allMacros.length > 0 && isConnectionCoveredByMacro(conn, allMacros))
1212
1229
  continue;
1230
+ if (droppedCoerceIds.has(conn.from.node) || droppedCoerceIds.has(conn.to.node))
1231
+ continue;
1213
1232
  const fromScope = conn.from.scope ? `:${conn.from.scope}` : '';
1214
1233
  const toScope = conn.to.scope ? `:${conn.to.scope}` : '';
1215
1234
  lines.push(` * @connect ${conn.from.node}.${conn.from.port}${fromScope} -> ${conn.to.node}.${conn.to.port}${toScope}`);
@@ -9671,7 +9671,7 @@ var VERSION;
9671
9671
  var init_generated_version = __esm({
9672
9672
  "src/generated-version.ts"() {
9673
9673
  "use strict";
9674
- VERSION = "0.21.13";
9674
+ VERSION = "0.21.15";
9675
9675
  }
9676
9676
  });
9677
9677
 
@@ -18090,7 +18090,7 @@ var init_port_tag_utils = __esm({
18090
18090
  });
18091
18091
 
18092
18092
  // src/sugar-optimizer.ts
18093
- function validatePathMacro(path50, connections, instances) {
18093
+ function validatePathMacro(path50, connections, instances, nodeTypes, startPorts, exitPorts) {
18094
18094
  const instanceIds = new Set(instances.map((inst) => inst.id));
18095
18095
  for (const step of path50.steps) {
18096
18096
  if (step.node === "Start" || step.node === "Exit") continue;
@@ -18126,21 +18126,85 @@ function validatePathMacro(path50, connections, instances) {
18126
18126
  return false;
18127
18127
  }
18128
18128
  }
18129
+ if (nodeTypes && startPorts && exitPorts) {
18130
+ const instanceMap = new Map(instances.map((inst) => [inst.id, inst]));
18131
+ const nodeTypeMap = /* @__PURE__ */ new Map();
18132
+ for (const nt of nodeTypes) {
18133
+ nodeTypeMap.set(nt.name, nt);
18134
+ if (nt.functionName !== nt.name) {
18135
+ nodeTypeMap.set(nt.functionName, nt);
18136
+ }
18137
+ }
18138
+ const getNodeType2 = (nodeId) => {
18139
+ const inst = instanceMap.get(nodeId);
18140
+ if (!inst) return void 0;
18141
+ return nodeTypeMap.get(inst.nodeType);
18142
+ };
18143
+ const getOutputPorts = (nodeId) => {
18144
+ if (nodeId === "Start") return startPorts;
18145
+ const nt = getNodeType2(nodeId);
18146
+ return nt?.outputs || {};
18147
+ };
18148
+ const getInputPorts = (nodeId) => {
18149
+ if (nodeId === "Exit") return exitPorts;
18150
+ const nt = getNodeType2(nodeId);
18151
+ return nt?.inputs || {};
18152
+ };
18153
+ const { steps } = path50;
18154
+ for (let i = 0; i < steps.length - 1; i++) {
18155
+ const nextId = steps[i + 1].node;
18156
+ if (nextId === "Exit") continue;
18157
+ const nextInputs = getInputPorts(nextId);
18158
+ for (const [inputName] of Object.entries(nextInputs)) {
18159
+ if (isControlFlowPort(inputName)) continue;
18160
+ for (let j = i; j >= 0; j--) {
18161
+ const ancestorId = steps[j].node;
18162
+ const ancestorOutputs = getOutputPorts(ancestorId);
18163
+ if (inputName in ancestorOutputs && !isControlFlowPort(inputName)) {
18164
+ const key = `${ancestorId}.${inputName}->${nextId}.${inputName}`;
18165
+ if (!connKeys.has(key)) {
18166
+ return false;
18167
+ }
18168
+ break;
18169
+ }
18170
+ }
18171
+ }
18172
+ }
18173
+ }
18129
18174
  return true;
18130
18175
  }
18131
- function filterStaleMacros(macros, connections, instances) {
18176
+ function filterStaleMacros(macros, connections, instances, nodeTypes, startPorts, exitPorts) {
18132
18177
  const instanceIds = new Set(instances.map((i) => i.id));
18133
18178
  instanceIds.add("Start");
18134
18179
  instanceIds.add("Exit");
18180
+ const connKeys = /* @__PURE__ */ new Set();
18181
+ for (const conn of connections) {
18182
+ if (!conn.from.scope && !conn.to.scope) {
18183
+ connKeys.add(`${conn.from.node}.${conn.from.port}->${conn.to.node}.${conn.to.port}`);
18184
+ }
18185
+ }
18135
18186
  return macros.filter((macro) => {
18136
- if (macro.type === "path") return validatePathMacro(macro, connections, instances);
18187
+ if (macro.type === "path") return validatePathMacro(macro, connections, instances, nodeTypes, startPorts, exitPorts);
18137
18188
  if (macro.type === "fanOut") {
18138
18189
  if (!instanceIds.has(macro.source.node)) return false;
18139
- return macro.targets.every((t) => instanceIds.has(t.node));
18190
+ return macro.targets.every((t) => {
18191
+ if (!instanceIds.has(t.node)) return false;
18192
+ const targetPort = t.port ?? macro.source.port;
18193
+ return connKeys.has(`${macro.source.node}.${macro.source.port}->${t.node}.${targetPort}`);
18194
+ });
18140
18195
  }
18141
18196
  if (macro.type === "fanIn") {
18142
18197
  if (!instanceIds.has(macro.target.node)) return false;
18143
- return macro.sources.every((s) => instanceIds.has(s.node));
18198
+ return macro.sources.every((s) => {
18199
+ if (!instanceIds.has(s.node)) return false;
18200
+ const sourcePort = s.port ?? macro.target.port;
18201
+ return connKeys.has(`${s.node}.${sourcePort}->${macro.target.node}.${macro.target.port}`);
18202
+ });
18203
+ }
18204
+ if (macro.type === "coerce") {
18205
+ const toCoerce = `${macro.source.node}.${macro.source.port}->${macro.instanceId}.value`;
18206
+ const fromCoerce = `${macro.instanceId}.result->${macro.target.node}.${macro.target.port}`;
18207
+ return connKeys.has(toCoerce) && connKeys.has(fromCoerce);
18144
18208
  }
18145
18209
  return true;
18146
18210
  });
@@ -18775,8 +18839,19 @@ var init_annotation_generator = __esm({
18775
18839
  const existingMacros = filterStaleMacros(
18776
18840
  workflow.macros || [],
18777
18841
  workflow.connections,
18778
- workflow.instances
18842
+ workflow.instances,
18843
+ workflow.nodeTypes,
18844
+ workflow.startPorts,
18845
+ workflow.exitPorts
18779
18846
  );
18847
+ const survivingCoerceIds = /* @__PURE__ */ new Set();
18848
+ for (const macro of existingMacros) {
18849
+ if (macro.type === "coerce") survivingCoerceIds.add(macro.instanceId);
18850
+ }
18851
+ const droppedCoerceIds = /* @__PURE__ */ new Set();
18852
+ for (const id of coerceInstanceIds) {
18853
+ if (!survivingCoerceIds.has(id)) droppedCoerceIds.add(id);
18854
+ }
18780
18855
  const detected = detectSugarPatterns(
18781
18856
  workflow.connections,
18782
18857
  workflow.instances,
@@ -18825,6 +18900,7 @@ var init_annotation_generator = __esm({
18825
18900
  if (!workflow.options?.autoConnect) {
18826
18901
  workflow.connections.forEach((conn) => {
18827
18902
  if (allMacros.length > 0 && isConnectionCoveredByMacroStatic(conn, allMacros)) return;
18903
+ if (droppedCoerceIds.has(conn.from.node) || droppedCoerceIds.has(conn.to.node)) return;
18828
18904
  const fromScope = conn.from.scope ? `:${conn.from.scope}` : "";
18829
18905
  const toScope = conn.to.scope ? `:${conn.to.scope}` : "";
18830
18906
  lines.push(` * @connect ${conn.from.node}.${conn.from.port}${fromScope} -> ${conn.to.node}.${conn.to.port}${toScope}`);
@@ -19679,12 +19755,15 @@ function generateWorkflowJSDoc(ast, options = {}) {
19679
19755
  const macroInstanceIds = /* @__PURE__ */ new Set();
19680
19756
  const macroChildIds = /* @__PURE__ */ new Set();
19681
19757
  const macroScopeNames = /* @__PURE__ */ new Set();
19758
+ const allCoerceInstanceIds = /* @__PURE__ */ new Set();
19682
19759
  if (ast.macros && ast.macros.length > 0) {
19683
19760
  for (const macro of ast.macros) {
19684
19761
  if (macro.type === "map") {
19685
19762
  macroInstanceIds.add(macro.instanceId);
19686
19763
  macroChildIds.add(macro.childId);
19687
19764
  macroScopeNames.add(`${macro.instanceId}.iterate`);
19765
+ } else if (macro.type === "coerce") {
19766
+ allCoerceInstanceIds.add(macro.instanceId);
19688
19767
  }
19689
19768
  }
19690
19769
  }
@@ -19744,6 +19823,7 @@ function generateWorkflowJSDoc(ast, options = {}) {
19744
19823
  const autoPositions = computeAutoPositions(ast);
19745
19824
  for (const instance of ast.instances) {
19746
19825
  if (macroInstanceIds.has(instance.id)) continue;
19826
+ if (allCoerceInstanceIds.has(instance.id)) continue;
19747
19827
  let inst = instance;
19748
19828
  if (inst.config?.x === void 0 || inst.config?.y === void 0) {
19749
19829
  const autoPos = autoPositions.get(inst.id);
@@ -19768,8 +19848,19 @@ function generateWorkflowJSDoc(ast, options = {}) {
19768
19848
  const existingMacros = filterStaleMacros(
19769
19849
  ast.macros || [],
19770
19850
  ast.connections,
19771
- ast.instances
19851
+ ast.instances,
19852
+ ast.nodeTypes,
19853
+ ast.startPorts,
19854
+ ast.exitPorts
19772
19855
  );
19856
+ const survivingCoerceIds = /* @__PURE__ */ new Set();
19857
+ for (const macro of existingMacros) {
19858
+ if (macro.type === "coerce") survivingCoerceIds.add(macro.instanceId);
19859
+ }
19860
+ const droppedCoerceIds = /* @__PURE__ */ new Set();
19861
+ for (const id of allCoerceInstanceIds) {
19862
+ if (!survivingCoerceIds.has(id)) droppedCoerceIds.add(id);
19863
+ }
19773
19864
  const detected = detectSugarPatterns(
19774
19865
  ast.connections,
19775
19866
  ast.instances,
@@ -19818,6 +19909,7 @@ function generateWorkflowJSDoc(ast, options = {}) {
19818
19909
  if (!ast.options?.autoConnect) {
19819
19910
  for (const conn of ast.connections) {
19820
19911
  if (allMacros.length > 0 && isConnectionCoveredByMacro(conn, allMacros)) continue;
19912
+ if (droppedCoerceIds.has(conn.from.node) || droppedCoerceIds.has(conn.to.node)) continue;
19821
19913
  const fromScope = conn.from.scope ? `:${conn.from.scope}` : "";
19822
19914
  const toScope = conn.to.scope ? `:${conn.to.scope}` : "";
19823
19915
  lines.push(
@@ -93117,7 +93209,7 @@ function displayInstalledPackage(pkg) {
93117
93209
  // src/cli/index.ts
93118
93210
  init_logger();
93119
93211
  init_error_utils();
93120
- var version2 = true ? "0.21.13" : "0.0.0-dev";
93212
+ var version2 = true ? "0.21.15" : "0.0.0-dev";
93121
93213
  var program2 = new Command();
93122
93214
  program2.name("fw").description("Flow Weaver Annotations - Compile and validate workflow files").option("-v, --version", "Output the current version").option("--no-color", "Disable colors").option("--color", "Force colors").on("option:version", () => {
93123
93215
  logger.banner(version2);
@@ -1,2 +1,2 @@
1
- export declare const VERSION = "0.21.13";
1
+ export declare const VERSION = "0.21.15";
2
2
  //# sourceMappingURL=generated-version.d.ts.map
@@ -1,3 +1,3 @@
1
1
  // Auto-generated by scripts/generate-version.ts — do not edit manually
2
- export const VERSION = '0.21.13';
2
+ export const VERSION = '0.21.15';
3
3
  //# sourceMappingURL=generated-version.js.map
@@ -10,19 +10,24 @@ export interface DetectedSugar {
10
10
  paths: TPathMacro[];
11
11
  }
12
12
  /**
13
- * Validate that a @path macro's control-flow connections still exist
14
- * in the actual connection set. Also checks that all step nodes exist
15
- * as instances (or are Start/Exit).
13
+ * Validate that a @path macro's connections still exist in the actual
14
+ * connection set. Checks both control-flow AND data connections (scope
15
+ * walking). Also checks that all step nodes exist as instances (or are
16
+ * Start/Exit).
16
17
  *
17
18
  * Returns true if the path is still valid, false if it should be dropped.
19
+ *
20
+ * When nodeTypes/startPorts/exitPorts are provided, data connections are
21
+ * validated using the same scope-walking algorithm that expandPathMacros
22
+ * uses. Without them, only control-flow is checked (legacy behavior).
18
23
  */
19
- export declare function validatePathMacro(path: TPathMacro, connections: TConnectionAST[], instances: TNodeInstanceAST[]): boolean;
24
+ export declare function validatePathMacro(path: TPathMacro, connections: TConnectionAST[], instances: TNodeInstanceAST[], nodeTypes?: TNodeTypeAST[], startPorts?: Record<string, TPortDefinition>, exitPorts?: Record<string, TPortDefinition>): boolean;
20
25
  /**
21
- * Filter existing macros, removing any @path macros whose control-flow
22
- * connections no longer exist in the connection set.
26
+ * Filter existing macros, removing any @path macros whose connections
27
+ * (control-flow AND data) no longer exist in the connection set.
23
28
  * Non-path macros are passed through unchanged.
24
29
  */
25
- export declare function filterStaleMacros(macros: TWorkflowMacro[], connections: TConnectionAST[], instances: TNodeInstanceAST[]): TWorkflowMacro[];
30
+ export declare function filterStaleMacros(macros: TWorkflowMacro[], connections: TConnectionAST[], instances: TNodeInstanceAST[], nodeTypes?: TNodeTypeAST[], startPorts?: Record<string, TPortDefinition>, exitPorts?: Record<string, TPortDefinition>): TWorkflowMacro[];
26
31
  /**
27
32
  * Detect @path routes from a set of connections.
28
33
  *
@@ -10,13 +10,18 @@ import { isControlFlowPort } from './constants.js';
10
10
  // Path Validation (for stale macro detection during round-trip)
11
11
  // =============================================================================
12
12
  /**
13
- * Validate that a @path macro's control-flow connections still exist
14
- * in the actual connection set. Also checks that all step nodes exist
15
- * as instances (or are Start/Exit).
13
+ * Validate that a @path macro's connections still exist in the actual
14
+ * connection set. Checks both control-flow AND data connections (scope
15
+ * walking). Also checks that all step nodes exist as instances (or are
16
+ * Start/Exit).
16
17
  *
17
18
  * Returns true if the path is still valid, false if it should be dropped.
19
+ *
20
+ * When nodeTypes/startPorts/exitPorts are provided, data connections are
21
+ * validated using the same scope-walking algorithm that expandPathMacros
22
+ * uses. Without them, only control-flow is checked (legacy behavior).
18
23
  */
19
- export function validatePathMacro(path, connections, instances) {
24
+ export function validatePathMacro(path, connections, instances, nodeTypes, startPorts, exitPorts) {
20
25
  const instanceIds = new Set(instances.map(inst => inst.id));
21
26
  // Check all step nodes exist as instances (or are Start/Exit)
22
27
  for (const step of path.steps) {
@@ -61,29 +66,110 @@ export function validatePathMacro(path, connections, instances) {
61
66
  return false;
62
67
  }
63
68
  }
69
+ // Validate data connections (scope walking) — same algorithm as
70
+ // expandPathMacros and detectSugarPatterns Step 3.
71
+ // If any data connection that the path would auto-generate is missing,
72
+ // the path is stale (a connection was explicitly removed).
73
+ if (nodeTypes && startPorts && exitPorts) {
74
+ const instanceMap = new Map(instances.map(inst => [inst.id, inst]));
75
+ const nodeTypeMap = new Map();
76
+ for (const nt of nodeTypes) {
77
+ nodeTypeMap.set(nt.name, nt);
78
+ if (nt.functionName !== nt.name) {
79
+ nodeTypeMap.set(nt.functionName, nt);
80
+ }
81
+ }
82
+ const getNodeType = (nodeId) => {
83
+ const inst = instanceMap.get(nodeId);
84
+ if (!inst)
85
+ return undefined;
86
+ return nodeTypeMap.get(inst.nodeType);
87
+ };
88
+ const getOutputPorts = (nodeId) => {
89
+ if (nodeId === 'Start')
90
+ return startPorts;
91
+ const nt = getNodeType(nodeId);
92
+ return nt?.outputs || {};
93
+ };
94
+ const getInputPorts = (nodeId) => {
95
+ if (nodeId === 'Exit')
96
+ return exitPorts;
97
+ const nt = getNodeType(nodeId);
98
+ return nt?.inputs || {};
99
+ };
100
+ const { steps } = path;
101
+ for (let i = 0; i < steps.length - 1; i++) {
102
+ const nextId = steps[i + 1].node;
103
+ if (nextId === 'Exit')
104
+ continue;
105
+ const nextInputs = getInputPorts(nextId);
106
+ for (const [inputName] of Object.entries(nextInputs)) {
107
+ if (isControlFlowPort(inputName))
108
+ continue;
109
+ // Walk backward through path steps to find nearest ancestor with same-name output
110
+ for (let j = i; j >= 0; j--) {
111
+ const ancestorId = steps[j].node;
112
+ const ancestorOutputs = getOutputPorts(ancestorId);
113
+ if (inputName in ancestorOutputs && !isControlFlowPort(inputName)) {
114
+ const key = `${ancestorId}.${inputName}->${nextId}.${inputName}`;
115
+ if (!connKeys.has(key)) {
116
+ return false;
117
+ }
118
+ break;
119
+ }
120
+ }
121
+ }
122
+ }
123
+ }
64
124
  return true;
65
125
  }
66
126
  /**
67
- * Filter existing macros, removing any @path macros whose control-flow
68
- * connections no longer exist in the connection set.
127
+ * Filter existing macros, removing any @path macros whose connections
128
+ * (control-flow AND data) no longer exist in the connection set.
69
129
  * Non-path macros are passed through unchanged.
70
130
  */
71
- export function filterStaleMacros(macros, connections, instances) {
131
+ export function filterStaleMacros(macros, connections, instances, nodeTypes, startPorts, exitPorts) {
72
132
  const instanceIds = new Set(instances.map(i => i.id));
73
133
  instanceIds.add('Start');
74
134
  instanceIds.add('Exit');
135
+ // Build connection key set for quick lookup
136
+ const connKeys = new Set();
137
+ for (const conn of connections) {
138
+ if (!conn.from.scope && !conn.to.scope) {
139
+ connKeys.add(`${conn.from.node}.${conn.from.port}->${conn.to.node}.${conn.to.port}`);
140
+ }
141
+ }
75
142
  return macros.filter(macro => {
76
143
  if (macro.type === 'path')
77
- return validatePathMacro(macro, connections, instances);
144
+ return validatePathMacro(macro, connections, instances, nodeTypes, startPorts, exitPorts);
78
145
  if (macro.type === 'fanOut') {
79
146
  if (!instanceIds.has(macro.source.node))
80
147
  return false;
81
- return macro.targets.every(t => instanceIds.has(t.node));
148
+ // Validate that all fanOut connections still exist
149
+ return macro.targets.every(t => {
150
+ if (!instanceIds.has(t.node))
151
+ return false;
152
+ const targetPort = t.port ?? macro.source.port;
153
+ return connKeys.has(`${macro.source.node}.${macro.source.port}->${t.node}.${targetPort}`);
154
+ });
82
155
  }
83
156
  if (macro.type === 'fanIn') {
84
157
  if (!instanceIds.has(macro.target.node))
85
158
  return false;
86
- return macro.sources.every(s => instanceIds.has(s.node));
159
+ // Validate that all fanIn connections still exist
160
+ return macro.sources.every(s => {
161
+ if (!instanceIds.has(s.node))
162
+ return false;
163
+ const sourcePort = s.port ?? macro.target.port;
164
+ return connKeys.has(`${s.node}.${sourcePort}->${macro.target.node}.${macro.target.port}`);
165
+ });
166
+ }
167
+ if (macro.type === 'coerce') {
168
+ // Validate that both coerce connections still exist:
169
+ // source -> coerceInstance.value AND coerceInstance.result -> target
170
+ const toCoerce = `${macro.source.node}.${macro.source.port}->${macro.instanceId}.value`;
171
+ const fromCoerce = `${macro.instanceId}.result->${macro.target.node}.${macro.target.port}`;
172
+ return connKeys.has(toCoerce) && connKeys.has(fromCoerce);
87
173
  }
88
174
  return true;
89
175
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@synergenius/flow-weaver",
3
- "version": "0.21.13",
3
+ "version": "0.21.15",
4
4
  "description": "Deterministic workflow compiler for AI agents. Compiles to standalone TypeScript, no runtime dependencies.",
5
5
  "private": false,
6
6
  "type": "module",
@@ -31,6 +31,10 @@
31
31
  "types": "./dist/doc-metadata/index.d.ts",
32
32
  "default": "./dist/doc-metadata/index.js"
33
33
  },
34
+ "./docs": {
35
+ "types": "./dist/docs/index.d.ts",
36
+ "default": "./dist/docs/index.js"
37
+ },
34
38
  "./ast": {
35
39
  "types": "./dist/ast/index.d.ts",
36
40
  "default": "./dist/ast/index.js"