@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.
@@ -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
- return { source, target };
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
- return visitorInstance.visit(cst);
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.
@@ -12263,7 +12263,21 @@ var init_theme = __esm({
12263
12263
  });
12264
12264
 
12265
12265
  // src/validator.ts
12266
- var DOCS_BASE, ERROR_DOC_URLS, WorkflowValidator, validator;
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
- lines.push(
28297
- `${indent}const ${varName} = ${getCall}({ id: '${sourceNode}', portName: '${sourcePort}', executionIndex: ${sourceIdx}${nonNullAssert} }) as ${portType};`
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, _idx) => {
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
- attempts.push(
28317
- `(${sourceIdx} !== undefined ? ${getCall}({ id: '${sourceNode}', portName: '${sourcePort}', executionIndex: ${sourceIdx} }) : undefined)`
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
- return { source, target };
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
- return visitorInstance3.visit(cst);
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.2" : "0.0.0-dev";
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({
@@ -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
- lines.push(`${indent}const ${varName} = ${getCall}({ id: '${sourceNode}', portName: '${sourcePort}', executionIndex: ${sourceIdx}${nonNullAssert} }) as ${portType};`);
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, _idx) => {
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
- attempts.push(`(${sourceIdx} !== undefined ? ${getCall}({ id: '${sourceNode}', portName: '${sourcePort}', executionIndex: ${sourceIdx} }) : undefined)`);
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);
@@ -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.2",
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",