okengine 0.21.1 → 0.23.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 (342) hide show
  1. package/AGENTS.md +11 -4
  2. package/LICENSE +202 -0
  3. package/NOTICE +2 -0
  4. package/README.md +15 -9
  5. package/manifest.v1.schema.json +54 -0
  6. package/package.json +53 -38
  7. package/site/content/docs/ai/mcp.mdx +13 -9
  8. package/site/content/docs/ai/skills.mdx +2 -2
  9. package/site/content/docs/client/calling.mdx +27 -19
  10. package/site/content/docs/client/index.mdx +1 -1
  11. package/site/content/docs/elements/ai/agents.mdx +61 -13
  12. package/site/content/docs/elements/ai/decide.mdx +536 -0
  13. package/site/content/docs/elements/ai/events.mdx +373 -0
  14. package/site/content/docs/elements/ai/index.mdx +9 -3
  15. package/site/content/docs/elements/ai/meta.json +1 -1
  16. package/site/content/docs/elements/ai/prompts.mdx +4 -2
  17. package/site/content/docs/elements/channel/email.mdx +1 -1
  18. package/site/content/docs/elements/channel/index.mdx +1 -1
  19. package/site/content/docs/elements/flow/http.mdx +4 -4
  20. package/site/content/docs/elements/flow/index.mdx +3 -3
  21. package/site/content/docs/elements/flow/meta.json +1 -1
  22. package/site/content/docs/elements/gate/auth.mdx +4 -1
  23. package/site/content/docs/elements/gate/tenancy.mdx +1 -1
  24. package/site/content/docs/elements/store/index.mdx +1 -1
  25. package/site/content/docs/elements/store/kv.mdx +5 -5
  26. package/site/content/docs/elements/vault/rotation.mdx +15 -0
  27. package/site/content/docs/plugins/otp.mdx +31 -31
  28. package/site/content/docs/recipes/dragonfly.mdx +5 -5
  29. package/site/content/docs/recipes/index.mdx +6 -6
  30. package/site/content/docs/recipes/mailpit.mdx +1 -1
  31. package/site/content/docs/recipes/meilisearch.mdx +1 -1
  32. package/site/content/docs/recipes/pgdog.mdx +1 -1
  33. package/site/content/docs/recipes/postgres.mdx +12 -8
  34. package/site/content/docs/recipes/redis.mdx +4 -4
  35. package/site/content/docs/reference/cli.mdx +2 -1
  36. package/site/content/docs/reference/configuration.mdx +11 -9
  37. package/site/content/docs/reference/errors.mdx +12 -2
  38. package/site/content/docs/reference/fx.mdx +35 -21
  39. package/site/content/docs/reference/idempotency.mdx +192 -0
  40. package/site/content/docs/reference/index.mdx +6 -1
  41. package/site/content/docs/reference/meta.json +2 -1
  42. package/site/content/docs/understand/meta.json +1 -1
  43. package/site/content/docs/{elements/flow → understand}/routing.mdx +11 -10
  44. package/site/content/docs/understand/the-architecture.mdx +14 -12
  45. package/site/content/docs/understand/try-it.mdx +3 -3
  46. package/src/auth/api-key-sql.test.ts +44 -0
  47. package/src/auth/api-key-sql.ts +1 -0
  48. package/src/auth/api-keys.ts +30 -12
  49. package/src/auth/config.ts +6 -2
  50. package/src/auth/gate-auth.test.ts +20 -0
  51. package/src/cli/agents-md.test.ts +25 -0
  52. package/src/cli/ask-vault-gaps.test.ts +38 -1
  53. package/src/cli/ask-vault-gaps.ts +35 -2
  54. package/src/cli/competitor-mention-removal.test.ts +3 -3
  55. package/src/cli/decide.ts +174 -0
  56. package/src/cli/decision-lock-watch.test.ts +45 -0
  57. package/src/cli/decision-lock-watch.ts +51 -0
  58. package/src/cli/dev-app-runner.ts +34 -8
  59. package/src/cli/dev-auth-secret.test.ts +49 -0
  60. package/src/cli/dev-auth-secret.ts +49 -0
  61. package/src/cli/dev-hot.test.ts +41 -0
  62. package/src/cli/dev-hot.ts +48 -0
  63. package/src/cli/dev.test.ts +1 -1
  64. package/src/cli/dev.ts +24 -6
  65. package/src/cli/docker-cli.test.ts +1 -1
  66. package/src/cli/eval.ts +142 -1
  67. package/src/cli/index.ts +14 -1
  68. package/src/cli/load-config.images.test.ts +12 -12
  69. package/src/cli/load-config.ts +2 -2
  70. package/src/cli/mcp-from-console.ts +48 -0
  71. package/src/cli/registry.ts +35 -1
  72. package/src/cli/vault-cmd.test.ts +23 -0
  73. package/src/cli/vault-cmd.ts +7 -0
  74. package/src/client/agent.ts +307 -0
  75. package/src/client/create.ts +18 -1
  76. package/src/client/transport.test.ts +245 -0
  77. package/src/client/transport.ts +133 -19
  78. package/src/client/types.ts +29 -3
  79. package/src/client-react/index.ts +7 -1
  80. package/src/client-react/use-agent-run.test.ts +271 -0
  81. package/src/client-react/use-agent-run.ts +193 -0
  82. package/src/compiler/ai-approval.test.ts +36 -0
  83. package/src/compiler/ai-repair.test.ts +22 -0
  84. package/src/compiler/decisions.extract.test.ts +164 -0
  85. package/src/compiler/effects-infer.ts +753 -17
  86. package/src/compiler/extract.test.ts +37 -0
  87. package/src/compiler/extract.ts +381 -24
  88. package/src/compiler/fixtures/skyport.expected.json +24 -0
  89. package/src/compiler/fx-follow.test.ts +164 -0
  90. package/src/compiler/fx-index.ts +399 -0
  91. package/src/compiler/response.ts +35 -2
  92. package/src/config/index.ts +21 -3
  93. package/src/console/server/ai-runs-flows.ts +269 -0
  94. package/src/console/server/ai-runs.test.ts +323 -0
  95. package/src/console/server/ai-runs.ts +562 -0
  96. package/src/console/server/ai.ts +12 -1
  97. package/src/console/server/app.ts +10 -1
  98. package/src/console/server/decisions-flows.ts +157 -0
  99. package/src/console/server/decisions.test.ts +66 -0
  100. package/src/console/server/decisions.ts +229 -0
  101. package/src/console/server/flows-invoke.test.ts +2 -2
  102. package/src/console/server/flows.ts +50 -2
  103. package/src/console/server/invoke-user-flow.ts +9 -0
  104. package/src/console/server/runs-ingest.ts +16 -0
  105. package/src/console/server/serve.ts +5 -0
  106. package/src/console/server/state.ts +25 -1
  107. package/src/console/server/store-stats.test.ts +47 -0
  108. package/src/console/server/store-stats.ts +92 -18
  109. package/src/console/ui-next/dist/assets/access-page-DDKkhT9v.js +4 -0
  110. package/src/console/ui-next/dist/assets/agent-disclosure-62Xts8Xi.js +1 -0
  111. package/src/console/ui-next/dist/assets/{cache-glyph-92uM5MO7.js → cache-glyph-DyeoKHKA.js} +1 -1
  112. package/src/console/ui-next/dist/assets/{call-pii-button-DtGVPtcs.js → call-pii-button-DvfpLXsU.js} +1 -1
  113. package/src/console/ui-next/dist/assets/{collapsible-DN7l6zmC.js → collapsible-BY03SeCg.js} +1 -1
  114. package/src/console/ui-next/dist/assets/confirm-sheet-DZfuJXP4.js +1 -0
  115. package/src/console/ui-next/dist/assets/copy-inline-button-BTjPVjRT.js +1 -0
  116. package/src/console/ui-next/dist/assets/decisions-page-CgkKlA56.js +1 -0
  117. package/src/console/ui-next/dist/assets/detail-header-Dk5LipF6.js +1 -0
  118. package/src/console/ui-next/dist/assets/dropdown-menu-CRGrj5JU.js +1 -0
  119. package/src/console/ui-next/dist/assets/duration-tone-Y6HLjyJ5.js +9 -0
  120. package/src/console/ui-next/dist/assets/element-icons-C6t8Vlml.js +1 -0
  121. package/src/console/ui-next/dist/assets/explorer-chrome-Dslh2yE8.js +1 -0
  122. package/src/console/ui-next/dist/assets/explorer-empty-CIVg7Jgb.js +1 -0
  123. package/src/console/ui-next/dist/assets/flows-page-DLPfNy-_.js +1 -0
  124. package/src/console/ui-next/dist/assets/{highlighted-json-BlAEVgNW.js → highlighted-json-D7nNzpgB.js} +1 -1
  125. package/src/console/ui-next/dist/assets/{http-method-BDf7OAHv.js → http-method-DdL19zzh.js} +1 -1
  126. package/src/console/ui-next/dist/assets/index-BlrN49Hs.css +2 -0
  127. package/src/console/ui-next/dist/assets/index-C0lc_s9d.js +58 -0
  128. package/src/console/ui-next/dist/assets/observability-page-CRlcyLCC.js +8 -0
  129. package/src/console/ui-next/dist/assets/{replica-lag-B3GLNVfF.js → replica-lag-Bb3FWUu9.js} +7 -7
  130. package/src/console/ui-next/dist/assets/request-meta-DYBMWhX8.js +1 -0
  131. package/src/console/ui-next/dist/assets/shortcut-keys-DTE1sjTC.js +1 -0
  132. package/src/console/ui-next/dist/assets/store-page-CcE-SXC8.js +41 -0
  133. package/src/console/ui-next/dist/assets/trace-detail-sheet-CKMSEQ4s.js +2 -0
  134. package/src/console/ui-next/dist/assets/tree-expand-toggle-QOfk0M0H.js +55 -0
  135. package/src/console/ui-next/dist/assets/units-page-DKC1CDDd.js +1 -0
  136. package/src/console/ui-next/dist/assets/use-vault-list-BAyQG8Re.js +1 -0
  137. package/src/console/ui-next/dist/assets/vault-page-B54v4Yit.js +2 -0
  138. package/src/console/ui-next/dist/index.html +4 -3
  139. package/src/console/ui-next/seed-parked-approval.ts +126 -0
  140. package/src/console/ui-next/src/client.ts +224 -1
  141. package/src/console/ui-next/src/components/explorer/detail-header.tsx +9 -0
  142. package/src/console/ui-next/src/components/explorer/explorer-start-toggle.tsx +5 -1
  143. package/src/console/ui-next/src/components/ui/sheet-form.tsx +21 -12
  144. package/src/console/ui-next/src/features/flows/decisions/decisions-page.tsx +216 -0
  145. package/src/console/ui-next/src/features/flows/decisions/resolve.test.ts +48 -0
  146. package/src/console/ui-next/src/features/flows/decisions/resolve.ts +111 -0
  147. package/src/console/ui-next/src/features/flows/flows-page.tsx +10 -0
  148. package/src/console/ui-next/src/features/flows/graph/element-map.ts +1 -0
  149. package/src/console/ui-next/src/features/flows/traces/effect-kind.ts +6 -0
  150. package/src/console/ui-next/src/features/flows/traces/effect-summary.test.ts +24 -0
  151. package/src/console/ui-next/src/features/flows/traces/effect-summary.ts +24 -0
  152. package/src/console/ui-next/src/features/flows/traces/http-status.ts +58 -0
  153. package/src/console/ui-next/src/features/flows/traces/request-client-card.tsx +355 -0
  154. package/src/console/ui-next/src/features/flows/traces/request-client.test.ts +114 -0
  155. package/src/console/ui-next/src/features/flows/traces/request-client.ts +423 -0
  156. package/src/console/ui-next/src/features/flows/traces/request-input-view.test.ts +52 -6
  157. package/src/console/ui-next/src/features/flows/traces/request-input-view.ts +85 -19
  158. package/src/console/ui-next/src/features/flows/traces/trace-detail-sheet.tsx +505 -80
  159. package/src/console/ui-next/src/features/flows/traces/trace-lanes.test.ts +61 -0
  160. package/src/console/ui-next/src/features/flows/traces/trace-lanes.ts +78 -0
  161. package/src/console/ui-next/src/features/flows/traces/trace-nav.test.ts +48 -0
  162. package/src/console/ui-next/src/features/flows/traces/trace-nav.ts +57 -0
  163. package/src/console/ui-next/src/features/flows/traces/trace-request-section.tsx +275 -96
  164. package/src/console/ui-next/src/features/flows/traces/traces-pane.tsx +16 -2
  165. package/src/console/ui-next/src/features/observability/detail/ai-runs.tsx +369 -0
  166. package/src/console/ui-next/src/features/observability/observability-page.tsx +31 -1
  167. package/src/console/ui-next/src/features/observability/state/observability-selection.ts +18 -3
  168. package/src/console/ui-next/src/features/store/performance/kv-performance-panel.tsx +12 -1
  169. package/src/console/ui-next/src/features/store/performance/performance-panel.tsx +14 -1
  170. package/src/console/ui-next/src/features/units/call/call-api-panel.tsx +1 -1
  171. package/src/console/ui-next/src/features/units/detail/flow-contract-panel.tsx +15 -2
  172. package/src/console/ui-next/src/features/units/explorer/units-tree.tsx +73 -18
  173. package/src/console/ui-next/src/features/units/lib/unit-tree.test.ts +21 -0
  174. package/src/console/ui-next/src/features/units/lib/unit-tree.ts +82 -0
  175. package/src/console/ui-next/src/features/units/units-page.tsx +25 -7
  176. package/src/console/ui-next/src/router.tsx +17 -0
  177. package/src/console/ui-next/ui-next-seed-runs.ts +23 -0
  178. package/src/console/ui-next/vite-console-kernel-plugin.ts +43 -3
  179. package/src/console/ui-next/vite.config.ts +9 -0
  180. package/src/docker/docker.test.ts +13 -13
  181. package/src/docker/images-config.test.ts +12 -12
  182. package/src/docker/recipes/dragonfly.ts +3 -2
  183. package/src/docker/recipes/redis.ts +1 -1
  184. package/src/docker/stack-id.test.ts +1 -1
  185. package/src/drivers/ai-anthropic.ts +212 -21
  186. package/src/drivers/ai-mock.ts +34 -1
  187. package/src/drivers/ai-openai-compatible.ts +67 -26
  188. package/src/drivers/ai-preconnect.ts +18 -0
  189. package/src/drivers/ai-stream.test.ts +62 -0
  190. package/src/drivers/ai-types.ts +20 -0
  191. package/src/drivers/journal-postgres.ts +455 -3
  192. package/src/drivers/meilisearch.ts +2 -0
  193. package/src/drivers/memory-ddl.test.ts +18 -0
  194. package/src/drivers/memory.ts +21 -1
  195. package/src/drivers/pg-rls.test.ts +31 -0
  196. package/src/drivers/pg-rls.ts +15 -0
  197. package/src/drivers/postgres.test.ts +9 -0
  198. package/src/drivers/postgres.ts +49 -11
  199. package/src/drivers/vault-builtin.test.ts +10 -0
  200. package/src/drivers/vault-builtin.ts +13 -1
  201. package/src/drivers/vault-remote-bag.ts +5 -1
  202. package/src/drivers/vault-types.ts +6 -1
  203. package/src/elements/ai/agent-event-slot.ts +27 -0
  204. package/src/elements/ai/approval-http.ts +207 -0
  205. package/src/elements/ai/approval.test.ts +741 -0
  206. package/src/elements/ai/approval.ts +228 -0
  207. package/src/elements/ai/decisions/bind.ts +163 -0
  208. package/src/elements/ai/decisions/certificate.test.ts +291 -0
  209. package/src/elements/ai/decisions/certificate.ts +377 -0
  210. package/src/elements/ai/decisions/certify.ts +314 -0
  211. package/src/elements/ai/decisions/decide.test.ts +953 -0
  212. package/src/elements/ai/decisions/e2e.test.ts +399 -0
  213. package/src/elements/ai/decisions/export.test.ts +120 -0
  214. package/src/elements/ai/decisions/export.ts +146 -0
  215. package/src/elements/ai/decisions/fixtures/openrouter-request.json +24 -0
  216. package/src/elements/ai/decisions/fixtures/openrouter-response.json +22 -0
  217. package/src/elements/ai/decisions/fixtures/typesafe-request.json +19 -0
  218. package/src/elements/ai/decisions/fixtures/typesafe-response.json +13 -0
  219. package/src/elements/ai/decisions/http.test.ts +154 -0
  220. package/src/elements/ai/decisions/http.ts +240 -0
  221. package/src/elements/ai/decisions/labels.test.ts +282 -0
  222. package/src/elements/ai/decisions/labels.ts +257 -0
  223. package/src/elements/ai/decisions/openrouter.live.test.ts +136 -0
  224. package/src/elements/ai/decisions/openrouter.ts +36 -0
  225. package/src/elements/ai/decisions/provider.ts +102 -0
  226. package/src/elements/ai/decisions/typesafe.ts +36 -0
  227. package/src/elements/ai/declare.ts +252 -1
  228. package/src/elements/ai/events.test.ts +379 -0
  229. package/src/elements/ai/events.ts +138 -0
  230. package/src/elements/ai/run-events.test.ts +461 -0
  231. package/src/elements/ai/run-events.ts +522 -0
  232. package/src/elements/ai/runtime.ts +1186 -105
  233. package/src/elements/ai/stream-turn.ts +214 -0
  234. package/src/elements/ai/stream.live.test.ts +76 -0
  235. package/src/elements/ai/subagent.test.ts +136 -0
  236. package/src/elements/ai.test.ts +497 -3
  237. package/src/elements/ai.ts +16 -1
  238. package/src/elements/clock/durable.ts +27 -1
  239. package/src/elements/store/emit-drizzle.ts +2 -2
  240. package/src/elements/store/live-default.test.ts +2 -0
  241. package/src/elements/store/live-http.test.ts +1 -0
  242. package/src/elements/store/resource-list-docs.test.ts +2 -2
  243. package/src/elements/store/resource.test.ts +3 -3
  244. package/src/elements/store/schema-decl.test.ts +1 -0
  245. package/src/elements/store/sql-condition.ts +24 -7
  246. package/src/elements/store/sql-session.test.ts +51 -0
  247. package/src/elements/store/sql-session.ts +111 -1
  248. package/src/elements/store/upsert-app.test.ts +1 -1
  249. package/src/elements/vault/audit.test.ts +137 -0
  250. package/src/elements/vault/audit.ts +106 -13
  251. package/src/elements/vault/boot-chain.ts +9 -1
  252. package/src/elements/vault/builtin-adapter.ts +33 -3
  253. package/src/i18n/catalogs/ar.ts +9 -0
  254. package/src/i18n/catalogs/en.ts +9 -0
  255. package/src/index.ts +2 -0
  256. package/src/kernel/abort-scope.ts +33 -2
  257. package/src/kernel/agent-event-store.ts +448 -0
  258. package/src/kernel/app.ts +294 -19
  259. package/src/kernel/auto-cache.test.ts +6 -6
  260. package/src/kernel/boot-bind/ai.ts +3 -0
  261. package/src/kernel/boot-bind/store.test.ts +26 -0
  262. package/src/kernel/boot-bind/store.ts +82 -6
  263. package/src/kernel/boot-bind/vault.ts +1 -0
  264. package/src/kernel/boot.ts +34 -5
  265. package/src/kernel/builtin-errors.ts +10 -0
  266. package/src/kernel/capability.ts +3 -0
  267. package/src/kernel/client-descriptor.ts +1 -0
  268. package/src/kernel/concurrency.test.ts +33 -0
  269. package/src/kernel/decision-label-store.ts +441 -0
  270. package/src/kernel/effects-stamping.test.ts +2 -2
  271. package/src/kernel/effects.test.ts +2 -1
  272. package/src/kernel/effects.ts +13 -1
  273. package/src/kernel/element-registries.ts +3 -0
  274. package/src/kernel/errors-compiler.ts +20 -0
  275. package/src/kernel/errors-text.ts +4 -0
  276. package/src/kernel/errors.ts +2 -0
  277. package/src/kernel/external-effects.test.ts +36 -0
  278. package/src/kernel/flow.test.ts +2 -3
  279. package/src/kernel/flow.ts +24 -0
  280. package/src/kernel/fx-decide.ts +761 -0
  281. package/src/kernel/fx-fetch.ts +5 -1
  282. package/src/kernel/fx.test.ts +12 -5
  283. package/src/kernel/fx.ts +316 -29
  284. package/src/kernel/http-frame.test.ts +94 -0
  285. package/src/kernel/http-frame.ts +203 -0
  286. package/src/kernel/idempotency-store.ts +578 -0
  287. package/src/kernel/idempotency.test.ts +556 -0
  288. package/src/kernel/idempotency.ts +304 -0
  289. package/src/kernel/index.ts +1 -0
  290. package/src/kernel/journal.ts +160 -0
  291. package/src/kernel/json-result.ts +10 -0
  292. package/src/kernel/pipeline.test.ts +45 -0
  293. package/src/kernel/sse-id.ts +32 -0
  294. package/src/manifest/diff.ts +1 -0
  295. package/src/manifest/types.ts +37 -2
  296. package/src/mcp/ai-tools.test.ts +202 -0
  297. package/src/mcp/authorization.ts +66 -0
  298. package/src/mcp/session.ts +3 -0
  299. package/src/mcp/tools.ts +168 -0
  300. package/src/plugins/auth-delivery.mailpit.integration.test.ts +1 -1
  301. package/src/plugins/otp.test.ts +65 -1
  302. package/src/plugins/otp.ts +59 -7
  303. package/src/release/absolute-regression.test.ts +63 -0
  304. package/src/release/http-graph.test.ts +27 -0
  305. package/src/release/http-graph.ts +95 -0
  306. package/src/release/index.ts +3 -0
  307. package/src/release/limits.ts +17 -2
  308. package/src/release/measure.ts +144 -28
  309. package/src/runs/collect.ts +4 -1
  310. package/src/runs/parquet.test.ts +27 -1
  311. package/src/runs/parquet.ts +19 -0
  312. package/src/runs/types.ts +31 -0
  313. package/src/runtime/json-code-block.test.ts +106 -47
  314. package/src/runtime/json-code-block.ts +1209 -46
  315. package/src/term.test.ts +13 -0
  316. package/src/term.ts +81 -49
  317. package/src/test/create-test-app.test.ts +3 -1
  318. package/src/test/create-test-app.ts +40 -0
  319. package/src/test/live-signals.test.ts +1 -0
  320. package/src/test/provisions.integration.test.ts +1 -0
  321. package/src/test/tenant-isolation.test.ts +1 -0
  322. package/src/console/ui-next/dist/assets/PlusSignIcon-CwG3nxfu.js +0 -1
  323. package/src/console/ui-next/dist/assets/access-page-Bgt9bq2r.js +0 -4
  324. package/src/console/ui-next/dist/assets/agent-disclosure-B43CXZZR.js +0 -1
  325. package/src/console/ui-next/dist/assets/copy-inline-button-CAYD18cr.js +0 -1
  326. package/src/console/ui-next/dist/assets/detail-header-DVWNjWjg.js +0 -1
  327. package/src/console/ui-next/dist/assets/dropdown-menu-4h2LOVXM.js +0 -1
  328. package/src/console/ui-next/dist/assets/duration-tone-BmIR9FV8.js +0 -9
  329. package/src/console/ui-next/dist/assets/element-icons-BI8cJgdh.js +0 -1
  330. package/src/console/ui-next/dist/assets/explorer-empty-CJs5A-wm.js +0 -1
  331. package/src/console/ui-next/dist/assets/flows-page-BMHs-IzK.js +0 -1
  332. package/src/console/ui-next/dist/assets/index-BKpaes3n.js +0 -63
  333. package/src/console/ui-next/dist/assets/index-VxoEz295.css +0 -2
  334. package/src/console/ui-next/dist/assets/observability-page-WnVLI-0j.js +0 -4
  335. package/src/console/ui-next/dist/assets/request-meta-DMbnAe3f.js +0 -1
  336. package/src/console/ui-next/dist/assets/shortcut-keys-3ILd8oGn.js +0 -1
  337. package/src/console/ui-next/dist/assets/store-page-KvFDingJ.js +0 -41
  338. package/src/console/ui-next/dist/assets/trace-detail-sheet-Htk8Cm9t.js +0 -2
  339. package/src/console/ui-next/dist/assets/tree-expand-toggle-CFMPWX4f.js +0 -55
  340. package/src/console/ui-next/dist/assets/units-page-C4NdNuxP.js +0 -1
  341. package/src/console/ui-next/dist/assets/use-vault-list-CT4-gajj.js +0 -1
  342. package/src/console/ui-next/dist/assets/vault-page-CBYl0LW_.js +0 -2
