@synergenius/flow-weaver 0.9.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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.1" : "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.1",
4
4
  "description": "Deterministic workflow compiler for AI agents. Compiles to standalone TypeScript, no runtime dependencies.",
5
5
  "private": false,
6
6
  "type": "module",