@synergenius/flow-weaver 0.10.2 → 0.10.4

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.
@@ -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
  /**
@@ -53839,7 +53974,7 @@ function buildCoerceSuggestion(quoted, targetType) {
53839
53974
  if (portRefs.length < 2) return null;
53840
53975
  const coerceType = COERCE_TARGET_TYPES[targetType.toUpperCase()] || targetType.toLowerCase();
53841
53976
  if (!["string", "number", "boolean", "json", "object"].includes(coerceType)) return null;
53842
- return `@coerce c1 ${portRefs[0]} -> ${portRefs[1]} as ${coerceType}`;
53977
+ return `@connect ${portRefs[0]} -> ${portRefs[1]} as ${coerceType}`;
53843
53978
  }
53844
53979
  function extractCyclePath(message) {
53845
53980
  const match2 = message.match(/:\s*(.+ -> .+)/);
@@ -53948,7 +54083,7 @@ var errorMappers = {
53948
54083
  return {
53949
54084
  title: "Type Mismatch",
53950
54085
  explanation: `Type mismatch: you're connecting a ${source} to a ${target}. The value will be automatically converted, but this might cause unexpected behavior.`,
53951
- fix: coerceSuggestion ? `Add an explicit coercion: \`${coerceSuggestion}\`, change one of the port types, or use @strictTypes to turn this into an error.` : `Add a @coerce annotation between the two ports, change one of the port types, or use @strictTypes to turn this into an error.`,
54086
+ fix: coerceSuggestion ? `Use \`${coerceSuggestion}\` for explicit coercion, change one of the port types, or use @strictTypes to turn this into an error.` : `Add \`as <type>\` to the @connect annotation (e.g. \`as string\`), change one of the port types, or use @strictTypes to turn this into an error.`,
53952
54087
  code: error2.code
53953
54088
  };
53954
54089
  },
@@ -54047,7 +54182,7 @@ var errorMappers = {
54047
54182
  return {
54048
54183
  title: "Type Incompatible",
54049
54184
  explanation: `Type mismatch: ${source} to ${target}. With @strictTypes enabled, this is an error instead of a warning.`,
54050
- fix: coerceSuggestion ? `Add an explicit coercion: \`${coerceSuggestion}\`, change one of the port types, or remove @strictTypes to allow implicit coercions.` : `Add a @coerce annotation between the ports, change one of the port types, or remove @strictTypes to allow implicit coercions.`,
54185
+ fix: coerceSuggestion ? `Use \`${coerceSuggestion}\` for explicit coercion, change one of the port types, or remove @strictTypes to allow implicit coercions.` : `Add \`as <type>\` to the @connect annotation (e.g. \`as number\`), change one of the port types, or remove @strictTypes to allow implicit coercions.`,
54051
54186
  code: error2.code
54052
54187
  };
54053
54188
  },
@@ -54060,7 +54195,7 @@ var errorMappers = {
54060
54195
  return {
54061
54196
  title: "Unusual Type Coercion",
54062
54197
  explanation: `Converting ${source} to ${target} is technically valid but semantically unusual and may produce unexpected behavior.`,
54063
- fix: coerceSuggestion ? `If intentional, add an explicit coercion: \`${coerceSuggestion}\`, or use @strictTypes to enforce type safety.` : `If intentional, add an explicit @coerce annotation, or use @strictTypes to enforce type safety.`,
54198
+ fix: coerceSuggestion ? `If intentional, use \`${coerceSuggestion}\` for explicit coercion, or use @strictTypes to enforce type safety.` : `If intentional, add \`as <type>\` to the @connect annotation, or use @strictTypes to enforce type safety.`,
54064
54199
  code: error2.code
54065
54200
  };
54066
54201
  },
@@ -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";
@@ -54294,7 +54464,7 @@ var errorMappers = {
54294
54464
  return {
54295
54465
  title: "Lossy Type Conversion",
54296
54466
  explanation: `Converting ${source} to ${target} may lose data or produce unexpected results (e.g., NaN, truncation).`,
54297
- fix: coerceSuggestion ? `Add an explicit coercion: \`${coerceSuggestion}\`, or use @strictTypes to enforce type safety.` : `Add an explicit conversion with @coerce, or use @strictTypes to enforce type safety.`,
54467
+ fix: coerceSuggestion ? `Use \`${coerceSuggestion}\` for explicit coercion, or use @strictTypes to enforce type safety.` : `Add \`as <type>\` to the @connect annotation for explicit conversion, or use @strictTypes to enforce type safety.`,
54298
54468
  code: error2.code
54299
54469
  };
54300
54470
  },
@@ -61418,24 +61588,46 @@ path[data-source].port-hover { opacity: 1; }
61418
61588
  /* Info panel */
