@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,582 @@
1
+ import * as SunCalc from 'suncalc';
2
+
3
+ /**
4
+ * EV charge scheduler — pure decision logic for a demo solar/EV charging flow.
5
+ * Every decision is a pure function so an Emberflow flow can drive it
6
+ * deterministically (constants, formulas, thresholds, priorities, edge cases).
7
+ *
8
+ * Two notes on shape:
9
+ * 1. `getThresholds` takes trends as an explicit parameter rather than reading
10
+ * a module-level rolling-reading window.
11
+ * 2. Time-dependent helpers take a `now` ISO string (default: current time) and
12
+ * resolve the wall-clock HOUR from UTC (`getUTCHours`) so scenarios pinned
13
+ * with a `now` (…Z) are reproducible on any machine. Sunrise/sunset come
14
+ * from suncalc as absolute UTC instants, so `isSolarHours` /
15
+ * `hoursUntilSunset` are machine-independent.
16
+ */
17
+
18
+ // Demo location (Berlin). Overridable per call.
19
+ export const LAT = 52.52;
20
+ export const LON = 13.405;
21
+
22
+ // EV constants (verbatim)
23
+ export const EV_TARGET_BATTERY = 80; // Target for night/grid charging
24
+ export const EV_SOLAR_TARGET = 90; // Target for solar excess charging (AC is gentle, free energy)
25
+ export const EV_CAPACITY_KWH = 77.4; // EV6 Long Range usable capacity
26
+ export const EV_CHARGE_RATE_KW = 2.3; // Home EVSE charge rate (10A single phase)
27
+ export const EV_LOW_BATTERY = 50; // Below this, EV gets priority over home battery
28
+ export const EV_NIGHT_CHARGE_BELOW = 35; // Below this at night, force a grid charge to 80%
29
+
30
+ // Thresholds (watts) — reactive mode (verbatim)
31
+ export const START_SURPLUS_W = 1500;
32
+ export const STOP_SURPLUS_W = 500;
33
+ export const START_MIN_SOC = 30;
34
+ export const STOP_MIN_SOC = 20;
35
+ export const COOLDOWN_MS = 10 * 60_000;
36
+
37
+ // Predictive thresholds (verbatim)
38
+ export const PREDICTIVE_BATTERY_SOC = 90;
39
+ export const PREDICTIVE_MIN_PPV = 2000;
40
+ export const SUNSET_HOURS_THRESHOLD = 2;
41
+ export const GOLDEN_HOUR_START_W = 800;
42
+ export const GOLDEN_HOUR_STOP_W = 200;
43
+
44
+ // Worth-it calculation: minimum kWh a session must deliver to justify the wake +
45
+ // command overhead (~13 min at 2.3kW — below this it's not worth the wear).
46
+ export const MIN_SESSION_KWH = 0.5;
47
+
48
+ // Trend tracking — rolling window of last N readings (verbatim).
49
+ export const TREND_WINDOW = 6;
50
+
51
+ export type EvChargeState = 'idle' | 'waiting' | 'charging' | 'cooldown' | 'night_charging';
52
+
53
+ export interface Reading {
54
+ time?: number;
55
+ surplus: number;
56
+ homeSoc: number;
57
+ ppv: number;
58
+ }
59
+
60
+ export interface Forecast {
61
+ remainingKwh: number;
62
+ strongHoursLeft: number;
63
+ }
64
+
65
+ // ============================================================================
66
+ // Sun times — suncalc, with a `now` override for determinism.
67
+ // ============================================================================
68
+
69
+ export interface SunClock {
70
+ isSolarHours: boolean;
71
+ hoursUntilSunset: number;
72
+ sunrise: string;
73
+ sunset: string;
74
+ /** UTC wall-clock hour (0–23). */
75
+ hour: number;
76
+ /** True inside the 23:00–05:00 night-charge window. */
77
+ isNightWindow: boolean;
78
+ }
79
+
80
+ function resolveNow(now?: string): Date {
81
+ return now ? new Date(now) : new Date();
82
+ }
83
+
84
+ /** Sun position + derived time flags for the given instant/location. */
85
+ export function sunTimes(now?: string, lat: number = LAT, lon: number = LON): SunClock {
86
+ const d = resolveNow(now);
87
+ const times = SunCalc.getTimes(d, lat, lon);
88
+ // @types/suncalc types sunrise/sunset as possibly null (polar edge cases);
89
+ // for this latitude they are always present. Fall back to the instant itself.
90
+ const sunrise = times.sunrise ?? d;
91
+ const sunset = times.sunset ?? d;
92
+ const isSolarHours = d > sunrise && d < sunset;
93
+ const hoursUntilSunset = Math.max(0, (sunset.getTime() - d.getTime()) / 3_600_000);
94
+ const hour = d.getUTCHours();
95
+ // Verbatim night gate: production returns early "outside night" when
96
+ // `hour >= 5 && hour < 23`, i.e. the night window is hour >= 23 || hour < 5.
97
+ const isNightWindow = hour >= 23 || hour < 5;
98
+ return {
99
+ isSolarHours,
100
+ hoursUntilSunset,
101
+ sunrise: sunrise.toISOString(),
102
+ sunset: sunset.toISOString(),
103
+ hour,
104
+ isNightWindow,
105
+ };
106
+ }
107
+
108
+ // ============================================================================
109
+ // Surplus, kWh math (verbatim)
110
+ // ============================================================================
111
+
112
+ export interface SurplusResult {
113
+ surplus: number;
114
+ /** 'export' when grid export is positive, else 'ppv-load'. */
115
+ branch: 'export' | 'ppv-load';
116
+ }
117
+
118
+ /**
119
+ * Verbatim surplus calc (evaluateCharge):
120
+ * surplus = gridExport > 0 ? gridExport : max(0, ppv - loadPower)
121
+ */
122
+ export function computeSurplus(ppv: number, loadPower: number, gridExport: number): SurplusResult {
123
+ if (gridExport > 0) return { surplus: gridExport, branch: 'export' };
124
+ return { surplus: Math.max(0, ppv - loadPower), branch: 'ppv-load' };
125
+ }
126
+
127
+ /** How many kWh the EV needs to reach target (verbatim evKwhNeeded). */
128
+ export function evKwhNeeded(currentPct: number, target: number = EV_TARGET_BATTERY): number {
129
+ const pctNeeded = target - currentPct;
130
+ if (pctNeeded <= 0) return 0;
131
+ return (pctNeeded / 100) * EV_CAPACITY_KWH;
132
+ }
133
+
134
+ /**
135
+ * Estimate deliverable kWh from remaining solar (verbatim estimateSessionKwh).
136
+ * With a forecast: remaining kWh capped by charge rate over the hours left.
137
+ * Without: assume current surplus sustains for the remaining sun hours.
138
+ */
139
+ export function estimateSessionKwh(
140
+ surplusW: number,
141
+ sunHoursLeft: number,
142
+ forecast: Forecast | null,
143
+ ): number {
144
+ if (forecast) {
145
+ return Math.min(forecast.remainingKwh, EV_CHARGE_RATE_KW * sunHoursLeft);
146
+ }
147
+ return (Math.min(surplusW, EV_CHARGE_RATE_KW * 1000) / 1000) * sunHoursLeft;
148
+ }
149
+
150
+ // ============================================================================
151
+ // Trends + dynamic cooldown (verbatim getTrend / getCooldownMs)
152
+ // ============================================================================
153
+
154
+ /** Per-cycle slope of `key` across the reading window (verbatim getTrend). */
155
+ export function getTrend(readings: Reading[], key: 'surplus' | 'homeSoc' | 'ppv'): number {
156
+ if (readings.length < 2) return 0;
157
+ const first = readings[0];
158
+ const last = readings[readings.length - 1];
159
+ const cycles = readings.length - 1;
160
+ return (last[key] - first[key]) / cycles;
161
+ }
162
+
163
+ export interface TrendWindow {
164
+ surplusTrend: number;
165
+ socTrend: number;
166
+ ppvTrend: number;
167
+ stdDev: number;
168
+ cooldownMs: number;
169
+ }
170
+
171
+ /**
172
+ * Dynamic cooldown from surplus volatility (verbatim getCooldownMs):
173
+ * < 3 readings → default 10 min
174
+ * stdDev < 200 → stable → 5 min
175
+ * stdDev > 800 → volatile → 15 min
176
+ * else → default 10 min
177
+ */
178
+ export function getCooldownMs(readings: Reading[]): number {
179
+ if (readings.length < 3) return COOLDOWN_MS;
180
+ const surpluses = readings.map((r) => r.surplus);
181
+ const mean = surpluses.reduce((a, b) => a + b, 0) / surpluses.length;
182
+ const variance = surpluses.reduce((sum, s) => sum + (s - mean) ** 2, 0) / surpluses.length;
183
+ const stdDev = Math.sqrt(variance);
184
+ if (stdDev < 200) return 5 * 60_000;
185
+ if (stdDev > 800) return 15 * 60_000;
186
+ return COOLDOWN_MS;
187
+ }
188
+
189
+ /** stdDev of surplus across the window (0 for <3 readings, mirroring the cooldown gate). */
190
+ export function surplusStdDev(readings: Reading[]): number {
191
+ if (readings.length < 1) return 0;
192
+ const surpluses = readings.map((r) => r.surplus);
193
+ const mean = surpluses.reduce((a, b) => a + b, 0) / surpluses.length;
194
+ const variance = surpluses.reduce((sum, s) => sum + (s - mean) ** 2, 0) / surpluses.length;
195
+ return Math.sqrt(variance);
196
+ }
197
+
198
+ /** Bundle the trend-window derivations one Trend Window node emits. */
199
+ export function computeTrendWindow(readings: Reading[]): TrendWindow {
200
+ return {
201
+ surplusTrend: getTrend(readings, 'surplus'),
202
+ socTrend: getTrend(readings, 'homeSoc'),
203
+ ppvTrend: getTrend(readings, 'ppv'),
204
+ stdDev: surplusStdDev(readings),
205
+ cooldownMs: getCooldownMs(readings),
206
+ };
207
+ }
208
+
209
+ // ============================================================================
210
+ // Thresholds (verbatim getThresholds; trends made an explicit param)
211
+ // ============================================================================
212
+
213
+ export interface Trends {
214
+ surplusTrend: number;
215
+ socTrend: number;
216
+ }
217
+
218
+ export interface ThresholdResult {
219
+ startW: number;
220
+ stopW: number;
221
+ skipReason: string | null;
222
+ reasons: string[];
223
+ }
224
+
225
+ /**
226
+ * Verbatim port of getThresholds. The ONLY change: `trends` (surplus + home-SOC
227
+ * per-cycle slopes) is passed in explicitly instead of read from module state
228
+ * via `getTrend(...)`. Defaults to zero slopes (no trend override), matching a
229
+ * cold start with fewer than two readings.
230
+ */
231
+ export function getThresholds(
232
+ homeSoc: number,
233
+ ppv: number,
234
+ sunHoursLeft: number,
235
+ evBattery: number | null,
236
+ forecast: Forecast | null,
237
+ trends: Trends = { surplusTrend: 0, socTrend: 0 },
238
+ ): ThresholdResult {
239
+ const reasons: string[] = [];
240
+ // Default thresholds must cover the EV charge rate (2.3kW) so we don't
241
+ // primarily import from grid. Home battery can buffer the small shortfall.
242
+ let startW = 2000;
243
+ let stopW = 800;
244
+ let skipReason: string | null = null;
245
+
246
+ const evUrgent = evBattery !== null && evBattery < EV_LOW_BATTERY;
247
+ const homeHasBuffer = homeSoc >= 30;
248
+
249
+ if (evUrgent) {
250
+ startW = 1200;
251
+ stopW = 400;
252
+ reasons.push(`ev urgent: ${evBattery}% < ${EV_LOW_BATTERY}%, accepting some grid`);
253
+ } else if (homeHasBuffer) {
254
+ startW = 1200;
255
+ stopW = 400;
256
+ reasons.push(`home ${homeSoc}% has buffer — EV priority, home tops up EV shortfall`);
257
+ } else {
258
+ reasons.push(`home ${homeSoc}% flat — need ${startW}W solar to avoid grid import`);
259
+ }
260
+
261
+ // --- Predictive: battery nearly full + strong production → export imminent ---
262
+ if (homeSoc >= PREDICTIVE_BATTERY_SOC && ppv >= PREDICTIVE_MIN_PPV) {
263
+ startW = Math.min(startW, 500);
264
+ stopW = Math.min(stopW, 200);
265
+ reasons.push(`predictive: home SOC ${homeSoc}% + ppv ${Math.round(ppv)}W → export imminent`);
266
+ }
267
+
268
+ // --- Golden hour: sun setting soon, capture remaining solar ---
269
+ if (sunHoursLeft > 0 && sunHoursLeft <= SUNSET_HOURS_THRESHOLD && !skipReason) {
270
+ startW = Math.min(startW, GOLDEN_HOUR_START_W);
271
+ stopW = Math.min(stopW, GOLDEN_HOUR_STOP_W);
272
+ reasons.push(`golden hour: ${sunHoursLeft.toFixed(1)}h until sunset`);
273
+ }
274
+
275
+ // --- Worth-it check: is the remaining solar enough to justify a session? ---
276
+ const inBonusZone = evBattery !== null && evBattery >= EV_TARGET_BATTERY && evBattery < EV_SOLAR_TARGET;
277
+ if (evBattery !== null && forecast && !skipReason) {
278
+ const estimatedKwh = estimateSessionKwh(ppv, sunHoursLeft, forecast);
279
+ const target = inBonusZone ? EV_SOLAR_TARGET : EV_TARGET_BATTERY;
280
+ const needed = evKwhNeeded(evBattery, target);
281
+ if (estimatedKwh < MIN_SESSION_KWH && needed > 2) {
282
+ skipReason = `not worth it: ~${estimatedKwh.toFixed(1)}kWh available vs ${needed.toFixed(0)}kWh needed`;
283
+ reasons.push(skipReason);
284
+ }
285
+ if (evBattery >= 65 && estimatedKwh < 1.5) {
286
+ skipReason = `EV at ${evBattery}%, only ~${estimatedKwh.toFixed(1)}kWh solar remaining — not worth a session`;
287
+ reasons.push(skipReason);
288
+ }
289
+ }
290
+
291
+ // --- Trend-aware: surplus rising fast → be more aggressive ---
292
+ const surplusTrend = trends.surplusTrend;
293
+ if (surplusTrend > 200) {
294
+ startW = Math.min(startW, 1000);
295
+ reasons.push(`trend: surplus rising ${Math.round(surplusTrend)}W/cycle`);
296
+ }
297
+
298
+ // --- SOC climbing fast → battery about to fill ---
299
+ const socTrend = trends.socTrend;
300
+ if (socTrend > 2 && homeSoc > 75) {
301
+ startW = Math.min(startW, 600);
302
+ reasons.push(`trend: home SOC rising ${socTrend.toFixed(1)}%/cycle from ${homeSoc}%`);
303
+ }
304
+
305
+ return { startW, stopW, skipReason, reasons };
306
+ }
307
+
308
+ // ============================================================================
309
+ // Unscheduled-charge guard (verbatim shouldStopUnscheduledCharge)
310
+ // ============================================================================
311
+
312
+ export interface UnscheduledGuardInput {
313
+ charging: boolean;
314
+ state: EvChargeState;
315
+ surplus: number;
316
+ startW: number;
317
+ skipReason: string | null;
318
+ homeSoc: number;
319
+ effectiveMinSoc: number;
320
+ }
321
+
322
+ export function shouldStopUnscheduledCharge({
323
+ charging,
324
+ state,
325
+ surplus,
326
+ startW,
327
+ skipReason,
328
+ homeSoc,
329
+ effectiveMinSoc,
330
+ }: UnscheduledGuardInput): boolean {
331
+ if (!charging) return false;
332
+ if (state === 'charging' || state === 'cooldown' || state === 'night_charging') return false;
333
+ if (skipReason) return true;
334
+ if (surplus < startW) return true;
335
+ if (homeSoc < effectiveMinSoc) return true;
336
+ return false;
337
+ }
338
+
339
+ // ============================================================================
340
+ // Night charge decision (verbatim getNightChargeDecision)
341
+ // ============================================================================
342
+
343
+ export interface NightChargeDecision {
344
+ shouldCharge: boolean;
345
+ factors: string[];
346
+ }
347
+
348
+ export function getNightChargeDecision(
349
+ evBattery: number,
350
+ tomorrowKwh: number | null,
351
+ ): NightChargeDecision {
352
+ const factors: string[] = [];
353
+ const criticallyLow = evBattery < EV_NIGHT_CHARGE_BELOW;
354
+ const poorForecast = tomorrowKwh !== null && tomorrowKwh < 10; // < 10kWh expected = poor day
355
+ const evModerate = evBattery < 65;
356
+
357
+ if (criticallyLow) {
358
+ factors.push(`EV critically low at ${evBattery}% (< ${EV_NIGHT_CHARGE_BELOW}%)`);
359
+ } else if (evModerate && poorForecast) {
360
+ factors.push(`EV at ${evBattery}% with poor forecast tomorrow (${tomorrowKwh?.toFixed(1)}kWh)`);
361
+ }
362
+
363
+ return { shouldCharge: factors.length > 0, factors };
364
+ }
365
+
366
+ // ============================================================================
367
+ // Charge decision — verbatim branch logic of evaluateCharge (lines 629–705).
368
+ // ============================================================================
369
+
370
+ export type ChargeAction = 'start' | 'stop' | 'hold_waiting' | 'hold_charging';
371
+
372
+ export interface ChargeDecisionInput {
373
+ state: 'idle' | 'waiting' | 'charging';
374
+ surplus: number;
375
+ startW: number;
376
+ stopW: number;
377
+ homeSoc: number;
378
+ skipReason: string | null;
379
+ }
380
+
381
+ export interface ChargeDecisionResult {
382
+ action: ChargeAction;
383
+ nextState: EvChargeState;
384
+ reason: string;
385
+ }
386
+
387
+ /**
388
+ * Verbatim port of evaluateCharge's idle/waiting/charging branch (lines
389
+ * 629–705). effectiveMinSoc and effectiveStopSoc are both 0 in production
390
+ * (morning home SOC near 0% is normal after overnight discharge — the surplus
391
+ * threshold already prevents grid import), so the SOC comparisons never trip
392
+ * here; they are preserved verbatim for fidelity.
393
+ */
394
+ export function decideCharge({
395
+ state,
396
+ surplus,
397
+ startW,
398
+ stopW,
399
+ homeSoc,
400
+ skipReason,
401
+ }: ChargeDecisionInput): ChargeDecisionResult {
402
+ const effectiveMinSoc = 0;
403
+ const effectiveStopSoc = 0;
404
+
405
+ if (state === 'idle' || state === 'waiting') {
406
+ if (skipReason) {
407
+ return { action: 'hold_waiting', nextState: 'waiting', reason: skipReason };
408
+ }
409
+ if (surplus >= startW && homeSoc >= effectiveMinSoc) {
410
+ return {
411
+ action: 'start',
412
+ nextState: 'cooldown',
413
+ reason: `surplus ${Math.round(surplus)}W ≥ ${startW}W threshold, starting charge`,
414
+ };
415
+ }
416
+ if (surplus < startW) {
417
+ return {
418
+ action: 'hold_waiting',
419
+ nextState: 'waiting',
420
+ reason: `surplus ${Math.round(surplus)}W < ${startW}W threshold`,
421
+ };
422
+ }
423
+ return {
424
+ action: 'hold_waiting',
425
+ nextState: 'waiting',
426
+ reason: `home SOC ${homeSoc}% < ${effectiveMinSoc}% threshold`,
427
+ };
428
+ }
429
+
430
+ // state === 'charging'
431
+ if (surplus < stopW || homeSoc < effectiveStopSoc) {
432
+ const reason =
433
+ surplus < stopW
434
+ ? `surplus ${Math.round(surplus)}W < ${stopW}W, stopping charge`
435
+ : `home SOC ${homeSoc}% < ${effectiveStopSoc}%, stopping charge`;
436
+ return { action: 'stop', nextState: 'cooldown', reason };
437
+ }
438
+ return {
439
+ action: 'hold_charging',
440
+ nextState: 'charging',
441
+ reason: `charging — surplus ${Math.round(surplus)}W, home SOC ${homeSoc}%`,
442
+ };
443
+ }
444
+
445
+ // ============================================================================
446
+ // Target check — verbatim "EV not plugged in or at target" gate (lines 553–572)
447
+ // ============================================================================
448
+
449
+ export type TargetBranch = 'notPlugged' | 'atTarget' | 'belowTarget';
450
+
451
+ export interface TargetCheckResult {
452
+ branch: TargetBranch;
453
+ effectiveTarget: number;
454
+ reason: string;
455
+ }
456
+
457
+ /**
458
+ * Verbatim port of the plugged-in / at-target gate. Effective target is the
459
+ * solar target (90%) during solar hours, else the grid target (80%).
460
+ */
461
+ export function checkTarget(
462
+ pluggedIn: boolean,
463
+ evBattery: number | null,
464
+ isSolarHours: boolean,
465
+ ): TargetCheckResult {
466
+ const effectiveTarget = isSolarHours ? EV_SOLAR_TARGET : EV_TARGET_BATTERY;
467
+ if (!pluggedIn) {
468
+ return { branch: 'notPlugged', effectiveTarget, reason: 'not plugged in' };
469
+ }
470
+ if (evBattery !== null && evBattery >= effectiveTarget) {
471
+ return {
472
+ branch: 'atTarget',
473
+ effectiveTarget,
474
+ reason: `battery at ${evBattery}% (target ${effectiveTarget}%)`,
475
+ };
476
+ }
477
+ return {
478
+ branch: 'belowTarget',
479
+ effectiveTarget,
480
+ reason: `battery at ${evBattery ?? '?'}% below target ${effectiveTarget}%`,
481
+ };
482
+ }
483
+
484
+ // ============================================================================
485
+ // Reconcile — verbatim "adopt an externally-started charge" gate (lines 604–610)
486
+ // ============================================================================
487
+
488
+ export type ReconcileDecision = 'adopt' | 'hold';
489
+
490
+ export interface ReconcileResult {
491
+ decision: ReconcileDecision;
492
+ nextState: EvChargeState;
493
+ reason: string;
494
+ }
495
+
496
+ /**
497
+ * Verbatim port: if the car is charging but the scheduler isn't in a
498
+ * charging/night/cooldown state, and current rules allow it, adopt the session.
499
+ */
500
+ export function reconcile(charging: boolean, state: EvChargeState): ReconcileResult {
501
+ if (charging && state !== 'charging' && state !== 'night_charging' && state !== 'cooldown') {
502
+ return {
503
+ decision: 'adopt',
504
+ nextState: 'charging',
505
+ reason: 'reconciled: car was already charging and scheduler conditions are met',
506
+ };
507
+ }
508
+ return { decision: 'hold', nextState: state, reason: 'no reconciliation needed' };
509
+ }
510
+
511
+ // ============================================================================
512
+ // Night-charge sizing (verbatim from evaluateNightCharge)
513
+ // ============================================================================
514
+
515
+ export interface NightChargeEstimate {
516
+ neededKwh: number;
517
+ hoursToCharge: number;
518
+ }
519
+
520
+ /** kWh needed to reach the grid target + hours at the home charge rate (verbatim). */
521
+ export function nightChargeEstimate(evBattery: number): NightChargeEstimate {
522
+ const neededKwh = evKwhNeeded(evBattery); // target = EV_TARGET_BATTERY (80)
523
+ return { neededKwh, hoursToCharge: neededKwh / EV_CHARGE_RATE_KW };
524
+ }
525
+
526
+ // ============================================================================
527
+ // Morning summary outlook (verbatim from the morning-summary block, lines 416–435)
528
+ // ============================================================================
529
+
530
+ export interface MorningSummary {
531
+ lines: string[];
532
+ outlook: 'good' | 'tight' | 'unlikely' | 'at-target' | 'not-applicable';
533
+ }
534
+
535
+ /**
536
+ * Verbatim port of the morning-summary outlook lines. `remainingKwh` is today's
537
+ * remaining solar forecast; `evBattery`/`pluggedIn` come from the Kia snapshot.
538
+ * Outlook thresholds: Good when remaining > needed×1.5, Tight when > needed,
539
+ * else Unlikely; EV at/above the solar target reports "at target".
540
+ */
541
+ export function morningSummary(
542
+ homeSoc: number | null,
543
+ evBattery: number | null,
544
+ pluggedIn: boolean,
545
+ todayKwh: number | null,
546
+ remainingKwh: number | null,
547
+ strongHoursLeft: number | null,
548
+ ): MorningSummary {
549
+ const homeStr = homeSoc ?? '?';
550
+ const evStr = evBattery ?? '?';
551
+ const plugged = pluggedIn ? 'plugged in' : 'not plugged in';
552
+ const todayStr = todayKwh != null ? todayKwh.toFixed(1) : '?';
553
+ const strongStr = strongHoursLeft ?? '?';
554
+ const remainStr = remainingKwh != null ? remainingKwh.toFixed(1) : '?';
555
+
556
+ const lines = [
557
+ `Home battery: ${homeStr}%`,
558
+ `EV: ${evStr}% (${plugged})`,
559
+ `Solar forecast: ${todayStr}kWh today (${remainStr}kWh remaining, ${strongStr}h strong sun)`,
560
+ ];
561
+
562
+ let outlook: MorningSummary['outlook'] = 'not-applicable';
563
+ if (pluggedIn && typeof evBattery === 'number' && evBattery < EV_SOLAR_TARGET) {
564
+ const needed = evKwhNeeded(evBattery, EV_SOLAR_TARGET);
565
+ const remaining = remainingKwh ?? 0;
566
+ if (remaining > needed * 1.5) {
567
+ outlook = 'good';
568
+ lines.push(`Outlook: Good — ${remaining.toFixed(1)}kWh forecast should cover ${needed.toFixed(1)}kWh EV needs`);
569
+ } else if (remaining > needed) {
570
+ outlook = 'tight';
571
+ lines.push(`Outlook: Tight — ${remaining.toFixed(1)}kWh forecast vs ${needed.toFixed(1)}kWh EV needs (home battery gets priority)`);
572
+ } else {
573
+ outlook = 'unlikely';
574
+ lines.push(`Outlook: Unlikely to fully charge EV today (${remaining.toFixed(1)}kWh forecast vs ${needed.toFixed(1)}kWh needed)`);
575
+ }
576
+ } else if (typeof evBattery === 'number' && evBattery >= EV_SOLAR_TARGET) {
577
+ outlook = 'at-target';
578
+ lines.push(`EV already at target (${EV_SOLAR_TARGET}%) — no charging needed`);
579
+ }
580
+
581
+ return { lines, outlook };
582
+ }