@synergenius/flow-weaver 0.10.2 → 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/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 +185 -15
- 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 +1 -1
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"]
|
|
@@ -28164,6 +28225,43 @@ function generateScopeFunctionClosure(scopeName, parentNodeId, parentNodeType, w
|
|
|
28164
28225
|
|
|
28165
28226
|
// src/generator/code-utils.ts
|
|
28166
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
|
+
}
|
|
28167
28265
|
function toValidIdentifier(nodeId) {
|
|
28168
28266
|
let sanitized = nodeId.replace(/[^a-zA-Z0-9_$]/g, "_");
|
|
28169
28267
|
if (/^[0-9]/.test(sanitized)) {
|
|
@@ -28293,9 +28391,18 @@ function buildNodeArgumentsWithContext(opts) {
|
|
|
28293
28391
|
`${indent}const ${varName} = ${varName}_resolved.fn as ${portType};`
|
|
28294
28392
|
);
|
|
28295
28393
|
} else {
|
|
28296
|
-
|
|
28297
|
-
|
|
28298
|
-
)
|
|
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
|
+
}
|
|
28299
28406
|
}
|
|
28300
28407
|
} else {
|
|
28301
28408
|
const validConnections = connections.filter((conn) => {
|
|
@@ -28309,13 +28416,19 @@ function buildNodeArgumentsWithContext(opts) {
|
|
|
28309
28416
|
return;
|
|
28310
28417
|
}
|
|
28311
28418
|
const attempts = [];
|
|
28312
|
-
validConnections.forEach((conn
|
|
28419
|
+
validConnections.forEach((conn) => {
|
|
28313
28420
|
const sourceNode = conn.from.node;
|
|
28314
28421
|
const sourcePort = conn.from.port;
|
|
28315
28422
|
const sourceIdx = isStartNode(sourceNode) ? "startIdx" : `${toValidIdentifier(sourceNode)}Idx`;
|
|
28316
|
-
|
|
28317
|
-
|
|
28318
|
-
|
|
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
|
+
}
|
|
28319
28432
|
});
|
|
28320
28433
|
const ternary = attempts.join(" ?? ");
|
|
28321
28434
|
const portType = mapToTypeScript(portConfig.dataType, portConfig.tsType);
|
|
@@ -43545,12 +43658,16 @@ var ConnectParser = class extends CstParser {
|
|
|
43545
43658
|
super(allTokens);
|
|
43546
43659
|
this.performSelfAnalysis();
|
|
43547
43660
|
}
|
|
43548
|
-
// Entry rule for connect line
|
|
43661
|
+
// Entry rule for connect line: @connect A.port -> B.port [as type]
|
|
43549
43662
|
connectLine = this.RULE("connectLine", () => {
|
|
43550
43663
|
this.CONSUME(ConnectTag);
|
|
43551
43664
|
this.SUBRULE(this.portRef, { LABEL: "sourceRef" });
|
|
43552
43665
|
this.CONSUME(Arrow);
|
|
43553
43666
|
this.SUBRULE2(this.portRef, { LABEL: "targetRef" });
|
|
43667
|
+
this.OPTION(() => {
|
|
43668
|
+
this.CONSUME(AsKeyword);
|
|
43669
|
+
this.CONSUME(Identifier, { LABEL: "coerceType" });
|
|
43670
|
+
});
|
|
43554
43671
|
});
|
|
43555
43672
|
// node.port or node.port:scope
|
|
43556
43673
|
portRef = this.RULE("portRef", () => {
|
|
@@ -43573,7 +43690,17 @@ var ConnectVisitor = class extends BaseVisitor3 {
|
|
|
43573
43690
|
connectLine(ctx) {
|
|
43574
43691
|
const source = this.visit(ctx.sourceRef);
|
|
43575
43692
|
const target = this.visit(ctx.targetRef);
|
|
43576
|
-
|
|
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;
|
|
43577
43704
|
}
|
|
43578
43705
|
portRef(ctx) {
|
|
43579
43706
|
const nodeId = ctx.nodeId[0].image;
|
|
@@ -43607,7 +43734,14 @@ function parseConnectLine(input, warnings) {
|
|
|
43607
43734
|
);
|
|
43608
43735
|
return null;
|
|
43609
43736
|
}
|
|
43610
|
-
|
|
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;
|
|
43611
43745
|
}
|
|
43612
43746
|
function getConnectGrammar() {
|
|
43613
43747
|
return parserInstance3.getSerializedGastProductions();
|
|
@@ -45307,7 +45441,7 @@ var JSDocParser = class {
|
|
|
45307
45441
|
warnings.push(`Invalid @connect tag format: @connect ${comment}`);
|
|
45308
45442
|
return;
|
|
45309
45443
|
}
|
|
45310
|
-
const { source, target } = result;
|
|
45444
|
+
const { source, target, coerce: coerce2 } = result;
|
|
45311
45445
|
const line = tag.getStartLineNumber();
|
|
45312
45446
|
config2.connections.push({
|
|
45313
45447
|
from: {
|
|
@@ -45320,7 +45454,8 @@ var JSDocParser = class {
|
|
|
45320
45454
|
port: target.portName,
|
|
45321
45455
|
...target.scope && { scope: target.scope }
|
|
45322
45456
|
},
|
|
45323
|
-
sourceLocation: { line, column: 0 }
|
|
45457
|
+
sourceLocation: { line, column: 0 },
|
|
45458
|
+
...coerce2 && { coerce: coerce2 }
|
|
45324
45459
|
});
|
|
45325
45460
|
}
|
|
45326
45461
|
/**
|
|
@@ -54285,6 +54420,41 @@ var errorMappers = {
|
|
|
54285
54420
|
code: error2.code
|
|
54286
54421
|
};
|
|
54287
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
|
+
},
|
|
54288
54458
|
LOSSY_TYPE_COERCION(error2) {
|
|
54289
54459
|
const types2 = extractTypes(error2.message);
|
|
54290
54460
|
const source = types2?.source || "unknown";
|
|
@@ -96730,7 +96900,7 @@ function displayInstalledPackage(pkg) {
|
|
|
96730
96900
|
}
|
|
96731
96901
|
|
|
96732
96902
|
// src/cli/index.ts
|
|
96733
|
-
var version2 = true ? "0.10.
|
|
96903
|
+
var version2 = true ? "0.10.3" : "0.0.0-dev";
|
|
96734
96904
|
var program2 = new Command();
|
|
96735
96905
|
program2.name("flow-weaver").description("Flow Weaver Annotations - Compile and validate workflow files").version(version2, "-v, --version", "Output the current version");
|
|
96736
96906
|
program2.configureOutput({
|
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