61419
61589
  #info-panel {
61420
61590
  position: fixed; bottom: 52px; left: 16px;
61421
- max-width: 320px; min-width: 200px;
61591
+ max-width: 480px; min-width: 260px;
61422
61592
  background: ${surfaceMain}; border: 1px solid ${borderSubtle};
61423
61593
  border-radius: 8px; padding: 12px 16px;
61424
61594
  font-size: 13px; line-height: 1.5;
61425
61595
  box-shadow: 0 2px 8px rgba(0,0,0,0.2);
61426
61596
  z-index: 10; display: none;
61597
+ max-height: calc(100vh - 120px); overflow-y: auto;
61427
61598
  }
61428
61599
  #info-panel.visible { display: block; }
61429
61600
  #info-panel h3 {
61430
61601
  font-size: 14px; font-weight: 700; margin-bottom: 6px;
61431
61602
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
61432
61603
  }
61604
+ #info-panel .node-desc { color: ${textMed}; font-size: 12px; margin-bottom: 8px; font-style: italic; }
61433
61605
  #info-panel .info-section { margin-bottom: 6px; }
61434
61606
  #info-panel .info-label { font-size: 11px; font-weight: 600; color: ${textLow}; text-transform: uppercase; letter-spacing: 0.5px; }
61435
61607
  #info-panel .info-value { color: ${textMed}; }
61436
61608
  #info-panel .port-list { list-style: none; padding: 0; }
61437
61609
  #info-panel .port-list li { padding: 1px 0; }
61438
61610
  #info-panel .port-list li::before { content: '\\2022'; margin-right: 6px; color: ${textLow}; }
