@synergenius/flow-weaver 0.3.0 → 0.4.0
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 +1 -0
- package/dist/annotation-generator.js +36 -0
- package/dist/api/generate-in-place.js +39 -0
- package/dist/api/generate.js +11 -1
- package/dist/api/manipulation/nodes.js +22 -0
- package/dist/ast/types.d.ts +27 -1
- package/dist/built-in-nodes/index.d.ts +1 -0
- package/dist/built-in-nodes/index.js +1 -0
- package/dist/built-in-nodes/invoke-workflow.js +12 -1
- package/dist/built-in-nodes/mock-types.d.ts +2 -0
- package/dist/built-in-nodes/wait-for-agent.d.ts +13 -0
- package/dist/built-in-nodes/wait-for-agent.js +26 -0
- package/dist/chevrotain-parser/fan-parser.d.ts +38 -0
- package/dist/chevrotain-parser/fan-parser.js +149 -0
- package/dist/chevrotain-parser/grammar-diagrams.d.ts +1 -0
- package/dist/chevrotain-parser/grammar-diagrams.js +3 -0
- package/dist/chevrotain-parser/index.d.ts +3 -1
- package/dist/chevrotain-parser/index.js +3 -1
- package/dist/chevrotain-parser/tokens.d.ts +2 -0
- package/dist/chevrotain-parser/tokens.js +10 -0
- package/dist/cli/commands/diagram.d.ts +2 -1
- package/dist/cli/commands/diagram.js +9 -6
- package/dist/cli/commands/run.js +59 -1
- package/dist/cli/flow-weaver.mjs +1396 -77
- package/dist/cli/index.js +23 -36
- package/dist/diagram/geometry.js +47 -5
- package/dist/diagram/html-viewer.d.ts +12 -0
- package/dist/diagram/html-viewer.js +399 -0
- package/dist/diagram/index.d.ts +12 -0
- package/dist/diagram/index.js +22 -0
- package/dist/diagram/types.d.ts +1 -0
- package/dist/doc-metadata/extractors/annotations.js +282 -1
- package/dist/doc-metadata/types.d.ts +6 -0
- package/dist/generator/control-flow.d.ts +13 -0
- package/dist/generator/control-flow.js +74 -0
- package/dist/generator/inngest.js +23 -0
- package/dist/generator/unified.js +122 -2
- package/dist/jsdoc-parser.d.ts +24 -0
- package/dist/jsdoc-parser.js +41 -1
- package/dist/mcp/agent-channel.d.ts +35 -0
- package/dist/mcp/agent-channel.js +61 -0
- package/dist/mcp/run-registry.d.ts +29 -0
- package/dist/mcp/run-registry.js +24 -0
- package/dist/mcp/tools-diagram.d.ts +1 -1
- package/dist/mcp/tools-diagram.js +15 -7
- package/dist/mcp/tools-editor.js +75 -3
- package/dist/mcp/workflow-executor.d.ts +28 -0
- package/dist/mcp/workflow-executor.js +62 -1
- package/dist/parser.d.ts +8 -0
- package/dist/parser.js +100 -0
- package/dist/runtime/ExecutionContext.d.ts +2 -0
- package/dist/runtime/ExecutionContext.js +2 -0
- package/dist/runtime/events.d.ts +1 -1
- package/dist/sugar-optimizer.js +28 -3
- package/dist/validator.d.ts +8 -0
- package/dist/validator.js +92 -0
- package/docs/reference/advanced-annotations.md +431 -0
- package/docs/reference/built-in-nodes.md +225 -0
- package/docs/reference/cli-reference.md +882 -0
- package/docs/reference/compilation.md +351 -0
- package/docs/reference/concepts.md +400 -0
- package/docs/reference/debugging.md +255 -0
- package/docs/reference/deployment.md +207 -0
- package/docs/reference/error-codes.md +686 -0
- package/docs/reference/export-interface.md +229 -0
- package/docs/reference/iterative-development.md +186 -0
- package/docs/reference/jsdoc-grammar.md +471 -0
- package/docs/reference/marketplace.md +205 -0
- package/docs/reference/node-conversion.md +308 -0
- package/docs/reference/patterns.md +161 -0
- package/docs/reference/scaffold.md +160 -0
- package/docs/reference/tutorial.md +519 -0
- package/package.json +1 -1
|
@@ -82,9 +82,21 @@ export async function executeWorkflowFromFile(filePath, params, options) {
|
|
|
82
82
|
if (options?.mocks) {
|
|
83
83
|
globalThis.__fw_mocks__ = options.mocks;
|
|
84
84
|
}
|
|
85
|
+
// Set agent channel for waitForAgent pause/resume
|
|
86
|
+
if (options?.agentChannel) {
|
|
87
|
+
globalThis.__fw_agent_channel__ = options.agentChannel;
|
|
88
|
+
}
|
|
85
89
|
// Dynamic import using file:// URL for cross-platform compatibility
|
|
86
90
|
// (Windows paths like C:\... break with bare import() — "Received protocol 'c:'")
|
|
87
91
|
const mod = await import(pathToFileURL(tmpFile).href);
|
|
92
|
+
// Register exported functions for local invokeWorkflow resolution
|
|
93
|
+
const workflowRegistry = {};
|
|
94
|
+
for (const [key, value] of Object.entries(mod)) {
|
|
95
|
+
if (typeof value === 'function' && key !== '__esModule') {
|
|
96
|
+
workflowRegistry[key] = value;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
globalThis.__fw_workflow_registry__ = workflowRegistry;
|
|
88
100
|
// Find the target exported function
|
|
89
101
|
const exportedFn = findExportedFunction(mod, options?.workflowName);
|
|
90
102
|
if (!exportedFn) {
|
|
@@ -99,13 +111,15 @@ export async function executeWorkflowFromFile(filePath, params, options) {
|
|
|
99
111
|
result,
|
|
100
112
|
functionName: exportedFn.name,
|
|
101
113
|
executionTime,
|
|
102
|
-
...(includeTrace && { trace }),
|
|
114
|
+
...(includeTrace && { trace, summary: computeTraceSummary(trace) }),
|
|
103
115
|
};
|
|
104
116
|
}
|
|
105
117
|
finally {
|
|
106
118
|
// Clean up globals
|
|
107
119
|
delete globalThis.__fw_debugger__;
|
|
108
120
|
delete globalThis.__fw_mocks__;
|
|
121
|
+
delete globalThis.__fw_workflow_registry__;
|
|
122
|
+
delete globalThis.__fw_agent_channel__;
|
|
109
123
|
// Clean up temp files
|
|
110
124
|
try {
|
|
111
125
|
fs.unlinkSync(tmpFile);
|
|
@@ -117,6 +131,53 @@ export async function executeWorkflowFromFile(filePath, params, options) {
|
|
|
117
131
|
catch { /* ignore */ }
|
|
118
132
|
}
|
|
119
133
|
}
|
|
134
|
+
/** Compute a concise summary from raw trace events. */
|
|
135
|
+
export function computeTraceSummary(trace) {
|
|
136
|
+
if (trace.length === 0) {
|
|
137
|
+
return { totalNodes: 0, succeeded: 0, failed: 0, cancelled: 0, nodeTimings: [], totalDurationMs: 0 };
|
|
138
|
+
}
|
|
139
|
+
const nodeStartTimes = new Map();
|
|
140
|
+
const nodeFinalStatus = new Map();
|
|
141
|
+
const nodeTimings = [];
|
|
142
|
+
for (const event of trace) {
|
|
143
|
+
if (event.type !== 'STATUS_CHANGED' || !event.data)
|
|
144
|
+
continue;
|
|
145
|
+
const id = event.data.id;
|
|
146
|
+
const status = event.data.status;
|
|
147
|
+
if (!id || !status)
|
|
148
|
+
continue;
|
|
149
|
+
if (status === 'RUNNING') {
|
|
150
|
+
nodeStartTimes.set(id, event.timestamp);
|
|
151
|
+
}
|
|
152
|
+
if (status === 'SUCCEEDED' || status === 'FAILED' || status === 'CANCELLED') {
|
|
153
|
+
nodeFinalStatus.set(id, status);
|
|
154
|
+
const startTime = nodeStartTimes.get(id);
|
|
155
|
+
if (startTime !== undefined) {
|
|
156
|
+
nodeTimings.push({ nodeId: id, durationMs: event.timestamp - startTime });
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
let succeeded = 0;
|
|
161
|
+
let failed = 0;
|
|
162
|
+
let cancelled = 0;
|
|
163
|
+
for (const status of nodeFinalStatus.values()) {
|
|
164
|
+
if (status === 'SUCCEEDED')
|
|
165
|
+
succeeded++;
|
|
166
|
+
else if (status === 'FAILED')
|
|
167
|
+
failed++;
|
|
168
|
+
else if (status === 'CANCELLED')
|
|
169
|
+
cancelled++;
|
|
170
|
+
}
|
|
171
|
+
const totalDurationMs = trace[trace.length - 1].timestamp - trace[0].timestamp;
|
|
172
|
+
return {
|
|
173
|
+
totalNodes: nodeFinalStatus.size,
|
|
174
|
+
succeeded,
|
|
175
|
+
failed,
|
|
176
|
+
cancelled,
|
|
177
|
+
nodeTimings,
|
|
178
|
+
totalDurationMs,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
120
181
|
function findExportedFunction(mod, preferredName) {
|
|
121
182
|
// If a preferred name is specified, try it first
|
|
122
183
|
if (preferredName && typeof mod[preferredName] === 'function') {
|
package/dist/parser.d.ts
CHANGED
|
@@ -135,6 +135,14 @@ export declare class AnnotationParser {
|
|
|
135
135
|
* Processes all paths together for shared deduplication.
|
|
136
136
|
*/
|
|
137
137
|
private expandPathMacros;
|
|
138
|
+
/**
|
|
139
|
+
* Expand @fanOut macros into 1-to-N connections.
|
|
140
|
+
*/
|
|
141
|
+
private expandFanOutMacros;
|
|
142
|
+
/**
|
|
143
|
+
* Expand @fanIn macros into N-to-1 connections.
|
|
144
|
+
*/
|
|
145
|
+
private expandFanInMacros;
|
|
138
146
|
/**
|
|
139
147
|
* Generate automatic connections for @autoConnect workflows.
|
|
140
148
|
* Wires nodes in declaration order as a linear pipeline:
|
package/dist/parser.js
CHANGED
|
@@ -916,6 +916,14 @@ export class AnnotationParser {
|
|
|
916
916
|
if (config.paths && config.paths.length > 0) {
|
|
917
917
|
this.expandPathMacros(config.paths, instances, connections, allAvailableNodeTypes, startPorts, exitPorts, macros, errors, warnings);
|
|
918
918
|
}
|
|
919
|
+
// Expand @fanOut macros into 1-to-N connections
|
|
920
|
+
if (config.fanOuts && config.fanOuts.length > 0) {
|
|
921
|
+
this.expandFanOutMacros(config.fanOuts, instances, connections, startPorts, exitPorts, macros, errors);
|
|
922
|
+
}
|
|
923
|
+
// Expand @fanIn macros into N-to-1 connections
|
|
924
|
+
if (config.fanIns && config.fanIns.length > 0) {
|
|
925
|
+
this.expandFanInMacros(config.fanIns, instances, connections, startPorts, exitPorts, macros, errors);
|
|
926
|
+
}
|
|
919
927
|
// Include ALL available nodeTypes in the workflow AST, plus imported npm types.
|
|
920
928
|
// Previously this filtered to only nodeTypes used by instances, but that caused
|
|
921
929
|
// a bug: when creating a new nodeType and then adding its first instance,
|
|
@@ -1502,6 +1510,98 @@ export class AnnotationParser {
|
|
|
1502
1510
|
});
|
|
1503
1511
|
}
|
|
1504
1512
|
}
|
|
1513
|
+
/**
|
|
1514
|
+
* Expand @fanOut macros into 1-to-N connections.
|
|
1515
|
+
*/
|
|
1516
|
+
expandFanOutMacros(fanOutConfigs, instances, connections, startPorts, exitPorts, macros, errors) {
|
|
1517
|
+
const instanceIds = new Set(instances.map(i => i.id));
|
|
1518
|
+
instanceIds.add('Start');
|
|
1519
|
+
instanceIds.add('Exit');
|
|
1520
|
+
for (const config of fanOutConfigs) {
|
|
1521
|
+
const { source, targets } = config;
|
|
1522
|
+
// Validate source node exists
|
|
1523
|
+
if (!instanceIds.has(source.node)) {
|
|
1524
|
+
errors.push(`@fanOut: source node "${source.node}" does not exist`);
|
|
1525
|
+
continue;
|
|
1526
|
+
}
|
|
1527
|
+
let valid = true;
|
|
1528
|
+
for (const target of targets) {
|
|
1529
|
+
if (!instanceIds.has(target.node)) {
|
|
1530
|
+
errors.push(`@fanOut: target node "${target.node}" does not exist`);
|
|
1531
|
+
valid = false;
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
if (!valid)
|
|
1535
|
+
continue;
|
|
1536
|
+
// Create connections
|
|
1537
|
+
for (const target of targets) {
|
|
1538
|
+
const targetPort = target.port ?? source.port;
|
|
1539
|
+
const conn = {
|
|
1540
|
+
type: 'Connection',
|
|
1541
|
+
from: { node: source.node, port: source.port },
|
|
1542
|
+
to: { node: target.node, port: targetPort },
|
|
1543
|
+
};
|
|
1544
|
+
// Deduplicate
|
|
1545
|
+
const exists = connections.some(c => c.from.node === conn.from.node && c.from.port === conn.from.port &&
|
|
1546
|
+
c.to.node === conn.to.node && c.to.port === conn.to.port);
|
|
1547
|
+
if (!exists) {
|
|
1548
|
+
connections.push(conn);
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
// Store macro for round-trip preservation
|
|
1552
|
+
macros.push({
|
|
1553
|
+
type: 'fanOut',
|
|
1554
|
+
source: { node: source.node, port: source.port },
|
|
1555
|
+
targets: targets.map(t => t.port ? { node: t.node, port: t.port } : { node: t.node }),
|
|
1556
|
+
});
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
/**
|
|
1560
|
+
* Expand @fanIn macros into N-to-1 connections.
|
|
1561
|
+
*/
|
|
1562
|
+
expandFanInMacros(fanInConfigs, instances, connections, startPorts, exitPorts, macros, errors) {
|
|
1563
|
+
const instanceIds = new Set(instances.map(i => i.id));
|
|
1564
|
+
instanceIds.add('Start');
|
|
1565
|
+
instanceIds.add('Exit');
|
|
1566
|
+
for (const config of fanInConfigs) {
|
|
1567
|
+
const { sources, target } = config;
|
|
1568
|
+
// Validate target node exists
|
|
1569
|
+
if (!instanceIds.has(target.node)) {
|
|
1570
|
+
errors.push(`@fanIn: target node "${target.node}" does not exist`);
|
|
1571
|
+
continue;
|
|
1572
|
+
}
|
|
1573
|
+
let valid = true;
|
|
1574
|
+
for (const source of sources) {
|
|
1575
|
+
if (!instanceIds.has(source.node)) {
|
|
1576
|
+
errors.push(`@fanIn: source node "${source.node}" does not exist`);
|
|
1577
|
+
valid = false;
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
if (!valid)
|
|
1581
|
+
continue;
|
|
1582
|
+
// Create connections
|
|
1583
|
+
for (const source of sources) {
|
|
1584
|
+
const sourcePort = source.port ?? target.port;
|
|
1585
|
+
const conn = {
|
|
1586
|
+
type: 'Connection',
|
|
1587
|
+
from: { node: source.node, port: sourcePort },
|
|
1588
|
+
to: { node: target.node, port: target.port },
|
|
1589
|
+
};
|
|
1590
|
+
// Deduplicate
|
|
1591
|
+
const exists = connections.some(c => c.from.node === conn.from.node && c.from.port === conn.from.port &&
|
|
1592
|
+
c.to.node === conn.to.node && c.to.port === conn.to.port);
|
|
1593
|
+
if (!exists) {
|
|
1594
|
+
connections.push(conn);
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
// Store macro for round-trip preservation
|
|
1598
|
+
macros.push({
|
|
1599
|
+
type: 'fanIn',
|
|
1600
|
+
sources: sources.map(s => s.port ? { node: s.node, port: s.port } : { node: s.node }),
|
|
1601
|
+
target: { node: target.node, port: target.port },
|
|
1602
|
+
});
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1505
1605
|
/**
|
|
1506
1606
|
* Generate automatic connections for @autoConnect workflows.
|
|
1507
1607
|
* Wires nodes in declaration order as a linear pipeline:
|
|
@@ -65,6 +65,8 @@ export class GeneratedExecutionContext {
|
|
|
65
65
|
portName: address.portName,
|
|
66
66
|
executionIndex: address.executionIndex,
|
|
67
67
|
key: 'default',
|
|
68
|
+
...(address.scope !== undefined && { scope: address.scope }),
|
|
69
|
+
...(address.side !== undefined && { side: address.side }),
|
|
68
70
|
},
|
|
69
71
|
value: actualValue,
|
|
70
72
|
});
|
package/dist/runtime/events.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type TStatusType = "RUNNING" | "SCHEDULED" | "SUCCEEDED" | "FAILED" | "CANCELLED" | "PENDING";
|
|
1
|
+
export type TStatusType = "RUNNING" | "SCHEDULED" | "SUCCEEDED" | "FAILED" | "CANCELLED" | "PENDING" | "WAITING_FOR_AGENT";
|
|
2
2
|
export type TVariableIdentification = {
|
|
3
3
|
nodeTypeName: string;
|
|
4
4
|
id: string;
|
package/dist/sugar-optimizer.js
CHANGED
|
@@ -69,10 +69,23 @@ export function validatePathMacro(path, connections, instances) {
|
|
|
69
69
|
* Non-path macros are passed through unchanged.
|
|
70
70
|
*/
|
|
71
71
|
export function filterStaleMacros(macros, connections, instances) {
|
|
72
|
+
const instanceIds = new Set(instances.map(i => i.id));
|
|
73
|
+
instanceIds.add('Start');
|
|
74
|
+
instanceIds.add('Exit');
|
|
72
75
|
return macros.filter(macro => {
|
|
73
|
-
if (macro.type
|
|
74
|
-
return
|
|
75
|
-
|
|
76
|
+
if (macro.type === 'path')
|
|
77
|
+
return validatePathMacro(macro, connections, instances);
|
|
78
|
+
if (macro.type === 'fanOut') {
|
|
79
|
+
if (!instanceIds.has(macro.source.node))
|
|
80
|
+
return false;
|
|
81
|
+
return macro.targets.every(t => instanceIds.has(t.node));
|
|
82
|
+
}
|
|
83
|
+
if (macro.type === 'fanIn') {
|
|
84
|
+
if (!instanceIds.has(macro.target.node))
|
|
85
|
+
return false;
|
|
86
|
+
return macro.sources.every(s => instanceIds.has(s.node));
|
|
87
|
+
}
|
|
88
|
+
return true;
|
|
76
89
|
});
|
|
77
90
|
}
|
|
78
91
|
/**
|
|
@@ -381,6 +394,18 @@ function buildExistingMacroCoverage(macros) {
|
|
|
381
394
|
covered.add(step.node);
|
|
382
395
|
}
|
|
383
396
|
}
|
|
397
|
+
else if (macro.type === 'fanOut') {
|
|
398
|
+
covered.add(macro.source.node);
|
|
399
|
+
for (const t of macro.targets) {
|
|
400
|
+
covered.add(t.node);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
else if (macro.type === 'fanIn') {
|
|
404
|
+
for (const s of macro.sources) {
|
|
405
|
+
covered.add(s.node);
|
|
406
|
+
}
|
|
407
|
+
covered.add(macro.target.node);
|
|
408
|
+
}
|
|
384
409
|
}
|
|
385
410
|
return covered;
|
|
386
411
|
}
|
package/dist/validator.d.ts
CHANGED
|
@@ -86,6 +86,14 @@ export declare class WorkflowValidator {
|
|
|
86
86
|
* from opposite branches (onSuccess vs onFailure) of the same branching node.
|
|
87
87
|
*/
|
|
88
88
|
private areMutuallyExclusive;
|
|
89
|
+
/**
|
|
90
|
+
* Validate inner graph topology for nodes that have scoped ports.
|
|
91
|
+
*
|
|
92
|
+
* For each scoped node instance, checks that:
|
|
93
|
+
* 1. Inner nodes (children) have their required inputs connected within the scope
|
|
94
|
+
* 2. Scoped input ports (callback returns) have connections from inner nodes
|
|
95
|
+
*/
|
|
96
|
+
private validateScopeTopology;
|
|
89
97
|
private normalizeTypeString;
|
|
90
98
|
}
|
|
91
99
|
export declare const validator: WorkflowValidator;
|
package/dist/validator.js
CHANGED
|
@@ -118,6 +118,7 @@ export class WorkflowValidator {
|
|
|
118
118
|
this.validateCycles(workflow);
|
|
119
119
|
this.validateMultipleInputConnections(workflow, instanceMap);
|
|
120
120
|
this.validateAnnotationSignatureConsistency(workflow);
|
|
121
|
+
this.validateScopeTopology(workflow, instanceMap);
|
|
121
122
|
// Deduplicate cascading errors: if a node has UNKNOWN_NODE_TYPE,
|
|
122
123
|
// suppress UNKNOWN_SOURCE_NODE, UNKNOWN_TARGET_NODE, and UNDEFINED_NODE
|
|
123
124
|
// that reference the same node IDs (they're just noise).
|
|
@@ -953,6 +954,97 @@ export class WorkflowValidator {
|
|
|
953
954
|
const branches = new Set(branchInfos.map((info) => info.branch));
|
|
954
955
|
return branches.size > 1;
|
|
955
956
|
}
|
|
957
|
+
/**
|
|
958
|
+
* Validate inner graph topology for nodes that have scoped ports.
|
|
959
|
+
*
|
|
960
|
+
* For each scoped node instance, checks that:
|
|
961
|
+
* 1. Inner nodes (children) have their required inputs connected within the scope
|
|
962
|
+
* 2. Scoped input ports (callback returns) have connections from inner nodes
|
|
963
|
+
*/
|
|
964
|
+
validateScopeTopology(workflow, instanceMap) {
|
|
965
|
+
// Find all instances that have scoped ports
|
|
966
|
+
for (const instance of workflow.instances) {
|
|
967
|
+
const nodeType = instanceMap.get(instance.id);
|
|
968
|
+
if (!nodeType)
|
|
969
|
+
continue;
|
|
970
|
+
// Collect scoped port pairs per scope name
|
|
971
|
+
const scopeNames = new Set();
|
|
972
|
+
for (const portDef of Object.values(nodeType.outputs)) {
|
|
973
|
+
if (portDef.scope)
|
|
974
|
+
scopeNames.add(portDef.scope);
|
|
975
|
+
}
|
|
976
|
+
for (const portDef of Object.values(nodeType.inputs)) {
|
|
977
|
+
if (portDef.scope)
|
|
978
|
+
scopeNames.add(portDef.scope);
|
|
979
|
+
}
|
|
980
|
+
if (scopeNames.size === 0)
|
|
981
|
+
continue;
|
|
982
|
+
for (const scopeName of scopeNames) {
|
|
983
|
+
// Find children in this scope
|
|
984
|
+
const childIds = [];
|
|
985
|
+
for (const child of workflow.instances) {
|
|
986
|
+
if (child.parent &&
|
|
987
|
+
child.parent.id === instance.id &&
|
|
988
|
+
child.parent.scope === scopeName) {
|
|
989
|
+
childIds.push(child.id);
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
if (childIds.length === 0)
|
|
993
|
+
continue;
|
|
994
|
+
// Collect scoped connections (connections with scope tags)
|
|
995
|
+
const scopedConnections = workflow.connections.filter((conn) => (conn.from.scope === scopeName && conn.from.node === instance.id) ||
|
|
996
|
+
(conn.to.scope === scopeName && conn.to.node === instance.id) ||
|
|
997
|
+
(conn.from.scope === scopeName && childIds.includes(conn.from.node)) ||
|
|
998
|
+
(conn.to.scope === scopeName && childIds.includes(conn.to.node)));
|
|
999
|
+
// Check: each child's required inputs must be satisfied within the scope
|
|
1000
|
+
for (const childId of childIds) {
|
|
1001
|
+
const childType = instanceMap.get(childId);
|
|
1002
|
+
if (!childType)
|
|
1003
|
+
continue;
|
|
1004
|
+
for (const [portName, portConfig] of Object.entries(childType.inputs)) {
|
|
1005
|
+
if (isExecutePort(portName))
|
|
1006
|
+
continue;
|
|
1007
|
+
if (portConfig.scope)
|
|
1008
|
+
continue; // Skip scoped ports on children
|
|
1009
|
+
if (portConfig.optional || portConfig.default !== undefined)
|
|
1010
|
+
continue;
|
|
1011
|
+
// Check instance expression overrides
|
|
1012
|
+
const childInstance = workflow.instances.find((i) => i.id === childId);
|
|
1013
|
+
const instancePortConfig = childInstance?.config?.portConfigs?.find((pc) => pc.portName === portName && (pc.direction == null || pc.direction === 'INPUT'));
|
|
1014
|
+
if (portConfig.expression || instancePortConfig?.expression !== undefined)
|
|
1015
|
+
continue;
|
|
1016
|
+
// Check if connected by any connection (scoped, inter-child, or outer)
|
|
1017
|
+
const isConnected = workflow.connections.some((conn) => conn.to.node === childId && conn.to.port === portName);
|
|
1018
|
+
if (!isConnected) {
|
|
1019
|
+
this.errors.push({
|
|
1020
|
+
type: 'error',
|
|
1021
|
+
code: 'SCOPE_MISSING_REQUIRED_INPUT',
|
|
1022
|
+
message: `Scoped child "${childId}" has unconnected required input "${portName}" within scope "${scopeName}" of "${instance.id}".`,
|
|
1023
|
+
node: childId,
|
|
1024
|
+
location: childInstance?.sourceLocation,
|
|
1025
|
+
});
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
// Check: scoped INPUT ports should have connections from inner nodes
|
|
1030
|
+
const scopedInputPorts = Object.entries(nodeType.inputs).filter(([_, portDef]) => portDef.scope === scopeName);
|
|
1031
|
+
for (const [portName] of scopedInputPorts) {
|
|
1032
|
+
const hasConnection = scopedConnections.some((conn) => conn.to.node === instance.id &&
|
|
1033
|
+
conn.to.port === portName &&
|
|
1034
|
+
conn.to.scope === scopeName);
|
|
1035
|
+
if (!hasConnection) {
|
|
1036
|
+
this.warnings.push({
|
|
1037
|
+
type: 'warning',
|
|
1038
|
+
code: 'SCOPE_UNUSED_INPUT',
|
|
1039
|
+
message: `Scoped input port "${portName}" of "${instance.id}" (scope "${scopeName}") has no connection from inner nodes. Data will not flow back from the scope.`,
|
|
1040
|
+
node: instance.id,
|
|
1041
|
+
location: this.getInstanceLocation(workflow, instance.id),
|
|
1042
|
+
});
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
956
1048
|
normalizeTypeString(type) {
|
|
957
1049
|
let n = type;
|
|
958
1050
|
// Remove all whitespace
|