@synergenius/flow-weaver 0.8.3 → 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.
package/dist/cli/index.js CHANGED
@@ -31,6 +31,8 @@ import { migrateCommand } from './commands/migrate.js';
31
31
  import { changelogCommand } from './commands/changelog.js';
32
32
  import { stripCommand } from './commands/strip.js';
33
33
  import { docsListCommand, docsReadCommand, docsSearchCommand } from './commands/docs.js';
34
+ import { statusCommand } from './commands/status.js';
35
+ import { implementCommand } from './commands/implement.js';
34
36
  import { marketInitCommand, marketPackCommand, marketPublishCommand, marketInstallCommand, marketSearchCommand, marketListCommand, } from './commands/market.js';
35
37
  import { logger } from './utils/logger.js';
36
38
  import { getErrorMessage } from '../utils/error-utils.js';
@@ -589,6 +591,36 @@ program
589
591
  process.exit(1);
590
592
  }
591
593
  });
594
+ // Status command
595
+ program
596
+ .command('status <input>')
597
+ .description('Report implementation progress for stub workflows')
598
+ .option('-w, --workflow-name <name>', 'Specific workflow name')
599
+ .option('--json', 'Output as JSON', false)
600
+ .action(async (input, options) => {
601
+ try {
602
+ await statusCommand(input, options);
603
+ }
604
+ catch (error) {
605
+ logger.error(`Command failed: ${getErrorMessage(error)}`);
606
+ process.exit(1);
607
+ }
608
+ });
609
+ // Implement command
610
+ program
611
+ .command('implement <input> <node>')
612
+ .description('Replace a stub node with a real function skeleton')
613
+ .option('-w, --workflow-name <name>', 'Specific workflow name')
614
+ .option('-p, --preview', 'Preview the generated code without writing', false)
615
+ .action(async (input, node, options) => {
616
+ try {
617
+ await implementCommand(input, node, options);
618
+ }
619
+ catch (error) {
620
+ logger.error(`Command failed: ${getErrorMessage(error)}`);
621
+ process.exit(1);
622
+ }
623
+ });
592
624
  // Changelog command
593
625
  program
594
626
  .command('changelog')
@@ -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);
@@ -1487,7 +1487,11 @@ branchingNodes = new Set() // Branching nodes set for port-aware STEP guards
1487
1487
  });
1488
1488
  const resultVar = `${safeId}Result`;
1489
1489
  const awaitKeyword = nodeType.isAsync ? 'await ' : '';