61611
+ #info-panel .port-type { color: ${textLow}; font-size: 11px; margin-left: 4px; font-family: 'SF Mono', 'Fira Code', 'Consolas', monospace; }
61612
+ #info-panel pre {
61613
+ background: ${isDark ? "#161625" : "#f0f1fa"}; border: 1px solid ${borderSubtle};
61614
+ border-radius: 6px; padding: 10px; overflow-x: auto;
61615
+ font-family: 'SF Mono', 'Fira Code', 'Consolas', monospace;
61616
+ font-size: 12px; line-height: 1.6; white-space: pre;
61617
+ max-height: 300px; overflow-y: auto; margin: 6px 0 0;
61618
+ color: ${isDark ? "#e6edf3" : "#1a2340"};
61619
+ }
61620
+ .hl-kw { color: ${isDark ? "#8e9eff" : "#4040bf"}; }
61621
+ .hl-str { color: ${isDark ? "#ff7b72" : "#c4432b"}; }
61622
+ .hl-num { color: ${isDark ? "#f0a050" : "#b35e14"}; }
61623
+ .hl-cm { color: #6a737d; font-style: italic; }
61624
+ .hl-fn { color: ${isDark ? "#d2a8ff" : "#7c3aed"}; }
61625
+ .hl-ty { color: ${isDark ? "#d2a8ff" : "#7c3aed"}; }
61626
+ .hl-pn { color: ${isDark ? "#b8bdd0" : "#4a5578"}; }
61627
+ .hl-ann { color: ${isDark ? "#8e9eff" : "#4040bf"}; font-weight: 600; font-style: normal; }
61628
+ .hl-arr { color: ${isDark ? "#79c0ff" : "#0969da"}; font-weight: 600; font-style: normal; }
61629
+ .hl-id { color: ${isDark ? "#e6edf3" : "#1a2340"}; font-style: normal; }
61630
+ .hl-sc { color: ${isDark ? "#d2a8ff" : "#7c3aed"}; font-style: italic; }
61439
61631
 
61440
61632
  /* Branding badge */
61441
61633
  #branding {
@@ -61490,8 +61682,8 @@ path[data-source].port-hover { opacity: 1; }
61490
61682
  <circle cx="10" cy="10" r="1.5" fill="${dotColor}" opacity="0.6"/>
61491
61683
  </pattern>
61492
61684
  </defs>
61493
- <rect x="-100000" y="-100000" width="200000" height="200000" fill="${bg}"/>
61494
- <rect x="-100000" y="-100000" width="200000" height="200000" fill="url(#viewer-dots)"/>
61685
+ <rect x="-100000" y="-100000" width="200000" height="200000" fill="${bg}" pointer-events="none"/>
61686
+ <rect x="-100000" y="-100000" width="200000" height="200000" fill="url(#viewer-dots)" pointer-events="none"/>
61495
61687
  <g id="diagram">${inner}</g>
61496
61688
  </svg>
61497
61689
  <div id="controls">
@@ -61521,6 +61713,7 @@ path[data-source].port-hover { opacity: 1; }
61521
61713
  </a>
61522
61714
  <div id="scroll-hint">Use <kbd id="mod-key">Ctrl</kbd> + scroll to zoom</div>
61523
61715
  <div id="studio-hint">Like rearranging? <a href="https://flowweaver.ai" target="_blank" rel="noopener">Flow Weaver Studio</a> saves your layouts.</div>
61716
+ <script>var nodeSources = ${JSON.stringify(options.nodeSources ?? {})};</script>
61524
61717
  <script>
61525
61718
  (function() {
61526
61719
  'use strict';
@@ -61605,6 +61798,7 @@ path[data-source].port-hover { opacity: 1; }
61605
61798
 
61606
61799
  // ---- Pan (drag) + Node drag ----
61607
61800
  var draggedNodeId = null, dragNodeStart = null, didDragNode = false;
61801
+ var clickTarget = null; // stash the real target before setPointerCapture steals it
61608
61802
  var dragCount = 0, nudgeIndex = 0, nudgeTimer = null;
61609
61803
  var nudgeMessages = [
61610
61804
  'Like rearranging? <a href="https://flowweaver.ai" target="_blank" rel="noopener">Flow Weaver Studio</a> saves your layouts.',
@@ -61619,6 +61813,8 @@ path[data-source].port-hover { opacity: 1; }
61619
61813
 
61620
61814
  canvas.addEventListener('pointerdown', function(e) {
61621
61815
  if (e.button !== 0) return;
61816
+ clickTarget = e.target; // stash before setPointerCapture redirects events
61817
+ didDrag = false;
61622
61818
  // Check if clicking on a node body (walk up to detect data-node-id)
61623
61819
  var t = e.target;
61624
61820
  while (t && t !== canvas) {
@@ -61635,7 +61831,6 @@ path[data-source].port-hover { opacity: 1; }
61635
61831
  }
61636
61832
  // Canvas pan
61637
61833
  pointerDown = true;
61638
- didDrag = false;
61639
61834
  dragLast = { x: e.clientX, y: e.clientY };
61640
61835
  canvas.setPointerCapture(e.pointerId);
61641
61836
  });
@@ -62088,14 +62283,28 @@ path[data-source].port-hover { opacity: 1; }
62088
62283
  // Build info panel
62089
62284
  infoTitle.textContent = labelText;
62090
62285
  var html = '';
62286
+ var src = nodeSources[nodeId];
62287
+ if (src && src.description) {
62288
+ html += '<div class="node-desc">' + escapeH(src.description) + '</div>';
62289
+ }
62290
+ var portInfo = (src && src.ports) ? src.ports : {};
62291
+ function portLabel(name) {
62292
+ var p = portInfo[name];
62293
+ var label = escapeH(name);
62294
+ if (p) {
62295
+ var typeStr = p.tsType || p.type;
62296
+ if (typeStr) label += ' <span class="port-type">' + escapeH(typeStr) + '</span>';
62297
+ }
62298
+ return label;
62299
+ }
62091
62300
  if (inputs.length) {
62092
62301
  html += '<div class="info-section"><div class="info-label">Inputs</div><ul class="port-list">';
62093
- inputs.forEach(function(n) { html += '<li>' + escapeH(n) + '</li>'; });
62302
+ inputs.forEach(function(n) { html += '<li>' + portLabel(n) + '</li>'; });
62094
62303
  html += '</ul></div>';
62095
62304
  }
62096
62305
  if (outputs.length) {
62097
62306
  html += '<div class="info-section"><div class="info-label">Outputs</div><ul class="port-list">';
62098
- outputs.forEach(function(n) { html += '<li>' + escapeH(n) + '</li>'; });
62307
+ outputs.forEach(function(n) { html += '<li>' + portLabel(n) + '</li>'; });
62099
62308
  html += '</ul></div>';
62100
62309
  }
62101
62310
  if (connectedNodes.size) {
@@ -62103,6 +62312,10 @@ path[data-source].port-hover { opacity: 1; }
62103
62312
  html += Array.from(connectedNodes).map(escapeH).join(', ');
62104
62313
  html += '</div></div>';
62105
62314
  }
62315
+ if (src && src.source) {
62316
+ html += '<div class="info-section"><div class="info-label">Source</div>';
62317
+ html += '<pre><code>' + highlightTS(src.source) + '</code></pre></div>';
62318
+ }
62106
62319
  infoBody.innerHTML = html;
62107
62320
  infoPanel.classList.add('visible');
62108
62321
  }
@@ -62111,10 +62324,130 @@ path[data-source].port-hover { opacity: 1; }
62111
62324
  return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
62112
62325
  }
62113
62326
 
62327
+ var fwAnnotations = 'flowWeaver,input,output,step,node,connect,param,returns,fwImport,label,scope,position,color,icon,tag,map,path,name,description,expression,executeWhen,pullExecution,strictTypes,autoConnect,port,trigger,cancelOn,retries,timeout,throttle';
62328
+
62329
+ function highlightJSDoc(block) {
62330
+ var annSet = fwAnnotations.split(',');
62331
+ var out = '';
62332
+ var re = /(@[a-zA-Z]+)|(-&gt;)|(\\.[a-zA-Z_][a-zA-Z0-9_]*)|("[^"]*")|('\\''[^'\\'']*'\\'')|(-?[0-9]+(?:\\.[0-9]+)?)|([a-zA-Z_][a-zA-Z0-9_]*)|([^@a-zA-Z0-9"'\\-.]+)/g;
62333
+ var m;
62334
+ while ((m = re.exec(block)) !== null) {
62335
+ if (m[1]) {
62336
+ var tag = m[1].slice(1);
62337
+ if (annSet.indexOf(tag) >= 0) {
62338
+ out += '<span class="hl-ann">' + m[0] + '</span>';
62339
+ } else {
62340
+ out += '<span class="hl-ann">' + m[0] + '</span>';
62341
+ }
62342
+ } else if (m[2]) {
62343
+ out += '<span class="hl-arr">' + m[2] + '</span>';
62344
+ } else if (m[3]) {
62345
+ // .portName scope reference
62346
+ out += '<span class="hl-sc">' + m[3] + '</span>';
62347
+ } else if (m[4] || m[5]) {
62348
+ out += '<span class="hl-str">' + (m[4] || m[5]) + '</span>';
62349
+ } else if (m[6]) {
62350
+ out += '<span class="hl-num">' + m[6] + '</span>';
62351
+ } else if (m[7]) {
62352
+ var tys = 'string,number,boolean,any,void,never,unknown,STEP,STRING,NUMBER,BOOLEAN,ARRAY,OBJECT,FUNCTION,ANY';
62353
+ if (tys.split(',').indexOf(m[7]) >= 0) {
62354
+ out += '<span class="hl-ty">' + m[7] + '</span>';
62355
+ } else {
62356
+ out += '<span class="hl-id">' + m[7] + '</span>';
62357
+ }
62358
+ } else {
62359
+ out += m[0];
62360
+ }
62361
+ }
62362
+ return out;
62363
+ }
62364
+
62365
+ function highlightTS(code) {
62366
+ var tokens = [];
62367
+ var i = 0;
62368
+ while (i < code.length) {
62369
+ // Line comments
62370
+ if (code[i] === '/' && code[i+1] === '/') {
62371
+ var end = code.indexOf('\\n', i);
62372
+ if (end === -1) end = code.length;
62373
+ tokens.push({ t: 'cm', v: code.slice(i, end) });
62374
+ i = end;
62375
+ continue;
62376
+ }
62377
+ // Block comments (detect JSDoc for annotation highlighting)
62378
+ if (code[i] === '/' && code[i+1] === '*') {
62379
+ var end = code.indexOf('*/', i + 2);
62380
+ if (end === -1) end = code.length; else end += 2;
62381
+ var block = code.slice(i, end);
62382
+ var hasFW = /@(flowWeaver|input|output|step|node|connect|param|returns)/.test(block);
62383
+ if (hasFW) {
62384
+ tokens.push({ t: 'jsdoc', v: block });
62385
+ } else {
62386
+ tokens.push({ t: 'cm', v: block });
62387
+ }
62388
+ i = end;
62389
+ continue;
62390
+ }
62391
+ // Strings
62392
+ if (code[i] === "'" || code[i] === '"' || code[i] === '\`') {
62393
+ var q = code[i], j = i + 1;
62394
+ while (j < code.length && code[j] !== q) { if (code[j] === '\\\\') j++; j++; }
62395
+ tokens.push({ t: 'str', v: code.slice(i, j + 1) });
62396
+ i = j + 1;
62397
+ continue;
62398
+ }
62399
+ // Numbers
62400
+ if (/[0-9]/.test(code[i]) && (i === 0 || /[^a-zA-Z_$]/.test(code[i-1]))) {
62401
+ var j = i;
62402
+ while (j < code.length && /[0-9a-fA-FxX._]/.test(code[j])) j++;
62403
+ tokens.push({ t: 'num', v: code.slice(i, j) });
62404
+ i = j;
62405
+ continue;
62406
+ }
62407
+ // Words
62408
+ if (/[a-zA-Z_$]/.test(code[i])) {
62409
+ var j = i;
62410
+ while (j < code.length && /[a-zA-Z0-9_$]/.test(code[j])) j++;
62411
+ var w = code.slice(i, j);
62412
+ var kws = 'async,await,break,case,catch,class,const,continue,default,delete,do,else,export,extends,finally,for,from,function,if,import,in,instanceof,let,new,of,return,switch,throw,try,typeof,var,void,while,yield';
62413
+ var tys = 'string,number,boolean,any,void,never,unknown,null,undefined,true,false,Promise,Record,Map,Set,Array,Partial,Required,Omit,Pick';
62414
+ if (kws.split(',').indexOf(w) >= 0) {
62415
+ tokens.push({ t: 'kw', v: w });
62416
+ } else if (tys.split(',').indexOf(w) >= 0) {
62417
+ tokens.push({ t: 'ty', v: w });
62418
+ } else if (j < code.length && code[j] === '(') {
62419
+ tokens.push({ t: 'fn', v: w });
62420
+ } else {
62421
+ tokens.push({ t: '', v: w });
62422
+ }
62423
+ i = j;
62424
+ continue;
62425
+ }
62426
+ // Punctuation
62427
+ if (/[{}()\\[\\];:.,<>=!&|?+\\-*/%^~@]/.test(code[i])) {
62428
+ tokens.push({ t: 'pn', v: code[i] });
62429
+ i++;
62430
+ continue;
62431
+ }
62432
+ // Whitespace and other
62433
+ tokens.push({ t: '', v: code[i] });
62434
+ i++;
62435
+ }
62436
+ return tokens.map(function(tk) {
62437
+ if (tk.t === 'jsdoc') {
62438
+ return '<span class="hl-cm">' + highlightJSDoc(escapeH(tk.v)) + '</span>';
62439
+ }
62440
+ var v = escapeH(tk.v);
62441
+ return tk.t ? '<span class="hl-' + tk.t + '">' + v + '</span>' : v;
62442
+ }).join('');
62443
+ }
62444
+
62114
62445
  // Delegate click: port click > node click > background
62446
+ // Use clickTarget (stashed from pointerdown) because setPointerCapture redirects click to canvas
62115
62447
  canvas.addEventListener('click', function(e) {
62116
62448
  if (didDrag || didDragNode) { didDragNode = false; return; }
62117
- var target = e.target;
62449
+ var target = clickTarget || e.target;
62450
+ clickTarget = null;
62118
62451
  while (target && target !== canvas) {
62119
62452
  if (target.hasAttribute && target.hasAttribute('data-port-id')) {
62120
62453
  e.stopPropagation();
@@ -62155,24 +62488,57 @@ function fileToSVG(filePath, options = {}) {
62155
62488
  return pickAndRender(result.workflows, options);
62156
62489
  }
62157
62490
  function fileToHTML(filePath, options = {}) {
62158
- const svg = fileToSVG(filePath, options);
62159
- return wrapSVGInHTML(svg, { title: options.workflowName, theme: options.theme });
62491
+ const result = parser.parse(filePath);
62492
+ const ast = pickWorkflow(result.workflows, options);
62493
+ const svg = workflowToSVG(ast, options);
62494
+ return wrapSVGInHTML(svg, { title: options.workflowName ?? ast.name, theme: options.theme, nodeSources: buildNodeSourceMap(ast) });
62160
62495
  }
62161
- function pickAndRender(workflows, options) {
62496
+ function buildNodeSourceMap(ast) {
62497
+ const typeMap = new Map(ast.nodeTypes.map((nt) => [nt.functionName, nt]));
62498
+ const map3 = {};
62499
+ for (const inst of ast.instances) {
62500
+ const nt = typeMap.get(inst.nodeType);
62501
+ if (!nt) continue;
62502
+ const ports = {};
62503
+ for (const [name, def] of Object.entries(nt.inputs ?? {})) {
62504
+ ports[name] = { type: def.dataType, tsType: def.tsType };
62505
+ }
62506
+ for (const [name, def] of Object.entries(nt.outputs ?? {})) {
62507
+ ports[name] = { type: def.dataType, tsType: def.tsType };
62508
+ }
62509
+ map3[inst.id] = { description: nt.description, source: nt.functionText, ports };
62510
+ }
62511
+ const startPorts = {};
62512
+ for (const [name, def] of Object.entries(ast.startPorts ?? {})) {
62513
+ startPorts[name] = { type: def.dataType, tsType: def.tsType };
62514
+ }
62515
+ if (Object.keys(startPorts).length) {
62516
+ map3["Start"] = { description: ast.description, ports: startPorts };
62517
+ }
62518
+ const exitPorts = {};
62519
+ for (const [name, def] of Object.entries(ast.exitPorts ?? {})) {
62520
+ exitPorts[name] = { type: def.dataType, tsType: def.tsType };
62521
+ }
62522
+ if (Object.keys(exitPorts).length) {
62523
+ map3["Exit"] = { ports: exitPorts };
62524
+ }
62525
+ return map3;
62526
+ }
62527
+ function pickWorkflow(workflows, options) {
62162
62528
  if (workflows.length === 0) {
62163
62529
  throw new Error("No workflows found in source code");
62164
62530
  }
62165
- let workflow;
62166
62531
  if (options.workflowName) {
62167
62532
  const found = workflows.find((w) => w.name === options.workflowName);
62168
62533
  if (!found) {
62169
62534
  throw new Error(`Workflow "${options.workflowName}" not found. Available: ${workflows.map((w) => w.name).join(", ")}`);
62170
62535
  }
62171
- workflow = found;
62172
- } else {
62173
- workflow = workflows[0];
62536
+ return found;
62174
62537
  }
62175
- return workflowToSVG(workflow, options);
62538
+ return workflows[0];
62539
+ }
62540
+ function pickAndRender(workflows, options) {
62541
+ return workflowToSVG(pickWorkflow(workflows, options), options);
62176
62542
  }
62177
62543
 
62178
62544
  // src/cli/commands/diagram.ts
@@ -96730,7 +97096,7 @@ function displayInstalledPackage(pkg) {
96730
97096
  }
96731
97097
 
96732
97098
  // src/cli/index.ts
96733
- var version2 = true ? "0.10.2" : "0.0.0-dev";
97099
+ var version2 = true ? "0.10.4" : "0.0.0-dev";
96734
97100
  var program2 = new Command();
96735
97101
  program2.name("flow-weaver").description("Flow Weaver Annotations - Compile and validate workflow files").version(version2, "-v, --version", "Output the current version");
96736
97102
  program2.configureOutput({