@foresthubai/workflow-core 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (339) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +14 -0
  3. package/README.md +63 -0
  4. package/dist/api/index.d.ts +7 -0
  5. package/dist/api/index.d.ts.map +1 -0
  6. package/dist/api/index.js +2 -0
  7. package/dist/api/index.js.map +1 -0
  8. package/dist/api/workflow.d.ts +607 -0
  9. package/dist/api/workflow.d.ts.map +1 -0
  10. package/dist/api/workflow.js +6 -0
  11. package/dist/api/workflow.js.map +1 -0
  12. package/dist/channel/Channel.d.ts +10 -0
  13. package/dist/channel/Channel.d.ts.map +1 -0
  14. package/dist/channel/Channel.js +2 -0
  15. package/dist/channel/Channel.js.map +1 -0
  16. package/dist/channel/ChannelDefinition.d.ts +27 -0
  17. package/dist/channel/ChannelDefinition.d.ts.map +1 -0
  18. package/dist/channel/ChannelDefinition.js +50 -0
  19. package/dist/channel/ChannelDefinition.js.map +1 -0
  20. package/dist/channel/index.d.ts +7 -0
  21. package/dist/channel/index.d.ts.map +1 -0
  22. package/dist/channel/index.js +4 -0
  23. package/dist/channel/index.js.map +1 -0
  24. package/dist/channel/serialization.d.ts +17 -0
  25. package/dist/channel/serialization.d.ts.map +1 -0
  26. package/dist/channel/serialization.js +63 -0
  27. package/dist/channel/serialization.js.map +1 -0
  28. package/dist/deploy/index.d.ts +2 -0
  29. package/dist/deploy/index.d.ts.map +1 -0
  30. package/dist/deploy/index.js +2 -0
  31. package/dist/deploy/index.js.map +1 -0
  32. package/dist/deploy/requirements.d.ts +17 -0
  33. package/dist/deploy/requirements.d.ts.map +1 -0
  34. package/dist/deploy/requirements.js +41 -0
  35. package/dist/deploy/requirements.js.map +1 -0
  36. package/dist/diagnostics/__fixtures__/diagnosticFixtures.d.ts +28 -0
  37. package/dist/diagnostics/__fixtures__/diagnosticFixtures.d.ts.map +1 -0
  38. package/dist/diagnostics/__fixtures__/diagnosticFixtures.js +125 -0
  39. package/dist/diagnostics/__fixtures__/diagnosticFixtures.js.map +1 -0
  40. package/dist/diagnostics/diagnostics.d.ts +128 -0
  41. package/dist/diagnostics/diagnostics.d.ts.map +1 -0
  42. package/dist/diagnostics/diagnostics.js +783 -0
  43. package/dist/diagnostics/diagnostics.js.map +1 -0
  44. package/dist/diagnostics/index.d.ts +3 -0
  45. package/dist/diagnostics/index.d.ts.map +1 -0
  46. package/dist/diagnostics/index.js +2 -0
  47. package/dist/diagnostics/index.js.map +1 -0
  48. package/dist/edge/Edge.d.ts +22 -0
  49. package/dist/edge/Edge.d.ts.map +1 -0
  50. package/dist/edge/Edge.js +2 -0
  51. package/dist/edge/Edge.js.map +1 -0
  52. package/dist/edge/EdgeDefinition.d.ts +10 -0
  53. package/dist/edge/EdgeDefinition.d.ts.map +1 -0
  54. package/dist/edge/EdgeDefinition.js +36 -0
  55. package/dist/edge/EdgeDefinition.js.map +1 -0
  56. package/dist/edge/EdgeType.d.ts +6 -0
  57. package/dist/edge/EdgeType.d.ts.map +1 -0
  58. package/dist/edge/EdgeType.js +7 -0
  59. package/dist/edge/EdgeType.js.map +1 -0
  60. package/dist/edge/index.d.ts +6 -0
  61. package/dist/edge/index.d.ts.map +1 -0
  62. package/dist/edge/index.js +6 -0
  63. package/dist/edge/index.js.map +1 -0
  64. package/dist/edge/serialization.d.ts +18 -0
  65. package/dist/edge/serialization.d.ts.map +1 -0
  66. package/dist/edge/serialization.js +76 -0
  67. package/dist/edge/serialization.js.map +1 -0
  68. package/dist/expression/index.d.ts +5 -0
  69. package/dist/expression/index.d.ts.map +1 -0
  70. package/dist/expression/index.js +3 -0
  71. package/dist/expression/index.js.map +1 -0
  72. package/dist/expression/parser.d.ts +14 -0
  73. package/dist/expression/parser.d.ts.map +1 -0
  74. package/dist/expression/parser.js +309 -0
  75. package/dist/expression/parser.js.map +1 -0
  76. package/dist/expression/types.d.ts +11 -0
  77. package/dist/expression/types.d.ts.map +1 -0
  78. package/dist/expression/types.js +20 -0
  79. package/dist/expression/types.js.map +1 -0
  80. package/dist/function/FunctionDeclaration.d.ts +43 -0
  81. package/dist/function/FunctionDeclaration.d.ts.map +1 -0
  82. package/dist/function/FunctionDeclaration.js +17 -0
  83. package/dist/function/FunctionDeclaration.js.map +1 -0
  84. package/dist/function/index.d.ts +4 -0
  85. package/dist/function/index.d.ts.map +1 -0
  86. package/dist/function/index.js +3 -0
  87. package/dist/function/index.js.map +1 -0
  88. package/dist/function/serialization.d.ts +20 -0
  89. package/dist/function/serialization.d.ts.map +1 -0
  90. package/dist/function/serialization.js +26 -0
  91. package/dist/function/serialization.js.map +1 -0
  92. package/dist/id/index.d.ts +7 -0
  93. package/dist/id/index.d.ts.map +1 -0
  94. package/dist/id/index.js +9 -0
  95. package/dist/id/index.js.map +1 -0
  96. package/dist/index.d.ts +11 -0
  97. package/dist/index.d.ts.map +1 -0
  98. package/dist/index.js +14 -0
  99. package/dist/index.js.map +1 -0
  100. package/dist/memory/Memory.d.ts +12 -0
  101. package/dist/memory/Memory.d.ts.map +1 -0
  102. package/dist/memory/Memory.js +2 -0
  103. package/dist/memory/Memory.js.map +1 -0
  104. package/dist/memory/MemoryDefinition.d.ts +16 -0
  105. package/dist/memory/MemoryDefinition.d.ts.map +1 -0
  106. package/dist/memory/MemoryDefinition.js +2 -0
  107. package/dist/memory/MemoryDefinition.js.map +1 -0
  108. package/dist/memory/MemoryFileDefinition.d.ts +8 -0
  109. package/dist/memory/MemoryFileDefinition.d.ts.map +1 -0
  110. package/dist/memory/MemoryFileDefinition.js +36 -0
  111. package/dist/memory/MemoryFileDefinition.js.map +1 -0
  112. package/dist/memory/MemoryRegistry.d.ts +17 -0
  113. package/dist/memory/MemoryRegistry.d.ts.map +1 -0
  114. package/dist/memory/MemoryRegistry.js +29 -0
  115. package/dist/memory/MemoryRegistry.js.map +1 -0
  116. package/dist/memory/VectorDatabaseDefinition.d.ts +7 -0
  117. package/dist/memory/VectorDatabaseDefinition.d.ts.map +1 -0
  118. package/dist/memory/VectorDatabaseDefinition.js +20 -0
  119. package/dist/memory/VectorDatabaseDefinition.js.map +1 -0
  120. package/dist/memory/index.d.ts +9 -0
  121. package/dist/memory/index.d.ts.map +1 -0
  122. package/dist/memory/index.js +6 -0
  123. package/dist/memory/index.js.map +1 -0
  124. package/dist/memory/serialization.d.ts +11 -0
  125. package/dist/memory/serialization.d.ts.map +1 -0
  126. package/dist/memory/serialization.js +44 -0
  127. package/dist/memory/serialization.js.map +1 -0
  128. package/dist/migration/index.d.ts +5 -0
  129. package/dist/migration/index.d.ts.map +1 -0
  130. package/dist/migration/index.js +4 -0
  131. package/dist/migration/index.js.map +1 -0
  132. package/dist/migration/migrate.d.ts +11 -0
  133. package/dist/migration/migrate.d.ts.map +1 -0
  134. package/dist/migration/migrate.js +52 -0
  135. package/dist/migration/migrate.js.map +1 -0
  136. package/dist/migration/migrations.d.ts +22 -0
  137. package/dist/migration/migrations.d.ts.map +1 -0
  138. package/dist/migration/migrations.js +10 -0
  139. package/dist/migration/migrations.js.map +1 -0
  140. package/dist/migration/version.d.ts +9 -0
  141. package/dist/migration/version.d.ts.map +1 -0
  142. package/dist/migration/version.js +9 -0
  143. package/dist/migration/version.js.map +1 -0
  144. package/dist/model/LLMModelDefinition.d.ts +7 -0
  145. package/dist/model/LLMModelDefinition.d.ts.map +1 -0
  146. package/dist/model/LLMModelDefinition.js +11 -0
  147. package/dist/model/LLMModelDefinition.js.map +1 -0
  148. package/dist/model/Model.d.ts +21 -0
  149. package/dist/model/Model.d.ts.map +1 -0
  150. package/dist/model/Model.js +15 -0
  151. package/dist/model/Model.js.map +1 -0
  152. package/dist/model/ModelDefinition.d.ts +15 -0
  153. package/dist/model/ModelDefinition.d.ts.map +1 -0
  154. package/dist/model/ModelDefinition.js +2 -0
  155. package/dist/model/ModelDefinition.js.map +1 -0
  156. package/dist/model/ModelRegistry.d.ts +17 -0
  157. package/dist/model/ModelRegistry.d.ts.map +1 -0
  158. package/dist/model/ModelRegistry.js +27 -0
  159. package/dist/model/ModelRegistry.js.map +1 -0
  160. package/dist/model/index.d.ts +8 -0
  161. package/dist/model/index.d.ts.map +1 -0
  162. package/dist/model/index.js +5 -0
  163. package/dist/model/index.js.map +1 -0
  164. package/dist/model/serialization.d.ts +8 -0
  165. package/dist/model/serialization.d.ts.map +1 -0
  166. package/dist/model/serialization.js +25 -0
  167. package/dist/model/serialization.js.map +1 -0
  168. package/dist/node/AgentNode.d.ts +21 -0
  169. package/dist/node/AgentNode.d.ts.map +1 -0
  170. package/dist/node/AgentNode.js +60 -0
  171. package/dist/node/AgentNode.js.map +1 -0
  172. package/dist/node/DataNode.d.ts +14 -0
  173. package/dist/node/DataNode.d.ts.map +1 -0
  174. package/dist/node/DataNode.js +26 -0
  175. package/dist/node/DataNode.js.map +1 -0
  176. package/dist/node/FunctionNode.d.ts +24 -0
  177. package/dist/node/FunctionNode.d.ts.map +1 -0
  178. package/dist/node/FunctionNode.js +44 -0
  179. package/dist/node/FunctionNode.js.map +1 -0
  180. package/dist/node/InputNode.d.ts +46 -0
  181. package/dist/node/InputNode.d.ts.map +1 -0
  182. package/dist/node/InputNode.js +133 -0
  183. package/dist/node/InputNode.js.map +1 -0
  184. package/dist/node/LogicNode.d.ts +13 -0
  185. package/dist/node/LogicNode.d.ts.map +1 -0
  186. package/dist/node/LogicNode.js +19 -0
  187. package/dist/node/LogicNode.js.map +1 -0
  188. package/dist/node/MqttNode.d.ts +27 -0
  189. package/dist/node/MqttNode.d.ts.map +1 -0
  190. package/dist/node/MqttNode.js +96 -0
  191. package/dist/node/MqttNode.js.map +1 -0
  192. package/dist/node/Node.d.ts +49 -0
  193. package/dist/node/Node.d.ts.map +1 -0
  194. package/dist/node/Node.js +9 -0
  195. package/dist/node/Node.js.map +1 -0
  196. package/dist/node/NodeDefinition.d.ts +30 -0
  197. package/dist/node/NodeDefinition.d.ts.map +1 -0
  198. package/dist/node/NodeDefinition.js +2 -0
  199. package/dist/node/NodeDefinition.js.map +1 -0
  200. package/dist/node/NodeRegistry.d.ts +19 -0
  201. package/dist/node/NodeRegistry.d.ts.map +1 -0
  202. package/dist/node/NodeRegistry.js +65 -0
  203. package/dist/node/NodeRegistry.js.map +1 -0
  204. package/dist/node/OutputNode.d.ts +23 -0
  205. package/dist/node/OutputNode.d.ts.map +1 -0
  206. package/dist/node/OutputNode.js +62 -0
  207. package/dist/node/OutputNode.js.map +1 -0
  208. package/dist/node/ToolNode.d.ts +12 -0
  209. package/dist/node/ToolNode.d.ts.map +1 -0
  210. package/dist/node/ToolNode.js +18 -0
  211. package/dist/node/ToolNode.js.map +1 -0
  212. package/dist/node/TriggerNode.d.ts +67 -0
  213. package/dist/node/TriggerNode.d.ts.map +1 -0
  214. package/dist/node/TriggerNode.js +172 -0
  215. package/dist/node/TriggerNode.js.map +1 -0
  216. package/dist/node/constants.d.ts +16 -0
  217. package/dist/node/constants.d.ts.map +1 -0
  218. package/dist/node/constants.js +18 -0
  219. package/dist/node/constants.js.map +1 -0
  220. package/dist/node/index.d.ts +12 -0
  221. package/dist/node/index.d.ts.map +1 -0
  222. package/dist/node/index.js +9 -0
  223. package/dist/node/index.js.map +1 -0
  224. package/dist/node/methods.d.ts +73 -0
  225. package/dist/node/methods.d.ts.map +1 -0
  226. package/dist/node/methods.js +261 -0
  227. package/dist/node/methods.js.map +1 -0
  228. package/dist/node/serialization.d.ts +25 -0
  229. package/dist/node/serialization.d.ts.map +1 -0
  230. package/dist/node/serialization.js +525 -0
  231. package/dist/node/serialization.js.map +1 -0
  232. package/dist/parameter/OutputParameter.d.ts +69 -0
  233. package/dist/parameter/OutputParameter.d.ts.map +1 -0
  234. package/dist/parameter/OutputParameter.js +6 -0
  235. package/dist/parameter/OutputParameter.js.map +1 -0
  236. package/dist/parameter/Parameter.d.ts +152 -0
  237. package/dist/parameter/Parameter.d.ts.map +1 -0
  238. package/dist/parameter/Parameter.js +86 -0
  239. package/dist/parameter/Parameter.js.map +1 -0
  240. package/dist/parameter/index.d.ts +5 -0
  241. package/dist/parameter/index.d.ts.map +1 -0
  242. package/dist/parameter/index.js +3 -0
  243. package/dist/parameter/index.js.map +1 -0
  244. package/dist/variable/Variable.d.ts +25 -0
  245. package/dist/variable/Variable.d.ts.map +1 -0
  246. package/dist/variable/Variable.js +2 -0
  247. package/dist/variable/Variable.js.map +1 -0
  248. package/dist/variable/index.d.ts +3 -0
  249. package/dist/variable/index.d.ts.map +1 -0
  250. package/dist/variable/index.js +5 -0
  251. package/dist/variable/index.js.map +1 -0
  252. package/dist/variable/operations.d.ts +37 -0
  253. package/dist/variable/operations.d.ts.map +1 -0
  254. package/dist/variable/operations.js +88 -0
  255. package/dist/variable/operations.js.map +1 -0
  256. package/dist/workflow/Workflow.d.ts +38 -0
  257. package/dist/workflow/Workflow.d.ts.map +1 -0
  258. package/dist/workflow/Workflow.js +8 -0
  259. package/dist/workflow/Workflow.js.map +1 -0
  260. package/dist/workflow/index.d.ts +4 -0
  261. package/dist/workflow/index.d.ts.map +1 -0
  262. package/dist/workflow/index.js +3 -0
  263. package/dist/workflow/index.js.map +1 -0
  264. package/dist/workflow/serialization.d.ts +43 -0
  265. package/dist/workflow/serialization.d.ts.map +1 -0
  266. package/dist/workflow/serialization.js +215 -0
  267. package/dist/workflow/serialization.js.map +1 -0
  268. package/package.json +105 -0
  269. package/src/api/index.ts +11 -0
  270. package/src/api/workflow.ts +607 -0
  271. package/src/channel/Channel.ts +11 -0
  272. package/src/channel/ChannelDefinition.ts +76 -0
  273. package/src/channel/index.ts +6 -0
  274. package/src/channel/serialization.ts +68 -0
  275. package/src/deploy/index.ts +1 -0
  276. package/src/deploy/requirements.test.ts +61 -0
  277. package/src/deploy/requirements.ts +41 -0
  278. package/src/diagnostics/__fixtures__/diagnosticFixtures.ts +158 -0
  279. package/src/diagnostics/diagnostics.test.ts +878 -0
  280. package/src/diagnostics/diagnostics.ts +936 -0
  281. package/src/diagnostics/index.ts +11 -0
  282. package/src/edge/Edge.ts +23 -0
  283. package/src/edge/EdgeDefinition.ts +45 -0
  284. package/src/edge/EdgeType.ts +19 -0
  285. package/src/edge/index.ts +8 -0
  286. package/src/edge/serialization.ts +83 -0
  287. package/src/expression/index.ts +4 -0
  288. package/src/expression/parser.ts +362 -0
  289. package/src/expression/types.ts +30 -0
  290. package/src/function/FunctionDeclaration.ts +54 -0
  291. package/src/function/index.ts +3 -0
  292. package/src/function/serialization.ts +40 -0
  293. package/src/globals.d.ts +9 -0
  294. package/src/id/index.ts +8 -0
  295. package/src/index.ts +22 -0
  296. package/src/memory/Memory.ts +15 -0
  297. package/src/memory/MemoryDefinition.ts +16 -0
  298. package/src/memory/MemoryFileDefinition.ts +37 -0
  299. package/src/memory/MemoryRegistry.ts +35 -0
  300. package/src/memory/VectorDatabaseDefinition.ts +21 -0
  301. package/src/memory/index.ts +8 -0
  302. package/src/memory/serialization.ts +47 -0
  303. package/src/migration/index.ts +4 -0
  304. package/src/migration/migrate.test.ts +44 -0
  305. package/src/migration/migrate.ts +58 -0
  306. package/src/migration/migrations.ts +24 -0
  307. package/src/migration/version.ts +9 -0
  308. package/src/model/LLMModelDefinition.ts +12 -0
  309. package/src/model/Model.ts +39 -0
  310. package/src/model/ModelDefinition.ts +15 -0
  311. package/src/model/ModelRegistry.ts +33 -0
  312. package/src/model/index.ts +7 -0
  313. package/src/model/serialization.ts +30 -0
  314. package/src/node/AgentNode.ts +82 -0
  315. package/src/node/DataNode.ts +41 -0
  316. package/src/node/FunctionNode.ts +76 -0
  317. package/src/node/InputNode.ts +185 -0
  318. package/src/node/LogicNode.ts +33 -0
  319. package/src/node/MqttNode.ts +127 -0
  320. package/src/node/Node.ts +61 -0
  321. package/src/node/NodeDefinition.ts +37 -0
  322. package/src/node/NodeRegistry.ts +85 -0
  323. package/src/node/OutputNode.ts +87 -0
  324. package/src/node/ToolNode.ts +32 -0
  325. package/src/node/TriggerNode.ts +272 -0
  326. package/src/node/constants.ts +16 -0
  327. package/src/node/index.ts +26 -0
  328. package/src/node/methods.ts +278 -0
  329. package/src/node/serialization.ts +544 -0
  330. package/src/parameter/OutputParameter.ts +68 -0
  331. package/src/parameter/Parameter.ts +243 -0
  332. package/src/parameter/index.ts +33 -0
  333. package/src/variable/Variable.ts +10 -0
  334. package/src/variable/index.ts +16 -0
  335. package/src/variable/operations.ts +106 -0
  336. package/src/workflow/Workflow.ts +41 -0
  337. package/src/workflow/index.ts +3 -0
  338. package/src/workflow/serialization.test.ts +240 -0
  339. package/src/workflow/serialization.ts +242 -0