@@ -57,10 +57,12 @@ export interface InferBinding {
57
57
  | "signal"
58
58
  | "clock"
59
59
  | "prompt"
60
+ | "agent"
60
61
  | "secret"
61
62
  | "template"
62
63
  | "flow"
63
64
  | "embed"
65
+ | "decision"
64
66
  | "table"
65
67
  | "mcp-server"
66
68
  | "mcp-tool"
@@ -71,8 +73,16 @@ export interface InferBinding {
71
73
  readonly facet?: "sql" | "kv" | "files" | "index";
72
74
  /** Prompt version when kind is `prompt`. */
73
75
  readonly version?: number;
76
+ /** Nested agent tools when kind is `agent`. */
77
+ readonly agentCalls?: readonly string[];
74
78
  }
75
79
 
80
+ /** What a callee that is passed `fx` resolves to. */
81
+ export type FxCalleeResolution =
82
+ | { readonly kind: "function"; readonly fn: AstNode; readonly file?: string }
83
+ | { readonly kind: "intrinsic"; readonly name: "liveQuery" | "applySearchEmbedCdc" }
84
+ | { readonly kind: "unresolved"; readonly label: string };
85
+
76
86
  /** Options for {@link inferEffects}. */
77
87
  export interface InferEffectsOptions {
78
88
  /** The `do` handler AST node (function / arrow). */
@@ -81,9 +91,25 @@ export interface InferEffectsOptions {
81
91
  readonly bindings: ReadonlyMap<string, InferBinding>;
82
92
  /**
83
93
  * True when the flow options already declare an `effects` object.
84
- * Required for `fx.raw` to stay cache-eligible.
94
+ * Required for `fx.raw` to stay cache-eligible. Opaque `fx` (a renamed
95
+ * parameter, an escaped chain alias, an unresolved callee) is allowed
96
+ * only when this is set; the declared set must still cover every effect
97
+ * the walk can see.
85
98
  */
86
99
  readonly hasExplicitEffects: boolean;
100
+ /** Manifest flow name, used in OKE1900. */
101
+ readonly flowName?: string;
102
+ /**
103
+ * Resolve a callee in the flow's file. Absent only for unit tests that
104
+ * never pass `fx` into another function.
105
+ */
106
+ /** File that contains `do`, so imported helpers resolve from there. */
107
+ readonly file?: string;
108
+ /**
109
+ * Resolve a callee as seen from `file`. Absent only for unit tests that
110
+ * never pass `fx` into another function.
111
+ */
112
+ readonly resolveCallee?: (callee: AstNode, file: string) => FxCalleeResolution;
87
113
  }
88
114
 
89
115
  /** Result of effect inference for one flow body. */
@@ -168,13 +194,19 @@ export function inferEffects(options: InferEffectsOptions): InferredEffects {
168
194
  const secrets = new Set<SecretRef>();
169
195
  const calls = new Set<FlowRef>();
170
196
  const fetches = new Set<FetchHostRef>();
197
+ const decides = new Set<string>();
171
198
  const steps: string[] = [];
172
199
  let usesRaw = false;
173
200
 
174
- for (const call of collectCallExpressions(options.doNode)) {
175
- const chain = fxMemberChain(call);
176
- if (!chain) continue;
201
+ const opaque: string[] = [];
202
+ const userIdRoots: AstNode[] = [options.doNode];
203
+ const intrinsics: Array<{
204
+ name: "liveQuery" | "applySearchEmbedCdc";
205
+ call: CallExpression;
206
+ }> = [];
207
+ const chains = collectFxChains(options, opaque, userIdRoots, intrinsics);
177
208
 
209
+ for (const { call, chain } of chains) {
178
210
  if (chain.rootMethod === "raw") {
179
211
  usesRaw = true;
180
212
  continue;
@@ -210,6 +242,17 @@ export function inferEffects(options: InferEffectsOptions): InferredEffects {
210
242
  continue;
211
243
  }
212
244
 
245
+ if (chain.rootMethod === "run" && call === chain.rootCall) {
246
+ const binding = resolveBinding(call.arguments[0], options.bindings);
247
+ const name =
248
+ stringArg(call.arguments[0]) ?? (binding?.kind === "agent" ? binding.ref : undefined);
249
+ if (name) asks.add(name);
250
+ if (binding?.kind === "agent") {
251
+ for (const child of binding.agentCalls ?? []) calls.add(child);
252
+ }
253
+ continue;
254
+ }
255
+
213
256
  if (chain.rootMethod === "ask" && call === chain.rootCall) {
214
257
  const ref = resolvePrompt(call.arguments[0], options.bindings);
215
258
  if (ref) asks.add(ref);
@@ -241,6 +284,14 @@ export function inferEffects(options: InferEffectsOptions): InferredEffects {
241
284
  continue;
242
285
  }
243
286
 
287
+ if (chain.rootMethod === "decide" && call === chain.rootCall) {
288
+ const binding = resolveBinding(call.arguments[0], options.bindings);
289
+ const name =
290
+ stringArg(call.arguments[0]) ?? (binding?.kind === "decision" ? binding.ref : undefined);
291
+ if (name) decides.add(name);
292
+ continue;
293
+ }
294
+
244
295
  if (chain.rootMethod === "fetch" && call === chain.rootCall) {
245
296
  // fx.fetch(url) — host becomes effects.fetches entry.
246
297
  const lit = stringArg(call.arguments[0]);
@@ -301,14 +352,14 @@ export function inferEffects(options: InferEffectsOptions): InferredEffects {
301
352
  }
302
353
 
303
354
  if (chain.rootMethod === "store") {
304
- const resolved = storeResourceFromCall(call, options.bindings);
355
+ const resolved = storeResourceFromCall(call, options.bindings, chain);
305
356
  if (!resolved) continue;
306
357
  const op = classifyStoreMethods(resolved.methods);
307
358
  if (op === "none") continue;
308
359
  // Skip incomplete chains (`select` before `.from`, `insert` before table) —
309
360
  // the sibling call that carries the table arg records the real resource.
310
361
  const leaf = resolved.methods[resolved.methods.length - 1]!;
311
- const hasTable = tableFromStoreChain(call, options.bindings) !== undefined;
362
+ const hasTable = tableFromStoreChain(call, options.bindings, chain) !== undefined;
312
363
  if (!hasTable && (leaf === "select" || leaf === "insert" || leaf === "update")) {
313
364
  continue;
314
365
  }
@@ -323,6 +374,20 @@ export function inferEffects(options: InferEffectsOptions): InferredEffects {
323
374
  }
324
375
  }
325
376
 
377
+ for (const item of intrinsics) {
378
+ noteIntrinsic(item.name, item.call, options.bindings, reads, writes, embeds, (detail) => {
379
+ opaque.push(detail);
380
+ });
381
+ }
382
+
383
+ if (opaque.length > 0 && !options.hasExplicitEffects) {
384
+ const flow = options.flowName ?? "flow";
385
+ const detail = opaque[0] ?? "hides fx from effect inference";
386
+ throw new Error(
387
+ `OKE1900: flow "${flow}" ${detail}. Name the parameter fx and keep chain aliases in the same function, or declare effects that include every effect inference can see.`,
388
+ );
389
+ }
390
+
326
391
  const effects: Effects = {};
327
392
  if (reads.size > 0) effects.reads = sortUnique([...reads]);
328
393
  if (writes.size > 0) effects.writes = sortUnique([...writes]);
@@ -333,17 +398,51 @@ export function inferEffects(options: InferEffectsOptions): InferredEffects {
333
398
  if (secrets.size > 0) effects.secrets = sortUnique([...secrets]);
334
399
  if (calls.size > 0) effects.calls = sortUnique([...calls]);
335
400
  if (fetches.size > 0) effects.fetches = sortUnique([...fetches]);
401
+ if (decides.size > 0) effects.decides = sortUnique([...decides]);
336
402
 
337
403
  return {
338
404
  effects,
339
405
  steps,
340
406
  usesRaw,
341
407
  cacheIneligible: usesRaw && !options.hasExplicitEffects,
342
- nondeterministic: asks.size > 0 || embeds.size > 0,
343
- readsUserId: containsAuthUserId(options.doNode),
408
+ nondeterministic: asks.size > 0 || embeds.size > 0 || decides.size > 0,
409
+ readsUserId: userIdRoots.some((node) => containsAuthUserId(node)),
344
410
  };
345
411
  }
346
412
 
413
+ const EFFECT_KEYS = [
414
+ "reads",
415
+ "writes",
416
+ "emits",
417
+ "sends",
418
+ "asks",
419
+ "embeds",
420
+ "secrets",
421
+ "calls",
422
+ "fetches",
423
+ "decides",
424
+ ] as const satisfies readonly (keyof Effects)[];
425
+
426
+ /**
427
+ * Keys in `inferred` that `declared` does not include.
428
+ *
429
+ * An explicit `effects` object is a floor: it may add keys inference
430
+ * cannot see, and it must include every key the walk can see.
431
+ *
432
+ * @param declared - Author-declared effects
433
+ * @param inferred - Effects the walk can see
434
+ */
435
+ export function effectsMissingFromDeclaration(declared: Effects, inferred: Effects): string[] {
436
+ const missing: string[] = [];
437
+ for (const key of EFFECT_KEYS) {
438
+ const have = new Set<string>(declared[key] ?? []);
439
+ for (const item of inferred[key] ?? []) {
440
+ if (!have.has(item)) missing.push(`${key} ${item}`);
441
+ }
442
+ }
443
+ return missing;
444
+ }
445
+
347
446
  /**
348
447
  * Walk an AST and collect every CallExpression.
349
448
  *
@@ -380,6 +479,8 @@ interface FxChain {
380
479
  rootMethod: string;
381
480
  methods: string[];
382
481
  rootCall: CallExpression;
482
+ /** Outermost call of an alias init, when the continuation is a later statement. */
483
+ aliasLeaf?: CallExpression;
383
484
  }
384
485
 
385
486
  /**
@@ -451,15 +552,16 @@ export function fxMemberChain(call: CallExpression): FxChain | null {
451
552
  export function storeResourceFromCall(
452
553
  call: CallExpression,
453
554
  bindings: ReadonlyMap<string, InferBinding>,
555
+ known?: FxChain,
454
556
  ): { resource: ResourceRef; methods: string[] } | undefined {
455
- const chain = fxMemberChain(call);
557
+ const chain = known ?? fxMemberChain(call);
456
558
  if (!chain || chain.rootMethod !== "store") return undefined;
457
559
 
458
560
  const storeArg = chain.rootCall.arguments[0];
459
561
  const storeBinding = resolveBinding(storeArg, bindings);
460
562
  const facet = storeBinding?.facet ?? "sql";
461
563
 
462
- const table = tableFromStoreChain(call, bindings);
564
+ const table = tableFromStoreChain(call, bindings, chain);
463
565
 
464
566
  if (table) {
465
567
  return {
@@ -506,18 +608,36 @@ export function storeResourceFromCall(
506
608
  function tableFromStoreChain(
507
609
  call: CallExpression,
508
610
  bindings: ReadonlyMap<string, InferBinding>,
611
+ chain?: FxChain,
612
+ ): string | undefined {
613
+ const found = tableFromStoreWalk(call, bindings, chain);
614
+ if (found) return found;
615
+ if (chain?.aliasLeaf && chain.aliasLeaf !== call) {
616
+ return tableFromStoreWalk(chain.aliasLeaf, bindings);
617
+ }
618
+ return undefined;
619
+ }
620
+
621
+ function tableFromStoreWalk(
622
+ call: CallExpression,
623
+ bindings: ReadonlyMap<string, InferBinding>,
624
+ chain?: FxChain,
509
625
  ): string | undefined {
510
626
  let current: AstNode | undefined = call;
511
- while (current && current.type === "CallExpression") {
627
+ const seen = new Set<AstNode>();
628
+ while (current && current.type === "CallExpression" && !seen.has(current)) {
629
+ seen.add(current);
512
630
  const c = current as CallExpression;
513
- const link = fxMemberChain(c);
514
- if (!link || link.rootMethod !== "store") break;
515
- const leaf = link.methods[link.methods.length - 1]!;
631
+ const link = chain ? undefined : fxMemberChain(c);
632
+ if (!chain && (!link || link.rootMethod !== "store")) break;
633
+ const leaf = chain ? methodNameOfCall(c) : link?.methods[link.methods.length - 1];
634
+ if (!leaf) break;
516
635
  if (TABLE_ARG_METHODS.has(leaf)) {
517
636
  // KV / files / index take a key, not a table. Treating `delete(key)` as
518
637
  // `delete(table)` stamps `kv:key` instead of `kv:drafts` and the runtime
519
638
  // then throws OKE1002 on the real namespace.
520
- const storeBinding = resolveBinding(link.rootCall.arguments[0], bindings);
639
+ const storeArg = chain?.rootCall.arguments[0] ?? link?.rootCall.arguments[0];
640
+ const storeBinding = resolveBinding(storeArg, bindings);
521
641
  if (storeBinding?.facet !== undefined && storeBinding.facet !== "sql") {
522
642
  // keep walking — no table on this link
523
643
  } else {
@@ -530,14 +650,24 @@ function tableFromStoreChain(
530
650
  }
531
651
  const callee = c.callee;
532
652
  if (callee.type === "MemberExpression") {
533
- current = (callee as AstNode & { object: AstNode }).object;
534
- continue;
653
+ const obj = (callee as AstNode & { object: AstNode }).object;
654
+ if (obj.type === "CallExpression") {
655
+ current = obj;
656
+ continue;
657
+ }
535
658
  }
536
659
  break;
537
660
  }
538
661
  return undefined;
539
662
  }
540
663
 
664
+ function methodNameOfCall(call: CallExpression): string | undefined {
665
+ const callee = call.callee;
666
+ if (callee.type !== "MemberExpression") return undefined;
667
+ const property = (callee as AstNode & { property?: AstNode }).property;
668
+ return identifierName(property);
669
+ }
670
+
541
671
  function classifyStoreMethods(methods: string[]): "read" | "write" | "both" | "none" {
542
672
  const ops = methods.slice(1);
543
673
  let read = false;
@@ -751,3 +881,609 @@ function containsAuthUserId(root: AstNode): boolean {
751
881
  });
752
882
  return found;
753
883
  }
884
+
885
+ interface ChainAlias {
886
+ readonly methods: readonly string[];
887
+ readonly rootMethod: string;
888
+ readonly rootCall: CallExpression;
889
+ readonly leafCall: CallExpression;
890
+ }
891
+
892
+ interface ChainHit {
893
+ readonly call: CallExpression;
894
+ readonly chain: FxChain;
895
+ }
896
+
897
+ const VALUE_WRAPPERS = new Set([
898
+ "ParenthesizedExpression",
899
+ "TSAsExpression",
900
+ "TSTypeAssertion",
901
+ "TSSatisfiesExpression",
902
+ "TSNonNullExpression",
903
+ "AwaitExpression",
904
+ ]);
905
+
906
+ function collectFxChains(
907
+ options: InferEffectsOptions,
908
+ opaque: string[],
909
+ userIdRoots: AstNode[],
910
+ intrinsics: Array<{ name: "liveQuery" | "applySearchEmbedCdc"; call: CallExpression }>,
911
+ ): ChainHit[] {
912
+ const hits: ChainHit[] = [];
913
+ const seen = new Set<AstNode>();
914
+
915
+ const follow = (
916
+ fn: AstNode,
917
+ file: string,
918
+ aliases: ReadonlyMap<string, ChainAlias>,
919
+ locals: ReadonlyMap<string, AstNode>,
920
+ ): void => {
921
+ analyzeFunction(fn, "fx", aliases, locals, file);
922
+ };
923
+
924
+ const analyzeFunction = (
925
+ fn: AstNode,
926
+ door: string,
927
+ outerAliases: ReadonlyMap<string, ChainAlias>,
928
+ outerLocals: ReadonlyMap<string, AstNode>,
929
+ file: string,
930
+ ): void => {
931
+ if (seen.has(fn)) return;
932
+ seen.add(fn);
933
+ userIdRoots.push(fn);
934
+ const body = (fn as AstNode & { body?: AstNode }).body;
935
+ if (!body) return;
936
+ analyzeBody(body, door, outerAliases, outerLocals, file);
937
+ };
938
+
939
+ const analyzeBody = (
940
+ body: AstNode,
941
+ door: string,
942
+ outerAliases: ReadonlyMap<string, ChainAlias>,
943
+ outerLocals: ReadonlyMap<string, AstNode>,
944
+ file: string,
945
+ ): void => {
946
+ const aliases = new Map(outerAliases);
947
+ const locals = new Map(outerLocals);
948
+ collectDirect(body, aliases, locals);
949
+ checkEscapes(body, door, new Set(aliases.keys()), (detail) => {
950
+ opaque.push(detail);
951
+ });
952
+ for (const call of callsSkippingNested(body)) {
953
+ const chain = chainFromCall(call, aliases);
954
+ if (chain) {
955
+ hits.push({ call, chain });
956
+ continue;
957
+ }
958
+ noteFxPass(call, door, aliases, locals, file, opaque, intrinsics, follow);
959
+ }
960
+ for (const nested of nestedFunctions(body)) {
961
+ if (seen.has(nested)) continue;
962
+ const shadows = functionParams(nested).some((param) => paramIdentifierName(param) === door);
963
+ if (shadows) continue;
964
+ analyzeFunction(nested, door, aliases, locals, file);
965
+ }
966
+ };
967
+
968
+ const startFile = options.file ?? "";
969
+ if (isFunctionNode(options.doNode)) {
970
+ const params = functionParams(options.doNode);
971
+ const second = params.length >= 2 ? params[1] : undefined;
972
+ if (second && paramIdentifierName(second) !== "fx") {
973
+ opaque.push("the do callback's second parameter is not named fx");
974
+ }
975
+ analyzeFunction(options.doNode, "fx", new Map(), new Map(), startFile);
976
+ } else {
977
+ analyzeBody(options.doNode, "fx", new Map(), new Map(), startFile);
978
+ }
979
+
980
+ return hits;
981
+
982
+ function noteFxPass(
983
+ call: CallExpression,
984
+ door: string,
985
+ aliases: ReadonlyMap<string, ChainAlias>,
986
+ locals: ReadonlyMap<string, AstNode>,
987
+ file: string,
988
+ reasons: string[],
989
+ foundIntrinsics: Array<{ name: "liveQuery" | "applySearchEmbedCdc"; call: CallExpression }>,
990
+ enter: (
991
+ fn: AstNode,
992
+ file: string,
993
+ aliases: ReadonlyMap<string, ChainAlias>,
994
+ locals: ReadonlyMap<string, AstNode>,
995
+ ) => void,
996
+ ): void {
997
+ const args = call.arguments ?? [];
998
+ for (let index = 0; index < args.length; index++) {
999
+ const arg = unwrapValue(args[index]);
1000
+ if (!arg || arg.type !== "Identifier") continue;
1001
+ const name = identifierName(arg);
1002
+ if (!name) continue;
1003
+ if (aliases.has(name)) {
1004
+ reasons.push(`passes chain alias "${name}" to a call`);
1005
+ continue;
1006
+ }
1007
+ if (name !== door) continue;
1008
+ const resolved = resolveCallee(call.callee, locals, options, file);
1009
+ if (resolved.kind === "function") {
1010
+ const param = functionParams(resolved.fn)[index];
1011
+ if (!param || paramIdentifierName(param) !== "fx") {
1012
+ reasons.push("passes fx to a parameter that is not named fx");
1013
+ continue;
1014
+ }
1015
+ const local = calleeLocal(call.callee, locals);
1016
+ enter(
1017
+ resolved.fn,
1018
+ resolved.file ?? file,
1019
+ local ? aliases : new Map(),
1020
+ local ? locals : new Map(),
1021
+ );
1022
+ continue;
1023
+ }
1024
+ if (resolved.kind === "intrinsic") {
1025
+ if (index !== 0) {
1026
+ reasons.push(`passes fx to ${resolved.name} in a position inference cannot read`);
1027
+ continue;
1028
+ }
1029
+ foundIntrinsics.push({ name: resolved.name, call });
1030
+ continue;
1031
+ }
1032
+ reasons.push(`passes fx to "${resolved.label}", which inference cannot resolve`);
1033
+ }
1034
+ }
1035
+ }
1036
+
1037
+ function calleeLocal(callee: AstNode, locals: ReadonlyMap<string, AstNode>): boolean {
1038
+ if (callee.type !== "Identifier") return false;
1039
+ const name = identifierName(callee);
1040
+ return name !== undefined && locals.has(name);
1041
+ }
1042
+
1043
+ function resolveCallee(
1044
+ callee: AstNode,
1045
+ locals: ReadonlyMap<string, AstNode>,
1046
+ options: InferEffectsOptions,
1047
+ file: string,
1048
+ ): FxCalleeResolution {
1049
+ if (callee.type === "Identifier") {
1050
+ const name = identifierName(callee);
1051
+ const local = name ? locals.get(name) : undefined;
1052
+ if (local) return { kind: "function", fn: local, file };
1053
+ }
1054
+ if (options.resolveCallee) return options.resolveCallee(callee, file);
1055
+ const label = identifierName(callee) ?? "callee";
1056
+ return { kind: "unresolved", label };
1057
+ }
1058
+
1059
+ function noteIntrinsic(
1060
+ name: "liveQuery" | "applySearchEmbedCdc",
1061
+ call: CallExpression,
1062
+ bindings: ReadonlyMap<string, InferBinding>,
1063
+ reads: Set<ResourceRef | SignalResourceRef | AuthApiKeysResourceRef | AuthTenantsResourceRef>,
1064
+ writes: Set<ResourceRef | AuthApiKeysResourceRef | AuthTenantsResourceRef>,
1065
+ embeds: Set<EmbedRef>,
1066
+ opaque: (detail: string) => void,
1067
+ ): void {
1068
+ if (name === "liveQuery") {
1069
+ const ref = sqlRefFromArg(call.arguments[1], bindings);
1070
+ if (!ref) {
1071
+ opaque("calls liveQuery with a table inference cannot resolve");
1072
+ return;
1073
+ }
1074
+ reads.add(ref as ResourceRef);
1075
+ return;
1076
+ }
1077
+ const sqlRef = stringArg(unwrapValueNode(call.arguments[5]));
1078
+ if (!sqlRef) {
1079
+ opaque("calls applySearchEmbedCdc with a sqlRef inference cannot resolve");
1080
+ return;
1081
+ }
1082
+ const ref = (sqlRef.includes(":") ? sqlRef : `sql:${sqlRef}`) as ResourceRef;
1083
+ reads.add(ref);
1084
+ writes.add(ref);
1085
+ const models = embedModelsFromColumns(call.arguments[3]);
1086
+ if (!models) {
1087
+ opaque("calls applySearchEmbedCdc with embed columns inference cannot resolve");
1088
+ return;
1089
+ }
1090
+ for (const model of models) embeds.add(model);
1091
+ }
1092
+
1093
+ function sqlRefFromArg(
1094
+ arg: AstNode | undefined,
1095
+ bindings: ReadonlyMap<string, InferBinding>,
1096
+ ): string | undefined {
1097
+ if (!arg) return undefined;
1098
+ const node = unwrapValue(arg);
1099
+ const lit = stringArg(node);
1100
+ if (lit) return lit.includes(":") ? lit : `sql:${lit}`;
1101
+ const binding = resolveBinding(node, bindings);
1102
+ if (binding?.kind === "table") return sqlTableRef(binding.ref);
1103
+ if (binding?.kind === "store") return binding.ref;
1104
+ return undefined;
1105
+ }
1106
+
1107
+ function embedModelsFromColumns(arg: AstNode | undefined): string[] | null {
1108
+ if (!arg) return null;
1109
+ const node = unwrapValue(arg);
1110
+ if (!node || node.type !== "ArrayExpression") return null;
1111
+ const elements = (node as AstNode & { elements?: readonly (AstNode | null)[] }).elements ?? [];
1112
+ const models: string[] = [];
1113
+ for (const element of elements) {
1114
+ if (!element || element.type !== "ObjectExpression") return null;
1115
+ const model = objectStringProp(element, "model");
1116
+ if (model === undefined) {
1117
+ if (objectHasProp(element, "model")) return null;
1118
+ models.push("default");
1119
+ continue;
1120
+ }
1121
+ models.push(model);
1122
+ }
1123
+ return models;
1124
+ }
1125
+
1126
+ function objectStringProp(obj: AstNode, key: string): string | undefined {
1127
+ for (const prop of objectProps(obj)) {
1128
+ if (propKeyName(prop) !== key) continue;
1129
+ return stringArg(unwrapValue((prop as AstNode & { value?: AstNode }).value));
1130
+ }
1131
+ return undefined;
1132
+ }
1133
+
1134
+ function objectHasProp(obj: AstNode, key: string): boolean {
1135
+ return objectProps(obj).some((prop) => propKeyName(prop) === key);
1136
+ }
1137
+
1138
+ function objectProps(obj: AstNode): AstNode[] {
1139
+ return ((obj as AstNode & { properties?: AstNode[] }).properties ?? []).filter(
1140
+ (prop) => prop.type === "Property" || prop.type === "ObjectProperty",
1141
+ );
1142
+ }
1143
+
1144
+ function propKeyName(prop: AstNode): string | undefined {
1145
+ const key = (prop as AstNode & { key?: AstNode }).key;
1146
+ return identifierName(key) ?? stringArg(key);
1147
+ }
1148
+
1149
+ function collectDirect(
1150
+ body: AstNode,
1151
+ aliases: Map<string, ChainAlias>,
1152
+ locals: Map<string, AstNode>,
1153
+ ): void {
1154
+ walkStatements(body, (node) => {
1155
+ if (node.type === "FunctionDeclaration") {
1156
+ const name = identifierName((node as AstNode & { id?: AstNode }).id);
1157
+ if (name) locals.set(name, node);
1158
+ return;
1159
+ }
1160
+ if (node.type !== "VariableDeclarator") return;
1161
+ const name = identifierName((node as AstNode & { id?: AstNode }).id);
1162
+ const init = (node as AstNode & { init?: AstNode | null }).init;
1163
+ if (!name || !init) return;
1164
+ const value = unwrapValue(init);
1165
+ if (value && isFunctionNode(value)) {
1166
+ locals.set(name, value);
1167
+ return;
1168
+ }
1169
+ const alias = aliasFromInit(init, aliases);
1170
+ if (alias) aliases.set(name, alias);
1171
+ });
1172
+ }
1173
+
1174
+ function aliasFromInit(init: AstNode, aliases: ReadonlyMap<string, ChainAlias>): ChainAlias | null {
1175
+ const node = unwrapValue(init);
1176
+ if (!node || node.type !== "CallExpression") return null;
1177
+ const chain = chainFromCall(node as CallExpression, aliases);
1178
+ if (!chain || chain.rootMethod !== "store") return null;
1179
+ // `const rows = await fx.store(db).select().from(table)` is the query
1180
+ // result. `const q = await fx.store(db)` is still the handle.
1181
+ if (expressionIsAwaited(init) && !isStoreHandle(chain)) return null;
1182
+ return {
1183
+ methods: chain.methods,
1184
+ rootMethod: chain.rootMethod,
1185
+ rootCall: chain.rootCall,
1186
+ leafCall: chain.aliasLeaf ?? (node as CallExpression),
1187
+ };
1188
+ }
1189
+
1190
+ function isStoreHandle(chain: FxChain): boolean {
1191
+ return chain.rootMethod === "store" && classifyStoreMethods(chain.methods) === "none";
1192
+ }
1193
+
1194
+ function expressionIsAwaited(node: AstNode): boolean {
1195
+ let current: AstNode | undefined = node;
1196
+ for (let i = 0; i < 8 && current; i++) {
1197
+ if (current.type === "AwaitExpression") return true;
1198
+ if (!VALUE_WRAPPERS.has(current.type)) return false;
1199
+ current =
1200
+ (current as AstNode & { expression?: AstNode }).expression ??
1201
+ (current as AstNode & { argument?: AstNode }).argument;
1202
+ }
1203
+ return false;
1204
+ }
1205
+
1206
+ function chainFromCall(
1207
+ call: CallExpression,
1208
+ aliases: ReadonlyMap<string, ChainAlias>,
1209
+ ): FxChain | null {
1210
+ const direct = fxMemberChain(call);
1211
+ if (direct) return direct;
1212
+
1213
+ let current: CallExpression | null = call;
1214
+ const methods: string[] = [];
1215
+ while (current) {
1216
+ const callee = current.callee;
1217
+ if (callee.type !== "MemberExpression") return null;
1218
+ const member = callee as AstNode & {
1219
+ object: AstNode;
1220
+ property: AstNode;
1221
+ computed?: boolean;
1222
+ };
1223
+ if (member.computed === true || member.property.type !== "Identifier") return null;
1224
+ methods.unshift((member.property as Identifier).name);
1225
+ if (member.object.type === "Identifier") {
1226
+ const alias = aliases.get((member.object as Identifier).name);
1227
+ if (!alias) return null;
1228
+ return {
1229
+ rootMethod: alias.rootMethod,
1230
+ methods: [...alias.methods, ...methods],
1231
+ rootCall: alias.rootCall,
1232
+ aliasLeaf: alias.leafCall,
1233
+ };
1234
+ }
1235
+ if (member.object.type === "CallExpression") {
1236
+ current = member.object as CallExpression;
1237
+ continue;
1238
+ }
1239
+ return null;
1240
+ }
1241
+ return null;
1242
+ }
1243
+
1244
+ function checkEscapes(
1245
+ root: AstNode,
1246
+ door: string,
1247
+ aliasNames: ReadonlySet<string>,
1248
+ report: (detail: string) => void,
1249
+ ): void {
1250
+ const rec = (node: AstNode, parent: AstNode | undefined, inCallArg: boolean): void => {
1251
+ if (node !== root && isFunctionNode(node)) return;
1252
+ if (node.type === "Identifier") {
1253
+ const name = (node as Identifier).name;
1254
+ const isAlias = aliasNames.has(name);
1255
+ const isDoor = name === door;
1256
+ if (
1257
+ (isAlias || isDoor) &&
1258
+ !isTypePosition(parent) &&
1259
+ !(isDoor && inCallArg) &&
1260
+ !isAllowedFxUse(node, parent)
1261
+ ) {
1262
+ report(describeFxEscape(name, parent, isAlias));
1263
+ }
1264
+ }
1265
+ if (node.type === "CallExpression") {
1266
+ const call = node as CallExpression;
1267
+ rec(call.callee, node, false);
1268
+ for (const arg of call.arguments ?? []) {
1269
+ if (arg) rec(arg, node, true);
1270
+ }
1271
+ return;
1272
+ }
1273
+ if (VALUE_WRAPPERS.has(node.type) && inCallArg) {
1274
+ const inner =
1275
+ (node as AstNode & { expression?: AstNode }).expression ??
1276
+ (node as AstNode & { argument?: AstNode }).argument;
1277
+ if (inner) rec(inner, node, true);
1278
+ return;
1279
+ }
1280
+ for (const value of Object.values(node)) {
1281
+ if (Array.isArray(value)) {
1282
+ for (const item of value) {
1283
+ if (isAstNode(item)) rec(item, node, inCallArg);
1284
+ }
1285
+ } else if (isAstNode(value)) {
1286
+ rec(value, node, inCallArg);
1287
+ }
1288
+ }
1289
+ };
1290
+ rec(root, undefined, false);
1291
+ }
1292
+
1293
+ function isAllowedFxUse(node: AstNode, parent: AstNode | undefined): boolean {
1294
+ if (!parent) return false;
1295
+ if (parent.type === "MemberExpression") {
1296
+ const member = parent as AstNode & { object?: AstNode; property?: AstNode; computed?: boolean };
1297
+ if (member.property === node && member.computed !== true) return true;
1298
+ if (member.object === node) return true;
1299
+ }
1300
+ if (parent.type === "VariableDeclarator" && (parent as AstNode & { id?: AstNode }).id === node) {
1301
+ return true;
1302
+ }
1303
+ if (
1304
+ (parent.type === "FunctionDeclaration" ||
1305
+ parent.type === "FunctionExpression" ||
1306
+ parent.type === "ArrowFunctionExpression") &&
1307
+ (parent as AstNode & { id?: AstNode }).id === node
1308
+ ) {
1309
+ return true;
1310
+ }
1311
+ return false;
1312
+ }
1313
+
1314
+ function describeFxEscape(name: string, parent: AstNode | undefined, isAlias: boolean): string {
1315
+ if (isAlias) {
1316
+ if (parent?.type === "ReturnStatement") return `returns chain alias "${name}"`;
1317
+ if (parent?.type === "CallExpression") return `passes chain alias "${name}" to a call`;
1318
+ if (
1319
+ parent?.type === "ArrayExpression" ||
1320
+ parent?.type === "ObjectExpression" ||
1321
+ parent?.type === "Property" ||
1322
+ parent?.type === "ObjectProperty"
1323
+ ) {
1324
+ return `puts chain alias "${name}" in an object or array`;
1325
+ }
1326
+ if (parent?.type === "AssignmentExpression") return `reassigns chain alias "${name}"`;
1327
+ return `lets chain alias "${name}" escape`;
1328
+ }
1329
+ if (parent?.type === "VariableDeclarator") return "aliases fx";
1330
+ if (
1331
+ parent?.type === "ObjectPattern" ||
1332
+ parent?.type === "ArrayPattern" ||
1333
+ parent?.type === "Property" ||
1334
+ parent?.type === "ObjectProperty" ||
1335
+ parent?.type === "RestElement"
1336
+ ) {
1337
+ return "destructures fx";
1338
+ }
1339
+ return "lets fx escape";
1340
+ }
1341
+
1342
+ function isTypePosition(parent: AstNode | undefined): boolean {
1343
+ if (!parent) return false;
1344
+ if (!parent.type.startsWith("TS")) return false;
1345
+ return !VALUE_WRAPPERS.has(parent.type);
1346
+ }
1347
+
1348
+ function callsSkippingNested(body: AstNode): CallExpression[] {
1349
+ const out: CallExpression[] = [];
1350
+ const take = (node: AstNode): void => {
1351
+ if (node.type === "CallExpression") out.push(node as CallExpression);
1352
+ };
1353
+ take(body);
1354
+ walkSkipFunctions(body, take);
1355
+ return out;
1356
+ }
1357
+
1358
+ function nestedFunctions(body: AstNode): AstNode[] {
1359
+ const out: AstNode[] = [];
1360
+ walkSkipFunctions(body, (node) => {
1361
+ if (isFunctionNode(node)) out.push(node);
1362
+ });
1363
+ return out;
1364
+ }
1365
+
1366
+ function walkSkipFunctions(root: AstNode, visit: (node: AstNode) => void): void {
1367
+ const rec = (node: AstNode): void => {
1368
+ for (const value of Object.values(node)) {
1369
+ if (Array.isArray(value)) {
1370
+ for (const item of value) {
1371
+ if (!isAstNode(item)) continue;
1372
+ visit(item);
1373
+ if (!isFunctionNode(item)) rec(item);
1374
+ }
1375
+ } else if (isAstNode(value)) {
1376
+ visit(value);
1377
+ if (!isFunctionNode(value)) rec(value);
1378
+ }
1379
+ }
1380
+ };
1381
+ rec(root);
1382
+ }
1383
+
1384
+ function walkStatements(node: AstNode, visit: (node: AstNode) => void): void {
1385
+ if (node.type === "BlockStatement" || node.type === "Program" || node.type === "StaticBlock") {
1386
+ const body = (node as AstNode & { body?: AstNode[] }).body ?? [];
1387
+ for (const stmt of body) walkStatement(stmt, visit);
1388
+ return;
1389
+ }
1390
+ walkSkipFunctions(node, (child) => {
1391
+ if (child.type === "VariableDeclarator" || child.type === "FunctionDeclaration") visit(child);
1392
+ });
1393
+ }
1394
+
1395
+ function walkStatement(stmt: AstNode, visit: (node: AstNode) => void): void {
1396
+ if (stmt.type === "FunctionDeclaration") {
1397
+ visit(stmt);
1398
+ return;
1399
+ }
1400
+ if (stmt.type === "VariableDeclaration") {
1401
+ const declarations = (stmt as AstNode & { declarations?: AstNode[] }).declarations ?? [];
1402
+ for (const decl of declarations) visit(decl);
1403
+ return;
1404
+ }
1405
+ if (stmt.type === "BlockStatement") {
1406
+ walkStatements(stmt, visit);
1407
+ return;
1408
+ }
1409
+ if (
1410
+ stmt.type === "IfStatement" ||
1411
+ stmt.type === "ForStatement" ||
1412
+ stmt.type === "ForInStatement" ||
1413
+ stmt.type === "ForOfStatement" ||
1414
+ stmt.type === "WhileStatement" ||
1415
+ stmt.type === "DoWhileStatement" ||
1416
+ stmt.type === "LabeledStatement"
1417
+ ) {
1418
+ const consequent = (stmt as AstNode & { consequent?: AstNode }).consequent;
1419
+ const alternate = (stmt as AstNode & { alternate?: AstNode }).alternate;
1420
+ const body = (stmt as AstNode & { body?: AstNode }).body;
1421
+ if (consequent) walkStatement(consequent, visit);
1422
+ if (alternate) walkStatement(alternate, visit);
1423
+ if (body && body !== consequent) walkStatement(body, visit);
1424
+ return;
1425
+ }
1426
+ if (stmt.type === "TryStatement") {
1427
+ const block = (stmt as AstNode & { block?: AstNode }).block;
1428
+ const handler = (stmt as AstNode & { handler?: AstNode }).handler;
1429
+ const finalizer = (stmt as AstNode & { finalizer?: AstNode }).finalizer;
1430
+ if (block) walkStatement(block, visit);
1431
+ const handlerBody = (handler as (AstNode & { body?: AstNode }) | undefined)?.body;
1432
+ if (handlerBody) walkStatement(handlerBody, visit);
1433
+ if (finalizer) walkStatement(finalizer, visit);
1434
+ return;
1435
+ }
1436
+ if (stmt.type === "SwitchStatement") {
1437
+ const cases = (stmt as AstNode & { cases?: AstNode[] }).cases ?? [];
1438
+ for (const item of cases) {
1439
+ const consequent = (item as AstNode & { consequent?: AstNode[] }).consequent ?? [];
1440
+ for (const child of consequent) walkStatement(child, visit);
1441
+ }
1442
+ }
1443
+ }
1444
+
1445
+ function functionParams(fn: AstNode): AstNode[] {
1446
+ return (fn as AstNode & { params?: AstNode[] }).params ?? [];
1447
+ }
1448
+
1449
+ function paramIdentifierName(param: AstNode): string | undefined {
1450
+ if (param.type === "Identifier") return identifierName(param);
1451
+ if (param.type === "AssignmentPattern" || param.type === "RestElement") {
1452
+ const inner =
1453
+ (param as AstNode & { left?: AstNode }).left ??
1454
+ (param as AstNode & { argument?: AstNode }).argument;
1455
+ return identifierName(inner);
1456
+ }
1457
+ return undefined;
1458
+ }
1459
+
1460
+ function isFunctionNode(node: AstNode): boolean {
1461
+ return (
1462
+ node.type === "ArrowFunctionExpression" ||
1463
+ node.type === "FunctionExpression" ||
1464
+ node.type === "FunctionDeclaration"
1465
+ );
1466
+ }
1467
+
1468
+ function unwrapValue(node: AstNode | undefined): AstNode | undefined {
1469
+ if (!node) return undefined;
1470
+ return unwrapValueNode(node);
1471
+ }
1472
+
1473
+ function unwrapValueNode(node: AstNode | undefined): AstNode | undefined {
1474
+ if (!node) return undefined;
1475
+ let current = node;
1476
+ for (let i = 0; i < 8; i++) {
1477
+ if (!VALUE_WRAPPERS.has(current.type)) break;
1478
+ const next =
1479
+ (current as AstNode & { expression?: AstNode }).expression ??
1480
+ (current as AstNode & { argument?: AstNode }).argument;
1481
+ if (!next) break;
1482
+ current = next;
1483
+ }
1484
+ return current;
1485
+ }
1486
+
1487
+ function isAstNode(value: unknown): value is AstNode {
1488
+ return typeof value === "object" && value !== null && typeof (value as AstNode).type === "string";
1489
+ }