@synergenius/flow-weaver 0.5.0 → 0.6.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/LICENSE +3 -2
- package/README.md +4 -3
- package/dist/annotation-generator.js +25 -2
- package/dist/ast/types.d.ts +20 -2
- package/dist/built-in-nodes/coercion-types.d.ts +15 -0
- package/dist/built-in-nodes/coercion-types.js +61 -0
- package/dist/chevrotain-parser/coerce-parser.d.ts +33 -0
- package/dist/chevrotain-parser/coerce-parser.js +103 -0
- package/dist/chevrotain-parser/index.d.ts +3 -1
- package/dist/chevrotain-parser/index.js +3 -1
- package/dist/chevrotain-parser/port-parser.js +2 -1
- package/dist/chevrotain-parser/tokens.d.ts +2 -0
- package/dist/chevrotain-parser/tokens.js +10 -0
- package/dist/cli/commands/run.d.ts +2 -0
- package/dist/cli/commands/run.js +30 -1
- package/dist/cli/flow-weaver.mjs +662 -50
- package/dist/cli/index.js +1 -0
- package/dist/doc-metadata/extractors/annotations.js +17 -0
- package/dist/doc-metadata/extractors/error-codes.js +50 -0
- package/dist/friendly-errors.js +131 -5
- package/dist/generator/inngest.js +27 -0
- package/dist/generator/unified.d.ts +7 -2
- package/dist/generator/unified.js +31 -3
- package/dist/jsdoc-parser.d.ts +14 -0
- package/dist/jsdoc-parser.js +19 -1
- package/dist/mcp/workflow-executor.d.ts +1 -0
- package/dist/mcp/workflow-executor.js +4 -2
- package/dist/parser.d.ts +4 -0
- package/dist/parser.js +69 -0
- package/dist/validator.d.ts +5 -0
- package/dist/validator.js +188 -14
- package/docs/reference/advanced-annotations.md +71 -2
- package/docs/reference/error-codes.md +7 -0
- package/package.json +1 -1
package/dist/validator.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { RESERVED_NODE_NAMES, isStartNode, isExitNode, isExecutePort, isReservedNodeName, } from './constants.js';
|
|
2
2
|
import { findClosestMatches } from './utils/string-distance.js';
|
|
3
3
|
import { parseFunctionSignature } from './jsdoc-port-sync/signature-parser.js';
|
|
4
|
+
import { checkTypeCompatibilityFromStrings } from './type-checker.js';
|
|
4
5
|
export class WorkflowValidator {
|
|
5
6
|
errors = [];
|
|
6
7
|
warnings = [];
|
|
@@ -383,12 +384,14 @@ export class WorkflowValidator {
|
|
|
383
384
|
}
|
|
384
385
|
const sourceType = sourcePortDef.dataType;
|
|
385
386
|
const targetType = targetPortDef.dataType;
|
|
387
|
+
const sourceTsType = sourcePortDef.tsType;
|
|
388
|
+
const targetTsType = targetPortDef.tsType;
|
|
386
389
|
// Validate STEP port connections - STEP must connect to STEP only
|
|
387
390
|
if (sourceType === 'STEP' && targetType !== 'STEP') {
|
|
388
391
|
this.errors.push({
|
|
389
392
|
type: 'error',
|
|
390
393
|
code: 'STEP_PORT_TYPE_MISMATCH',
|
|
391
|
-
message: `STEP port "${fromPort}" on node "${fromNode}" cannot connect to non-STEP port "${toPort}" (${targetType}) on node "${toNode}"`,
|
|
394
|
+
message: `STEP port "${fromPort}" on node "${fromNode}" cannot connect to non-STEP port "${toPort}" (${this.formatType(targetType, targetTsType)}) on node "${toNode}"`,
|
|
392
395
|
connection: conn,
|
|
393
396
|
location: connLocation,
|
|
394
397
|
});
|
|
@@ -398,7 +401,7 @@ export class WorkflowValidator {
|
|
|
398
401
|
this.errors.push({
|
|
399
402
|
type: 'error',
|
|
400
403
|
code: 'STEP_PORT_TYPE_MISMATCH',
|
|
401
|
-
message: `Non-STEP port "${fromPort}" (${sourceType}) on node "${fromNode}" cannot connect to STEP port "${toPort}" on node "${toNode}"`,
|
|
404
|
+
message: `Non-STEP port "${fromPort}" (${this.formatType(sourceType, sourceTsType)}) on node "${fromNode}" cannot connect to STEP port "${toPort}" on node "${toNode}"`,
|
|
402
405
|
connection: conn,
|
|
403
406
|
location: connLocation,
|
|
404
407
|
});
|
|
@@ -412,15 +415,20 @@ export class WorkflowValidator {
|
|
|
412
415
|
if (sourceType === targetType) {
|
|
413
416
|
// For OBJECT types, check if tsType differs (structural mismatch)
|
|
414
417
|
if (sourceType === 'OBJECT' && sourcePortDef.tsType && targetPortDef.tsType) {
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
418
|
+
const normalizedSource = this.normalizeTypeString(sourcePortDef.tsType);
|
|
419
|
+
const normalizedTarget = this.normalizeTypeString(targetPortDef.tsType);
|
|
420
|
+
if (normalizedSource !== normalizedTarget) {
|
|
421
|
+
// Use string-based compatibility check to suppress false positives (e.g. when one side is 'any')
|
|
422
|
+
const compat = checkTypeCompatibilityFromStrings(sourcePortDef.tsType, targetPortDef.tsType);
|
|
423
|
+
if (!compat.isCompatible) {
|
|
424
|
+
this.warnings.push({
|
|
425
|
+
type: 'warning',
|
|
426
|
+
code: 'OBJECT_TYPE_MISMATCH',
|
|
427
|
+
message: `Structural type mismatch: ${fromNode}.${fromPort} outputs "${sourcePortDef.tsType}" but ${toNode}.${toPort} expects "${targetPortDef.tsType}". Verify the object shapes are compatible.`,
|
|
428
|
+
connection: conn,
|
|
429
|
+
location: connLocation,
|
|
430
|
+
});
|
|
431
|
+
}
|
|
424
432
|
}
|
|
425
433
|
}
|
|
426
434
|
return;
|
|
@@ -451,7 +459,7 @@ export class WorkflowValidator {
|
|
|
451
459
|
pushTypeIssue({
|
|
452
460
|
type: 'warning',
|
|
453
461
|
code: 'LOSSY_TYPE_COERCION',
|
|
454
|
-
message: `Lossy type coercion from ${sourceType} to ${targetType} in connection ${fromNode}.${fromPort} → ${toNode}.${toPort}. ${reason}. Add @strictTypes to your workflow annotation to enforce type safety.`,
|
|
462
|
+
message: `Lossy type coercion from ${this.formatType(sourceType, sourceTsType)} to ${this.formatType(targetType, targetTsType)} in connection ${fromNode}.${fromPort} → ${toNode}.${toPort}. ${reason}. Add @strictTypes to your workflow annotation to enforce type safety.`,
|
|
455
463
|
connection: conn,
|
|
456
464
|
location: connLocation,
|
|
457
465
|
}, true);
|
|
@@ -474,7 +482,7 @@ export class WorkflowValidator {
|
|
|
474
482
|
pushTypeIssue({
|
|
475
483
|
type: 'warning',
|
|
476
484
|
code: 'UNUSUAL_TYPE_COERCION',
|
|
477
|
-
message: `Unusual type coercion from ${sourceType} to ${targetType} in connection ${fromNode}.${fromPort} → ${toNode}.${toPort}. ${reason}.`,
|
|
485
|
+
message: `Unusual type coercion from ${this.formatType(sourceType, sourceTsType)} to ${this.formatType(targetType, targetTsType)} in connection ${fromNode}.${fromPort} → ${toNode}.${toPort}. ${reason}.`,
|
|
478
486
|
connection: conn,
|
|
479
487
|
location: connLocation,
|
|
480
488
|
}, true);
|
|
@@ -485,7 +493,7 @@ export class WorkflowValidator {
|
|
|
485
493
|
pushTypeIssue({
|
|
486
494
|
type: 'warning',
|
|
487
495
|
code: 'TYPE_MISMATCH',
|
|
488
|
-
message: `Type mismatch in connection ${fromNode}.${fromPort} (${sourceType}) → ${toNode}.${toPort} (${targetType}). Runtime coercion will be attempted.`,
|
|
496
|
+
message: `Type mismatch in connection ${fromNode}.${fromPort} (${this.formatType(sourceType, sourceTsType)}) → ${toNode}.${toPort} (${this.formatType(targetType, targetTsType)}). Runtime coercion will be attempted.`,
|
|
489
497
|
connection: conn,
|
|
490
498
|
location: connLocation,
|
|
491
499
|
}, true);
|
|
@@ -979,6 +987,31 @@ export class WorkflowValidator {
|
|
|
979
987
|
}
|
|
980
988
|
if (scopeNames.size === 0)
|
|
981
989
|
continue;
|
|
990
|
+
// Check: connections with scope qualifiers must reference valid scope names on this instance
|
|
991
|
+
for (const conn of workflow.connections) {
|
|
992
|
+
if (conn.from.scope && conn.from.node === instance.id) {
|
|
993
|
+
if (!scopeNames.has(conn.from.scope)) {
|
|
994
|
+
this.errors.push({
|
|
995
|
+
type: 'error',
|
|
996
|
+
code: 'SCOPE_WRONG_SCOPE_NAME',
|
|
997
|
+
message: `Connection from "${instance.id}.${conn.from.port}" uses scope qualifier ":${conn.from.scope}" but node "${instance.id}" does not define scope "${conn.from.scope}". Available scopes: ${[...scopeNames].join(', ')}.`,
|
|
998
|
+
connection: conn,
|
|
999
|
+
location: this.getConnectionLocation(conn),
|
|
1000
|
+
});
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
if (conn.to.scope && conn.to.node === instance.id) {
|
|
1004
|
+
if (!scopeNames.has(conn.to.scope)) {
|
|
1005
|
+
this.errors.push({
|
|
1006
|
+
type: 'error',
|
|
1007
|
+
code: 'SCOPE_WRONG_SCOPE_NAME',
|
|
1008
|
+
message: `Connection to "${instance.id}.${conn.to.port}" uses scope qualifier ":${conn.to.scope}" but node "${instance.id}" does not define scope "${conn.to.scope}". Available scopes: ${[...scopeNames].join(', ')}.`,
|
|
1009
|
+
connection: conn,
|
|
1010
|
+
location: this.getConnectionLocation(conn),
|
|
1011
|
+
});
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
982
1015
|
for (const scopeName of scopeNames) {
|
|
983
1016
|
// Find children in this scope
|
|
984
1017
|
const childIds = [];
|
|
@@ -996,6 +1029,119 @@ export class WorkflowValidator {
|
|
|
996
1029
|
(conn.to.scope === scopeName && conn.to.node === instance.id) ||
|
|
997
1030
|
(conn.from.scope === scopeName && childIds.includes(conn.from.node)) ||
|
|
998
1031
|
(conn.to.scope === scopeName && childIds.includes(conn.to.node)));
|
|
1032
|
+
// Check: scoped connections reference actual scoped ports on the parent
|
|
1033
|
+
for (const conn of scopedConnections) {
|
|
1034
|
+
if (conn.from.node === instance.id && conn.from.scope === scopeName) {
|
|
1035
|
+
const portDef = nodeType.outputs[conn.from.port];
|
|
1036
|
+
if (!portDef) {
|
|
1037
|
+
const availablePorts = Object.entries(nodeType.outputs)
|
|
1038
|
+
.filter(([, p]) => p.scope === scopeName)
|
|
1039
|
+
.map(([n]) => n);
|
|
1040
|
+
this.errors.push({
|
|
1041
|
+
type: 'error',
|
|
1042
|
+
code: 'SCOPE_UNKNOWN_PORT',
|
|
1043
|
+
message: `Scoped connection references non-existent output port "${conn.from.port}" on "${instance.id}" in scope "${scopeName}". Available scoped outputs: ${availablePorts.join(', ') || 'none'}.`,
|
|
1044
|
+
connection: conn,
|
|
1045
|
+
location: this.getConnectionLocation(conn),
|
|
1046
|
+
});
|
|
1047
|
+
}
|
|
1048
|
+
else if (portDef.scope !== scopeName) {
|
|
1049
|
+
this.errors.push({
|
|
1050
|
+
type: 'error',
|
|
1051
|
+
code: 'SCOPE_UNKNOWN_PORT',
|
|
1052
|
+
message: `Output port "${conn.from.port}" on "${instance.id}" is not a scoped port of scope "${scopeName}"${portDef.scope ? ` (it belongs to scope "${portDef.scope}")` : ' (it is an unscoped port)'}.`,
|
|
1053
|
+
connection: conn,
|
|
1054
|
+
location: this.getConnectionLocation(conn),
|
|
1055
|
+
});
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
if (conn.to.node === instance.id && conn.to.scope === scopeName) {
|
|
1059
|
+
const portDef = nodeType.inputs[conn.to.port];
|
|
1060
|
+
if (!portDef) {
|
|
1061
|
+
const availablePorts = Object.entries(nodeType.inputs)
|
|
1062
|
+
.filter(([, p]) => p.scope === scopeName)
|
|
1063
|
+
.map(([n]) => n);
|
|
1064
|
+
this.errors.push({
|
|
1065
|
+
type: 'error',
|
|
1066
|
+
code: 'SCOPE_UNKNOWN_PORT',
|
|
1067
|
+
message: `Scoped connection references non-existent input port "${conn.to.port}" on "${instance.id}" in scope "${scopeName}". Available scoped inputs: ${availablePorts.join(', ') || 'none'}.`,
|
|
1068
|
+
connection: conn,
|
|
1069
|
+
location: this.getConnectionLocation(conn),
|
|
1070
|
+
});
|
|
1071
|
+
}
|
|
1072
|
+
else if (portDef.scope !== scopeName) {
|
|
1073
|
+
this.errors.push({
|
|
1074
|
+
type: 'error',
|
|
1075
|
+
code: 'SCOPE_UNKNOWN_PORT',
|
|
1076
|
+
message: `Input port "${conn.to.port}" on "${instance.id}" is not a scoped port of scope "${scopeName}"${portDef.scope ? ` (it belongs to scope "${portDef.scope}")` : ' (it is an unscoped port)'}.`,
|
|
1077
|
+
connection: conn,
|
|
1078
|
+
location: this.getConnectionLocation(conn),
|
|
1079
|
+
});
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
// Check: scoped connections must stay within the scope boundary
|
|
1084
|
+
for (const conn of scopedConnections) {
|
|
1085
|
+
if (conn.from.scope === scopeName && childIds.includes(conn.from.node)) {
|
|
1086
|
+
if (conn.to.node !== instance.id && !childIds.includes(conn.to.node)) {
|
|
1087
|
+
this.errors.push({
|
|
1088
|
+
type: 'error',
|
|
1089
|
+
code: 'SCOPE_CONNECTION_OUTSIDE',
|
|
1090
|
+
message: `Scoped connection from "${conn.from.node}.${conn.from.port}" targets "${conn.to.node}" which is not inside scope "${scopeName}" of "${instance.id}".`,
|
|
1091
|
+
connection: conn,
|
|
1092
|
+
location: this.getConnectionLocation(conn),
|
|
1093
|
+
});
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
if (conn.to.scope === scopeName && childIds.includes(conn.to.node)) {
|
|
1097
|
+
if (conn.from.node !== instance.id && !childIds.includes(conn.from.node)) {
|
|
1098
|
+
this.errors.push({
|
|
1099
|
+
type: 'error',
|
|
1100
|
+
code: 'SCOPE_CONNECTION_OUTSIDE',
|
|
1101
|
+
message: `Scoped connection to "${conn.to.node}.${conn.to.port}" sources from "${conn.from.node}" which is not inside scope "${scopeName}" of "${instance.id}".`,
|
|
1102
|
+
connection: conn,
|
|
1103
|
+
location: this.getConnectionLocation(conn),
|
|
1104
|
+
});
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
// Check: type compatibility for scoped connections between parent and children
|
|
1109
|
+
for (const conn of scopedConnections) {
|
|
1110
|
+
// Parent scoped output -> child input
|
|
1111
|
+
if (conn.from.node === instance.id && conn.from.scope === scopeName) {
|
|
1112
|
+
const parentPort = nodeType.outputs[conn.from.port];
|
|
1113
|
+
const childType = instanceMap.get(conn.to.node);
|
|
1114
|
+
const childPort = childType?.inputs[conn.to.port];
|
|
1115
|
+
if (parentPort && childPort && parentPort.dataType !== 'STEP' && childPort.dataType !== 'STEP') {
|
|
1116
|
+
if (parentPort.dataType !== childPort.dataType && parentPort.dataType !== 'ANY' && childPort.dataType !== 'ANY') {
|
|
1117
|
+
this.warnings.push({
|
|
1118
|
+
type: 'warning',
|
|
1119
|
+
code: 'SCOPE_PORT_TYPE_MISMATCH',
|
|
1120
|
+
message: `Type mismatch in scope "${scopeName}": "${instance.id}.${conn.from.port}" outputs ${this.formatType(parentPort.dataType, parentPort.tsType)} but "${conn.to.node}.${conn.to.port}" expects ${this.formatType(childPort.dataType, childPort.tsType)}.`,
|
|
1121
|
+
connection: conn,
|
|
1122
|
+
location: this.getConnectionLocation(conn),
|
|
1123
|
+
});
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
// Child output -> parent scoped input
|
|
1128
|
+
if (conn.to.node === instance.id && conn.to.scope === scopeName) {
|
|
1129
|
+
const parentPort = nodeType.inputs[conn.to.port];
|
|
1130
|
+
const childType = instanceMap.get(conn.from.node);
|
|
1131
|
+
const childPort = childType?.outputs[conn.from.port];
|
|
1132
|
+
if (parentPort && childPort && parentPort.dataType !== 'STEP' && childPort.dataType !== 'STEP') {
|
|
1133
|
+
if (parentPort.dataType !== childPort.dataType && parentPort.dataType !== 'ANY' && childPort.dataType !== 'ANY') {
|
|
1134
|
+
this.warnings.push({
|
|
1135
|
+
type: 'warning',
|
|
1136
|
+
code: 'SCOPE_PORT_TYPE_MISMATCH',
|
|
1137
|
+
message: `Type mismatch in scope "${scopeName}": "${conn.from.node}.${conn.from.port}" outputs ${this.formatType(childPort.dataType, childPort.tsType)} but "${instance.id}.${conn.to.port}" expects ${this.formatType(parentPort.dataType, parentPort.tsType)}.`,
|
|
1138
|
+
connection: conn,
|
|
1139
|
+
location: this.getConnectionLocation(conn),
|
|
1140
|
+
});
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
999
1145
|
// Check: each child's required inputs must be satisfied within the scope
|
|
1000
1146
|
for (const childId of childIds) {
|
|
1001
1147
|
const childType = instanceMap.get(childId);
|
|
@@ -1042,9 +1188,37 @@ export class WorkflowValidator {
|
|
|
1042
1188
|
});
|
|
1043
1189
|
}
|
|
1044
1190
|
}
|
|
1191
|
+
// Check: each child should have at least one scoped connection to/from the parent
|
|
1192
|
+
for (const childId of childIds) {
|
|
1193
|
+
const hasConnectionFromParent = scopedConnections.some((conn) => conn.from.node === instance.id &&
|
|
1194
|
+
conn.from.scope === scopeName &&
|
|
1195
|
+
conn.to.node === childId);
|
|
1196
|
+
const hasConnectionToParent = scopedConnections.some((conn) => conn.to.node === instance.id &&
|
|
1197
|
+
conn.to.scope === scopeName &&
|
|
1198
|
+
conn.from.node === childId);
|
|
1199
|
+
if (!hasConnectionFromParent && !hasConnectionToParent) {
|
|
1200
|
+
this.warnings.push({
|
|
1201
|
+
type: 'warning',
|
|
1202
|
+
code: 'SCOPE_ORPHANED_CHILD',
|
|
1203
|
+
message: `Child node "${childId}" is declared inside scope "${scopeName}" of "${instance.id}" but has no scoped connections to or from the parent. It is disconnected from the scope's data flow.`,
|
|
1204
|
+
node: childId,
|
|
1205
|
+
location: this.getInstanceLocation(workflow, childId),
|
|
1206
|
+
});
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1045
1209
|
}
|
|
1046
1210
|
}
|
|
1047
1211
|
}
|
|
1212
|
+
/**
|
|
1213
|
+
* Format a type for display in error messages.
|
|
1214
|
+
* Prefers the structural TypeScript type when available, falling back to the enum name.
|
|
1215
|
+
*/
|
|
1216
|
+
formatType(dataType, tsType) {
|
|
1217
|
+
if (tsType) {
|
|
1218
|
+
return `${tsType} (${dataType})`;
|
|
1219
|
+
}
|
|
1220
|
+
return dataType;
|
|
1221
|
+
}
|
|
1048
1222
|
normalizeTypeString(type) {
|
|
1049
1223
|
let n = type;
|
|
1050
1224
|
// Remove all whitespace
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: Advanced Annotations
|
|
3
|
-
description: Pull execution, execution strategies, merge strategies, auto-connect, strict types, path
|
|
4
|
-
keywords: [pullExecution, executeWhen, mergeStrategy, autoConnect, strictTypes, path, map, sugar, attributes, expr, portOrder, portLabel, minimized, multi-workflow, CONJUNCTION, DISJUNCTION, FIRST, LAST, COLLECT, MERGE, CONCAT]
|
|
3
|
+
description: Pull execution, execution strategies, merge strategies, auto-connect, strict types, path, map, fan-out, fan-in, node attributes, and multi-workflow files
|
|
4
|
+
keywords: [pullExecution, executeWhen, mergeStrategy, autoConnect, strictTypes, path, map, fanOut, fanIn, sugar, attributes, expr, portOrder, portLabel, minimized, multi-workflow, CONJUNCTION, DISJUNCTION, FIRST, LAST, COLLECT, MERGE, CONCAT]
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Advanced Annotations
|
|
@@ -178,6 +178,75 @@ This is equivalent to manually writing:
|
|
|
178
178
|
|
|
179
179
|
---
|
|
180
180
|
|
|
181
|
+
## Fan-Out / Fan-In (`@fanOut`, `@fanIn`)
|
|
182
|
+
|
|
183
|
+
Fan macros reduce boilerplate when broadcasting a single output to many targets, or merging many sources into a single input. Both expand to individual `@connect` lines during compilation.
|
|
184
|
+
|
|
185
|
+
### `@fanOut` — One to Many
|
|
186
|
+
|
|
187
|
+
Broadcasts a single output port to multiple targets:
|
|
188
|
+
|
|
189
|
+
```typescript
|
|
190
|
+
/**
|
|
191
|
+
* @flowWeaver workflow
|
|
192
|
+
* @node a processA
|
|
193
|
+
* @node b processB
|
|
194
|
+
* @node c processC
|
|
195
|
+
*
|
|
196
|
+
* @fanOut Start.data -> a, b, c
|
|
197
|
+
* @connect a.result -> Exit.resultA
|
|
198
|
+
* @connect b.result -> Exit.resultB
|
|
199
|
+
* @connect c.result -> Exit.resultC
|
|
200
|
+
*/
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
This expands to:
|
|
204
|
+
```
|
|
205
|
+
@connect Start.data -> a.data
|
|
206
|
+
@connect Start.data -> b.data
|
|
207
|
+
@connect Start.data -> c.data
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
You can specify explicit target ports when the names don't match the source:
|
|
211
|
+
|
|
212
|
+
```
|
|
213
|
+
@fanOut Start.data -> a.input1, b.input2, c.rawData
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Without an explicit port, the target port defaults to the source port name.
|
|
217
|
+
|
|
218
|
+
### `@fanIn` — Many to One
|
|
219
|
+
|
|
220
|
+
Merges multiple output ports into a single target:
|
|
221
|
+
|
|
222
|
+
```typescript
|
|
223
|
+
/**
|
|
224
|
+
* @flowWeaver workflow
|
|
225
|
+
* @node a processA
|
|
226
|
+
* @node b processB
|
|
227
|
+
* @node c processC
|
|
228
|
+
* @node agg aggregate
|
|
229
|
+
*
|
|
230
|
+
* @fanIn a.result, b.result, c.result -> agg.items
|
|
231
|
+
* @connect agg.merged -> Exit.result
|
|
232
|
+
*/
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
This expands to:
|
|
236
|
+
```
|
|
237
|
+
@connect a.result -> agg.items
|
|
238
|
+
@connect b.result -> agg.items
|
|
239
|
+
@connect c.result -> agg.items
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
The target port should have a `[mergeStrategy:COLLECT]` (or another merge strategy) to combine multiple inputs — otherwise the validator will flag `MULTIPLE_CONNECTIONS_TO_INPUT`.
|
|
243
|
+
|
|
244
|
+
### Round-Trip Preservation
|
|
245
|
+
|
|
246
|
+
Both macros are preserved through parse-regenerate round-trips. The compiler stores the original macro and regenerates the annotation rather than expanding to individual `@connect` lines.
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
|
|
181
250
|
## Strict Types (`@strictTypes`)
|
|
182
251
|
|
|
183
252
|
By default, type mismatches between connected ports produce warnings. With `@strictTypes`, they become errors.
|
|
@@ -657,6 +657,10 @@ These codes apply to AI agent workflows that use LLM, tool-executor, and memory
|
|
|
657
657
|
| MISSING_REQUIRED_INPUT | Required input has no connection/default/expression |
|
|
658
658
|
| CYCLE_DETECTED | Graph contains a loop |
|
|
659
659
|
| INVALID_EXIT_PORT_TYPE | Exit onSuccess/onFailure is not STEP type |
|
|
660
|
+
| SCOPE_MISSING_REQUIRED_INPUT | Required input port on a scoped child has no connection |
|
|
661
|
+
| SCOPE_WRONG_SCOPE_NAME | Connection uses a scope name not defined on the node |
|
|
662
|
+
| SCOPE_CONNECTION_OUTSIDE | Scoped connection references a node outside the scope |
|
|
663
|
+
| SCOPE_UNKNOWN_PORT | Connection references a port that is not a scoped port of the specified scope |
|
|
660
664
|
| AGENT_LLM_MISSING_ERROR_HANDLER | LLM node's onFailure port is unconnected |
|
|
661
665
|
<!-- AUTO:END error_summary_table -->
|
|
662
666
|
|
|
@@ -679,6 +683,9 @@ These codes apply to AI agent workflows that use LLM, tool-executor, and memory
|
|
|
679
683
|
| UNUSED_OUTPUT_PORT | Output port data is discarded |
|
|
680
684
|
| UNREACHABLE_EXIT_PORT | Exit port has no incoming connection |
|
|
681
685
|
| MULTIPLE_EXIT_CONNECTIONS | Exit port has multiple sources |
|
|
686
|
+
| SCOPE_UNUSED_INPUT | Scoped input port has no connection from inner nodes |
|
|
687
|
+
| SCOPE_PORT_TYPE_MISMATCH | Type mismatch between scoped port and connected child port |
|
|
688
|
+
| SCOPE_ORPHANED_CHILD | Child node in scope has no scoped connections to parent |
|
|
682
689
|
| AGENT_UNGUARDED_TOOL_EXECUTOR | Tool executor has no upstream human-approval gate |
|
|
683
690
|
| AGENT_MISSING_MEMORY_IN_LOOP | Loop has LLM but no conversation memory node |
|
|
684
691
|
| AGENT_LLM_NO_FALLBACK | LLM onFailure routes directly to Exit |
|
package/package.json
CHANGED