@synergenius/flow-weaver 0.10.1 → 0.10.3
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 +7 -3
- package/dist/ast/types.d.ts +2 -0
- package/dist/chevrotain-parser/connect-parser.d.ts +5 -0
- package/dist/chevrotain-parser/connect-parser.js +24 -4
- package/dist/cli/flow-weaver.mjs +335 -25
- package/dist/diagram/html-viewer.js +31 -6
- package/dist/diagram/renderer.js +3 -3
- package/dist/friendly-errors.js +35 -0
- package/dist/generator/code-utils.d.ts +12 -1
- package/dist/generator/code-utils.js +88 -3
- package/dist/jsdoc-parser.js +2 -1
- package/dist/validator.js +56 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
# @synergenius/flow-weaver
|
|
2
2
|
|
|
3
|
-
[](https://www.npmjs.com/package/@synergenius/flow-weaver)
|
|
4
|
+
[](https://www.npmjs.com/package/@synergenius/flow-weaver)
|
|
5
|
+
[](https://github.com/synergenius-fw/flow-weaver/actions/workflows/ci.yml)
|
|
6
|
+
[](https://github.com/synergenius-fw/flow-weaver/actions/workflows/ci.yml)
|
|
7
|
+
[](https://codecov.io/gh/synergenius-fw/flow-weaver)
|
|
8
|
+
[](./LICENSE)
|
|
9
|
+
[](https://nodejs.org)
|
|
6
10
|
|
|
7
11
|
**Build AI agent workflows visually. Ship them as your own code.**
|
|
8
12
|
|
package/dist/ast/types.d.ts
CHANGED
|
@@ -459,6 +459,8 @@ export type TConnectionAST = {
|
|
|
459
459
|
metadata?: TConnectionMetadata;
|
|
460
460
|
/** Pre-computed type compatibility (set during parsing when ts-morph Types are available) */
|
|
461
461
|
typeCompatibility?: TTypeCompatibility;
|
|
462
|
+
/** Explicit type coercion for this connection (e.g., 'number' wraps value with Number()) */
|
|
463
|
+
coerce?: TCoerceTargetType;
|
|
462
464
|
};
|
|
463
465
|
/**
|
|
464
466
|
* Port Reference - Identifies a specific port on a node.
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Parser for @connect declarations using Chevrotain.
|
|
5
5
|
*/
|
|
6
|
+
import type { TCoerceTargetType } from '../ast/types.js';
|
|
6
7
|
export interface PortReference {
|
|
7
8
|
nodeId: string;
|
|
8
9
|
portName: string;
|
|
@@ -11,6 +12,10 @@ export interface PortReference {
|
|
|
11
12
|
export interface ConnectParseResult {
|
|
12
13
|
source: PortReference;
|
|
13
14
|
target: PortReference;
|
|
15
|
+
/** Explicit type coercion (from `as <type>` suffix) */
|
|
16
|
+
coerce?: TCoerceTargetType;
|
|
17
|
+
/** Set when `as <type>` uses an unrecognized type (cleared after warning is emitted) */
|
|
18
|
+
invalidCoerceType?: string;
|
|
14
19
|
}
|
|
15
20
|
/**
|
|
16
21
|
* Parse a @connect line and return structured result.
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Parser for @connect declarations using Chevrotain.
|
|
5
5
|
*/
|
|
6
6
|
import { CstParser } from 'chevrotain';
|
|
7
|
-
import { JSDocLexer, ConnectTag, Identifier, Arrow, Dot, Colon, allTokens } from './tokens.js';
|
|
7
|
+
import { JSDocLexer, ConnectTag, Identifier, Arrow, Dot, Colon, AsKeyword, allTokens } from './tokens.js';
|
|
8
8
|
// =============================================================================
|
|
9
9
|
// Parser Definition
|
|
10
10
|
// =============================================================================
|
|
@@ -13,12 +13,16 @@ class ConnectParser extends CstParser {
|
|
|
13
13
|
super(allTokens);
|
|
14
14
|
this.performSelfAnalysis();
|
|
15
15
|
}
|
|
16
|
-
// Entry rule for connect line
|
|
16
|
+
// Entry rule for connect line: @connect A.port -> B.port [as type]
|
|
17
17
|
connectLine = this.RULE('connectLine', () => {
|
|
18
18
|
this.CONSUME(ConnectTag);
|
|
19
19
|
this.SUBRULE(this.portRef, { LABEL: 'sourceRef' });
|
|
20
20
|
this.CONSUME(Arrow);
|
|
21
21
|
this.SUBRULE2(this.portRef, { LABEL: 'targetRef' });
|
|
22
|
+
this.OPTION(() => {
|
|
23
|
+
this.CONSUME(AsKeyword);
|
|
24
|
+
this.CONSUME(Identifier, { LABEL: 'coerceType' });
|
|
25
|
+
});
|
|
22
26
|
});
|
|
23
27
|
// node.port or node.port:scope
|
|
24
28
|
portRef = this.RULE('portRef', () => {
|
|
@@ -47,7 +51,18 @@ class ConnectVisitor extends BaseVisitor {
|
|
|
47
51
|
connectLine(ctx) {
|
|
48
52
|
const source = this.visit(ctx.sourceRef);
|
|
49
53
|
const target = this.visit(ctx.targetRef);
|
|
50
|
-
|
|
54
|
+
const result = { source, target };
|
|
55
|
+
if (ctx.coerceType?.[0]) {
|
|
56
|
+
const validTypes = new Set(['string', 'number', 'boolean', 'json', 'object']);
|
|
57
|
+
const raw = ctx.coerceType[0].image;
|
|
58
|
+
if (validTypes.has(raw)) {
|
|
59
|
+
result.coerce = raw;
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
result.invalidCoerceType = raw;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return result;
|
|
51
66
|
}
|
|
52
67
|
portRef(ctx) {
|
|
53
68
|
const nodeId = ctx.nodeId[0].image;
|
|
@@ -87,7 +102,12 @@ export function parseConnectLine(input, warnings) {
|
|
|
87
102
|
` Expected format: @connect sourceNode.port -> targetNode.port`);
|
|
88
103
|
return null;
|
|
89
104
|
}
|
|
90
|
-
|
|
105
|
+
const result = visitorInstance.visit(cst);
|
|
106
|
+
if (result.invalidCoerceType) {
|
|
107
|
+
warnings.push(`Invalid coerce type "${result.invalidCoerceType}" in @connect. Valid types: string, number, boolean, json, object`);
|
|
108
|
+
delete result.invalidCoerceType;
|
|
109
|
+
}
|
|
110
|
+
return result;
|
|
91
111
|
}
|
|
92
112
|
/**
|
|
93
113
|
* Get serialized grammar for documentation/diagram generation.
|
package/dist/cli/flow-weaver.mjs
CHANGED
|
@@ -12263,7 +12263,21 @@ var init_theme = __esm({
|
|
|
12263
12263
|
});
|
|
12264
12264
|
|
|
12265
12265
|
// src/validator.ts
|
|
12266
|
-
|
|
12266
|
+
function suggestCoerceType(targetType) {
|
|
12267
|
+
switch (targetType) {
|
|
12268
|
+
case "STRING":
|
|
12269
|
+
return "string";
|
|
12270
|
+
case "NUMBER":
|
|
12271
|
+
return "number";
|
|
12272
|
+
case "BOOLEAN":
|
|
12273
|
+
return "boolean";
|
|
12274
|
+
case "OBJECT":
|
|
12275
|
+
return "object";
|
|
12276
|
+
default:
|
|
12277
|
+
return "<type>";
|
|
12278
|
+
}
|
|
12279
|
+
}
|
|
12280
|
+
var DOCS_BASE, ERROR_DOC_URLS, COERCE_OUTPUT_TYPE, WorkflowValidator, validator;
|
|
12267
12281
|
var init_validator = __esm({
|
|
12268
12282
|
"src/validator.ts"() {
|
|
12269
12283
|
"use strict";
|
|
@@ -12284,7 +12298,17 @@ var init_validator = __esm({
|
|
|
12284
12298
|
MISSING_EXIT_CONNECTION: `${DOCS_BASE}/concepts#start-and-exit`,
|
|
12285
12299
|
INFERRED_NODE_TYPE: `${DOCS_BASE}/node-conversion`,
|
|
12286
12300
|
DUPLICATE_CONNECTION: `${DOCS_BASE}/concepts#connections`,
|
|
12287
|
-
STUB_NODE: `${DOCS_BASE}/model-driven#stub-nodes
|
|
12301
|
+
STUB_NODE: `${DOCS_BASE}/model-driven#stub-nodes`,
|
|
12302
|
+
COERCE_TYPE_MISMATCH: `${DOCS_BASE}/compilation#type-coercion`,
|
|
12303
|
+
REDUNDANT_COERCE: `${DOCS_BASE}/compilation#type-coercion`,
|
|
12304
|
+
COERCE_ON_FUNCTION_PORT: `${DOCS_BASE}/compilation#type-coercion`
|
|
12305
|
+
};
|
|
12306
|
+
COERCE_OUTPUT_TYPE = {
|
|
12307
|
+
string: "STRING",
|
|
12308
|
+
number: "NUMBER",
|
|
12309
|
+
boolean: "BOOLEAN",
|
|
12310
|
+
json: "STRING",
|
|
12311
|
+
object: "OBJECT"
|
|
12288
12312
|
};
|
|
12289
12313
|
WorkflowValidator = class {
|
|
12290
12314
|
errors = [];
|
|
@@ -12710,7 +12734,27 @@ Add '@param ${fromPort}' to the workflow JSDoc and include it in the params obje
|
|
|
12710
12734
|
if (sourceType === "STEP" && targetType === "STEP") {
|
|
12711
12735
|
return;
|
|
12712
12736
|
}
|
|
12737
|
+
if (conn.coerce && (sourceType === "FUNCTION" || targetType === "FUNCTION")) {
|
|
12738
|
+
this.errors.push({
|
|
12739
|
+
type: "error",
|
|
12740
|
+
code: "COERCE_ON_FUNCTION_PORT",
|
|
12741
|
+
message: `Coercion \`as ${conn.coerce}\` cannot be used on FUNCTION ports in connection "${fromNode}.${fromPort}" \u2192 "${toNode}.${toPort}". FUNCTION values cannot be meaningfully coerced.`,
|
|
12742
|
+
connection: conn,
|
|
12743
|
+
location: connLocation
|
|
12744
|
+
});
|
|
12745
|
+
return;
|
|
12746
|
+
}
|
|
12713
12747
|
if (sourceType === targetType) {
|
|
12748
|
+
if (conn.coerce) {
|
|
12749
|
+
this.warnings.push({
|
|
12750
|
+
type: "warning",
|
|
12751
|
+
code: "REDUNDANT_COERCE",
|
|
12752
|
+
message: `Coercion \`as ${conn.coerce}\` on connection "${fromNode}.${fromPort}" \u2192 "${toNode}.${toPort}" is redundant \u2014 source and target are both ${sourceType}.`,
|
|
12753
|
+
connection: conn,
|
|
12754
|
+
location: connLocation
|
|
12755
|
+
});
|
|
12756
|
+
return;
|
|
12757
|
+
}
|
|
12714
12758
|
if (sourceType === "OBJECT" && sourcePortDef.tsType && targetPortDef.tsType) {
|
|
12715
12759
|
const normalizedSource = this.normalizeTypeString(sourcePortDef.tsType);
|
|
12716
12760
|
const normalizedTarget = this.normalizeTypeString(targetPortDef.tsType);
|
|
@@ -12732,6 +12776,23 @@ Add '@param ${fromPort}' to the workflow JSDoc and include it in the params obje
|
|
|
12732
12776
|
if (sourceType === "ANY" || targetType === "ANY") {
|
|
12733
12777
|
return;
|
|
12734
12778
|
}
|
|
12779
|
+
if (conn.coerce) {
|
|
12780
|
+
const producedType = COERCE_OUTPUT_TYPE[conn.coerce];
|
|
12781
|
+
if (producedType === targetType) {
|
|
12782
|
+
return;
|
|
12783
|
+
}
|
|
12784
|
+
pushTypeIssue(
|
|
12785
|
+
{
|
|
12786
|
+
type: "warning",
|
|
12787
|
+
code: "COERCE_TYPE_MISMATCH",
|
|
12788
|
+
message: `Coercion \`as ${conn.coerce}\` produces ${producedType} but target port "${toPort}" on "${toNode}" expects ${targetType}. Use \`as ${suggestCoerceType(targetType)}\` instead.`,
|
|
12789
|
+
connection: conn,
|
|
12790
|
+
location: connLocation
|
|
12791
|
+
},
|
|
12792
|
+
true
|
|
12793
|
+
);
|
|
12794
|
+
return;
|
|
12795
|
+
}
|
|
12735
12796
|
const safeCoercions = [
|
|
12736
12797
|
["NUMBER", "STRING"],
|
|
12737
12798
|
["BOOLEAN", "STRING"]
|
|
@@ -16881,6 +16942,121 @@ var require_browser = __commonJS({
|
|
|
16881
16942
|
}
|
|
16882
16943
|
});
|
|
16883
16944
|
|
|
16945
|
+
// node_modules/has-flag/index.js
|
|
16946
|
+
var require_has_flag = __commonJS({
|
|
16947
|
+
"node_modules/has-flag/index.js"(exports2, module2) {
|
|
16948
|
+
"use strict";
|
|
16949
|
+
module2.exports = (flag, argv = process.argv) => {
|
|
16950
|
+
const prefix = flag.startsWith("-") ? "" : flag.length === 1 ? "-" : "--";
|
|
16951
|
+
const position = argv.indexOf(prefix + flag);
|
|
16952
|
+
const terminatorPosition = argv.indexOf("--");
|
|
16953
|
+
return position !== -1 && (terminatorPosition === -1 || position < terminatorPosition);
|
|
16954
|
+
};
|
|
16955
|
+
}
|
|
16956
|
+
});
|
|
16957
|
+
|
|
16958
|
+
// node_modules/supports-color/index.js
|
|
16959
|
+
var require_supports_color = __commonJS({
|
|
16960
|
+
"node_modules/supports-color/index.js"(exports2, module2) {
|
|
16961
|
+
"use strict";
|
|
16962
|
+
var os3 = __require("os");
|
|
16963
|
+
var tty = __require("tty");
|
|
16964
|
+
var hasFlag = require_has_flag();
|
|
16965
|
+
var { env } = process;
|
|
16966
|
+
var forceColor;
|
|
16967
|
+
if (hasFlag("no-color") || hasFlag("no-colors") || hasFlag("color=false") || hasFlag("color=never")) {
|
|
16968
|
+
forceColor = 0;
|
|
16969
|
+
} else if (hasFlag("color") || hasFlag("colors") || hasFlag("color=true") || hasFlag("color=always")) {
|
|
16970
|
+
forceColor = 1;
|
|
16971
|
+
}
|
|
16972
|
+
if ("FORCE_COLOR" in env) {
|
|
16973
|
+
if (env.FORCE_COLOR === "true") {
|
|
16974
|
+
forceColor = 1;
|
|
16975
|
+
} else if (env.FORCE_COLOR === "false") {
|
|
16976
|
+
forceColor = 0;
|
|
16977
|
+
} else {
|
|
16978
|
+
forceColor = env.FORCE_COLOR.length === 0 ? 1 : Math.min(parseInt(env.FORCE_COLOR, 10), 3);
|
|
16979
|
+
}
|
|
16980
|
+
}
|
|
16981
|
+
function translateLevel(level) {
|
|
16982
|
+
if (level === 0) {
|
|
16983
|
+
return false;
|
|
16984
|
+
}
|
|
16985
|
+
return {
|
|
16986
|
+
level,
|
|
16987
|
+
hasBasic: true,
|
|
16988
|
+
has256: level >= 2,
|
|
16989
|
+
has16m: level >= 3
|
|
16990
|
+
};
|
|
16991
|
+
}
|
|
16992
|
+
function supportsColor(haveStream, streamIsTTY) {
|
|
16993
|
+
if (forceColor === 0) {
|
|
16994
|
+
return 0;
|
|
16995
|
+
}
|
|
16996
|
+
if (hasFlag("color=16m") || hasFlag("color=full") || hasFlag("color=truecolor")) {
|
|
16997
|
+
return 3;
|
|
16998
|
+
}
|
|
16999
|
+
if (hasFlag("color=256")) {
|
|
17000
|
+
return 2;
|
|
17001
|
+
}
|
|
17002
|
+
if (haveStream && !streamIsTTY && forceColor === void 0) {
|
|
17003
|
+
return 0;
|
|
17004
|
+
}
|
|
17005
|
+
const min = forceColor || 0;
|
|
17006
|
+
if (env.TERM === "dumb") {
|
|
17007
|
+
return min;
|
|
17008
|
+
}
|
|
17009
|
+
if (process.platform === "win32") {
|
|
17010
|
+
const osRelease = os3.release().split(".");
|
|
17011
|
+
if (Number(osRelease[0]) >= 10 && Number(osRelease[2]) >= 10586) {
|
|
17012
|
+
return Number(osRelease[2]) >= 14931 ? 3 : 2;
|
|
17013
|
+
}
|
|
17014
|
+
return 1;
|
|
17015
|
+
}
|
|
17016
|
+
if ("CI" in env) {
|
|
17017
|
+
if (["TRAVIS", "CIRCLECI", "APPVEYOR", "GITLAB_CI", "GITHUB_ACTIONS", "BUILDKITE"].some((sign) => sign in env) || env.CI_NAME === "codeship") {
|
|
17018
|
+
return 1;
|
|
17019
|
+
}
|
|
17020
|
+
return min;
|
|
17021
|
+
}
|
|
17022
|
+
if ("TEAMCITY_VERSION" in env) {
|
|
17023
|
+
return /^(9\.(0*[1-9]\d*)\.|\d{2,}\.)/.test(env.TEAMCITY_VERSION) ? 1 : 0;
|
|
17024
|
+
}
|
|
17025
|
+
if (env.COLORTERM === "truecolor") {
|
|
17026
|
+
return 3;
|
|
17027
|
+
}
|
|
17028
|
+
if ("TERM_PROGRAM" in env) {
|
|
17029
|
+
const version3 = parseInt((env.TERM_PROGRAM_VERSION || "").split(".")[0], 10);
|
|
17030
|
+
switch (env.TERM_PROGRAM) {
|
|
17031
|
+
case "iTerm.app":
|
|
17032
|
+
return version3 >= 3 ? 3 : 2;
|
|
17033
|
+
case "Apple_Terminal":
|
|
17034
|
+
return 2;
|
|
17035
|
+
}
|
|
17036
|
+
}
|
|
17037
|
+
if (/-256(color)?$/i.test(env.TERM)) {
|
|
17038
|
+
return 2;
|
|
17039
|
+
}
|
|
17040
|
+
if (/^screen|^xterm|^vt100|^vt220|^rxvt|color|ansi|cygwin|linux/i.test(env.TERM)) {
|
|
17041
|
+
return 1;
|
|
17042
|
+
}
|
|
17043
|
+
if ("COLORTERM" in env) {
|
|
17044
|
+
return 1;
|
|
17045
|
+
}
|
|
17046
|
+
return min;
|
|
17047
|
+
}
|
|
17048
|
+
function getSupportLevel(stream2) {
|
|
17049
|
+
const level = supportsColor(stream2, stream2 && stream2.isTTY);
|
|
17050
|
+
return translateLevel(level);
|
|
17051
|
+
}
|
|
17052
|
+
module2.exports = {
|
|
17053
|
+
supportsColor: getSupportLevel,
|
|
17054
|
+
stdout: translateLevel(supportsColor(true, tty.isatty(1))),
|
|
17055
|
+
stderr: translateLevel(supportsColor(true, tty.isatty(2)))
|
|
17056
|
+
};
|
|
17057
|
+
}
|
|
17058
|
+
});
|
|
17059
|
+
|
|
16884
17060
|
// node_modules/debug/src/node.js
|
|
16885
17061
|
var require_node = __commonJS({
|
|
16886
17062
|
"node_modules/debug/src/node.js"(exports2, module2) {
|
|
@@ -16899,7 +17075,7 @@ var require_node = __commonJS({
|
|
|
16899
17075
|
);
|
|
16900
17076
|
exports2.colors = [6, 2, 3, 4, 5, 1];
|
|
16901
17077
|
try {
|
|
16902
|
-
const supportsColor =
|
|
17078
|
+
const supportsColor = require_supports_color();
|
|
16903
17079
|
if (supportsColor && (supportsColor.stderr || supportsColor).level >= 2) {
|
|
16904
17080
|
exports2.colors = [
|
|
16905
17081
|
20,
|
|
@@ -28049,6 +28225,43 @@ function generateScopeFunctionClosure(scopeName, parentNodeId, parentNodeType, w
|
|
|
28049
28225
|
|
|
28050
28226
|
// src/generator/code-utils.ts
|
|
28051
28227
|
init_type_mappings();
|
|
28228
|
+
var COERCION_EXPRESSIONS = {
|
|
28229
|
+
string: "String",
|
|
28230
|
+
number: "Number",
|
|
28231
|
+
boolean: "Boolean",
|
|
28232
|
+
json: "JSON.stringify",
|
|
28233
|
+
object: "JSON.parse"
|
|
28234
|
+
};
|
|
28235
|
+
function resolveSourcePortDataType(workflow, sourceNodeId, sourcePort) {
|
|
28236
|
+
if (isStartNode(sourceNodeId)) {
|
|
28237
|
+
return workflow.startPorts?.[sourcePort]?.dataType;
|
|
28238
|
+
}
|
|
28239
|
+
if (isExitNode(sourceNodeId)) {
|
|
28240
|
+
return workflow.exitPorts?.[sourcePort]?.dataType;
|
|
28241
|
+
}
|
|
28242
|
+
const instance = workflow.instances.find((i) => i.id === sourceNodeId);
|
|
28243
|
+
if (!instance) return void 0;
|
|
28244
|
+
const nodeType = workflow.nodeTypes.find(
|
|
28245
|
+
(nt) => nt.name === instance.nodeType || nt.functionName === instance.nodeType
|
|
28246
|
+
);
|
|
28247
|
+
if (!nodeType) return void 0;
|
|
28248
|
+
return nodeType.outputs?.[sourcePort]?.dataType;
|
|
28249
|
+
}
|
|
28250
|
+
function getCoercionWrapper(connection, sourceDataType, targetDataType) {
|
|
28251
|
+
if (connection.coerce) {
|
|
28252
|
+
return COERCION_EXPRESSIONS[connection.coerce];
|
|
28253
|
+
}
|
|
28254
|
+
if (!sourceDataType || !targetDataType || sourceDataType === targetDataType) return null;
|
|
28255
|
+
if (sourceDataType === "STEP" || targetDataType === "STEP") return null;
|
|
28256
|
+
if (sourceDataType === "ANY" || targetDataType === "ANY") return null;
|
|
28257
|
+
if (targetDataType === "STRING" && sourceDataType !== "STRING") {
|
|
28258
|
+
return "String";
|
|
28259
|
+
}
|
|
28260
|
+
if (sourceDataType === "BOOLEAN" && targetDataType === "NUMBER") {
|
|
28261
|
+
return "Number";
|
|
28262
|
+
}
|
|
28263
|
+
return null;
|
|
28264
|
+
}
|
|
28052
28265
|
function toValidIdentifier(nodeId) {
|
|
28053
28266
|
let sanitized = nodeId.replace(/[^a-zA-Z0-9_$]/g, "_");
|
|
28054
28267
|
if (/^[0-9]/.test(sanitized)) {
|
|
@@ -28178,9 +28391,18 @@ function buildNodeArgumentsWithContext(opts) {
|
|
|
28178
28391
|
`${indent}const ${varName} = ${varName}_resolved.fn as ${portType};`
|
|
28179
28392
|
);
|
|
28180
28393
|
} else {
|
|
28181
|
-
|
|
28182
|
-
|
|
28183
|
-
)
|
|
28394
|
+
const sourceDataType = resolveSourcePortDataType(workflow, sourceNode, sourcePort);
|
|
28395
|
+
const coerceExpr = getCoercionWrapper(connection, sourceDataType, portConfig.dataType);
|
|
28396
|
+
const getExpr = `${getCall}({ id: '${sourceNode}', portName: '${sourcePort}', executionIndex: ${sourceIdx}${nonNullAssert} })`;
|
|
28397
|
+
if (coerceExpr) {
|
|
28398
|
+
lines.push(
|
|
28399
|
+
`${indent}const ${varName} = ${coerceExpr}(${getExpr}) as ${portType};`
|
|
28400
|
+
);
|
|
28401
|
+
} else {
|
|
28402
|
+
lines.push(
|
|
28403
|
+
`${indent}const ${varName} = ${getExpr} as ${portType};`
|
|
28404
|
+
);
|
|
28405
|
+
}
|
|
28184
28406
|
}
|
|
28185
28407
|
} else {
|
|
28186
28408
|
const validConnections = connections.filter((conn) => {
|
|
@@ -28194,13 +28416,19 @@ function buildNodeArgumentsWithContext(opts) {
|
|
|
28194
28416
|
return;
|
|
28195
28417
|
}
|
|
28196
28418
|
const attempts = [];
|
|
28197
|
-
validConnections.forEach((conn
|
|
28419
|
+
validConnections.forEach((conn) => {
|
|
28198
28420
|
const sourceNode = conn.from.node;
|
|
28199
28421
|
const sourcePort = conn.from.port;
|
|
28200
28422
|
const sourceIdx = isStartNode(sourceNode) ? "startIdx" : `${toValidIdentifier(sourceNode)}Idx`;
|
|
28201
|
-
|
|
28202
|
-
|
|
28203
|
-
|
|
28423
|
+
const getExpr = `${getCall}({ id: '${sourceNode}', portName: '${sourcePort}', executionIndex: ${sourceIdx} })`;
|
|
28424
|
+
if (portConfig.dataType !== "FUNCTION") {
|
|
28425
|
+
const sourceDataType = resolveSourcePortDataType(workflow, sourceNode, sourcePort);
|
|
28426
|
+
const coerceExpr = getCoercionWrapper(conn, sourceDataType, portConfig.dataType);
|
|
28427
|
+
const wrapped = coerceExpr ? `${coerceExpr}(${getExpr})` : getExpr;
|
|
28428
|
+
attempts.push(`(${sourceIdx} !== undefined ? ${wrapped} : undefined)`);
|
|
28429
|
+
} else {
|
|
28430
|
+
attempts.push(`(${sourceIdx} !== undefined ? ${getExpr} : undefined)`);
|
|
28431
|
+
}
|
|
28204
28432
|
});
|
|
28205
28433
|
const ternary = attempts.join(" ?? ");
|
|
28206
28434
|
const portType = mapToTypeScript(portConfig.dataType, portConfig.tsType);
|
|
@@ -43430,12 +43658,16 @@ var ConnectParser = class extends CstParser {
|
|
|
43430
43658
|
super(allTokens);
|
|
43431
43659
|
this.performSelfAnalysis();
|
|
43432
43660
|
}
|
|
43433
|
-
// Entry rule for connect line
|
|
43661
|
+
// Entry rule for connect line: @connect A.port -> B.port [as type]
|
|
43434
43662
|
connectLine = this.RULE("connectLine", () => {
|
|
43435
43663
|
this.CONSUME(ConnectTag);
|
|
43436
43664
|
this.SUBRULE(this.portRef, { LABEL: "sourceRef" });
|
|
43437
43665
|
this.CONSUME(Arrow);
|
|
43438
43666
|
this.SUBRULE2(this.portRef, { LABEL: "targetRef" });
|
|
43667
|
+
this.OPTION(() => {
|
|
43668
|
+
this.CONSUME(AsKeyword);
|
|
43669
|
+
this.CONSUME(Identifier, { LABEL: "coerceType" });
|
|
43670
|
+
});
|
|
43439
43671
|
});
|
|
43440
43672
|
// node.port or node.port:scope
|
|
43441
43673
|
portRef = this.RULE("portRef", () => {
|
|
@@ -43458,7 +43690,17 @@ var ConnectVisitor = class extends BaseVisitor3 {
|
|
|
43458
43690
|
connectLine(ctx) {
|
|
43459
43691
|
const source = this.visit(ctx.sourceRef);
|
|
43460
43692
|
const target = this.visit(ctx.targetRef);
|
|
43461
|
-
|
|
43693
|
+
const result = { source, target };
|
|
43694
|
+
if (ctx.coerceType?.[0]) {
|
|
43695
|
+
const validTypes = /* @__PURE__ */ new Set(["string", "number", "boolean", "json", "object"]);
|
|
43696
|
+
const raw = ctx.coerceType[0].image;
|
|
43697
|
+
if (validTypes.has(raw)) {
|
|
43698
|
+
result.coerce = raw;
|
|
43699
|
+
} else {
|
|
43700
|
+
result.invalidCoerceType = raw;
|
|
43701
|
+
}
|
|
43702
|
+
}
|
|
43703
|
+
return result;
|
|
43462
43704
|
}
|
|
43463
43705
|
portRef(ctx) {
|
|
43464
43706
|
const nodeId = ctx.nodeId[0].image;
|
|
@@ -43492,7 +43734,14 @@ function parseConnectLine(input, warnings) {
|
|
|
43492
43734
|
);
|
|
43493
43735
|
return null;
|
|
43494
43736
|
}
|
|
43495
|
-
|
|
43737
|
+
const result = visitorInstance3.visit(cst);
|
|
43738
|
+
if (result.invalidCoerceType) {
|
|
43739
|
+
warnings.push(
|
|
43740
|
+
`Invalid coerce type "${result.invalidCoerceType}" in @connect. Valid types: string, number, boolean, json, object`
|
|
43741
|
+
);
|
|
43742
|
+
delete result.invalidCoerceType;
|
|
43743
|
+
}
|
|
43744
|
+
return result;
|
|
43496
43745
|
}
|
|
43497
43746
|
function getConnectGrammar() {
|
|
43498
43747
|
return parserInstance3.getSerializedGastProductions();
|
|
@@ -45192,7 +45441,7 @@ var JSDocParser = class {
|
|
|
45192
45441
|
warnings.push(`Invalid @connect tag format: @connect ${comment}`);
|
|
45193
45442
|
return;
|
|
45194
45443
|
}
|
|
45195
|
-
const { source, target } = result;
|
|
45444
|
+
const { source, target, coerce: coerce2 } = result;
|
|
45196
45445
|
const line = tag.getStartLineNumber();
|
|
45197
45446
|
config2.connections.push({
|
|
45198
45447
|
from: {
|
|
@@ -45205,7 +45454,8 @@ var JSDocParser = class {
|
|
|
45205
45454
|
port: target.portName,
|
|
45206
45455
|
...target.scope && { scope: target.scope }
|
|
45207
45456
|
},
|
|
45208
|
-
sourceLocation: { line, column: 0 }
|
|
45457
|
+
sourceLocation: { line, column: 0 },
|
|
45458
|
+
...coerce2 && { coerce: coerce2 }
|
|
45209
45459
|
});
|
|
45210
45460
|
}
|
|
45211
45461
|
/**
|
|
@@ -54170,6 +54420,41 @@ var errorMappers = {
|
|
|
54170
54420
|
code: error2.code
|
|
54171
54421
|
};
|
|
54172
54422
|
},
|
|
54423
|
+
COERCE_TYPE_MISMATCH(error2) {
|
|
54424
|
+
const coerceMatch = error2.message.match(/`as (\w+)`/);
|
|
54425
|
+
const coerceType = coerceMatch?.[1] || "unknown";
|
|
54426
|
+
const expectsMatch = error2.message.match(/expects (\w+)/);
|
|
54427
|
+
const expectedType = expectsMatch?.[1] || "unknown";
|
|
54428
|
+
const suggestedType = COERCE_TARGET_TYPES[expectedType.toUpperCase()] || expectedType.toLowerCase();
|
|
54429
|
+
return {
|
|
54430
|
+
title: "Wrong Coercion Type",
|
|
54431
|
+
explanation: `The \`as ${coerceType}\` coercion produces the wrong type for the target port. The target expects ${expectedType}.`,
|
|
54432
|
+
fix: `Change \`as ${coerceType}\` to \`as ${suggestedType}\` in the @connect annotation.`,
|
|
54433
|
+
code: error2.code
|
|
54434
|
+
};
|
|
54435
|
+
},
|
|
54436
|
+
REDUNDANT_COERCE(error2) {
|
|
54437
|
+
const coerceMatch = error2.message.match(/`as (\w+)`/);
|
|
54438
|
+
const coerceType = coerceMatch?.[1] || "unknown";
|
|
54439
|
+
const bothMatch = error2.message.match(/both (\w+)/);
|
|
54440
|
+
const dataType = bothMatch?.[1] || "the same type";
|
|
54441
|
+
return {
|
|
54442
|
+
title: "Redundant Coercion",
|
|
54443
|
+
explanation: `The \`as ${coerceType}\` coercion is unnecessary because both the source and target ports are already ${dataType}.`,
|
|
54444
|
+
fix: `Remove \`as ${coerceType}\` from the @connect annotation \u2014 no coercion is needed.`,
|
|
54445
|
+
code: error2.code
|
|
54446
|
+
};
|
|
54447
|
+
},
|
|
54448
|
+
COERCE_ON_FUNCTION_PORT(error2) {
|
|
54449
|
+
const coerceMatch = error2.message.match(/`as (\w+)`/);
|
|
54450
|
+
const coerceType = coerceMatch?.[1] || "unknown";
|
|
54451
|
+
return {
|
|
54452
|
+
title: "Coercion on Function Port",
|
|
54453
|
+
explanation: `The \`as ${coerceType}\` coercion cannot be applied to FUNCTION ports. Function values are callable references and cannot be meaningfully converted to other types.`,
|
|
54454
|
+
fix: `Remove \`as ${coerceType}\` from the @connect annotation. If you need to convert the function's return value, add a transformation node.`,
|
|
54455
|
+
code: error2.code
|
|
54456
|
+
};
|
|
54457
|
+
},
|
|
54173
54458
|
LOSSY_TYPE_COERCION(error2) {
|
|
54174
54459
|
const types2 = extractTypes(error2.message);
|
|
54175
54460
|
const source = types2?.source || "unknown";
|
|
@@ -61065,12 +61350,12 @@ function renderConnection(parts2, conn, gradIndex) {
|
|
|
61065
61350
|
` <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"/>`
|
|
61066
61351
|
);
|
|
61067
61352
|
}
|
|
61068
|
-
function renderScopeConnection(parts2, conn, allConnections) {
|
|
61353
|
+
function renderScopeConnection(parts2, conn, allConnections, parentNodeId) {
|
|
61069
61354
|
const gradIndex = allConnections.indexOf(conn);
|
|
61070
61355
|
if (gradIndex < 0) return;
|
|
61071
61356
|
const dashAttr = conn.isStepConnection ? "" : ' stroke-dasharray="8 4"';
|
|
61072
61357
|
parts2.push(
|
|
61073
|
-
` <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"/>`
|
|
61358
|
+
` <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" data-scope="${escapeXml(parentNodeId)}"/>`
|
|
61074
61359
|
);
|
|
61075
61360
|
}
|
|
61076
61361
|
function renderNodeBody(parts2, node, theme, indent) {
|
|
@@ -61114,7 +61399,7 @@ function renderScopedContent(parts2, node, theme, themeName, allConnections) {
|
|
|
61114
61399
|
` <rect x="${scopeX}" y="${scopeY}" width="${scopeW}" height="${scopeH}" rx="4" fill="none" stroke="${theme.scopeAreaStroke}" stroke-width="1" stroke-dasharray="4 2" opacity="0.5"/>`
|
|
61115
61400
|
);
|
|
61116
61401
|
for (const conn of node.scopeConnections ?? []) {
|
|
61117
|
-
renderScopeConnection(parts2, conn, allConnections);
|
|
61402
|
+
renderScopeConnection(parts2, conn, allConnections, node.id);
|
|
61118
61403
|
}
|
|
61119
61404
|
if (scopePorts) {
|
|
61120
61405
|
renderPortDots(parts2, node.id, scopePorts.inputs, scopePorts.outputs, themeName);
|
|
@@ -61662,7 +61947,7 @@ path[data-source].port-hover { opacity: 1; }
|
|
|
61662
61947
|
var connIndex = [];
|
|
61663
61948
|
content.querySelectorAll('path[data-source]').forEach(function(p) {
|
|
61664
61949
|
var src = p.getAttribute('data-source'), tgt = p.getAttribute('data-target');
|
|
61665
|
-
connIndex.push({ el: p, src: src, tgt: tgt, srcNode: src.split('.')[0], tgtNode: tgt.split('.')[0] });
|
|
61950
|
+
connIndex.push({ el: p, src: src, tgt: tgt, srcNode: src.split('.')[0], tgtNode: tgt.split('.')[0], scopeOf: p.getAttribute('data-scope') || null });
|
|
61666
61951
|
});
|
|
61667
61952
|
|
|
61668
61953
|
// Snapshot of original port positions for reset
|
|
@@ -61704,9 +61989,18 @@ path[data-source].port-hover { opacity: 1; }
|
|
|
61704
61989
|
off.dx += dx; off.dy += dy;
|
|
61705
61990
|
var tr = 'translate(' + off.dx + ',' + off.dy + ')';
|
|
61706
61991
|
|
|
61707
|
-
// Move node group
|
|
61992
|
+
// Move node group (if nested inside a scoped parent, subtract parent offset)
|
|
61708
61993
|
var nodeG = content.querySelector('.nodes [data-node-id="' + CSS.escape(nodeId) + '"]');
|
|
61709
|
-
if (nodeG)
|
|
61994
|
+
if (nodeG) {
|
|
61995
|
+
var parentNodeG = nodeG.parentElement ? nodeG.parentElement.closest('[data-node-id]') : null;
|
|
61996
|
+
if (parentNodeG) {
|
|
61997
|
+
var parentId = parentNodeG.getAttribute('data-node-id');
|
|
61998
|
+
var parentOff = nodeOffsets[parentId] || { dx: 0, dy: 0 };
|
|
61999
|
+
nodeG.setAttribute('transform', 'translate(' + (off.dx - parentOff.dx) + ',' + (off.dy - parentOff.dy) + ')');
|
|
62000
|
+
} else {
|
|
62001
|
+
nodeG.setAttribute('transform', tr);
|
|
62002
|
+
}
|
|
62003
|
+
}
|
|
61710
62004
|
|
|
61711
62005
|
// Move label
|
|
61712
62006
|
var labelG = content.querySelector('[data-label-for="' + CSS.escape(nodeId) + '"]');
|
|
@@ -61753,11 +62047,20 @@ path[data-source].port-hover { opacity: 1; }
|
|
|
61753
62047
|
});
|
|
61754
62048
|
}
|
|
61755
62049
|
|
|
61756
|
-
// Recalculate affected connection paths
|
|
62050
|
+
// Recalculate affected connection paths (skip scope connections when parent is dragged \u2014 they move with the group transform)
|
|
61757
62051
|
connIndex.forEach(function(c) {
|
|
62052
|
+
if (c.scopeOf === nodeId) return;
|
|
61758
62053
|
if (c.srcNode === nodeId || c.tgtNode === nodeId) {
|
|
61759
62054
|
var sp = portPositions[c.src], tp = portPositions[c.tgt];
|
|
61760
|
-
if (sp && tp)
|
|
62055
|
+
if (sp && tp) {
|
|
62056
|
+
if (c.scopeOf) {
|
|
62057
|
+
// Scope connection paths live inside the parent group; use parent-local coords
|
|
62058
|
+
var pOff = nodeOffsets[c.scopeOf] || { dx: 0, dy: 0 };
|
|
62059
|
+
c.el.setAttribute('d', computeConnectionPath(sp.cx - pOff.dx, sp.cy - pOff.dy, tp.cx - pOff.dx, tp.cy - pOff.dy));
|
|
62060
|
+
} else {
|
|
62061
|
+
c.el.setAttribute('d', computeConnectionPath(sp.cx, sp.cy, tp.cx, tp.cy));
|
|
62062
|
+
}
|
|
62063
|
+
}
|
|
61761
62064
|
}
|
|
61762
62065
|
if (nodeG) {
|
|
61763
62066
|
var children = nodeG.querySelectorAll(':scope > g[data-node-id]');
|
|
@@ -61765,7 +62068,14 @@ path[data-source].port-hover { opacity: 1; }
|
|
|
61765
62068
|
var childId = childG.getAttribute('data-node-id');
|
|
61766
62069
|
if (c.srcNode === childId || c.tgtNode === childId) {
|
|
61767
62070
|
var sp = portPositions[c.src], tp = portPositions[c.tgt];
|
|
61768
|
-
if (sp && tp)
|
|
62071
|
+
if (sp && tp) {
|
|
62072
|
+
if (c.scopeOf) {
|
|
62073
|
+
var pOff = nodeOffsets[c.scopeOf] || { dx: 0, dy: 0 };
|
|
62074
|
+
c.el.setAttribute('d', computeConnectionPath(sp.cx - pOff.dx, sp.cy - pOff.dy, tp.cx - pOff.dx, tp.cy - pOff.dy));
|
|
62075
|
+
} else {
|
|
62076
|
+
c.el.setAttribute('d', computeConnectionPath(sp.cx, sp.cy, tp.cx, tp.cy));
|
|
62077
|
+
}
|
|
62078
|
+
}
|
|
61769
62079
|
}
|
|
61770
62080
|
});
|
|
61771
62081
|
}
|
|
@@ -96590,7 +96900,7 @@ function displayInstalledPackage(pkg) {
|
|
|
96590
96900
|
}
|
|
96591
96901
|
|
|
96592
96902
|
// src/cli/index.ts
|
|
96593
|
-
var version2 = true ? "0.10.
|
|
96903
|
+
var version2 = true ? "0.10.3" : "0.0.0-dev";
|
|
96594
96904
|
var program2 = new Command();
|
|
96595
96905
|
program2.name("flow-weaver").description("Flow Weaver Annotations - Compile and validate workflow files").version(version2, "-v, --version", "Output the current version");
|
|
96596
96906
|
program2.configureOutput({
|
|
@@ -481,7 +481,7 @@ path[data-source].port-hover { opacity: 1; }
|
|
|
481
481
|
var connIndex = [];
|
|
482
482
|
content.querySelectorAll('path[data-source]').forEach(function(p) {
|
|
483
483
|
var src = p.getAttribute('data-source'), tgt = p.getAttribute('data-target');
|
|
484
|
-
connIndex.push({ el: p, src: src, tgt: tgt, srcNode: src.split('.')[0], tgtNode: tgt.split('.')[0] });
|
|
484
|
+
connIndex.push({ el: p, src: src, tgt: tgt, srcNode: src.split('.')[0], tgtNode: tgt.split('.')[0], scopeOf: p.getAttribute('data-scope') || null });
|
|
485
485
|
});
|
|
486
486
|
|
|
487
487
|
// Snapshot of original port positions for reset
|
|
@@ -523,9 +523,18 @@ path[data-source].port-hover { opacity: 1; }
|
|
|
523
523
|
off.dx += dx; off.dy += dy;
|
|
524
524
|
var tr = 'translate(' + off.dx + ',' + off.dy + ')';
|
|
525
525
|
|
|
526
|
-
// Move node group
|
|
526
|
+
// Move node group (if nested inside a scoped parent, subtract parent offset)
|
|
527
527
|
var nodeG = content.querySelector('.nodes [data-node-id="' + CSS.escape(nodeId) + '"]');
|
|
528
|
-
if (nodeG)
|
|
528
|
+
if (nodeG) {
|
|
529
|
+
var parentNodeG = nodeG.parentElement ? nodeG.parentElement.closest('[data-node-id]') : null;
|
|
530
|
+
if (parentNodeG) {
|
|
531
|
+
var parentId = parentNodeG.getAttribute('data-node-id');
|
|
532
|
+
var parentOff = nodeOffsets[parentId] || { dx: 0, dy: 0 };
|
|
533
|
+
nodeG.setAttribute('transform', 'translate(' + (off.dx - parentOff.dx) + ',' + (off.dy - parentOff.dy) + ')');
|
|
534
|
+
} else {
|
|
535
|
+
nodeG.setAttribute('transform', tr);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
529
538
|
|
|
530
539
|
// Move label
|
|
531
540
|
var labelG = content.querySelector('[data-label-for="' + CSS.escape(nodeId) + '"]');
|
|
@@ -572,11 +581,20 @@ path[data-source].port-hover { opacity: 1; }
|
|
|
572
581
|
});
|
|
573
582
|
}
|
|
574
583
|
|
|
575
|
-
// Recalculate affected connection paths
|
|
584
|
+
// Recalculate affected connection paths (skip scope connections when parent is dragged — they move with the group transform)
|
|
576
585
|
connIndex.forEach(function(c) {
|
|
586
|
+
if (c.scopeOf === nodeId) return;
|
|
577
587
|
if (c.srcNode === nodeId || c.tgtNode === nodeId) {
|
|
578
588
|
var sp = portPositions[c.src], tp = portPositions[c.tgt];
|
|
579
|
-
if (sp && tp)
|
|
589
|
+
if (sp && tp) {
|
|
590
|
+
if (c.scopeOf) {
|
|
591
|
+
// Scope connection paths live inside the parent group; use parent-local coords
|
|
592
|
+
var pOff = nodeOffsets[c.scopeOf] || { dx: 0, dy: 0 };
|
|
593
|
+
c.el.setAttribute('d', computeConnectionPath(sp.cx - pOff.dx, sp.cy - pOff.dy, tp.cx - pOff.dx, tp.cy - pOff.dy));
|
|
594
|
+
} else {
|
|
595
|
+
c.el.setAttribute('d', computeConnectionPath(sp.cx, sp.cy, tp.cx, tp.cy));
|
|
596
|
+
}
|
|
597
|
+
}
|
|
580
598
|
}
|
|
581
599
|
if (nodeG) {
|
|
582
600
|
var children = nodeG.querySelectorAll(':scope > g[data-node-id]');
|
|
@@ -584,7 +602,14 @@ path[data-source].port-hover { opacity: 1; }
|
|
|
584
602
|
var childId = childG.getAttribute('data-node-id');
|
|
585
603
|
if (c.srcNode === childId || c.tgtNode === childId) {
|
|
586
604
|
var sp = portPositions[c.src], tp = portPositions[c.tgt];
|
|
587
|
-
if (sp && tp)
|
|
605
|
+
if (sp && tp) {
|
|
606
|
+
if (c.scopeOf) {
|
|
607
|
+
var pOff = nodeOffsets[c.scopeOf] || { dx: 0, dy: 0 };
|
|
608
|
+
c.el.setAttribute('d', computeConnectionPath(sp.cx - pOff.dx, sp.cy - pOff.dy, tp.cx - pOff.dx, tp.cy - pOff.dy));
|
|
609
|
+
} else {
|
|
610
|
+
c.el.setAttribute('d', computeConnectionPath(sp.cx, sp.cy, tp.cx, tp.cy));
|
|
611
|
+
}
|
|
612
|
+
}
|
|
588
613
|
}
|
|
589
614
|
});
|
|
590
615
|
}
|
package/dist/diagram/renderer.js
CHANGED
|
@@ -100,12 +100,12 @@ function renderConnection(parts, conn, gradIndex) {
|
|
|
100
100
|
const dashAttr = conn.isStepConnection ? '' : ' stroke-dasharray="8 4"';
|
|
101
101
|
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"/>`);
|
|
102
102
|
}
|
|
103
|
-
function renderScopeConnection(parts, conn, allConnections) {
|
|
103
|
+
function renderScopeConnection(parts, conn, allConnections, parentNodeId) {
|
|
104
104
|
const gradIndex = allConnections.indexOf(conn);
|
|
105
105
|
if (gradIndex < 0)
|
|
106
106
|
return;
|
|
107
107
|
const dashAttr = conn.isStepConnection ? '' : ' stroke-dasharray="8 4"';
|
|
108
|
-
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"/>`);
|
|
108
|
+
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" data-scope="${escapeXml(parentNodeId)}"/>`);
|
|
109
109
|
}
|
|
110
110
|
// ---- Node rendering ----
|
|
111
111
|
/** Render node body rect + icon */
|
|
@@ -147,7 +147,7 @@ function renderScopedContent(parts, node, theme, themeName, allConnections) {
|
|
|
147
147
|
parts.push(` <rect x="${scopeX}" y="${scopeY}" width="${scopeW}" height="${scopeH}" rx="4" fill="none" stroke="${theme.scopeAreaStroke}" stroke-width="1" stroke-dasharray="4 2" opacity="0.5"/>`);
|
|
148
148
|
// Scope connections (before ports so ports appear on top)
|
|
149
149
|
for (const conn of node.scopeConnections ?? []) {
|
|
150
|
-
renderScopeConnection(parts, conn, allConnections);
|
|
150
|
+
renderScopeConnection(parts, conn, allConnections, node.id);
|
|
151
151
|
}
|
|
152
152
|
// Scope port dots (before children so dots sit on top of connections)
|
|
153
153
|
if (scopePorts) {
|
package/dist/friendly-errors.js
CHANGED
|
@@ -505,6 +505,41 @@ const errorMappers = {
|
|
|
505
505
|
code: error.code,
|
|
506
506
|
};
|
|
507
507
|
},
|
|
508
|
+
COERCE_TYPE_MISMATCH(error) {
|
|
509
|
+
const coerceMatch = error.message.match(/`as (\w+)`/);
|
|
510
|
+
const coerceType = coerceMatch?.[1] || 'unknown';
|
|
511
|
+
const expectsMatch = error.message.match(/expects (\w+)/);
|
|
512
|
+
const expectedType = expectsMatch?.[1] || 'unknown';
|
|
513
|
+
const suggestedType = COERCE_TARGET_TYPES[expectedType.toUpperCase()] || expectedType.toLowerCase();
|
|
514
|
+
return {
|
|
515
|
+
title: 'Wrong Coercion Type',
|
|
516
|
+
explanation: `The \`as ${coerceType}\` coercion produces the wrong type for the target port. The target expects ${expectedType}.`,
|
|
517
|
+
fix: `Change \`as ${coerceType}\` to \`as ${suggestedType}\` in the @connect annotation.`,
|
|
518
|
+
code: error.code,
|
|
519
|
+
};
|
|
520
|
+
},
|
|
521
|
+
REDUNDANT_COERCE(error) {
|
|
522
|
+
const coerceMatch = error.message.match(/`as (\w+)`/);
|
|
523
|
+
const coerceType = coerceMatch?.[1] || 'unknown';
|
|
524
|
+
const bothMatch = error.message.match(/both (\w+)/);
|
|
525
|
+
const dataType = bothMatch?.[1] || 'the same type';
|
|
526
|
+
return {
|
|
527
|
+
title: 'Redundant Coercion',
|
|
528
|
+
explanation: `The \`as ${coerceType}\` coercion is unnecessary because both the source and target ports are already ${dataType}.`,
|
|
529
|
+
fix: `Remove \`as ${coerceType}\` from the @connect annotation — no coercion is needed.`,
|
|
530
|
+
code: error.code,
|
|
531
|
+
};
|
|
532
|
+
},
|
|
533
|
+
COERCE_ON_FUNCTION_PORT(error) {
|
|
534
|
+
const coerceMatch = error.message.match(/`as (\w+)`/);
|
|
535
|
+
const coerceType = coerceMatch?.[1] || 'unknown';
|
|
536
|
+
return {
|
|
537
|
+
title: 'Coercion on Function Port',
|
|
538
|
+
explanation: `The \`as ${coerceType}\` coercion cannot be applied to FUNCTION ports. Function values are callable references and cannot be meaningfully converted to other types.`,
|
|
539
|
+
fix: `Remove \`as ${coerceType}\` from the @connect annotation. If you need to convert the function's return value, add a transformation node.`,
|
|
540
|
+
code: error.code,
|
|
541
|
+
};
|
|
542
|
+
},
|
|
508
543
|
LOSSY_TYPE_COERCION(error) {
|
|
509
544
|
const types = extractTypes(error.message);
|
|
510
545
|
const source = types?.source || 'unknown';
|
|
@@ -1,4 +1,15 @@
|
|
|
1
|
-
import type { TNodeTypeAST, TWorkflowAST, TMergeStrategy } from '../ast/index.js';
|
|
1
|
+
import type { TNodeTypeAST, TWorkflowAST, TMergeStrategy, TConnectionAST, TDataType } from '../ast/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Get the coercion expression to wrap a value, if coercion is needed.
|
|
4
|
+
* Returns null if no coercion needed.
|
|
5
|
+
*
|
|
6
|
+
* Priority:
|
|
7
|
+
* 1. Explicit coerce on the connection (from `as <type>` annotation)
|
|
8
|
+
* 2. Auto-coercion for safe pairs:
|
|
9
|
+
* - anything -> STRING (String() never fails)
|
|
10
|
+
* - BOOLEAN -> NUMBER (well-defined: false->0, true->1)
|
|
11
|
+
*/
|
|
12
|
+
export declare function getCoercionWrapper(connection: TConnectionAST, sourceDataType: TDataType | undefined, targetDataType: TDataType | undefined): string | null;
|
|
2
13
|
/**
|
|
3
14
|
* Sanitize a node ID to be a valid JavaScript identifier.
|
|
4
15
|
* Replaces non-alphanumeric characters (except _ and $) with underscores.
|
|
@@ -1,6 +1,72 @@
|
|
|
1
1
|
import { RESERVED_PORT_NAMES, isStartNode, isExitNode, isExecutePort, isSuccessPort, isFailurePort, } from '../constants.js';
|
|
2
2
|
import { generateScopeFunctionClosure } from './scope-function-generator.js';
|
|
3
3
|
import { mapToTypeScript } from '../type-mappings.js';
|
|
4
|
+
/** Map coercion target type to inline JS expression */
|
|
5
|
+
const COERCION_EXPRESSIONS = {
|
|
6
|
+
string: 'String',
|
|
7
|
+
number: 'Number',
|
|
8
|
+
boolean: 'Boolean',
|
|
9
|
+
json: 'JSON.stringify',
|
|
10
|
+
object: 'JSON.parse',
|
|
11
|
+
};
|
|
12
|
+
/** Map TDataType to TCoerceTargetType for auto-coercion */
|
|
13
|
+
const DATATYPE_TO_COERCE = {
|
|
14
|
+
STRING: 'string',
|
|
15
|
+
NUMBER: 'number',
|
|
16
|
+
BOOLEAN: 'boolean',
|
|
17
|
+
OBJECT: 'object',
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Resolve the dataType of a source port by looking up the node instance -> node type -> outputs.
|
|
21
|
+
*/
|
|
22
|
+
function resolveSourcePortDataType(workflow, sourceNodeId, sourcePort) {
|
|
23
|
+
if (isStartNode(sourceNodeId)) {
|
|
24
|
+
return workflow.startPorts?.[sourcePort]?.dataType;
|
|
25
|
+
}
|
|
26
|
+
if (isExitNode(sourceNodeId)) {
|
|
27
|
+
return workflow.exitPorts?.[sourcePort]?.dataType;
|
|
28
|
+
}
|
|
29
|
+
const instance = workflow.instances.find((i) => i.id === sourceNodeId);
|
|
30
|
+
if (!instance)
|
|
31
|
+
return undefined;
|
|
32
|
+
const nodeType = workflow.nodeTypes.find((nt) => nt.name === instance.nodeType || nt.functionName === instance.nodeType);
|
|
33
|
+
if (!nodeType)
|
|
34
|
+
return undefined;
|
|
35
|
+
return nodeType.outputs?.[sourcePort]?.dataType;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Get the coercion expression to wrap a value, if coercion is needed.
|
|
39
|
+
* Returns null if no coercion needed.
|
|
40
|
+
*
|
|
41
|
+
* Priority:
|
|
42
|
+
* 1. Explicit coerce on the connection (from `as <type>` annotation)
|
|
43
|
+
* 2. Auto-coercion for safe pairs:
|
|
44
|
+
* - anything -> STRING (String() never fails)
|
|
45
|
+
* - BOOLEAN -> NUMBER (well-defined: false->0, true->1)
|
|
46
|
+
*/
|
|
47
|
+
export function getCoercionWrapper(connection, sourceDataType, targetDataType) {
|
|
48
|
+
// Explicit coerce on connection
|
|
49
|
+
if (connection.coerce) {
|
|
50
|
+
return COERCION_EXPRESSIONS[connection.coerce];
|
|
51
|
+
}
|
|
52
|
+
// No auto-coercion if types are unknown or same
|
|
53
|
+
if (!sourceDataType || !targetDataType || sourceDataType === targetDataType)
|
|
54
|
+
return null;
|
|
55
|
+
// Skip STEP and ANY ports — no coercion needed
|
|
56
|
+
if (sourceDataType === 'STEP' || targetDataType === 'STEP')
|
|
57
|
+
return null;
|
|
58
|
+
if (sourceDataType === 'ANY' || targetDataType === 'ANY')
|
|
59
|
+
return null;
|
|
60
|
+
// Auto-coerce: anything -> STRING
|
|
61
|
+
if (targetDataType === 'STRING' && sourceDataType !== 'STRING') {
|
|
62
|
+
return 'String';
|
|
63
|
+
}
|
|
64
|
+
// Auto-coerce: BOOLEAN -> NUMBER
|
|
65
|
+
if (sourceDataType === 'BOOLEAN' && targetDataType === 'NUMBER') {
|
|
66
|
+
return 'Number';
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
4
70
|
/**
|
|
5
71
|
* Sanitize a node ID to be a valid JavaScript identifier.
|
|
6
72
|
* Replaces non-alphanumeric characters (except _ and $) with underscores.
|
|
@@ -197,7 +263,16 @@ export function buildNodeArgumentsWithContext(opts) {
|
|
|
197
263
|
lines.push(`${indent}const ${varName} = ${varName}_resolved.fn as ${portType};`);
|
|
198
264
|
}
|
|
199
265
|
else {
|
|
200
|
-
|
|
266
|
+
// Check for coercion (explicit or auto)
|
|
267
|
+
const sourceDataType = resolveSourcePortDataType(workflow, sourceNode, sourcePort);
|
|
268
|
+
const coerceExpr = getCoercionWrapper(connection, sourceDataType, portConfig.dataType);
|
|
269
|
+
const getExpr = `${getCall}({ id: '${sourceNode}', portName: '${sourcePort}', executionIndex: ${sourceIdx}${nonNullAssert} })`;
|
|
270
|
+
if (coerceExpr) {
|
|
271
|
+
lines.push(`${indent}const ${varName} = ${coerceExpr}(${getExpr}) as ${portType};`);
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
lines.push(`${indent}const ${varName} = ${getExpr} as ${portType};`);
|
|
275
|
+
}
|
|
201
276
|
}
|
|
202
277
|
}
|
|
203
278
|
else {
|
|
@@ -214,11 +289,21 @@ export function buildNodeArgumentsWithContext(opts) {
|
|
|
214
289
|
return;
|
|
215
290
|
}
|
|
216
291
|
const attempts = [];
|
|
217
|
-
validConnections.forEach((conn
|
|
292
|
+
validConnections.forEach((conn) => {
|
|
218
293
|
const sourceNode = conn.from.node;
|
|
219
294
|
const sourcePort = conn.from.port;
|
|
220
295
|
const sourceIdx = isStartNode(sourceNode) ? 'startIdx' : `${toValidIdentifier(sourceNode)}Idx`;
|
|
221
|
-
|
|
296
|
+
const getExpr = `${getCall}({ id: '${sourceNode}', portName: '${sourcePort}', executionIndex: ${sourceIdx} })`;
|
|
297
|
+
// Per-connection coercion: each source gets its own coercion wrapper
|
|
298
|
+
if (portConfig.dataType !== 'FUNCTION') {
|
|
299
|
+
const sourceDataType = resolveSourcePortDataType(workflow, sourceNode, sourcePort);
|
|
300
|
+
const coerceExpr = getCoercionWrapper(conn, sourceDataType, portConfig.dataType);
|
|
301
|
+
const wrapped = coerceExpr ? `${coerceExpr}(${getExpr})` : getExpr;
|
|
302
|
+
attempts.push(`(${sourceIdx} !== undefined ? ${wrapped} : undefined)`);
|
|
303
|
+
}
|
|
304
|
+
else {
|
|
305
|
+
attempts.push(`(${sourceIdx} !== undefined ? ${getExpr} : undefined)`);
|
|
306
|
+
}
|
|
222
307
|
});
|
|
223
308
|
const ternary = attempts.join(' ?? ');
|
|
224
309
|
const portType = mapToTypeScript(portConfig.dataType, portConfig.tsType);
|
package/dist/jsdoc-parser.js
CHANGED
|
@@ -889,7 +889,7 @@ export class JSDocParser {
|
|
|
889
889
|
warnings.push(`Invalid @connect tag format: @connect ${comment}`);
|
|
890
890
|
return;
|
|
891
891
|
}
|
|
892
|
-
const { source, target } = result;
|
|
892
|
+
const { source, target, coerce } = result;
|
|
893
893
|
// Capture source location from tag
|
|
894
894
|
const line = tag.getStartLineNumber();
|
|
895
895
|
config.connections.push({
|
|
@@ -904,6 +904,7 @@ export class JSDocParser {
|
|
|
904
904
|
...(target.scope && { scope: target.scope }),
|
|
905
905
|
},
|
|
906
906
|
sourceLocation: { line, column: 0 },
|
|
907
|
+
...(coerce && { coerce }),
|
|
907
908
|
});
|
|
908
909
|
}
|
|
909
910
|
/**
|
package/dist/validator.js
CHANGED
|
@@ -17,7 +17,25 @@ const ERROR_DOC_URLS = {
|
|
|
17
17
|
INFERRED_NODE_TYPE: `${DOCS_BASE}/node-conversion`,
|
|
18
18
|
DUPLICATE_CONNECTION: `${DOCS_BASE}/concepts#connections`,
|
|
19
19
|
STUB_NODE: `${DOCS_BASE}/model-driven#stub-nodes`,
|
|
20
|
+
COERCE_TYPE_MISMATCH: `${DOCS_BASE}/compilation#type-coercion`,
|
|
21
|
+
REDUNDANT_COERCE: `${DOCS_BASE}/compilation#type-coercion`,
|
|
22
|
+
COERCE_ON_FUNCTION_PORT: `${DOCS_BASE}/compilation#type-coercion`,
|
|
20
23
|
};
|
|
24
|
+
/** Map coerce type to the dataType it produces */
|
|
25
|
+
const COERCE_OUTPUT_TYPE = {
|
|
26
|
+
string: 'STRING', number: 'NUMBER', boolean: 'BOOLEAN',
|
|
27
|
+
json: 'STRING', object: 'OBJECT',
|
|
28
|
+
};
|
|
29
|
+
/** Suggest the correct `as <type>` for a given target dataType */
|
|
30
|
+
function suggestCoerceType(targetType) {
|
|
31
|
+
switch (targetType) {
|
|
32
|
+
case 'STRING': return 'string';
|
|
33
|
+
case 'NUMBER': return 'number';
|
|
34
|
+
case 'BOOLEAN': return 'boolean';
|
|
35
|
+
case 'OBJECT': return 'object';
|
|
36
|
+
default: return '<type>';
|
|
37
|
+
}
|
|
38
|
+
}
|
|
21
39
|
export class WorkflowValidator {
|
|
22
40
|
errors = [];
|
|
23
41
|
warnings = [];
|
|
@@ -478,8 +496,30 @@ export class WorkflowValidator {
|
|
|
478
496
|
if (sourceType === 'STEP' && targetType === 'STEP') {
|
|
479
497
|
return;
|
|
480
498
|
}
|
|
499
|
+
// Block coercion on FUNCTION ports — coercing a function value is nonsensical
|
|
500
|
+
if (conn.coerce && (sourceType === 'FUNCTION' || targetType === 'FUNCTION')) {
|
|
501
|
+
this.errors.push({
|
|
502
|
+
type: 'error',
|
|
503
|
+
code: 'COERCE_ON_FUNCTION_PORT',
|
|
504
|
+
message: `Coercion \`as ${conn.coerce}\` cannot be used on FUNCTION ports in connection "${fromNode}.${fromPort}" → "${toNode}.${toPort}". FUNCTION values cannot be meaningfully coerced.`,
|
|
505
|
+
connection: conn,
|
|
506
|
+
location: connLocation,
|
|
507
|
+
});
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
481
510
|
// Same type - check for structural compatibility if both are OBJECT
|
|
482
511
|
if (sourceType === targetType) {
|
|
512
|
+
// Redundant coerce: same types don't need coercion
|
|
513
|
+
if (conn.coerce) {
|
|
514
|
+
this.warnings.push({
|
|
515
|
+
type: 'warning',
|
|
516
|
+
code: 'REDUNDANT_COERCE',
|
|
517
|
+
message: `Coercion \`as ${conn.coerce}\` on connection "${fromNode}.${fromPort}" → "${toNode}.${toPort}" is redundant — source and target are both ${sourceType}.`,
|
|
518
|
+
connection: conn,
|
|
519
|
+
location: connLocation,
|
|
520
|
+
});
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
483
523
|
// For OBJECT types, check if tsType differs (structural mismatch)
|
|
484
524
|
if (sourceType === 'OBJECT' && sourcePortDef.tsType && targetPortDef.tsType) {
|
|
485
525
|
const normalizedSource = this.normalizeTypeString(sourcePortDef.tsType);
|
|
@@ -504,6 +544,22 @@ export class WorkflowValidator {
|
|
|
504
544
|
if (sourceType === 'ANY' || targetType === 'ANY') {
|
|
505
545
|
return;
|
|
506
546
|
}
|
|
547
|
+
// Validate explicit coerce: check that produced type matches target
|
|
548
|
+
if (conn.coerce) {
|
|
549
|
+
const producedType = COERCE_OUTPUT_TYPE[conn.coerce];
|
|
550
|
+
if (producedType === targetType) {
|
|
551
|
+
return; // Coerce correctly resolves the mismatch
|
|
552
|
+
}
|
|
553
|
+
// Wrong coerce type — produced type doesn't match target
|
|
554
|
+
pushTypeIssue({
|
|
555
|
+
type: 'warning',
|
|
556
|
+
code: 'COERCE_TYPE_MISMATCH',
|
|
557
|
+
message: `Coercion \`as ${conn.coerce}\` produces ${producedType} but target port "${toPort}" on "${toNode}" expects ${targetType}. Use \`as ${suggestCoerceType(targetType)}\` instead.`,
|
|
558
|
+
connection: conn,
|
|
559
|
+
location: connLocation,
|
|
560
|
+
}, true);
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
507
563
|
// Safe coercions (no warning)
|
|
508
564
|
const safeCoercions = [
|
|
509
565
|
['NUMBER', 'STRING'],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@synergenius/flow-weaver",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.3",
|
|
4
4
|
"description": "Deterministic workflow compiler for AI agents. Compiles to standalone TypeScript, no runtime dependencies.",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -145,6 +145,7 @@
|
|
|
145
145
|
"@types/js-yaml": "^4.0.9",
|
|
146
146
|
"@types/node": "^20.11.0",
|
|
147
147
|
"@types/react": "^19.0.0",
|
|
148
|
+
"@vitest/coverage-v8": "^4.0.18",
|
|
148
149
|
"esbuild": "^0.27.2",
|
|
149
150
|
"prettier": "^3.1.1",
|
|
150
151
|
"rimraf": "6.1.2",
|