@@ -0,0 +1,783 @@
1
+ import { NodeCategory, NodeRegistry } from "../node";
2
+ import { getEdgeDefinition, isControlFlow } from "../edge";
3
+ import { getArguments, getNodeAvailableOutput, getOutputBinding, getPorts, isNodeUsedAsTool } from "../node/methods";
4
+ import { computeAvailableVariables, refToLookupKey } from "../variable";
5
+ import { isExpression, resolveExpression } from "../expression/types";
6
+ import { parseExpression } from "../expression/parser";
7
+ import { isParameterActive, isEmpty, resolveExpressionType, resolveChannelTypes, resolveMemoryTypes, resolveModelTypes } from "../parameter";
8
+ import { CHANNEL_DEFINITION } from "../channel";
9
+ import { MemoryRegistry } from "../memory";
10
+ import { ModelRegistry } from "../model";
11
+ import { buildFunctionNodeDef } from "../node/FunctionNode";
12
+ import { toFunctionInfo } from "../function";
13
+ import { MAIN_CANVAS_ID } from "../workflow/Workflow";
14
+ /**
15
+ * Compute diagnostics for a single node.
16
+ */
17
+ export function computeNodeDiagnostics(opts) {
18
+ const { canvasId, nodeId, nodeData, nodeDefinition, availableVariables, channels, memory, models, availableModelIds, edges, isStale = false, isDeleted = false, } = opts;
19
+ const diags = [];
20
+ // --- Function-specific diagnostics ---
21
+ if (isDeleted) {
22
+ diags.push({
23
+ severity: "error",
24
+ category: "function-deleted",
25
+ canvasId,
26
+ nodeId,
27
+ message: "Function has been deleted. Remove this node.",
28
+ });
29
+ }
30
+ else if (isStale) {
31
+ diags.push({
32
+ severity: "warning",
33
+ category: "function-stale",
34
+ canvasId,
35
+ nodeId,
36
+ message: "Function definition has changed. Please update this node.",
37
+ });
38
+ }
39
+ if (!nodeDefinition)
40
+ return diags;
41
+ const portDefinitions = getPorts(nodeData);
42
+ const parameters = getArguments(nodeData);
43
+ const usedAsToolInput = isNodeUsedAsTool(nodeId, nodeData, edges);
44
+ // --- Parameter diagnostics ---
45
+ for (const param of nodeDefinition.parameters) {
46
+ if (!isParameterActive(param, parameters, usedAsToolInput))
47
+ continue;
48
+ const value = parameters[param.id];
49
+ // invalid-expression (skip empty — caught by missing-required-param)
50
+ if (isExpression(value) && value.expression) {
51
+ const expr = resolveExpression(value, availableVariables);
52
+ // Resolve expressionType (static, args-only lambda, or derived from a referenced variable)
53
+ if (param.type === "expression") {
54
+ expr.expectedType = resolveExpressionType(param, parameters, availableVariables);
55
+ }
56
+ const parseRes = parseExpression(expr);
57
+ if (!parseRes.isValid) {
58
+ diags.push({
59
+ severity: "error",
60
+ category: "invalid-expression",
61
+ canvasId,
62
+ nodeId,
63
+ paramId: param.id,
64
+ message: `Invalid expression for "${param.label}": ${parseRes.errors.join(", ")}`,
65
+ });
66
+ }
67
+ }
68
+ // missing-required-param
69
+ if (!param.optional) {
70
+ const isEmptyExpression = isExpression(value) && !value.expression;
71
+ const isEmptyReference = param.type === "variableSelect" &&
72
+ (!value || (typeof value === "object" && value !== null && !value.varId));
73
+ if (isEmpty(value) || isEmptyExpression || isEmptyReference) {
74
+ diags.push({
75
+ severity: "error",
76
+ category: "missing-required-param",
77
+ canvasId,
78
+ nodeId,
79
+ paramId: param.id,
80
+ message: `Missing required parameter "${param.label}"`,
81
+ });
82
+ }
83
+ }
84
+ // invalid-reference: variableSelect points to deleted variable
85
+ if (param.type === "variableSelect" && value) {
86
+ const ref = value;
87
+ if (ref.varId) {
88
+ const key = refToLookupKey(ref);
89
+ if (!availableVariables[key]) {
90
+ diags.push({
91
+ severity: "error",
92
+ category: "invalid-reference",
93
+ canvasId,
94
+ nodeId,
95
+ paramId: param.id,
96
+ message: `"${param.label}" references a deleted variable`,
97
+ });
98
+ }
99
+ }
100
+ }
101
+ // invalid-reference: channelSelect points to deleted or incompatible channel
102
+ if (param.type === "channelSelect" && value) {
103
+ const channelId = value;
104
+ const channel = Object.values(channels).find((v) => v.id === channelId);
105
+ if (!channel) {
106
+ diags.push({
107
+ severity: "error",
108
+ category: "invalid-reference",
109
+ canvasId,
110
+ nodeId,
111
+ paramId: param.id,
112
+ message: `"${param.label}" references a deleted channel`,
113
+ });
114
+ }
115
+ else {
116
+ const channelParam = param;
117
+ const allowedTypes = resolveChannelTypes(channelParam, parameters);
118
+ if (!allowedTypes.includes(channel.type)) {
119
+ diags.push({
120
+ severity: "error",
121
+ category: "invalid-reference",
122
+ canvasId,
123
+ nodeId,
124
+ paramId: param.id,
125
+ message: `"${param.label}" references "${channel.label}" (${channel.type}), which is not a compatible channel type`,
126
+ });
127
+ }
128
+ }
129
+ }
130
+ // invalid-reference: memory-refs entries point to deleted memory files
131
+ if (param.type === "memory-refs" && Array.isArray(value) && memory) {
132
+ const refs = value;
133
+ const knownIds = new Set(Object.values(memory)
134
+ .filter((m) => m.type === "MemoryFile")
135
+ .map((m) => m.id));
136
+ const missing = refs.filter((r) => !r.id || !knownIds.has(r.id));
137
+ if (missing.length > 0) {
138
+ diags.push({
139
+ severity: "error",
140
+ category: "invalid-reference",
141
+ canvasId,
142
+ nodeId,
143
+ paramId: param.id,
144
+ message: `"${param.label}" references ${missing.length} deleted memory file${missing.length === 1 ? "" : "s"}`,
145
+ });
146
+ }
147
+ }
148
+ // invalid-reference: memorySelect points to deleted or incompatible memory
149
+ if (param.type === "memorySelect" && value && memory) {
150
+ const memoryId = value;
151
+ const mem = Object.values(memory).find((m) => m.id === memoryId);
152
+ if (!mem) {
153
+ diags.push({
154
+ severity: "error",
155
+ category: "invalid-reference",
156
+ canvasId,
157
+ nodeId,
158
+ paramId: param.id,
159
+ message: `"${param.label}" references a deleted memory`,
160
+ });
161
+ }
162
+ else {
163
+ const memoryParam = param;
164
+ const allowedTypes = resolveMemoryTypes(memoryParam, parameters);
165
+ if (!allowedTypes.includes(mem.type)) {
166
+ diags.push({
167
+ severity: "error",
168
+ category: "invalid-reference",
169
+ canvasId,
170
+ nodeId,
171
+ paramId: param.id,
172
+ message: `"${param.label}" references "${mem.label}" (${mem.type}), which is not a compatible memory type`,
173
+ });
174
+ }
175
+ }
176
+ }
177
+ // invalid-reference: modelSelect points to a deleted custom model or unknown catalog id
178
+ if (param.type === "modelSelect" && value && models) {
179
+ const modelId = value;
180
+ const custom = Object.values(models).find((m) => m.id === modelId);
181
+ if (custom) {
182
+ const modelParam = param;
183
+ const allowedTypes = resolveModelTypes(modelParam, parameters);
184
+ if (!allowedTypes.includes(custom.type)) {
185
+ diags.push({
186
+ severity: "error",
187
+ category: "invalid-reference",
188
+ canvasId,
189
+ nodeId,
190
+ paramId: param.id,
191
+ message: `"${param.label}" references "${custom.label}" (${custom.type}), which is not a compatible model type`,
192
+ });
193
+ }
194
+ }
195
+ else if (availableModelIds && !availableModelIds.has(modelId)) {
196
+ // Not a declared custom model and not in the supplied catalog → stale.
197
+ // Headlessly (no catalog) static ids can't be verified, so we don't flag.
198
+ diags.push({
199
+ severity: "error",
200
+ category: "invalid-reference",
201
+ canvasId,
202
+ nodeId,
203
+ paramId: param.id,
204
+ message: `"${param.label}" references a deleted model`,
205
+ });
206
+ }
207
+ }
208
+ }
209
+ // --- Output binding diagnostics (skip when node is used as tool — outputs are scoped out) ---
210
+ if (!usedAsToolInput) {
211
+ const availableOutput = getNodeAvailableOutput(nodeData);
212
+ for (const outputId of Object.keys(availableOutput)) {
213
+ const binding = getOutputBinding(nodeData, outputId);
214
+ // Inactive bindings are discarded — nothing to validate.
215
+ if (!binding || !binding.active || binding.mode !== "assign")
216
+ continue;
217
+ const outputDef = availableOutput[outputId];
218
+ if (!binding.target.srcId) {
219
+ diags.push({
220
+ severity: "error",
221
+ category: "assign-type-mismatch",
222
+ canvasId,
223
+ nodeId,
224
+ outputId,
225
+ message: `Output "${outputDef?.name ?? outputId}" has no variable selected`,
226
+ });
227
+ continue;
228
+ }
229
+ const key = refToLookupKey(binding.target);
230
+ const targetVar = availableVariables[key];
231
+ if (!targetVar) {
232
+ diags.push({
233
+ severity: "error",
234
+ category: "assign-type-mismatch",
235
+ canvasId,
236
+ nodeId,
237
+ outputId,
238
+ message: `Output "${outputDef?.name ?? outputId}" assigns to a deleted variable`,
239
+ });
240
+ }
241
+ else if (outputDef && targetVar.dataType !== outputDef.dataType) {
242
+ diags.push({
243
+ severity: "error",
244
+ category: "assign-type-mismatch",
245
+ canvasId,
246
+ nodeId,
247
+ outputId,
248
+ message: `Output "${outputDef.name}" (${outputDef.dataType}) cannot assign to "${targetVar.name}" (${targetVar.dataType})`,
249
+ });
250
+ }
251
+ }
252
+ // --- List-output declaration diagnostics ---
253
+ // Walk each list-output entry. Two layers of validation:
254
+ // 1. Per-entry: name non-empty, assign target valid + type-compatible.
255
+ // 2. Per-list: names unique within the list — the `name` field doubles as the
256
+ // JSON property name in the LLM's structured response, so duplicates would
257
+ // silently collide. Required for both modes (emit and assign).
258
+ const listOutputs = (nodeDefinition.outputs ?? []).filter((o) => o.type === "list");
259
+ for (const out of listOutputs) {
260
+ const entries = nodeData.arguments[out.id] ?? [];
261
+ // Build a name → indices map up-front; flag duplicates and empties on a per-entry basis.
262
+ const nameToIndices = new Map();
263
+ entries.forEach((entry, index) => {
264
+ const arr = nameToIndices.get(entry.name);
265
+ if (arr)
266
+ arr.push(index);
267
+ else
268
+ nameToIndices.set(entry.name, [index]);
269
+ });
270
+ entries.forEach((entry, index) => {
271
+ const outputId = `${out.id}[${index}]`;
272
+ // missing name (both modes)
273
+ if (!entry.name || entry.name.trim() === "") {
274
+ diags.push({
275
+ severity: "error",
276
+ category: "missing-required-param",
277
+ canvasId,
278
+ nodeId,
279
+ outputId,
280
+ message: `${out.label} entry #${index + 1} has no name`,
281
+ });
282
+ }
283
+ else {
284
+ const collisions = nameToIndices.get(entry.name) ?? [];
285
+ if (collisions.length > 1) {
286
+ diags.push({
287
+ severity: "error",
288
+ category: "duplicate-output-name",
289
+ canvasId,
290
+ nodeId,
291
+ outputId,
292
+ message: `${out.label} entry #${index + 1} has duplicate name "${entry.name}"`,
293
+ });
294
+ }
295
+ }
296
+ if (entry.mode !== "assign")
297
+ return;
298
+ // Use a synthetic outputId: `<listId>[<index>]`. Stable per-position, surfaces the
299
+ // error on the node badge. (ListOutputSection could light up individual rows from
300
+ // this key in a follow-up.)
301
+ if (!entry.target.srcId) {
302
+ diags.push({
303
+ severity: "error",
304
+ category: "assign-type-mismatch",
305
+ canvasId,
306
+ nodeId,
307
+ outputId,
308
+ message: `${out.label} entry #${index + 1} has no variable selected`,
309
+ });
310
+ return;
311
+ }
312
+ const key = refToLookupKey(entry.target);
313
+ const targetVar = availableVariables[key];
314
+ if (!targetVar) {
315
+ diags.push({
316
+ severity: "error",
317
+ category: "assign-type-mismatch",
318
+ canvasId,
319
+ nodeId,
320
+ outputId,
321
+ message: `${out.label} entry #${index + 1} assigns to a deleted variable`,
322
+ });
323
+ }
324
+ else if (targetVar.dataType !== entry.dataType) {
325
+ diags.push({
326
+ severity: "error",
327
+ category: "assign-type-mismatch",
328
+ canvasId,
329
+ nodeId,
330
+ outputId,
331
+ message: `${out.label} entry #${index + 1} (${entry.dataType}) cannot assign to "${targetVar.name}" (${targetVar.dataType})`,
332
+ });
333
+ }
334
+ });
335
+ }
336
+ }
337
+ // --- Port connectivity diagnostics ---
338
+ const controlInputs = portDefinitions.input.filter((p) => p.type === "control");
339
+ const toolInputPorts = portDefinitions.input.filter((p) => p.type === "tool");
340
+ const controlOutputs = portDefinitions.output.filter((p) => p.type === "control");
341
+ const connectedTargetHandles = new Set(edges.filter((e) => e.target === nodeId).map((e) => e.targetHandle));
342
+ const connectedSourceHandles = new Set(edges.filter((e) => e.source === nodeId).map((e) => e.sourceHandle));
343
+ // Check unconnected control inputs (applies to ALL nodes that have them)
344
+ if (controlInputs.length > 0) {
345
+ if (usedAsToolInput) {
346
+ for (const port of toolInputPorts) {
347
+ if (!connectedTargetHandles.has(port.id)) {
348
+ diags.push({
349
+ severity: "warning",
350
+ category: "unconnected-input",
351
+ canvasId,
352
+ nodeId,
353
+ message: `Node is not connected and will never run`,
354
+ });
355
+ }
356
+ }
357
+ }
358
+ else {
359
+ for (const port of controlInputs) {
360
+ if (!connectedTargetHandles.has(port.id)) {
361
+ diags.push({
362
+ severity: "warning",
363
+ category: "unconnected-input",
364
+ canvasId,
365
+ nodeId,
366
+ message: `Node is not connected and will never run`,
367
+ });
368
+ }
369
+ }
370
+ }
371
+ }
372
+ else if (toolInputPorts.length > 0) {
373
+ // Tool-only input nodes (e.g. WebSearchTool)
374
+ const hasAnyConnection = toolInputPorts.some((p) => connectedTargetHandles.has(p.id));
375
+ if (!hasAnyConnection) {
376
+ diags.push({
377
+ severity: "warning",
378
+ category: "tool-not-connected",
379
+ canvasId,
380
+ nodeId,
381
+ message: `"${nodeDefinition.label}" is not connected to an agent`,
382
+ });
383
+ }
384
+ }
385
+ // Check unconnected control outputs (triggers only — they are entry points that must lead somewhere)
386
+ if (controlOutputs.length > 0 && nodeDefinition.category === NodeCategory.Trigger) {
387
+ for (const port of controlOutputs) {
388
+ if (!connectedSourceHandles.has(port.id)) {
389
+ diags.push({
390
+ severity: "warning",
391
+ category: "unconnected-output",
392
+ canvasId,
393
+ nodeId,
394
+ message: `"${nodeDefinition.label}" has no outgoing connection — nothing will run after it`,
395
+ });
396
+ }
397
+ }
398
+ }
399
+ return diags;
400
+ }
401
+ /**
402
+ * Compute diagnostics for a single edge. Extracted from CustomEdge's useMemo.
403
+ */
404
+ export function computeEdgeDiagnostics(opts) {
405
+ const { canvasId, edgeId, edgeType, edgeData, availableVariables, sourceControlEdgeCount } = opts;
406
+ const diags = [];
407
+ const def = getEdgeDefinition(edgeType);
408
+ if (def.parameters.length === 0)
409
+ return diags;
410
+ const isBranching = sourceControlEdgeCount > 1;
411
+ for (const param of def.parameters) {
412
+ // Description is optional on agent output edges when not branching
413
+ if (param.id === "description" && !isBranching && (edgeType === "agentChoice" || edgeType === "agentDelegate")) {
414
+ continue;
415
+ }
416
+ const value = edgeData?.[param.id];
417
+ if (param.type === "expression") {
418
+ const exprValue = value;
419
+ if (!exprValue?.expression) {
420
+ diags.push({
421
+ severity: "error",
422
+ category: "missing-required-param",
423
+ canvasId,
424
+ edgeId,
425
+ paramId: param.id,
426
+ message: `Missing required parameter "${param.label}" on edge`,
427
+ });
428
+ continue;
429
+ }
430
+ if (isExpression(exprValue)) {
431
+ const expr = resolveExpression(exprValue, availableVariables);
432
+ const parseRes = parseExpression(expr);
433
+ if (!parseRes.isValid) {
434
+ diags.push({
435
+ severity: "error",
436
+ category: "invalid-expression",
437
+ canvasId,
438
+ edgeId,
439
+ paramId: param.id,
440
+ message: `Invalid expression for "${param.label}": ${parseRes.errors.join(", ")}`,
441
+ });
442
+ }
443
+ }
444
+ }
445
+ else {
446
+ if (!value) {
447
+ diags.push({
448
+ severity: "error",
449
+ category: "missing-required-param",
450
+ canvasId,
451
+ edgeId,
452
+ paramId: param.id,
453
+ message: `Missing required parameter "${param.label}" on edge`,
454
+ });
455
+ }
456
+ }
457
+ }
458
+ return diags;
459
+ }
460
+ /**
461
+ * Compute diagnostics for a single channel. Mirrors the parameter loop
462
+ * from computeNodeDiagnostics: filter to active params (per the type
463
+ * discriminator), then required-check each. Empty label is also flagged so
464
+ * the user has a non-blank name in `channelSelect` dropdowns.
465
+ */
466
+ export function validateChannel(channel) {
467
+ const diags = [];
468
+ if (!channel.label || channel.label.trim() === "") {
469
+ diags.push({
470
+ severity: "error",
471
+ category: "missing-required-param",
472
+ channelId: channel.id,
473
+ message: `Channel has no label`,
474
+ });
475
+ }
476
+ // `type` is mirrored into the args record so activation rules can read it.
477
+ const args = { ...channel.arguments, type: channel.type };
478
+ for (const param of CHANNEL_DEFINITION.parameters) {
479
+ if (param.id === "type")
480
+ continue; // top-level discriminator, always set
481
+ if (!isParameterActive(param, args, false))
482
+ continue;
483
+ if (param.optional)
484
+ continue;
485
+ const value = channel.arguments[param.id];
486
+ if (isEmpty(value)) {
487
+ diags.push({
488
+ severity: "error",
489
+ category: "missing-required-param",
490
+ channelId: channel.id,
491
+ paramId: param.id,
492
+ message: `Missing required parameter "${param.label}" on channel "${channel.label}"`,
493
+ });
494
+ }
495
+ }
496
+ return diags;
497
+ }
498
+ /**
499
+ * Compute diagnostics for a single memory primitive. Mirrors validateChannel:
500
+ * an empty label is flagged (so memorySelect/memory-refs dropdowns have a
501
+ * non-blank name), then each required parameter for the memory's type is checked.
502
+ */
503
+ export function validateMemory(mem) {
504
+ const diags = [];
505
+ if (!mem.label || mem.label.trim() === "") {
506
+ diags.push({
507
+ severity: "error",
508
+ category: "missing-required-param",
509
+ memoryId: mem.id,
510
+ message: `Memory has no label`,
511
+ });
512
+ }
513
+ const def = MemoryRegistry.getByType(mem.type);
514
+ for (const param of def?.parameters ?? []) {
515
+ if (!isParameterActive(param, mem.arguments, false))
516
+ continue;
517
+ if (param.optional)
518
+ continue;
519
+ const value = mem.arguments[param.id];
520
+ if (isEmpty(value)) {
521
+ diags.push({
522
+ severity: "error",
523
+ category: "missing-required-param",
524
+ memoryId: mem.id,
525
+ paramId: param.id,
526
+ message: `Missing required parameter "${param.label}" on memory "${mem.label}"`,
527
+ });
528
+ }
529
+ }
530
+ return diags;
531
+ }
532
+ /**
533
+ * Compute diagnostics for a single declared (custom) model. Mirrors
534
+ * validateMemory: an empty label is flagged, then each required parameter for
535
+ * the model's type is checked (LLMModel has none today, so this is label-only).
536
+ */
537
+ export function validateModel(model) {
538
+ const diags = [];
539
+ if (!model.label || model.label.trim() === "") {
540
+ diags.push({
541
+ severity: "error",
542
+ category: "missing-required-param",
543
+ modelId: model.id,
544
+ message: `Model has no label`,
545
+ });
546
+ }
547
+ const def = ModelRegistry.getByType(model.type);
548
+ for (const param of def?.parameters ?? []) {
549
+ if (!isParameterActive(param, model.arguments, false))
550
+ continue;
551
+ if (param.optional)
552
+ continue;
553
+ const value = model.arguments[param.id];
554
+ if (isEmpty(value)) {
555
+ diags.push({
556
+ severity: "error",
557
+ category: "missing-required-param",
558
+ modelId: model.id,
559
+ paramId: param.id,
560
+ message: `Missing required parameter "${param.label}" on model "${model.label}"`,
561
+ });
562
+ }
563
+ }
564
+ return diags;
565
+ }
566
+ /**
567
+ * Validate a function's bundled output assignments, keyed by `outputId` so a caller
568
+ * can ring the specific row. The single home for this logic — shared by the live
569
+ * config panel, the project-scoped {@link validateFunction}, and the full
570
+ * {@link validateWorkflowState}.
571
+ *
572
+ * Without `availableVariables` (no body scope) it can only flag a *missing*
573
+ * expression (the engine-invariant check). Given the body's variables it also
574
+ * resolves + parses each expression, catching invalid references and type mismatches
575
+ * — the same `resolveExpression`/`parseExpression` path node expression params use.
576
+ */
577
+ export function validateFunctionOutputs(outputs, availableVariables) {
578
+ const diags = [];
579
+ for (const out of outputs) {
580
+ const text = out.expression?.expression;
581
+ if (!text || text.trim() === "") {
582
+ diags.push({
583
+ severity: "error",
584
+ category: "missing-output-assignment",
585
+ outputId: out.uid,
586
+ message: `Missing return value assignment for "${out.name}"`,
587
+ });
588
+ }
589
+ else if (availableVariables) {
590
+ const parseRes = parseExpression(resolveExpression(out.expression, availableVariables));
591
+ if (!parseRes.isValid) {
592
+ diags.push({
593
+ severity: "error",
594
+ category: "invalid-expression",
595
+ outputId: out.uid,
596
+ message: `Invalid expression for "${out.name}": ${parseRes.errors.join(", ")}`,
597
+ });
598
+ }
599
+ }
600
+ }
601
+ return diags;
602
+ }
603
+ /**
604
+ * Compute diagnostics for a single function declaration (the project-scoped
605
+ * signature + bundled output assignments, independent of the body canvas). Flags an
606
+ * empty name and any output without an assigned expression. Expression *validity*
607
+ * (which needs the body's variable scope) is left to the scoped callers — the config
608
+ * panel and validateWorkflowState (keyed by the function's canvas).
609
+ */
610
+ export function validateFunction(fn, availableVariables) {
611
+ const diags = [];
612
+ if (!fn.name || fn.name.trim() === "") {
613
+ diags.push({
614
+ severity: "error",
615
+ category: "missing-required-param",
616
+ functionId: fn.id,
617
+ message: `Function has no name`,
618
+ });
619
+ }
620
+ // Pass the body scope through when the caller has it (the sidebar sync supplies the
621
+ // function's canvas variables) so invalid/typed expressions surface too, not just
622
+ // missing ones. Tag with functionId for the sidebar list ring / tab badge.
623
+ for (const d of validateFunctionOutputs(fn.outputs, availableVariables))
624
+ diags.push({ ...d, functionId: fn.id });
625
+ return diags;
626
+ }
627
+ /**
628
+ * Derive the function registry from a snapshot's canvases. Mirrors
629
+ * `computeAllFunctions()` in useFunctionRegistry exactly: every non-main
630
+ * canvas that carries a `functionInfo` is a function, keyed by canvas id
631
+ * (which equals the function id by invariant).
632
+ *
633
+ * Deriving from the snapshot rather than the module-level cache makes
634
+ * validation deterministic from its input alone — and removes the cache-lag
635
+ * window the store-bound path had.
636
+ */
637
+ function deriveFunctionRegistry(functions) {
638
+ const out = {};
639
+ for (const [id, decl] of Object.entries(functions))
640
+ out[id] = toFunctionInfo(decl);
641
+ return out;
642
+ }
643
+ /**
644
+ * Headless full-project validation. Pure: depends only on the passed
645
+ * {@link Workflow} (the in-memory domain shape) — no Zustand stores, no
646
+ * React, no DOM. Runnable in Node, a CLI, or a Claude Code skill.
647
+ *
648
+ * Two producers feed this: the editor reads its live stores into a
649
+ * `Workflow` literal; the CLI calls `deserialize(apiWorkflow)` from
650
+ * `../workflow/serialization` to convert the api JSON into this shape.
651
+ */
652
+ export function validateWorkflowState(state) {
653
+ const canvasData = state.canvases ?? {};
654
+ const functionDecls = state.functions ?? {};
655
+ const allFunctions = deriveFunctionRegistry(functionDecls);
656
+ const channels = state.channels ?? {};
657
+ const memory = state.memory ?? {};
658
+ const models = state.models ?? {};
659
+ const canvases = [];
660
+ let totalErrors = 0;
661
+ let totalWarnings = 0;
662
+ for (const [canvasId, canvas] of Object.entries(canvasData)) {
663
+ const { nodes, edges } = canvas;
664
+ // Each canvas is fully self-contained — function canvases do not see main-canvas variables.
665
+ const { lookup: availableVariables } = computeAvailableVariables(canvas.variables, edges);
666
+ const canvasDiags = [];
667
+ // Compute node diagnostics
668
+ for (const node of nodes) {
669
+ const nodeData = node;
670
+ // Resolve node definition
671
+ let nodeDefinition;
672
+ let isStale = false;
673
+ let isDeleted = false;
674
+ if (nodeData.type === "FunctionCall") {
675
+ const fnNode = nodeData;
676
+ const registryFn = allFunctions[fnNode.functionInfo.id];
677
+ isDeleted = !registryFn;
678
+ isStale = registryFn ? fnNode.functionInfo.version !== registryFn.version : false;
679
+ if (registryFn) {
680
+ nodeDefinition = buildFunctionNodeDef(registryFn);
681
+ }
682
+ else {
683
+ nodeDefinition = buildFunctionNodeDef(fnNode.functionInfo);
684
+ }
685
+ }
686
+ else {
687
+ nodeDefinition = NodeRegistry.getByType(nodeData.type);
688
+ }
689
+ const diags = computeNodeDiagnostics({
690
+ canvasId,
691
+ nodeId: node.id,
692
+ nodeData,
693
+ nodeDefinition,
694
+ availableVariables,
695
+ channels,
696
+ memory,
697
+ models,
698
+ edges,
699
+ isStale,
700
+ isDeleted,
701
+ });
702
+ canvasDiags.push(...diags);
703
+ }
704
+ // Compute edge diagnostics
705
+ const sourceControlCounts = new Map();
706
+ for (const edge of edges) {
707
+ if (isControlFlow(edge.type)) {
708
+ sourceControlCounts.set(edge.source, (sourceControlCounts.get(edge.source) ?? 0) + 1);
709
+ }
710
+ }
711
+ for (const edge of edges) {
712
+ const edgeType = (edge.type ?? "control");
713
+ const diags = computeEdgeDiagnostics({
714
+ canvasId,
715
+ edgeId: edge.id,
716
+ edgeType,
717
+ edgeData: edge.data,
718
+ availableVariables,
719
+ sourceControlEdgeCount: sourceControlCounts.get(edge.source) ?? 0,
720
+ });
721
+ canvasDiags.push(...diags);
722
+ }
723
+ // Output assignment diagnostics: the function's declaration (functions[canvasId])
724
+ // owns the bundled outputs; their expressions resolve against this body's scope.
725
+ const fnDecl = functionDecls[canvasId];
726
+ if (fnDecl) {
727
+ for (const d of validateFunctionOutputs(fnDecl.outputs, availableVariables)) {
728
+ canvasDiags.push({ ...d, canvasId });
729
+ }
730
+ }
731
+ // Only include canvases with issues
732
+ if (canvasDiags.length > 0) {
733
+ const errorCount = canvasDiags.filter((d) => d.severity === "error").length;
734
+ const warningCount = canvasDiags.filter((d) => d.severity === "warning").length;
735
+ // Derive canvas label
736
+ const canvasLabel = canvasId === MAIN_CANVAS_ID ? "Main" : (functionDecls[canvasId]?.name ?? canvasId);
737
+ canvases.push({
738
+ canvasId,
739
+ canvasLabel,
740
+ diagnostics: canvasDiags,
741
+ errorCount,
742
+ warningCount,
743
+ });
744
+ totalErrors += errorCount;
745
+ totalWarnings += warningCount;
746
+ }
747
+ }
748
+ // Project-scoped channel diagnostics — independent of canvas iteration.
749
+ const channelDiagnostics = [];
750
+ for (const channel of Object.values(channels)) {
751
+ channelDiagnostics.push(...validateChannel(channel));
752
+ }
753
+ for (const d of channelDiagnostics) {
754
+ if (d.severity === "error")
755
+ totalErrors++;
756
+ else
757
+ totalWarnings++;
758
+ }
759
+ // Project-scoped memory diagnostics — independent of canvas iteration.
760
+ const memoryDiagnostics = [];
761
+ for (const mem of Object.values(memory)) {
762
+ memoryDiagnostics.push(...validateMemory(mem));
763
+ }
764
+ for (const d of memoryDiagnostics) {
765
+ if (d.severity === "error")
766
+ totalErrors++;
767
+ else
768
+ totalWarnings++;
769
+ }
770
+ // Project-scoped declared-model diagnostics — independent of canvas iteration.
771
+ const modelDiagnostics = [];
772
+ for (const model of Object.values(models)) {
773
+ modelDiagnostics.push(...validateModel(model));
774
+ }
775
+ for (const d of modelDiagnostics) {
776
+ if (d.severity === "error")
777
+ totalErrors++;
778
+ else
779
+ totalWarnings++;
780
+ }
781
+ return { canvases, channelDiagnostics, memoryDiagnostics, modelDiagnostics, totalErrors, totalWarnings };
782
+ }
783
+ //# sourceMappingURL=diagnostics.js.map