@xdelivered/emberflow 0.2.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 (414) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +153 -0
  3. package/bin/commands.test.ts +185 -0
  4. package/bin/commands.ts +306 -0
  5. package/bin/emberflow.mjs +38 -0
  6. package/bin/init.test.ts +165 -0
  7. package/bin/init.ts +297 -0
  8. package/bin/runtime.mjs +105 -0
  9. package/bin/runtime.test.ts +167 -0
  10. package/bin/skillTargets.test.ts +108 -0
  11. package/bin/skillTargets.ts +36 -0
  12. package/bin/skillTemplates.test.ts +37 -0
  13. package/bin/tsconfig.json +23 -0
  14. package/dist/bin/commands.d.ts +24 -0
  15. package/dist/bin/commands.js +252 -0
  16. package/dist/bin/init.d.ts +17 -0
  17. package/dist/bin/init.js +241 -0
  18. package/dist/bin/runtime.d.mts +39 -0
  19. package/dist/bin/runtime.mjs +96 -0
  20. package/dist/bin/skillTargets.d.ts +13 -0
  21. package/dist/bin/skillTargets.js +22 -0
  22. package/dist/server/agents/claudeAdapter.d.ts +8 -0
  23. package/dist/server/agents/claudeAdapter.js +190 -0
  24. package/dist/server/agents/claudeParse.d.ts +2 -0
  25. package/dist/server/agents/claudeParse.js +162 -0
  26. package/dist/server/agents/codexAdapter.d.ts +13 -0
  27. package/dist/server/agents/codexAdapter.js +174 -0
  28. package/dist/server/agents/codexParse.d.ts +2 -0
  29. package/dist/server/agents/codexParse.js +74 -0
  30. package/dist/server/agents/detect.d.ts +23 -0
  31. package/dist/server/agents/detect.js +35 -0
  32. package/dist/server/agents/gitScope.d.ts +24 -0
  33. package/dist/server/agents/gitScope.js +95 -0
  34. package/dist/server/agents/modelRejectionHint.d.ts +8 -0
  35. package/dist/server/agents/modelRejectionHint.js +12 -0
  36. package/dist/server/agents/prompt.d.ts +79 -0
  37. package/dist/server/agents/prompt.js +364 -0
  38. package/dist/server/agents/runManager.d.ts +90 -0
  39. package/dist/server/agents/runManager.js +259 -0
  40. package/dist/server/agents/types.d.ts +15 -0
  41. package/dist/server/agents/types.js +1 -0
  42. package/dist/server/apiStore.d.ts +60 -0
  43. package/dist/server/apiStore.js +249 -0
  44. package/dist/server/auth/enforce.d.ts +1 -0
  45. package/dist/server/auth/enforce.js +6 -0
  46. package/dist/server/auth/types.d.ts +1 -0
  47. package/dist/server/auth/types.js +1 -0
  48. package/dist/server/auth/verifiers.d.ts +1 -0
  49. package/dist/server/auth/verifiers.js +4 -0
  50. package/dist/server/authAttach.d.ts +19 -0
  51. package/dist/server/authAttach.js +79 -0
  52. package/dist/server/cli.d.ts +7 -0
  53. package/dist/server/cli.js +804 -0
  54. package/dist/server/client.d.ts +130 -0
  55. package/dist/server/client.js +247 -0
  56. package/dist/server/environments.d.ts +103 -0
  57. package/dist/server/environments.js +486 -0
  58. package/dist/server/flowStore.d.ts +37 -0
  59. package/dist/server/flowStore.js +117 -0
  60. package/dist/server/httpOperations.d.ts +32 -0
  61. package/dist/server/httpOperations.js +62 -0
  62. package/dist/server/index.d.ts +1 -0
  63. package/dist/server/index.js +1022 -0
  64. package/dist/server/infrastructure.d.ts +51 -0
  65. package/dist/server/infrastructure.js +116 -0
  66. package/dist/server/login.d.ts +21 -0
  67. package/dist/server/login.js +44 -0
  68. package/dist/server/mcp.d.ts +1 -0
  69. package/dist/server/mcp.js +182 -0
  70. package/dist/server/migrateFlows.d.ts +8 -0
  71. package/dist/server/migrateFlows.js +25 -0
  72. package/dist/server/mockHandler.d.ts +15 -0
  73. package/dist/server/mockHandler.js +35 -0
  74. package/dist/server/nodesPayload.d.ts +14 -0
  75. package/dist/server/nodesPayload.js +14 -0
  76. package/dist/server/normalizeFlow.d.ts +21 -0
  77. package/dist/server/normalizeFlow.js +48 -0
  78. package/dist/server/openBrowser.d.ts +8 -0
  79. package/dist/server/openBrowser.js +16 -0
  80. package/dist/server/operationResult.d.ts +12 -0
  81. package/dist/server/operationResult.js +26 -0
  82. package/dist/server/pathGuard.d.ts +1 -0
  83. package/dist/server/pathGuard.js +16 -0
  84. package/dist/server/pathSafety.d.ts +20 -0
  85. package/dist/server/pathSafety.js +35 -0
  86. package/dist/server/projectConfig.d.ts +47 -0
  87. package/dist/server/projectConfig.js +61 -0
  88. package/dist/server/projectMode.d.ts +26 -0
  89. package/dist/server/projectMode.js +44 -0
  90. package/dist/server/redact.d.ts +6 -0
  91. package/dist/server/redact.js +37 -0
  92. package/dist/server/runRegistry.d.ts +84 -0
  93. package/dist/server/runRegistry.js +200 -0
  94. package/dist/server/runtime/RuntimeAdapter.d.ts +22 -0
  95. package/dist/server/runtime/RuntimeAdapter.js +1 -0
  96. package/dist/server/runtime/expressAdapter.d.ts +19 -0
  97. package/dist/server/runtime/expressAdapter.js +56 -0
  98. package/dist/server/scenarioTest.d.ts +14 -0
  99. package/dist/server/scenarioTest.js +66 -0
  100. package/dist/server/subflowRunner.d.ts +32 -0
  101. package/dist/server/subflowRunner.js +72 -0
  102. package/dist/server/testRunner.d.ts +70 -0
  103. package/dist/server/testRunner.js +151 -0
  104. package/dist/src/config.d.ts +3 -0
  105. package/dist/src/config.js +2 -0
  106. package/dist/src/engine/authVerify.d.ts +33 -0
  107. package/dist/src/engine/authVerify.js +63 -0
  108. package/dist/src/engine/diagnostics.d.ts +62 -0
  109. package/dist/src/engine/diagnostics.js +148 -0
  110. package/dist/src/engine/executor.d.ts +151 -0
  111. package/dist/src/engine/executor.js +684 -0
  112. package/dist/src/engine/httpError.d.ts +7 -0
  113. package/dist/src/engine/httpError.js +14 -0
  114. package/dist/src/engine/index.d.ts +11 -0
  115. package/dist/src/engine/index.js +11 -0
  116. package/dist/src/engine/output.d.ts +11 -0
  117. package/dist/src/engine/output.js +20 -0
  118. package/dist/src/engine/publish.d.ts +30 -0
  119. package/dist/src/engine/publish.js +95 -0
  120. package/dist/src/engine/registry.d.ts +34 -0
  121. package/dist/src/engine/registry.js +67 -0
  122. package/dist/src/engine/scenarios.d.ts +12 -0
  123. package/dist/src/engine/scenarios.js +19 -0
  124. package/dist/src/engine/schemaCheck.d.ts +4 -0
  125. package/dist/src/engine/schemaCheck.js +34 -0
  126. package/dist/src/engine/trace.d.ts +12 -0
  127. package/dist/src/engine/trace.js +12 -0
  128. package/dist/src/engine/types.d.ts +308 -0
  129. package/dist/src/engine/types.js +1 -0
  130. package/dist/src/engine/validation.d.ts +26 -0
  131. package/dist/src/engine/validation.js +318 -0
  132. package/dist/src/flows/anomaly-flows.d.ts +23 -0
  133. package/dist/src/flows/anomaly-flows.js +151 -0
  134. package/dist/src/flows/city-sweep-flow.d.ts +10 -0
  135. package/dist/src/flows/city-sweep-flow.js +127 -0
  136. package/dist/src/flows/login-flow.d.ts +13 -0
  137. package/dist/src/flows/login-flow.js +145 -0
  138. package/dist/src/flows/pradar-flows.d.ts +5 -0
  139. package/dist/src/flows/pradar-flows.js +375 -0
  140. package/dist/src/flows/weather-flow.d.ts +12 -0
  141. package/dist/src/flows/weather-flow.js +150 -0
  142. package/dist/src/lib/pathParams.d.ts +3 -0
  143. package/dist/src/lib/pathParams.js +6 -0
  144. package/dist/src/nodes/anomaly.d.ts +2 -0
  145. package/dist/src/nodes/anomaly.js +556 -0
  146. package/dist/src/nodes/flow-control.d.ts +7 -0
  147. package/dist/src/nodes/flow-control.js +270 -0
  148. package/dist/src/nodes/index.d.ts +3 -0
  149. package/dist/src/nodes/index.js +20 -0
  150. package/dist/src/nodes/login.d.ts +7 -0
  151. package/dist/src/nodes/login.js +158 -0
  152. package/dist/src/nodes/loops.d.ts +11 -0
  153. package/dist/src/nodes/loops.js +60 -0
  154. package/dist/src/nodes/pradar/index.d.ts +7 -0
  155. package/dist/src/nodes/pradar/index.js +9 -0
  156. package/dist/src/nodes/pradar/logic.d.ts +189 -0
  157. package/dist/src/nodes/pradar/logic.js +370 -0
  158. package/dist/src/nodes/pradar/nodes.d.ts +42 -0
  159. package/dist/src/nodes/pradar/nodes.js +801 -0
  160. package/dist/src/nodes/requireAuth.d.ts +20 -0
  161. package/dist/src/nodes/requireAuth.js +48 -0
  162. package/dist/src/nodes/response.d.ts +8 -0
  163. package/dist/src/nodes/response.js +33 -0
  164. package/dist/src/nodes/weather.d.ts +33 -0
  165. package/dist/src/nodes/weather.js +260 -0
  166. package/package.json +92 -0
  167. package/server/agentRoute.test.ts +157 -0
  168. package/server/agents/__fixtures__/fake-claude-writer.mjs +23 -0
  169. package/server/agents/__fixtures__/fake-claude.mjs +55 -0
  170. package/server/agents/__fixtures__/fake-codex-writer.mjs +27 -0
  171. package/server/agents/__fixtures__/fake-codex.mjs +52 -0
  172. package/server/agents/__fixtures__/fake-version-empty.mjs +3 -0
  173. package/server/agents/__fixtures__/fake-version-fail.mjs +4 -0
  174. package/server/agents/__fixtures__/fake-version-garbage.mjs +4 -0
  175. package/server/agents/__fixtures__/fake-version-good.mjs +4 -0
  176. package/server/agents/__fixtures__/fake-version-hang.mjs +3 -0
  177. package/server/agents/claudeAdapter.test.ts +127 -0
  178. package/server/agents/claudeAdapter.ts +212 -0
  179. package/server/agents/claudeParse.ts +193 -0
  180. package/server/agents/codexAdapter.test.ts +125 -0
  181. package/server/agents/codexAdapter.ts +198 -0
  182. package/server/agents/codexParse.test.ts +58 -0
  183. package/server/agents/codexParse.ts +126 -0
  184. package/server/agents/detect.test.ts +65 -0
  185. package/server/agents/detect.ts +43 -0
  186. package/server/agents/gitScope.test.ts +129 -0
  187. package/server/agents/gitScope.ts +110 -0
  188. package/server/agents/modelRejectionHint.test.ts +30 -0
  189. package/server/agents/modelRejectionHint.ts +14 -0
  190. package/server/agents/prompt.test.ts +613 -0
  191. package/server/agents/prompt.ts +527 -0
  192. package/server/agents/runManager.test.ts +251 -0
  193. package/server/agents/runManager.ts +286 -0
  194. package/server/agents/types.ts +17 -0
  195. package/server/apiMount.test.ts +591 -0
  196. package/server/apiStore.test.ts +324 -0
  197. package/server/apiStore.ts +241 -0
  198. package/server/auth/enforce.test.ts +61 -0
  199. package/server/auth/enforce.ts +6 -0
  200. package/server/auth/types.ts +5 -0
  201. package/server/auth/verifiers.test.ts +37 -0
  202. package/server/auth/verifiers.ts +9 -0
  203. package/server/authAttach.test.ts +175 -0
  204. package/server/authAttach.ts +99 -0
  205. package/server/cli.ts +847 -0
  206. package/server/cliAuth.test.ts +229 -0
  207. package/server/cliCreate.test.ts +131 -0
  208. package/server/cliDoctor.test.ts +326 -0
  209. package/server/cliTest.test.ts +735 -0
  210. package/server/client.ts +314 -0
  211. package/server/demoProject.test.ts +29 -0
  212. package/server/diagnosticsRoute.test.ts +169 -0
  213. package/server/environments.test.ts +620 -0
  214. package/server/environments.ts +603 -0
  215. package/server/environmentsRoute.test.ts +385 -0
  216. package/server/errorWorkflow.test.ts +205 -0
  217. package/server/flowStore.test.ts +84 -0
  218. package/server/flowStore.ts +125 -0
  219. package/server/httpOperations.test.ts +377 -0
  220. package/server/httpOperations.ts +96 -0
  221. package/server/index.ts +1111 -0
  222. package/server/infrastructure.test.ts +154 -0
  223. package/server/infrastructure.ts +161 -0
  224. package/server/infrastructureRoute.test.ts +105 -0
  225. package/server/login.test.ts +153 -0
  226. package/server/login.ts +67 -0
  227. package/server/mcp.ts +278 -0
  228. package/server/migrateFlows.test.ts +55 -0
  229. package/server/migrateFlows.ts +23 -0
  230. package/server/mockHandler.test.ts +138 -0
  231. package/server/mockHandler.ts +52 -0
  232. package/server/mockRun.test.ts +367 -0
  233. package/server/mockServe.test.ts +467 -0
  234. package/server/nodesPayload.test.ts +29 -0
  235. package/server/nodesPayload.ts +26 -0
  236. package/server/normalizeFlow.test.ts +80 -0
  237. package/server/normalizeFlow.ts +59 -0
  238. package/server/openBrowser.test.ts +20 -0
  239. package/server/openBrowser.ts +22 -0
  240. package/server/operationResult.test.ts +67 -0
  241. package/server/operationResult.ts +27 -0
  242. package/server/pathGuard.test.ts +19 -0
  243. package/server/pathGuard.ts +12 -0
  244. package/server/pathSafety.test.ts +38 -0
  245. package/server/pathSafety.ts +33 -0
  246. package/server/projectConfig.test.ts +67 -0
  247. package/server/projectConfig.ts +101 -0
  248. package/server/projectMode.test.ts +106 -0
  249. package/server/projectMode.ts +59 -0
  250. package/server/redact.test.ts +88 -0
  251. package/server/redact.ts +35 -0
  252. package/server/redactEgress.test.ts +281 -0
  253. package/server/runRegistry.ts +270 -0
  254. package/server/runsAuth.test.ts +119 -0
  255. package/server/runtime/RuntimeAdapter.ts +24 -0
  256. package/server/runtime/expressAdapter.test.ts +38 -0
  257. package/server/runtime/expressAdapter.ts +70 -0
  258. package/server/scenarioTest.test.ts +148 -0
  259. package/server/scenarioTest.ts +88 -0
  260. package/server/setupStatus.test.ts +127 -0
  261. package/server/subflowRunner.ts +104 -0
  262. package/server/testRunner.ts +216 -0
  263. package/server/tsconfig.json +20 -0
  264. package/server/workflowTestRoute.test.ts +297 -0
  265. package/src/App.tsx +233 -0
  266. package/src/components/AgentConsole.tsx +461 -0
  267. package/src/components/BranchRulesEditor.tsx +156 -0
  268. package/src/components/CreateModal.tsx +275 -0
  269. package/src/components/Dock.tsx +487 -0
  270. package/src/components/EnvironmentDialog.tsx +569 -0
  271. package/src/components/EnvironmentPicker.tsx +357 -0
  272. package/src/components/EnvironmentsDialog.tsx +144 -0
  273. package/src/components/ExecutionPager.tsx +67 -0
  274. package/src/components/InfraPanel.test.tsx +103 -0
  275. package/src/components/InfraPanel.tsx +171 -0
  276. package/src/components/Inspector.tsx +1182 -0
  277. package/src/components/Json.tsx +50 -0
  278. package/src/components/JsonModal.tsx +52 -0
  279. package/src/components/LogMessage.tsx +61 -0
  280. package/src/components/NodeRunModal.tsx +213 -0
  281. package/src/components/RunLogPanel.tsx +397 -0
  282. package/src/components/RunbookView.tsx +1085 -0
  283. package/src/components/Scenarios.tsx +476 -0
  284. package/src/components/SettingsDialog.tsx +229 -0
  285. package/src/components/Sidebar.tsx +436 -0
  286. package/src/components/StatusBar.tsx +302 -0
  287. package/src/components/Toolbar.tsx +663 -0
  288. package/src/components/WelcomeDialog.test.tsx +107 -0
  289. package/src/components/WelcomeDialog.tsx +412 -0
  290. package/src/components/agentMarkdown.test.tsx +91 -0
  291. package/src/components/agentMarkdown.tsx +158 -0
  292. package/src/components/ui/badge.tsx +31 -0
  293. package/src/components/ui/button.tsx +46 -0
  294. package/src/components/ui/combobox.tsx +114 -0
  295. package/src/components/ui/command.tsx +82 -0
  296. package/src/components/ui/dialog.tsx +73 -0
  297. package/src/components/ui/input.tsx +20 -0
  298. package/src/components/ui/popover.tsx +32 -0
  299. package/src/components/ui/resizable.tsx +44 -0
  300. package/src/components/ui/tabs.tsx +36 -0
  301. package/src/config.ts +4 -0
  302. package/src/engine/authVerify.ts +81 -0
  303. package/src/engine/diagnostics.test.ts +437 -0
  304. package/src/engine/diagnostics.ts +211 -0
  305. package/src/engine/executor.test.ts +1339 -0
  306. package/src/engine/executor.ts +840 -0
  307. package/src/engine/httpError.test.ts +12 -0
  308. package/src/engine/httpError.ts +16 -0
  309. package/src/engine/index.ts +11 -0
  310. package/src/engine/output.ts +21 -0
  311. package/src/engine/publish.test.ts +176 -0
  312. package/src/engine/publish.ts +127 -0
  313. package/src/engine/registry.test.ts +64 -0
  314. package/src/engine/registry.ts +80 -0
  315. package/src/engine/scenarios.test.ts +55 -0
  316. package/src/engine/scenarios.ts +29 -0
  317. package/src/engine/schemaCheck.test.ts +21 -0
  318. package/src/engine/schemaCheck.ts +35 -0
  319. package/src/engine/trace.test.ts +19 -0
  320. package/src/engine/trace.ts +23 -0
  321. package/src/engine/types.test.ts +22 -0
  322. package/src/engine/types.ts +316 -0
  323. package/src/engine/validation.test.ts +314 -0
  324. package/src/engine/validation.ts +354 -0
  325. package/src/flows/anomaly-flows.test.ts +81 -0
  326. package/src/flows/anomaly-flows.ts +202 -0
  327. package/src/flows/city-sweep-flow.ts +131 -0
  328. package/src/flows/flows.test.ts +30 -0
  329. package/src/flows/login-flow.ts +150 -0
  330. package/src/flows/pradar-flows.test.ts +204 -0
  331. package/src/flows/pradar-flows.ts +419 -0
  332. package/src/flows/weather-flow.ts +154 -0
  333. package/src/globals.css +147 -0
  334. package/src/lib/pathParams.test.ts +24 -0
  335. package/src/lib/pathParams.ts +6 -0
  336. package/src/lib/registerLens.test.ts +79 -0
  337. package/src/lib/registerLens.ts +59 -0
  338. package/src/lib/runbookModel.test.ts +61 -0
  339. package/src/lib/runbookModel.ts +290 -0
  340. package/src/lib/runbookProjection.test.ts +417 -0
  341. package/src/lib/runbookProjection.ts +254 -0
  342. package/src/lib/useTailAnchor.ts +53 -0
  343. package/src/lib/utils.ts +7 -0
  344. package/src/main.tsx +13 -0
  345. package/src/nodes/anomaly.test.ts +98 -0
  346. package/src/nodes/anomaly.ts +637 -0
  347. package/src/nodes/effects-scan.test.ts +44 -0
  348. package/src/nodes/flow-control.test.ts +280 -0
  349. package/src/nodes/flow-control.ts +305 -0
  350. package/src/nodes/index.ts +22 -0
  351. package/src/nodes/login.test.ts +81 -0
  352. package/src/nodes/login.ts +186 -0
  353. package/src/nodes/loops.ts +69 -0
  354. package/src/nodes/pradar/index.ts +11 -0
  355. package/src/nodes/pradar/logic.test.ts +288 -0
  356. package/src/nodes/pradar/logic.ts +582 -0
  357. package/src/nodes/pradar/nodes.test.ts +146 -0
  358. package/src/nodes/pradar/nodes.ts +955 -0
  359. package/src/nodes/requireAuth.test.ts +75 -0
  360. package/src/nodes/requireAuth.ts +55 -0
  361. package/src/nodes/response.test.ts +33 -0
  362. package/src/nodes/response.ts +39 -0
  363. package/src/nodes/traceDetail.test.ts +39 -0
  364. package/src/nodes/weather.test.ts +65 -0
  365. package/src/nodes/weather.ts +306 -0
  366. package/src/store/agentClient.test.ts +103 -0
  367. package/src/store/agentClient.ts +131 -0
  368. package/src/store/apiTree.test.ts +17 -0
  369. package/src/store/apiTree.ts +73 -0
  370. package/src/store/builderStore.test.ts +1234 -0
  371. package/src/store/builderStore.ts +1879 -0
  372. package/src/store/infraClient.ts +63 -0
  373. package/src/store/nodeMeta.test.ts +24 -0
  374. package/src/store/nodeMeta.ts +21 -0
  375. package/src/store/persistence.test.ts +15 -0
  376. package/src/store/persistence.ts +54 -0
  377. package/src/store/serverRunner.ts +381 -0
  378. package/src/store/setupClient.ts +57 -0
  379. package/studio-dist/assets/ibm-plex-mono-cyrillic-400-normal-BSMlKf0J.woff2 +0 -0
  380. package/studio-dist/assets/ibm-plex-mono-cyrillic-400-normal-CEL4l2ZJ.woff +0 -0
  381. package/studio-dist/assets/ibm-plex-mono-cyrillic-500-normal-Ael50iVv.woff +0 -0
  382. package/studio-dist/assets/ibm-plex-mono-cyrillic-500-normal-Bq9vWWag.woff2 +0 -0
  383. package/studio-dist/assets/ibm-plex-mono-cyrillic-ext-400-normal-DMdlQ8Kv.woff +0 -0
  384. package/studio-dist/assets/ibm-plex-mono-cyrillic-ext-400-normal-xuaO2J-f.woff2 +0 -0
  385. package/studio-dist/assets/ibm-plex-mono-cyrillic-ext-500-normal-BIfNGwUT.woff +0 -0
  386. package/studio-dist/assets/ibm-plex-mono-cyrillic-ext-500-normal-BqneJy0T.woff2 +0 -0
  387. package/studio-dist/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
  388. package/studio-dist/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
  389. package/studio-dist/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
  390. package/studio-dist/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
  391. package/studio-dist/assets/ibm-plex-mono-latin-ext-400-normal-BmRBH3aV.woff2 +0 -0
  392. package/studio-dist/assets/ibm-plex-mono-latin-ext-400-normal-D3D2R8hC.woff +0 -0
  393. package/studio-dist/assets/ibm-plex-mono-latin-ext-500-normal-CAhNIIs5.woff2 +0 -0
  394. package/studio-dist/assets/ibm-plex-mono-latin-ext-500-normal-CZ70TYgx.woff +0 -0
  395. package/studio-dist/assets/ibm-plex-mono-vietnamese-400-normal-BulugwFq.woff2 +0 -0
  396. package/studio-dist/assets/ibm-plex-mono-vietnamese-400-normal-DDuiU_S-.woff +0 -0
  397. package/studio-dist/assets/ibm-plex-mono-vietnamese-500-normal-C8zxqsMH.woff +0 -0
  398. package/studio-dist/assets/ibm-plex-mono-vietnamese-500-normal-DZ4AoWbu.woff2 +0 -0
  399. package/studio-dist/assets/index-DNJwW-hM.css +1 -0
  400. package/studio-dist/assets/index-O26dKRjW.js +65 -0
  401. package/studio-dist/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
  402. package/studio-dist/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
  403. package/studio-dist/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
  404. package/studio-dist/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
  405. package/studio-dist/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
  406. package/studio-dist/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
  407. package/studio-dist/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
  408. package/studio-dist/favicon.svg +1 -0
  409. package/studio-dist/icons.svg +24 -0
  410. package/studio-dist/index.html +14 -0
  411. package/templates/skills/emberflow-basics/SKILL.md +316 -0
  412. package/templates/skills/emberflow-model-process/SKILL.md +209 -0
  413. package/templates/skills/emberflow-new-workflow/SKILL.md +211 -0
  414. package/templates/skills/emberflow-review-workflow/SKILL.md +148 -0