1490
- if (nodeType.variant === 'COERCION') {
1490
+ if (nodeType.variant === 'STUB') {
1491
+ // Stub node: emit a runtime throw. The workflow was generated with generateStubs: true.
1492
+ lines.push(`${indent} throw new Error('Node "${instanceId}" uses stub type "${functionName}" which has no implementation.');`);
1493
+ }
1494
+ else if (nodeType.variant === 'COERCION') {
1491
1495
  // Coercion node: inline JS expression instead of function call
1492
1496
  const coerceExprMap = {
1493
1497
  __fw_toString: 'String',
@@ -12,6 +12,7 @@ import { registerExportTools } from './tools-export.js';
12
12
  import { registerMarketplaceTools } from './tools-marketplace.js';
13
13
  import { registerDiagramTools } from './tools-diagram.js';
14
14
  import { registerDocsTools } from './tools-docs.js';
15
+ import { registerModelTools } from './tools-model.js';
15
16
  import { registerResources } from './resources.js';
16
17
  function parseEventFilterFromEnv() {
17
18
  const filter = {};
@@ -72,6 +73,7 @@ export async function startMcpServer(options) {
72
73
  registerMarketplaceTools(mcp);
73
74
  registerDiagramTools(mcp);
74
75
  registerDocsTools(mcp);
76
+ registerModelTools(mcp);
75
77
  registerResources(mcp, connection, buffer);
76
78
  // Connect transport (only in stdio MCP mode)
77
79
  if (!options._testDeps && options.stdio) {
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function registerModelTools(mcp: McpServer): void;
3
+ //# sourceMappingURL=tools-model.d.ts.map
@@ -0,0 +1,253 @@
1
+ import { z } from 'zod';
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+ import { parseWorkflow, validateWorkflow } from '../api/index.js';
5
+ import { generateFunctionSignature } from '../annotation-generator.js';
6
+ import { makeToolResult, makeErrorResult } from './response-utils.js';
7
+ const stepSchema = z.object({
8
+ name: z.string().describe('Function name for this node'),
9
+ description: z.string().optional().describe('Brief description of what this node does'),
10
+ inputs: z.record(z.string()).describe('Input ports as { portName: dataType } (e.g. { email: "STRING" })'),
11
+ outputs: z.record(z.string()).describe('Output ports as { portName: dataType }'),
12
+ });
13
+ /**
14
+ * Find a `declare function <name>(...): ...;` declaration in source text,
15
+ * handling multiline signatures. Returns the full matched text and its
16
+ * leading indentation, or null if not found.
17
+ */
18
+ function findDeclareFunction(source, functionName) {
19
+ const lines = source.split('\n');
20
+ const startPattern = new RegExp(`^(\\s*)declare\\s+function\\s+${escapeRegex(functionName)}\\s*\\(`);
21
+ for (let i = 0; i < lines.length; i++) {
22
+ const m = lines[i].match(startPattern);
23
+ if (!m)
24
+ continue;
25
+ const indent = m[1] || '';
26
+ // Accumulate lines until we find one ending with ;
27
+ let accumulated = lines[i];
28
+ let j = i;
29
+ while (!accumulated.trimEnd().endsWith(';') && j < lines.length - 1) {
30
+ j++;
31
+ accumulated += '\n' + lines[j];
32
+ }
33
+ return { match: accumulated, indent };
34
+ }
35
+ return null;
36
+ }
37
+ export function registerModelTools(mcp) {
38
+ mcp.tool('fw_create_model', 'Create a new stub workflow model from a structured description. ' +
39
+ 'Generates a TypeScript file with declare function stubs and workflow annotations. ' +
40
+ 'The model can be validated structurally before any node is implemented.', {
41
+ name: z.string().describe('Workflow name (used as export name)'),
42
+ description: z.string().optional().describe('Workflow description'),
43
+ steps: z.array(stepSchema).min(1).describe('Node definitions in execution order'),
44
+ flow: z
45
+ .string()
46
+ .describe('Execution path, e.g. "Start -> validateEmail -> checkUserExists -> Exit"'),
47
+ filePath: z.string().describe('Output file path'),
48
+ }, async (args) => {
49
+ try {
50
+ const outputPath = path.resolve(args.filePath);
51
+ const lines = [];
52
+ // Generate declare function stubs
53
+ for (const step of args.steps) {
54
+ lines.push('/** @flowWeaver node */');
55
+ const params = Object.entries(step.inputs)
56
+ .map(([name, type]) => `${name}: ${mapTypeToTS(type)}`)
57
+ .join(', ');
58
+ const outputEntries = Object.entries(step.outputs);
59
+ const returnType = outputEntries.length === 1
60
+ ? mapTypeToTS(outputEntries[0][1])
61
+ : `{ ${outputEntries.map(([name, type]) => `${name}: ${mapTypeToTS(type)}`).join('; ')} }`;
62
+ lines.push(`declare function ${step.name}(${params}): ${returnType};`);
63
+ lines.push('');
64
+ }
65
+ // Parse the flow string — use full function names as instance IDs
66
+ const flowSteps = args.flow
67
+ .split('->')
68
+ .map((s) => s.trim())
69
+ .filter(Boolean);
70
+ // Build @node annotations using full name as both ID and type
71
+ const nodeAnnotations = args.steps.map((step) => `@node ${step.name} ${step.name}`);
72
+ // @path uses the same full names directly from the flow string
73
+ const pathAnnotation = `@path ${flowSteps.join(' -> ')}`;
74
+ // Generate workflow annotation
75
+ const jsdocLines = ['/**'];
76
+ if (args.description) {
77
+ jsdocLines.push(` * ${args.description}`);
78
+ jsdocLines.push(' *');
79
+ }
80
+ jsdocLines.push(' * @flowWeaver workflow @autoConnect');
81
+ for (const nodeAnn of nodeAnnotations) {
82
+ jsdocLines.push(` * ${nodeAnn}`);
83
+ }
84
+ jsdocLines.push(` * ${pathAnnotation}`);
85
+ jsdocLines.push(' */');
86
+ jsdocLines.push(`export const ${args.name} = 'flowWeaver:draft';`);
87
+ lines.push(...jsdocLines);
88
+ lines.push('');
89
+ const content = lines.join('\n');
90
+ const dir = path.dirname(outputPath);
91
+ if (!fs.existsSync(dir)) {
92
+ fs.mkdirSync(dir, { recursive: true });
93
+ }
94
+ fs.writeFileSync(outputPath, content, 'utf8');
95
+ return makeToolResult({
96
+ filePath: outputPath,
97
+ workflowName: args.name,
98
+ stubCount: args.steps.length,
99
+ nodes: args.steps.map((s) => s.name),
100
+ });
101
+ }
102
+ catch (err) {
103
+ return makeErrorResult('CREATE_MODEL_ERROR', `fw_create_model failed: ${err instanceof Error ? err.message : String(err)}`);
104
+ }
105
+ });
106
+ mcp.tool('fw_workflow_status', 'Report implementation progress for a workflow — which nodes are stubs, ' +
107
+ 'which are implemented, and whether the graph structure is valid.', {
108
+ filePath: z.string().describe('Path to the workflow file'),
109
+ workflowName: z.string().optional().describe('Specific workflow name if file has multiple'),
110
+ }, async (args) => {
111
+ try {
112
+ const filePath = path.resolve(args.filePath);
113
+ const parseResult = await parseWorkflow(filePath, { workflowName: args.workflowName });
114
+ if (parseResult.errors.length > 0) {
115
+ return makeErrorResult('PARSE_ERROR', `Parse errors:\n${parseResult.errors.join('\n')}`);
116
+ }
117
+ const ast = parseResult.ast;
118
+ const instanceTypeMap = new Map();
119
+ for (const nt of ast.nodeTypes) {
120
+ instanceTypeMap.set(nt.name, nt);
121
+ if (nt.functionName !== nt.name) {
122
+ instanceTypeMap.set(nt.functionName, nt);
123
+ }
124
+ }
125
+ const nodes = [];
126
+ const seen = new Set();
127
+ for (const instance of ast.instances) {
128
+ const nt = instanceTypeMap.get(instance.nodeType);
129
+ if (!nt || seen.has(nt.functionName))
130
+ continue;
131
+ seen.add(nt.functionName);
132
+ const inputs = {};
133
+ const outputs = {};
134
+ for (const [name, port] of Object.entries(nt.inputs)) {
135
+ if (name === 'execute')
136
+ continue;
137
+ inputs[name] = port.dataType;
138
+ }
139
+ for (const [name, port] of Object.entries(nt.outputs)) {
140
+ if (name === 'onSuccess' || name === 'onFailure')
141
+ continue;
142
+ outputs[name] = port.dataType;
143
+ }
144
+ nodes.push({
145
+ name: nt.functionName,
146
+ status: nt.variant === 'STUB' ? 'stub' : 'implemented',
147
+ inputs,
148
+ outputs,
149
+ });
150
+ }
151
+ const implemented = nodes.filter((n) => n.status === 'implemented').length;
152
+ const total = nodes.length;
153
+ // Draft validation for structural issues
154
+ const validation = validateWorkflow(ast, { mode: 'draft' });
155
+ const structuralErrors = validation.errors
156
+ .filter((e) => e.code !== 'STUB_NODE')
157
+ .map((e) => ({ message: e.message, code: e.code, node: e.node }));
158
+ return makeToolResult({
159
+ workflowName: ast.name,
160
+ implemented,
161
+ total,
162
+ percentage: total > 0 ? Math.round((implemented / total) * 100) : 100,
163
+ nodes,
164
+ structurallyValid: structuralErrors.length === 0,
165
+ structuralErrors,
166
+ });
167
+ }
168
+ catch (err) {
169
+ return makeErrorResult('STATUS_ERROR', `fw_workflow_status failed: ${err instanceof Error ? err.message : String(err)}`);
170
+ }
171
+ });
172
+ mcp.tool('fw_implement_node', 'Replace a stub node (declare function) with a real function skeleton, ' +
173
+ 'or write a provided implementation directly. Preserves existing JSDoc annotations.', {
174
+ filePath: z.string().describe('Path to the workflow file'),
175
+ nodeName: z.string().describe('Function name of the stub node to implement'),
176
+ implementation: z
177
+ .string()
178
+ .optional()
179
+ .describe('Full function body to write. If omitted, generates a skeleton.'),
180
+ workflowName: z.string().optional().describe('Specific workflow name'),
181
+ }, async (args) => {
182
+ try {
183
+ const filePath = path.resolve(args.filePath);
184
+ if (!fs.existsSync(filePath)) {
185
+ return makeErrorResult('FILE_NOT_FOUND', `File not found: ${args.filePath}`);
186
+ }
187
+ const parseResult = await parseWorkflow(filePath, { workflowName: args.workflowName });
188
+ if (parseResult.errors.length > 0) {
189
+ return makeErrorResult('PARSE_ERROR', `Parse errors:\n${parseResult.errors.join('\n')}`);
190
+ }
191
+ const ast = parseResult.ast;
192
+ const stubNt = ast.nodeTypes.find((nt) => nt.variant === 'STUB' && (nt.functionName === args.nodeName || nt.name === args.nodeName));
193
+ if (!stubNt) {
194
+ const existing = ast.nodeTypes.find((nt) => nt.functionName === args.nodeName || nt.name === args.nodeName);
195
+ if (existing) {
196
+ return makeErrorResult('ALREADY_IMPLEMENTED', `Node "${args.nodeName}" is already implemented.`);
197
+ }
198
+ const stubs = ast.nodeTypes
199
+ .filter((nt) => nt.variant === 'STUB')
200
+ .map((nt) => nt.functionName);
201
+ return makeErrorResult('NODE_NOT_FOUND', `Stub "${args.nodeName}" not found. Available stubs: ${stubs.join(', ') || 'none'}`);
202
+ }
203
+ const source = fs.readFileSync(filePath, 'utf8');
204
+ const found = findDeclareFunction(source, stubNt.functionName);
205
+ if (!found) {
206
+ return makeErrorResult('SOURCE_MISMATCH', `Could not find "declare function ${stubNt.functionName}" in source file.`);
207
+ }
208
+ let replacement;
209
+ if (args.implementation) {
210
+ replacement = args.implementation
211
+ .split('\n')
212
+ .map((line) => found.indent + line)
213
+ .join('\n');
214
+ }
215
+ else {
216
+ const implementedType = { ...stubNt, variant: 'FUNCTION' };
217
+ const signatureLines = generateFunctionSignature(implementedType);
218
+ replacement = signatureLines.map((line) => found.indent + line).join('\n');
219
+ }
220
+ const updated = source.replace(found.match, replacement);
221
+ fs.writeFileSync(filePath, updated, 'utf8');
222
+ return makeToolResult({
223
+ nodeName: stubNt.functionName,
224
+ filePath,
225
+ action: args.implementation ? 'implemented' : 'scaffolded',
226
+ });
227
+ }
228
+ catch (err) {
229
+ return makeErrorResult('IMPLEMENT_ERROR', `fw_implement_node failed: ${err instanceof Error ? err.message : String(err)}`);
230
+ }
231
+ });
232
+ }
233
+ function mapTypeToTS(dataType) {
234
+ const upper = dataType.toUpperCase();
235
+ switch (upper) {
236
+ case 'STRING':
237
+ return 'string';
238
+ case 'NUMBER':
239
+ return 'number';
240
+ case 'BOOLEAN':
241
+ return 'boolean';
242
+ case 'OBJECT':
243
+ return 'Record<string, unknown>';
244
+ case 'ARRAY':
245
+ return 'unknown[]';
246
+ default:
247
+ return 'unknown';
248
+ }
249
+ }
250
+ function escapeRegex(str) {
251
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
252
+ }
253
+ //# sourceMappingURL=tools-model.js.map
@@ -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
  }
@@ -4,6 +4,7 @@ export declare class WorkflowValidator {
4
4
  private errors;
5
5
  private warnings;
6
6
  private strictMode;
7
+ private draftMode;
7
8
  /** Look up instance sourceLocation by instance ID */
8
9
  private getInstanceLocation;
9
10
  /** Look up connection sourceLocation */
@@ -26,6 +27,7 @@ export declare class WorkflowValidator {
26
27
  validateNodeType(nodeType: TNodeTypeAST): string[];
27
28
  validate(workflow: TWorkflowAST, options?: {
28
29
  strictMode?: boolean;
30
+ mode?: 'strict' | 'draft';
29
31
  }): {
30
32
  valid: boolean;
31
33
  errors: TValidationError[];