@@ -0,0 +1,81 @@
1
+ // Browser-safe auth verification core. Lives here (not under server/auth/)
2
+ // because src/nodes/requireAuth.ts (bundled into the studio browser build via
3
+ // src/store/builderStore.ts -> src/nodes) must not import from server/. The
4
+ // server-side enforce.ts + verifiers.ts re-export/wrap this module so there's
5
+ // a single implementation instead of two diverging copies.
6
+ import { HttpError } from './httpError';
7
+ import type { AuthPolicy } from './types';
8
+
9
+ export interface VerifyResult {
10
+ user: unknown;
11
+ }
12
+
13
+ /** Given the request + the resolved secret value, return a user or throw HttpError(401). */
14
+ export type Verifier = (args: {
15
+ request: { headers: Record<string, unknown> };
16
+ policy: AuthPolicy;
17
+ secret: string | undefined;
18
+ }) => VerifyResult;
19
+
20
+ const bearerToken = (headers: Record<string, unknown>): string | undefined => {
21
+ const raw = headers['authorization'] ?? headers['Authorization'];
22
+ if (typeof raw !== 'string') return undefined;
23
+ const m = /^Bearer\s+(.+)$/i.exec(raw);
24
+ return m ? m[1] : undefined;
25
+ };
26
+
27
+ // Note: a plain string comparison is used here rather than a timing-safe
28
+ // comparison; timing-safe comparison is a later hardening item.
29
+ export const bearerVerifier: Verifier = ({ request, secret }) => {
30
+ if (secret === undefined) throw new HttpError(500, { error: 'auth misconfigured: secret not set' });
31
+ const token = bearerToken(request.headers);
32
+ if (!token || token !== secret) throw new HttpError(401, { error: 'unauthorized' });
33
+ return { user: { scheme: 'bearer' } };
34
+ };
35
+
36
+ export const apiKeyVerifier: Verifier = ({ request, policy, secret }) => {
37
+ if (secret === undefined) throw new HttpError(500, { error: 'auth misconfigured: secret not set' });
38
+ const headerName = (policy.header ?? 'x-api-key').toLowerCase();
39
+ const provided = request.headers[headerName];
40
+ if (typeof provided !== 'string' || provided !== secret) throw new HttpError(401, { error: 'unauthorized' });
41
+ return { user: { scheme: 'apiKey' } };
42
+ };
43
+
44
+ export class VerifierRegistry {
45
+ private map = new Map<string, Verifier>();
46
+ register(name: string, v: Verifier): void {
47
+ this.map.set(name, v);
48
+ }
49
+ get(name: string): Verifier | undefined {
50
+ return this.map.get(name);
51
+ }
52
+ }
53
+
54
+ export function createDefaultVerifierRegistry(): VerifierRegistry {
55
+ const reg = new VerifierRegistry();
56
+ reg.register('bearer', bearerVerifier);
57
+ reg.register('apiKey', apiKeyVerifier);
58
+ return reg;
59
+ }
60
+
61
+ /**
62
+ * Picks the verifier (policy.verify ? verifiers.get(policy.verify) : verifiers.get(policy.scheme)),
63
+ * fails closed with HttpError(500) if it's missing, resolves the secret from
64
+ * `secrets[policy.secretRef]`, and runs the verifier (its HttpError, if any, propagates).
65
+ */
66
+ export function enforceAuth(args: {
67
+ policy: AuthPolicy;
68
+ request: { headers: Record<string, unknown> };
69
+ secrets: Record<string, unknown>;
70
+ verifiers: VerifierRegistry;
71
+ }): VerifyResult {
72
+ const { policy, request, secrets, verifiers } = args;
73
+ const verifierName = policy.verify ?? policy.scheme;
74
+ const verifier = policy.verify ? verifiers.get(policy.verify) : verifiers.get(policy.scheme);
75
+ if (!verifier) {
76
+ throw new HttpError(500, { error: `auth misconfigured: no verifier ${verifierName}` });
77
+ }
78
+ const rawSecret = secrets[policy.secretRef];
79
+ const secret = typeof rawSecret === 'string' ? rawSecret : undefined;
80
+ return verifier({ request, policy, secret });
81
+ }
@@ -0,0 +1,437 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { diagnoseOperation } from './diagnostics';
3
+ import type { ScenarioDefinition, WorkflowDefinition, WorkflowNode } from './types';
4
+
5
+ function inputNode(config: Record<string, unknown> = {}): WorkflowNode {
6
+ return { id: 'input', type: 'Input', label: 'Input', position: { x: 0, y: 0 }, config };
7
+ }
8
+
9
+ function flow(over: Partial<WorkflowDefinition> = {}): WorkflowDefinition {
10
+ return {
11
+ id: 'f1',
12
+ name: 'Flow',
13
+ version: 1,
14
+ nodes: [],
15
+ edges: [],
16
+ createdAt: '2026-01-01T00:00:00.000Z',
17
+ updatedAt: '2026-01-01T00:00:00.000Z',
18
+ ...over,
19
+ };
20
+ }
21
+
22
+ function scenario(over: Partial<ScenarioDefinition> = {}): ScenarioDefinition {
23
+ return {
24
+ id: 's1',
25
+ name: 'Scenario',
26
+ input: {},
27
+ ...over,
28
+ };
29
+ }
30
+
31
+ describe('diagnoseOperation — missing-param-default', () => {
32
+ it('emits one warning per path param missing a default, in path order', () => {
33
+ const f = flow({
34
+ http: { method: 'GET', path: '/api/channels/:id/approvals/:approvalId' },
35
+ nodes: [inputNode()],
36
+ });
37
+ const diags = diagnoseOperation(f, undefined).filter((d) => d.code === 'missing-param-default');
38
+ expect(diags).toHaveLength(2);
39
+ expect(diags[0]).toMatchObject({ severity: 'warning', code: 'missing-param-default', param: 'id' });
40
+ expect(diags[1]).toMatchObject({ severity: 'warning', code: 'missing-param-default', param: 'approvalId' });
41
+ for (const d of diags) {
42
+ expect(d.message.length).toBeGreaterThan(0);
43
+ expect(d.message).toContain('Add');
44
+ }
45
+ });
46
+
47
+ it('emits only for the un-defaulted param when defaults are partial', () => {
48
+ const f = flow({
49
+ http: { method: 'GET', path: '/api/channels/:id/approvals/:approvalId' },
50
+ nodes: [inputNode({ defaults: { params: { id: 'c1' } } })],
51
+ });
52
+ const diags = diagnoseOperation(f, undefined).filter((d) => d.code === 'missing-param-default');
53
+ expect(diags).toHaveLength(1);
54
+ expect(diags[0]).toMatchObject({ code: 'missing-param-default', param: 'approvalId' });
55
+ });
56
+
57
+ it('does not emit when the default is an empty string — presence, not meaningfulness', () => {
58
+ const f = flow({
59
+ http: { method: 'GET', path: '/api/channels/:id' },
60
+ nodes: [inputNode({ defaults: { params: { id: '' } } })],
61
+ });
62
+ const diags = diagnoseOperation(f, undefined).filter((d) => d.code === 'missing-param-default');
63
+ expect(diags).toEqual([]);
64
+ });
65
+
66
+ it('does not emit when there is no Input node — nothing consumes params', () => {
67
+ const f = flow({
68
+ http: { method: 'GET', path: '/api/channels/:id' },
69
+ nodes: [],
70
+ });
71
+ const diags = diagnoseOperation(f, undefined).filter((d) => d.code === 'missing-param-default');
72
+ expect(diags).toEqual([]);
73
+ });
74
+
75
+ it('does not emit when the flow has no http trigger', () => {
76
+ const f = flow({ nodes: [inputNode()] });
77
+ const diags = diagnoseOperation(f, undefined).filter((d) => d.code === 'missing-param-default');
78
+ expect(diags).toEqual([]);
79
+ });
80
+
81
+ it('does not emit when the http path has no params', () => {
82
+ const f = flow({ http: { method: 'GET', path: '/api/channels' }, nodes: [inputNode()] });
83
+ const diags = diagnoseOperation(f, undefined).filter((d) => d.code === 'missing-param-default');
84
+ expect(diags).toEqual([]);
85
+ });
86
+ });
87
+
88
+ describe('diagnoseOperation — param-no-real-scenario', () => {
89
+ it('emits info for a param when no scenario supplies a non-empty value', () => {
90
+ const f = flow({
91
+ http: { method: 'GET', path: '/api/channels/:id' },
92
+ nodes: [inputNode({ defaults: { params: { id: 'c1' } } })],
93
+ });
94
+ const diags = diagnoseOperation(f, undefined).filter((d) => d.code === 'param-no-real-scenario');
95
+ expect(diags).toHaveLength(1);
96
+ expect(diags[0]).toMatchObject({ severity: 'info', code: 'param-no-real-scenario', param: 'id' });
97
+ expect(diags[0].message.length).toBeGreaterThan(0);
98
+ expect(diags[0].message).toContain('scenario');
99
+ });
100
+
101
+ it('is silenced by a scenario with a non-empty input.params value', () => {
102
+ const f = flow({
103
+ http: { method: 'GET', path: '/api/channels/:id' },
104
+ nodes: [inputNode({ defaults: { params: { id: 'c1' } } })],
105
+ });
106
+ const scenarios = [scenario({ input: { params: { id: 'real-id' } } })];
107
+ const diags = diagnoseOperation(f, scenarios).filter((d) => d.code === 'param-no-real-scenario');
108
+ expect(diags).toEqual([]);
109
+ });
110
+
111
+ it('is not silenced by a scenario with an empty-string value', () => {
112
+ const f = flow({
113
+ http: { method: 'GET', path: '/api/channels/:id' },
114
+ nodes: [inputNode({ defaults: { params: { id: 'c1' } } })],
115
+ });
116
+ const scenarios = [scenario({ input: { params: { id: '' } } })];
117
+ const diags = diagnoseOperation(f, scenarios).filter((d) => d.code === 'param-no-real-scenario');
118
+ expect(diags).toHaveLength(1);
119
+ });
120
+
121
+ it('is not silenced by a scenario missing the param entirely', () => {
122
+ const f = flow({
123
+ http: { method: 'GET', path: '/api/channels/:id' },
124
+ nodes: [inputNode({ defaults: { params: { id: 'c1' } } })],
125
+ });
126
+ const scenarios = [scenario({ input: {} })];
127
+ const diags = diagnoseOperation(f, scenarios).filter((d) => d.code === 'param-no-real-scenario');
128
+ expect(diags).toHaveLength(1);
129
+ });
130
+
131
+ it('does not emit when there is no Input node', () => {
132
+ const f = flow({ http: { method: 'GET', path: '/api/channels/:id' }, nodes: [] });
133
+ const diags = diagnoseOperation(f, undefined).filter((d) => d.code === 'param-no-real-scenario');
134
+ expect(diags).toEqual([]);
135
+ });
136
+ });
137
+
138
+ describe('diagnoseOperation — no-expects', () => {
139
+ it('emits info when there are zero scenarios', () => {
140
+ const f = flow();
141
+ const diags = diagnoseOperation(f, undefined).filter((d) => d.code === 'no-expects');
142
+ expect(diags).toHaveLength(1);
143
+ expect(diags[0]).toMatchObject({ severity: 'info', code: 'no-expects' });
144
+ expect(diags[0].message.length).toBeGreaterThan(0);
145
+ expect(diags[0].message).toContain('expect');
146
+ });
147
+
148
+ it('emits info when scenarios exist but none carry an expect', () => {
149
+ const f = flow();
150
+ const scenarios = [scenario({ expect: undefined }), scenario({ id: 's2', name: 'S2' })];
151
+ const diags = diagnoseOperation(f, scenarios).filter((d) => d.code === 'no-expects');
152
+ expect(diags).toHaveLength(1);
153
+ });
154
+
155
+ it('is silenced when at least one scenario carries an expect', () => {
156
+ const f = flow();
157
+ const scenarios = [scenario({ expect: { status: 200 } })];
158
+ const diags = diagnoseOperation(f, scenarios).filter((d) => d.code === 'no-expects');
159
+ expect(diags).toEqual([]);
160
+ });
161
+
162
+ it('is emitted regardless of whether the flow has an http trigger', () => {
163
+ const f = flow({ http: { method: 'GET', path: '/api/x' } });
164
+ const diags = diagnoseOperation(f, undefined).filter((d) => d.code === 'no-expects');
165
+ expect(diags).toHaveLength(1);
166
+ });
167
+ });
168
+
169
+ describe('diagnoseOperation — ordering', () => {
170
+ it('orders all missing-param-default first (path order), then param-no-real-scenario (path order), then no-expects', () => {
171
+ const f = flow({
172
+ http: { method: 'GET', path: '/api/channels/:id/approvals/:approvalId' },
173
+ nodes: [inputNode()],
174
+ });
175
+ const diags = diagnoseOperation(f, undefined);
176
+ expect(diags.map((d) => [d.code, d.param])).toEqual([
177
+ ['missing-param-default', 'id'],
178
+ ['missing-param-default', 'approvalId'],
179
+ ['param-no-real-scenario', 'id'],
180
+ ['param-no-real-scenario', 'approvalId'],
181
+ ['no-expects', undefined],
182
+ ]);
183
+ });
184
+
185
+ it('appends missing-node-mock after every other code', () => {
186
+ const f = flow({
187
+ nodes: [{ id: 'db1', type: 'Query', label: 'Query', position: { x: 0, y: 0 }, config: {} }],
188
+ });
189
+ const diags = diagnoseOperation(f, [scenario({ expect: { status: 200 } })], {
190
+ infraNodes: [{ id: 'db1', traceKind: 'db' }],
191
+ });
192
+ expect(diags.map((d) => d.code)).toEqual(['missing-node-mock']);
193
+ });
194
+ });
195
+
196
+ describe('diagnoseOperation — missing-node-mock', () => {
197
+ function dbNode(id = 'db1', label = 'Fetch Users'): WorkflowNode {
198
+ return { id, type: 'Query', label, position: { x: 0, y: 0 }, config: {} };
199
+ }
200
+
201
+ const covered = [scenario({ expect: { status: 200 } })];
202
+
203
+ it('emits info per uncovered infra node, with the binding message copy', () => {
204
+ const f = flow({ nodes: [dbNode()] });
205
+ const diags = diagnoseOperation(f, covered, { infraNodes: [{ id: 'db1', traceKind: 'db' }] });
206
+ expect(diags).toHaveLength(1);
207
+ expect(diags[0]).toMatchObject({ severity: 'info', code: 'missing-node-mock', nodeId: 'db1' });
208
+ expect(diags[0].message).toBe(
209
+ '"Fetch Users" touches infrastructure (db) but has no mock — plain (no-scenario) Mock runs will fail at it. ' +
210
+ 'Cover with AI writes mocks, or add one under "mocks" in the scenarios file.',
211
+ );
212
+ });
213
+
214
+ it('emits one diagnostic per uncovered infra node', () => {
215
+ const f = flow({ nodes: [dbNode('db1', 'A'), dbNode('db2', 'B')] });
216
+ const diags = diagnoseOperation(f, covered, {
217
+ infraNodes: [
218
+ { id: 'db1', traceKind: 'db' },
219
+ { id: 'db2', traceKind: 'http' },
220
+ ],
221
+ });
222
+ expect(diags.map((d) => d.nodeId)).toEqual(['db1', 'db2']);
223
+ });
224
+
225
+ it('is silenced by an op-level flow.mocks entry for that nodeId', () => {
226
+ const f = flow({ nodes: [dbNode()], mocks: { db1: { rows: [] } } });
227
+ const diags = diagnoseOperation(f, covered, { infraNodes: [{ id: 'db1', traceKind: 'db' }] });
228
+ expect(diags).toEqual([]);
229
+ });
230
+
231
+ it('prefers extras.opMocks over flow.mocks when both are present', () => {
232
+ const f = flow({ nodes: [dbNode()], mocks: {} });
233
+ const diags = diagnoseOperation(f, covered, {
234
+ infraNodes: [{ id: 'db1', traceKind: 'db' }],
235
+ opMocks: { db1: { rows: [] } },
236
+ });
237
+ expect(diags).toEqual([]);
238
+ });
239
+
240
+ it('is not emitted when extras is absent, even with an infra-shaped node', () => {
241
+ const f = flow({ nodes: [dbNode()] });
242
+ const diags = diagnoseOperation(f, covered);
243
+ expect(diags.filter((d) => d.code === 'missing-node-mock')).toEqual([]);
244
+ });
245
+
246
+ it('is not emitted when extras.infraNodes is present but empty', () => {
247
+ const f = flow({ nodes: [dbNode()] });
248
+ const diags = diagnoseOperation(f, covered, { infraNodes: [] });
249
+ expect(diags.filter((d) => d.code === 'missing-node-mock')).toEqual([]);
250
+ });
251
+ });
252
+
253
+ describe('diagnoseOperation — simulated-commit', () => {
254
+ const covered = [scenario({ expect: { status: 200 } })];
255
+
256
+ function mutationNode(id: string, type = 'CreateRecord', label = 'Create Record'): WorkflowNode {
257
+ return { id, type, label, position: { x: 0, y: 0 }, config: {} };
258
+ }
259
+
260
+ it('warns when a mutation node has a [SIMULATED] marker in its impl source', () => {
261
+ const f = flow({ nodes: [mutationNode('m1')] });
262
+ const source = 'async function CreateRecord(ctx) { if (commit) { console.log("[SIMULATED] write"); return { ok: true }; } }';
263
+ const diags = diagnoseOperation(f, covered, { mutationSourcesByNode: { m1: source } }).filter(
264
+ (d) => d.code === 'simulated-commit',
265
+ );
266
+ expect(diags).toHaveLength(1);
267
+ expect(diags[0]).toMatchObject({ severity: 'warning', code: 'simulated-commit', nodeId: 'm1' });
268
+ expect(diags[0].message).toContain('m1');
269
+ expect(diags[0].message).toContain('CreateRecord');
270
+ });
271
+
272
+ it('does not warn when the mutation node impl source is clean', () => {
273
+ const f = flow({ nodes: [mutationNode('m1')] });
274
+ const source = 'async function CreateRecord(ctx) { return db.insert(ctx.params); }';
275
+ const diags = diagnoseOperation(f, covered, { mutationSourcesByNode: { m1: source } }).filter(
276
+ (d) => d.code === 'simulated-commit',
277
+ );
278
+ expect(diags).toEqual([]);
279
+ });
280
+
281
+ it('does not warn for a non-mutation node even if its source contains [SIMULATED] (effects gate is the caller\'s job — absence from the map means it was gated out)', () => {
282
+ const f = flow({ nodes: [mutationNode('c1', 'ComputeStub', 'Compute')] });
283
+ // A compute node's source is never placed in mutationSourcesByNode by a
284
+ // well-behaved caller (effects !== 'mutation'), so it's simply absent here.
285
+ const diags = diagnoseOperation(f, covered, { mutationSourcesByNode: {} }).filter(
286
+ (d) => d.code === 'simulated-commit',
287
+ );
288
+ expect(diags).toEqual([]);
289
+ });
290
+
291
+ it('does not warn for an unregistered node type — absent from the map, no crash', () => {
292
+ const f = flow({ nodes: [mutationNode('u1', 'UnknownType')] });
293
+ const diags = diagnoseOperation(f, covered, { mutationSourcesByNode: {} }).filter(
294
+ (d) => d.code === 'simulated-commit',
295
+ );
296
+ expect(diags).toEqual([]);
297
+ });
298
+
299
+ it('is not emitted when extras is absent', () => {
300
+ const f = flow({ nodes: [mutationNode('m1')] });
301
+ const diags = diagnoseOperation(f, covered).filter((d) => d.code === 'simulated-commit');
302
+ expect(diags).toEqual([]);
303
+ });
304
+
305
+ it('flags each node instance separately — no dedupe per type', () => {
306
+ const f = flow({ nodes: [mutationNode('m1'), mutationNode('m2')] });
307
+ const source = 'function CreateRecord() { return "[SIMULATED] ok"; }';
308
+ const diags = diagnoseOperation(f, covered, {
309
+ mutationSourcesByNode: { m1: source, m2: source },
310
+ }).filter((d) => d.code === 'simulated-commit');
311
+ expect(diags.map((d) => d.nodeId)).toEqual(['m1', 'm2']);
312
+ });
313
+ });
314
+
315
+ describe('diagnoseOperation — inputmap-schema-mismatch', () => {
316
+ const covered = [scenario({ expect: { status: 200 } })];
317
+
318
+ function node(id: string, over: Partial<WorkflowNode> = {}): WorkflowNode {
319
+ return { id, type: 'X', label: id, position: { x: 0, y: 0 }, config: {}, ...over };
320
+ }
321
+
322
+ it('warns when an inputMap reads a field the source node never declares', () => {
323
+ const f = flow({
324
+ nodes: [
325
+ node('src'),
326
+ node('dst', { inputMap: { trends: { sourceNodeId: 'src', sourceField: 'trend' } } }),
327
+ ],
328
+ });
329
+ const diags = diagnoseOperation(f, covered, { outputFieldsByNode: { src: ['trendRows', 'rCV'] } }).filter(
330
+ (d) => d.code === 'inputmap-schema-mismatch',
331
+ );
332
+ expect(diags).toHaveLength(1);
333
+ expect(diags[0]).toMatchObject({ severity: 'warning', code: 'inputmap-schema-mismatch', nodeId: 'dst', param: 'trends' });
334
+ expect(diags[0].message).toContain('trend');
335
+ expect(diags[0].message).toContain('src');
336
+ });
337
+
338
+ it('checks only the FIRST dotted segment', () => {
339
+ const f = flow({
340
+ nodes: [
341
+ node('src'),
342
+ node('dst', { inputMap: { v: { sourceNodeId: 'src', sourceField: 'signals.0.signalId' } } }),
343
+ ],
344
+ });
345
+ const ok = diagnoseOperation(f, covered, { outputFieldsByNode: { src: ['signals'] } }).filter(
346
+ (d) => d.code === 'inputmap-schema-mismatch',
347
+ );
348
+ expect(ok).toEqual([]);
349
+ const bad = diagnoseOperation(f, covered, { outputFieldsByNode: { src: ['items'] } }).filter(
350
+ (d) => d.code === 'inputmap-schema-mismatch',
351
+ );
352
+ expect(bad).toHaveLength(1);
353
+ });
354
+
355
+ it('ignores a "$" whole-output sourceField (and a leading $.)', () => {
356
+ const f = flow({
357
+ nodes: [
358
+ node('src'),
359
+ node('a', { inputMap: { v: { sourceNodeId: 'src', sourceField: '$' } } }),
360
+ node('b', { inputMap: { v: { sourceNodeId: 'src', sourceField: '$.foo' } } }),
361
+ ],
362
+ });
363
+ // '$' → skip; '$.foo' → first real segment is 'foo', not declared → warn once.
364
+ const diags = diagnoseOperation(f, covered, { outputFieldsByNode: { src: ['bar'] } }).filter(
365
+ (d) => d.code === 'inputmap-schema-mismatch',
366
+ );
367
+ expect(diags.map((d) => d.nodeId)).toEqual(['b']);
368
+ });
369
+
370
+ it('does not fire for a source node absent from outputFieldsByNode (unregistered or no declared fields)', () => {
371
+ const f = flow({
372
+ nodes: [
373
+ node('src'),
374
+ node('dst', { inputMap: { v: { sourceNodeId: 'src', sourceField: 'anything' } } }),
375
+ ],
376
+ });
377
+ // Input/Subflow convention: no entry → never flagged.
378
+ const diags = diagnoseOperation(f, covered, { outputFieldsByNode: {} }).filter(
379
+ (d) => d.code === 'inputmap-schema-mismatch',
380
+ );
381
+ expect(diags).toEqual([]);
382
+ });
383
+
384
+ it('does not fire at all when extras.outputFieldsByNode is omitted', () => {
385
+ const f = flow({
386
+ nodes: [
387
+ node('src'),
388
+ node('dst', { inputMap: { v: { sourceNodeId: 'src', sourceField: 'nope' } } }),
389
+ ],
390
+ });
391
+ const diags = diagnoseOperation(f, covered).filter((d) => d.code === 'inputmap-schema-mismatch');
392
+ expect(diags).toEqual([]);
393
+ });
394
+ });
395
+
396
+ describe('diagnoseOperation — language-drift', () => {
397
+ const covered = [scenario({ expect: { status: 200 } })];
398
+
399
+ it('warns (info) when extras carries an explicit javascript language against a .ts config', () => {
400
+ const f = flow();
401
+ const diags = diagnoseOperation(f, covered, {
402
+ languageDrift: { projectLanguage: 'javascript', configPathExtension: '.ts' },
403
+ }).filter((d) => d.code === 'language-drift');
404
+ expect(diags).toHaveLength(1);
405
+ expect(diags[0]).toMatchObject({ severity: 'info', code: 'language-drift' });
406
+ expect(diags[0].message).toContain('javascript');
407
+ expect(diags[0].message).toContain('.ts');
408
+ });
409
+
410
+ it('warns (info) when extras carries an explicit typescript language against a .mjs config', () => {
411
+ const f = flow();
412
+ const diags = diagnoseOperation(f, covered, {
413
+ languageDrift: { projectLanguage: 'typescript', configPathExtension: '.mjs' },
414
+ }).filter((d) => d.code === 'language-drift');
415
+ expect(diags).toHaveLength(1);
416
+ expect(diags[0]).toMatchObject({ severity: 'info', code: 'language-drift' });
417
+ expect(diags[0].message).toContain('typescript');
418
+ expect(diags[0].message).toContain('.mjs');
419
+ });
420
+
421
+ it('is not emitted when extras.languageDrift is absent — inference agrees by construction', () => {
422
+ const f = flow();
423
+ const diags = diagnoseOperation(f, covered).filter((d) => d.code === 'language-drift');
424
+ expect(diags).toEqual([]);
425
+ });
426
+
427
+ it('is appended after every other code', () => {
428
+ const f = flow({
429
+ http: { method: 'GET', path: '/api/x/:id' },
430
+ nodes: [inputNode()],
431
+ });
432
+ const diags = diagnoseOperation(f, undefined, {
433
+ languageDrift: { projectLanguage: 'javascript', configPathExtension: '.ts' },
434
+ });
435
+ expect(diags[diags.length - 1].code).toBe('language-drift');
436
+ });
437
+ });