okengine 0.18.5 → 0.19.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 (510) hide show
  1. package/AGENTS.md +18 -10
  2. package/README.md +2 -2
  3. package/package.json +51 -50
  4. package/site/content/docs/ai/llms-txt.mdx +1 -1
  5. package/site/content/docs/ai/meta.json +1 -1
  6. package/site/content/docs/ai/skills.mdx +4 -1
  7. package/site/content/docs/ai/try-it.mdx +58 -0
  8. package/site/content/docs/client/auth.mdx +208 -0
  9. package/site/content/docs/client/calling.mdx +451 -0
  10. package/site/content/docs/client/index.mdx +209 -0
  11. package/site/content/docs/client/live.mdx +234 -0
  12. package/site/content/docs/client/meta.json +5 -0
  13. package/site/content/docs/client/react.mdx +249 -0
  14. package/site/content/docs/elements/ai/agents.mdx +232 -0
  15. package/site/content/docs/elements/ai/index.mdx +341 -0
  16. package/site/content/docs/elements/ai/mcp.mdx +275 -0
  17. package/site/content/docs/elements/ai/meta.json +5 -0
  18. package/site/content/docs/elements/ai/models.mdx +283 -0
  19. package/site/content/docs/elements/ai/prompts.mdx +256 -0
  20. package/site/content/docs/elements/channel/email.mdx +266 -0
  21. package/site/content/docs/elements/channel/index.mdx +344 -0
  22. package/site/content/docs/elements/channel/meta.json +5 -0
  23. package/site/content/docs/elements/channel/push.mdx +218 -0
  24. package/site/content/docs/elements/channel/receipts.mdx +206 -0
  25. package/site/content/docs/elements/channel/sms.mdx +264 -0
  26. package/site/content/docs/elements/channel/whatsapp.mdx +220 -0
  27. package/site/content/docs/elements/clock/index.mdx +300 -0
  28. package/site/content/docs/elements/clock/meta.json +5 -0
  29. package/site/content/docs/elements/clock/schedules.mdx +508 -0
  30. package/site/content/docs/elements/clock/sleep.mdx +445 -0
  31. package/site/content/docs/elements/flow/consumers.mdx +707 -0
  32. package/site/content/docs/elements/flow/http.mdx +1080 -0
  33. package/site/content/docs/elements/flow/index.mdx +618 -0
  34. package/site/content/docs/elements/flow/meta.json +5 -0
  35. package/site/content/docs/elements/flow/routing.mdx +559 -0
  36. package/site/content/docs/elements/flow/workflows.mdx +640 -0
  37. package/site/content/docs/elements/gate/auth.mdx +371 -0
  38. package/site/content/docs/elements/gate/authorization.mdx +298 -0
  39. package/site/content/docs/elements/gate/index.mdx +418 -0
  40. package/site/content/docs/elements/gate/meta.json +5 -0
  41. package/site/content/docs/elements/gate/rate-limits.mdx +365 -0
  42. package/site/content/docs/elements/gate/rls.mdx +303 -0
  43. package/site/content/docs/elements/gate/tenancy.mdx +372 -0
  44. package/site/content/docs/elements/index.mdx +20 -10
  45. package/site/content/docs/elements/signal/broadcast.mdx +517 -0
  46. package/site/content/docs/elements/signal/index.mdx +352 -0
  47. package/site/content/docs/elements/signal/live.mdx +590 -0
  48. package/site/content/docs/elements/signal/meta.json +5 -0
  49. package/site/content/docs/elements/signal/once.mdx +596 -0
  50. package/site/content/docs/elements/store/files.mdx +659 -0
  51. package/site/content/docs/elements/store/index.mdx +310 -0
  52. package/site/content/docs/elements/store/kv.mdx +629 -0
  53. package/site/content/docs/elements/store/meta.json +5 -0
  54. package/site/content/docs/elements/store/search.mdx +946 -0
  55. package/site/content/docs/elements/store/sql.mdx +937 -0
  56. package/site/content/docs/elements/vault/config.mdx +256 -0
  57. package/site/content/docs/elements/vault/index.mdx +356 -0
  58. package/site/content/docs/elements/vault/meta.json +5 -0
  59. package/site/content/docs/elements/vault/rotation.mdx +273 -0
  60. package/site/content/docs/elements/vault/secrets.mdx +285 -0
  61. package/site/content/docs/index.mdx +63 -36
  62. package/site/content/docs/meta.json +8 -7
  63. package/site/content/docs/plugins/cors.mdx +2 -1
  64. package/site/content/docs/plugins/csrf.mdx +4 -1
  65. package/site/content/docs/plugins/headers.mdx +2 -1
  66. package/site/content/docs/plugins/ip-allowlist.mdx +2 -1
  67. package/site/content/docs/plugins/magic-link.mdx +6 -0
  68. package/site/content/docs/plugins/maintenance-mode.mdx +2 -1
  69. package/site/content/docs/plugins/oauth.mdx +1 -1
  70. package/site/content/docs/plugins/otp.mdx +12 -1
  71. package/site/content/docs/plugins/passkey.mdx +39 -15
  72. package/site/content/docs/plugins/two-factor.mdx +91 -34
  73. package/site/content/docs/plugins/username.mdx +1 -1
  74. package/site/content/docs/recipes/caddy.mdx +3 -3
  75. package/site/content/docs/recipes/index.mdx +18 -28
  76. package/site/content/docs/recipes/mailpit.mdx +1 -1
  77. package/site/content/docs/recipes/meilisearch.mdx +3 -3
  78. package/site/content/docs/recipes/meta.json +1 -4
  79. package/site/content/docs/recipes/nginx.mdx +2 -2
  80. package/site/content/docs/recipes/openrouter.mdx +283 -0
  81. package/site/content/docs/recipes/pgdog.mdx +3 -3
  82. package/site/content/docs/recipes/postgres.mdx +1 -2
  83. package/site/content/docs/recipes/rustfs.mdx +2 -2
  84. package/site/content/docs/recipes/traefik.mdx +3 -3
  85. package/site/content/docs/reference/cli.mdx +231 -0
  86. package/site/content/docs/reference/configuration.mdx +30 -10
  87. package/site/content/docs/reference/environment-variables.mdx +93 -20
  88. package/site/content/docs/reference/errors.mdx +132 -29
  89. package/site/content/docs/reference/fx.mdx +137 -73
  90. package/site/content/docs/reference/index.mdx +13 -21
  91. package/site/content/docs/reference/meta.json +4 -5
  92. package/site/content/docs/reference/okid.mdx +42 -17
  93. package/site/content/docs/reference/plugins.mdx +4 -3
  94. package/site/content/docs/reference/security.mdx +197 -0
  95. package/site/content/docs/understand/meta.json +5 -0
  96. package/site/content/docs/understand/the-anatomy.mdx +132 -0
  97. package/site/content/docs/understand/the-model.mdx +32 -0
  98. package/site/content/docs/understand/the-problem.mdx +74 -0
  99. package/site/content/docs/understand/the-vocabulary.mdx +26 -0
  100. package/src/auth/api-keys.ts +2 -1
  101. package/src/auth/bindings.ts +64 -16
  102. package/src/auth/gate-auth.test.ts +6 -1
  103. package/src/auth/identity.ts +152 -5
  104. package/src/auth/index.ts +29 -0
  105. package/src/auth/invites.ts +2 -1
  106. package/src/auth/method-context.ts +41 -0
  107. package/src/auth/oauth-as/crypto.ts +2 -1
  108. package/src/auth/oauth-as/stores.ts +2 -1
  109. package/src/auth/operator.ts +2 -1
  110. package/src/auth/sessions-jwt.test.ts +77 -0
  111. package/src/auth/sessions.ts +20 -4
  112. package/src/auth/tenants.ts +3 -2
  113. package/src/auth/two-factor-challenge.test.ts +61 -0
  114. package/src/auth/two-factor-challenge.ts +284 -0
  115. package/src/auth/verification.ts +5 -0
  116. package/src/bench/REPORT.md +49 -0
  117. package/src/bench/g03-signal-once.bench.ts +1 -1
  118. package/src/bench/g10-observability-contention.bench.ts +1 -1
  119. package/src/bench/g17-hybrid-search.bench.ts +572 -0
  120. package/src/bench/load-app.ts +2 -10
  121. package/src/cli/ai-setup/ai-setup.test.ts +331 -42
  122. package/src/cli/ai-setup/apply.ts +294 -52
  123. package/src/cli/ai-setup/catalog.ts +238 -1342
  124. package/src/cli/ai-setup/index.ts +14 -99
  125. package/src/cli/ai-setup/prompts.ts +60 -443
  126. package/src/cli/ask-seed.test.ts +96 -0
  127. package/src/cli/ask-seed.ts +82 -0
  128. package/src/cli/ask-vault-gaps.test.ts +84 -0
  129. package/src/cli/ask-vault-gaps.ts +146 -0
  130. package/src/cli/build.ts +2 -2
  131. package/src/cli/client-add.test.ts +26 -1
  132. package/src/cli/client-add.ts +106 -27
  133. package/src/cli/db-seed.ts +2 -0
  134. package/src/cli/db.ts +150 -11
  135. package/src/cli/dev.ts +119 -211
  136. package/src/cli/docker-clean.ts +2 -2
  137. package/src/cli/docker-cli.test.ts +1 -1
  138. package/src/cli/doctor-diff.ts +4 -2
  139. package/src/cli/doctor-pii.test.ts +1 -1
  140. package/src/cli/doctor.ts +14 -1
  141. package/src/cli/hero-meta.test.ts +4 -2
  142. package/src/cli/load-config.images.test.ts +8 -10
  143. package/src/cli/load-config.ts +1 -1
  144. package/src/cli/registry.ts +22 -10
  145. package/src/cli/replay.ts +3 -1
  146. package/src/cli/start.ts +1 -1
  147. package/src/cli/tui/keys.ts +2 -3
  148. package/src/client/auth/cookies.ts +74 -0
  149. package/src/client/auth/create-auth-client.ts +636 -0
  150. package/src/client/auth/denials.ts +99 -0
  151. package/src/client/auth/session.ts +283 -0
  152. package/src/client/auth.test.ts +251 -0
  153. package/src/client/auth.ts +39 -114
  154. package/src/client/create-with-session.ts +245 -0
  155. package/src/client/create.ts +51 -5
  156. package/src/client/index.ts +11 -1
  157. package/src/client/live.ts +11 -79
  158. package/src/client/notes-contract.test.ts +5 -10
  159. package/src/client/sse.ts +137 -0
  160. package/src/client/stream.ts +147 -0
  161. package/src/client/transport.test.ts +32 -0
  162. package/src/client/transport.ts +123 -26
  163. package/src/client/types.ts +118 -14
  164. package/src/client-react/index.ts +185 -24
  165. package/src/client-react/use-live-query.ts +13 -3
  166. package/src/compiler/aot.test.ts +7 -4
  167. package/src/compiler/effects-embed.test.ts +56 -0
  168. package/src/compiler/effects-fetch.test.ts +43 -0
  169. package/src/compiler/effects-infer.ts +58 -2
  170. package/src/compiler/extract.test.ts +257 -43
  171. package/src/compiler/extract.ts +566 -48
  172. package/src/compiler/fixtures/skyport/src/flows/bookings/index.ts +7 -4
  173. package/src/compiler/fixtures/skyport/src/flows/bookings/signals.ts +2 -8
  174. package/src/compiler/fixtures/skyport.expected.json +4 -4
  175. package/src/compiler/fixtures/triggers/five-triggers.ts +14 -10
  176. package/src/compiler/response.ts +2 -2
  177. package/src/compiler/search-writer-isolation.test.ts +40 -0
  178. package/src/config/index.ts +11 -0
  179. package/src/console/server/app.ts +17 -23
  180. package/src/console/server/bind.ts +4 -0
  181. package/src/console/server/console.test.ts +2 -2
  182. package/src/console/server/flows-invoke.test.ts +50 -34
  183. package/src/console/server/flows.ts +436 -255
  184. package/src/console/server/runs-ingest.test.ts +6 -3
  185. package/src/console/server/serve.ts +1 -1
  186. package/src/console/server/signals.test.ts +1 -5
  187. package/src/console/server/signals.ts +8 -6
  188. package/src/console/server/state.ts +6 -1
  189. package/src/console/server/store.ts +1 -1
  190. package/src/console/ui-next/dist/assets/FileExportIcon-Ck-5od4R.js +1 -0
  191. package/src/console/ui-next/dist/assets/MoreHorizontalCircle01Icon-gMNGsE37.js +1 -0
  192. package/src/console/ui-next/dist/assets/PlusSignIcon-CwG3nxfu.js +1 -0
  193. package/src/console/ui-next/dist/assets/UnavailableIcon-D9cvHVPr.js +1 -0
  194. package/src/console/ui-next/dist/assets/UserIcon-DaE2PB5_.js +1 -0
  195. package/src/console/ui-next/dist/assets/access-page-DFeymU07.js +4 -0
  196. package/src/console/ui-next/dist/assets/agent-disclosure-U1rdfblp.js +1 -0
  197. package/src/console/ui-next/dist/assets/cache-glyph-BeFJeqBG.js +1 -0
  198. package/src/console/ui-next/dist/assets/call-pii-button-CVAONPii.js +1 -0
  199. package/src/console/ui-next/dist/assets/collapsible-D2A6NJ-3.js +1 -0
  200. package/src/console/ui-next/dist/assets/copy-inline-button-CAYD18cr.js +1 -0
  201. package/src/console/ui-next/dist/assets/dagre.esm-B1_XeuLP.js +1 -0
  202. package/src/console/ui-next/dist/assets/detail-header-DVWNjWjg.js +1 -0
  203. package/src/console/ui-next/dist/assets/dropdown-menu-4h2LOVXM.js +1 -0
  204. package/src/console/ui-next/dist/assets/duration-tone-Cgk_h5ja.js +9 -0
  205. package/src/console/ui-next/dist/assets/element-icons-BI8cJgdh.js +1 -0
  206. package/src/console/ui-next/dist/assets/explorer-empty-CJs5A-wm.js +1 -0
  207. package/src/console/ui-next/dist/assets/flows-page-Dss7941e.js +1 -0
  208. package/src/console/ui-next/dist/assets/highlighted-json-MYZQtRnw.js +154 -0
  209. package/src/console/ui-next/dist/assets/http-method-Jrh39p7A.js +1 -0
  210. package/src/console/ui-next/dist/assets/index-D0zS5rKO.css +2 -0
  211. package/src/console/ui-next/dist/assets/index-DXP2dBIF.js +63 -0
  212. package/src/console/ui-next/dist/assets/observability-page-HvolXxTI.js +4 -0
  213. package/src/console/ui-next/dist/assets/react-dom-Ddte4I-Q.js +9 -0
  214. package/src/console/ui-next/dist/assets/replica-lag-CSh2dzrb.js +18 -0
  215. package/src/console/ui-next/dist/assets/request-meta-BatF8KrK.js +1 -0
  216. package/src/console/ui-next/dist/assets/shortcut-keys-3ILd8oGn.js +1 -0
  217. package/src/console/ui-next/dist/assets/{sql-BskegiFM.js → sql-BsFa4tDR.js} +1 -1
  218. package/src/console/ui-next/dist/assets/store-page-eiKiHnNe.js +41 -0
  219. package/src/console/ui-next/dist/assets/trace-detail-sheet-CZkMeKS-.js +2 -0
  220. package/src/console/ui-next/dist/assets/tree-expand-toggle-CW8y5A2h.js +55 -0
  221. package/src/console/ui-next/dist/assets/units-page-B_RWJrEO.js +1 -0
  222. package/src/console/ui-next/dist/assets/{use-vault-list-uk4WVboC.js → use-vault-list-CT4-gajj.js} +1 -1
  223. package/src/console/ui-next/dist/assets/vault-page-CWrg-A68.js +2 -0
  224. package/src/console/ui-next/dist/assets/xyflow-CSyC6ryz.css +1 -0
  225. package/src/console/ui-next/dist/assets/xyflow-yApv7D4e.js +7 -0
  226. package/src/console/ui-next/dist/index.html +6 -13
  227. package/src/console/ui-next/seed-invoke-host.ts +21 -18
  228. package/src/console/ui-next/src/client.ts +7 -2
  229. package/src/console/ui-next/src/features/flows/graph/element-map.ts +2 -0
  230. package/src/console/ui-next/src/features/flows/traces/effect-kind.ts +12 -2
  231. package/src/console/ui-next/src/features/flows/traces/effect-summary.ts +4 -0
  232. package/src/console/ui-next/src/features/flows/traces/trace-detail-sheet.tsx +8 -0
  233. package/src/console/ui-next/src/features/flows/traces/waterfall-bars.ts +4 -0
  234. package/src/console/ui-next/src/features/flows/traces/waterfall-tooltip.ts +9 -3
  235. package/src/console/ui-next/src/features/store/detail/reveal-cell.tsx +7 -11
  236. package/src/console/ui-next/src/features/store/detail/store-row-detail-sheet.tsx +2 -2
  237. package/src/console/ui-next/src/features/store/lib/fields-from-table.ts +2 -0
  238. package/src/console/ui-next/src/features/store/lib/grid-model.test.ts +25 -1
  239. package/src/console/ui-next/src/features/store/lib/grid-model.ts +36 -1
  240. package/src/console/ui-next/src/features/store/query/query-results.tsx +3 -3
  241. package/src/console/ui-next/src/features/units/detail/effects-summary.tsx +2 -0
  242. package/src/console/ui-next/src/features/units/lib/call-read-safe.ts +3 -1
  243. package/src/console/ui-next/src/features/units/lib/fields-from-schema.ts +6 -0
  244. package/src/docker/ai-model-status.test.ts +1 -12
  245. package/src/docker/ai-model-status.ts +10 -38
  246. package/src/docker/compose-up.test.ts +100 -0
  247. package/src/docker/compose-up.ts +292 -0
  248. package/src/docker/compose.ts +4 -14
  249. package/src/docker/derive.ts +9 -34
  250. package/src/docker/docker.test.ts +16 -261
  251. package/src/docker/images-config.test.ts +13 -25
  252. package/src/docker/index.ts +1 -24
  253. package/src/docker/recipes/index.ts +0 -23
  254. package/src/docker/recipes/pgdog.ts +6 -4
  255. package/src/docker/stack-id.test.ts +2 -2
  256. package/src/docker/stack-id.ts +0 -1
  257. package/src/docker/types.ts +2 -2
  258. package/src/drivers/ai-anthropic.ts +10 -0
  259. package/src/drivers/ai-openai-compatible.ts +34 -7
  260. package/src/drivers/ai-providers.test.ts +0 -107
  261. package/src/drivers/ai-stream.test.ts +0 -34
  262. package/src/drivers/ai-types.ts +17 -8
  263. package/src/drivers/channel-fcm.ts +6 -1
  264. package/src/drivers/channel-msegat.ts +6 -1
  265. package/src/drivers/channel-resend.ts +5 -1
  266. package/src/drivers/channel-smtp.ts +5 -1
  267. package/src/drivers/channel-sndr.ts +10 -1
  268. package/src/drivers/channel-taqnyat-mail.ts +5 -1
  269. package/src/drivers/channel-taqnyat-whatsapp.ts +6 -1
  270. package/src/drivers/channel-taqnyat.ts +6 -1
  271. package/src/drivers/channel-types.ts +10 -0
  272. package/src/drivers/channel-unifonic.ts +6 -1
  273. package/src/drivers/channel-wa-cloud.ts +6 -1
  274. package/src/drivers/channel-webpush.ts +6 -1
  275. package/src/drivers/external.ts +33 -0
  276. package/src/drivers/index.ts +0 -10
  277. package/src/drivers/meilisearch.ts +6 -0
  278. package/src/drivers/oauth-types.ts +4 -0
  279. package/src/drivers/postgres.test.ts +74 -3
  280. package/src/drivers/postgres.ts +175 -6
  281. package/src/drivers/signal-types.ts +5 -5
  282. package/src/drivers/types.ts +7 -0
  283. package/src/drivers/vault-types.ts +5 -0
  284. package/src/elements/ai/declare.ts +17 -4
  285. package/src/elements/ai/eval.ts +1 -1
  286. package/src/elements/ai/mcp-http.ts +1 -1
  287. package/src/elements/ai/pii.ts +1 -1
  288. package/src/elements/ai/providers.test.ts +289 -0
  289. package/src/elements/ai/providers.ts +169 -0
  290. package/src/elements/ai/runtime.ts +68 -5
  291. package/src/elements/ai/schema.ts +2 -2
  292. package/src/elements/ai.test.ts +7 -7
  293. package/src/elements/ai.ts +17 -1
  294. package/src/elements/channel/runtime.ts +105 -12
  295. package/src/elements/clock/cron-fields.test.ts +144 -0
  296. package/src/elements/clock/cron-fields.ts +185 -0
  297. package/src/elements/clock/declare.ts +247 -3
  298. package/src/elements/clock.ts +21 -1
  299. package/src/elements/index.ts +15 -0
  300. package/src/elements/signal/chaos-child.ts +1 -2
  301. package/src/elements/signal/declare.ts +65 -37
  302. package/src/elements/signal/delivery-modes.test.ts +11 -31
  303. package/src/elements/signal/dry-run-replay.test.ts +1 -7
  304. package/src/elements/signal/dry-run-write-isolation.test.ts +1 -7
  305. package/src/elements/signal/key-ordering.test.ts +6 -26
  306. package/src/elements/signal/lease-reclaim.test.ts +2 -6
  307. package/src/elements/signal/optional-emit.test.ts +6 -9
  308. package/src/elements/signal/order-lifecycle.test.ts +4 -17
  309. package/src/elements/signal/orphan-messages.test.ts +4 -15
  310. package/src/elements/signal/reconcile.test.ts +2 -2
  311. package/src/elements/signal/runtime.ts +1 -1
  312. package/src/elements/signal/schema-emit.test.ts +4 -8
  313. package/src/elements/signal.test.ts +24 -49
  314. package/src/elements/signal.ts +9 -2
  315. package/src/elements/store/domain-ddl.test.ts +1 -1
  316. package/src/elements/store/live-http.test.ts +9 -4
  317. package/src/elements/store/prepare-row.test.ts +123 -0
  318. package/src/elements/store/resource.ts +43 -12
  319. package/src/elements/store/schema-decl.ts +117 -0
  320. package/src/elements/store/search-backfill.ts +205 -0
  321. package/src/elements/store/search-bind.ts +66 -0
  322. package/src/elements/store/search-bm25.ts +78 -0
  323. package/src/elements/store/search-ddl.ts +156 -0
  324. package/src/elements/store/search-embed-flow.ts +141 -0
  325. package/src/elements/store/search-errors.ts +34 -0
  326. package/src/elements/store/search-fusion.ts +112 -0
  327. package/src/elements/store/search-lsh.ts +179 -0
  328. package/src/elements/store/search-runtime.ts +300 -0
  329. package/src/elements/store/search.test.ts +144 -0
  330. package/src/elements/store/sql-session.test.ts +1 -0
  331. package/src/elements/store/sql-session.ts +83 -3
  332. package/src/elements/store/table.ts +23 -2
  333. package/src/elements/store/upsert-app.test.ts +1 -3
  334. package/src/elements/store.ts +50 -0
  335. package/src/full.ts +4 -2
  336. package/src/http.ts +4 -1
  337. package/src/i18n/catalogs/ar.ts +14 -6
  338. package/src/i18n/catalogs/en.ts +14 -6
  339. package/src/index.ts +6 -1
  340. package/src/kernel/adopt-barrel-fresh.test.ts +7 -7
  341. package/src/kernel/adopt-routes.ts +9 -1
  342. package/src/kernel/app-auth.ts +5 -0
  343. package/src/kernel/app.ts +102 -9
  344. package/src/kernel/auto-cache.test.ts +6 -12
  345. package/src/kernel/auto-registry.test.ts +3 -3
  346. package/src/kernel/boot-bind/ai.test.ts +22 -19
  347. package/src/kernel/boot-bind/ai.ts +5 -43
  348. package/src/kernel/boot-bind/clock.ts +23 -5
  349. package/src/kernel/boot-bind/honor-config.test.ts +4 -4
  350. package/src/kernel/boot.test.ts +11 -6
  351. package/src/kernel/boot.ts +66 -16
  352. package/src/kernel/boundary-contract.ts +93 -0
  353. package/src/kernel/budget.test.ts +1 -1
  354. package/src/kernel/call.ts +89 -0
  355. package/src/kernel/capability.ts +6 -0
  356. package/src/kernel/client-descriptor.ts +123 -0
  357. package/src/kernel/clock-timezone.test.ts +80 -0
  358. package/src/kernel/correlation.test.ts +1 -1
  359. package/src/kernel/dry-run.ts +11 -8
  360. package/src/kernel/effects-stamping.test.ts +42 -9
  361. package/src/kernel/effects.test.ts +4 -2
  362. package/src/kernel/effects.ts +51 -9
  363. package/src/kernel/errors-channel.ts +17 -0
  364. package/src/kernel/errors-live-resume.ts +3 -2
  365. package/src/kernel/errors-tenant.ts +7 -4
  366. package/src/kernel/errors.registry-helpers.ts +96 -0
  367. package/src/kernel/errors.registry.test.ts +139 -25
  368. package/src/kernel/errors.ts +102 -23
  369. package/src/kernel/external-effects.test.ts +196 -0
  370. package/src/kernel/flow.test.ts +13 -4
  371. package/src/kernel/flow.ts +69 -71
  372. package/src/kernel/fx-ask-telemetry.test.ts +2 -2
  373. package/src/kernel/fx-dead-letters.test.ts +3 -3
  374. package/src/kernel/fx-fetch.ts +55 -0
  375. package/src/kernel/fx-live-stream.ts +2 -2
  376. package/src/kernel/fx-live.test.ts +8 -8
  377. package/src/kernel/fx.test.ts +17 -11
  378. package/src/kernel/fx.ts +180 -57
  379. package/src/kernel/horizontal-child.ts +1 -1
  380. package/src/kernel/http-query.test.ts +3 -6
  381. package/src/kernel/index.ts +13 -3
  382. package/src/kernel/instance-id.ts +1 -1
  383. package/src/kernel/instances.test.ts +40 -0
  384. package/src/kernel/instances.ts +47 -9
  385. package/src/kernel/live-http.test.ts +6 -7
  386. package/src/kernel/live-http.ts +6 -2
  387. package/src/kernel/live-resume.test.ts +1 -1
  388. package/src/kernel/mcp-tool.test.ts +3 -3
  389. package/src/kernel/on.ts +98 -4
  390. package/src/kernel/pipeline.test.ts +12 -15
  391. package/src/kernel/plugin-elements.test.ts +1 -1
  392. package/src/kernel/stamp-http.test.ts +3 -3
  393. package/src/kernel/triggers.ts +210 -95
  394. package/src/kernel-entry.ts +0 -1
  395. package/src/manifest/diff.ts +11 -1
  396. package/src/manifest/fixtures/skyport.excerpt.json +1 -1
  397. package/src/manifest/types.ts +46 -2
  398. package/src/mcp/docs-index.ts +3 -3
  399. package/src/mcp/docs-mcp.test.ts +2 -2
  400. package/src/mcp/docs-tools.ts +1 -1
  401. package/src/okid.test.ts +56 -0
  402. package/src/okid.ts +55 -11
  403. package/src/plugins/anonymous.ts +8 -4
  404. package/src/plugins/auth/shared.ts +27 -3
  405. package/src/plugins/auth-delivery.mailpit.integration.test.ts +1 -1
  406. package/src/plugins/auth-methods.security.test.ts +561 -23
  407. package/src/plugins/config-source.ts +1 -1
  408. package/src/plugins/csrf.test.ts +65 -0
  409. package/src/plugins/index.ts +2 -0
  410. package/src/plugins/magic-link.ts +36 -16
  411. package/src/plugins/oauth/shared.ts +3 -1
  412. package/src/plugins/oauth.ts +22 -14
  413. package/src/plugins/otp.ts +37 -21
  414. package/src/plugins/passkey-webauthn.ts +9 -6
  415. package/src/plugins/passkey.ts +98 -26
  416. package/src/plugins/pre-account-hijack.test.ts +223 -0
  417. package/src/plugins/two-factor.ts +466 -37
  418. package/src/plugins/username.ts +19 -7
  419. package/src/release/build-lib.ts +1 -0
  420. package/src/release/limits.ts +2 -2
  421. package/src/release/measure.ts +1 -1
  422. package/src/runs/collect.test.ts +0 -2
  423. package/src/runtime/json-code-block.test.ts +250 -20
  424. package/src/runtime/json-code-block.ts +1261 -41
  425. package/src/term.test.ts +1 -1
  426. package/src/test/create-test-app.test.ts +12 -12
  427. package/src/test/live-signals.test.ts +6 -6
  428. package/src/test/provisions.integration.test.ts +10 -14
  429. package/src/test/reset-element-registries.ts +1 -1
  430. package/src/test/tenant-isolation.test.ts +12 -6
  431. package/site/content/docs/deployment/docker-swarm.mdx +0 -164
  432. package/site/content/docs/deployment/docker.mdx +0 -227
  433. package/site/content/docs/deployment/index.mdx +0 -83
  434. package/site/content/docs/deployment/kubernetes.mdx +0 -176
  435. package/site/content/docs/deployment/meta.json +0 -5
  436. package/site/content/docs/deployment/reverse-proxy.mdx +0 -234
  437. package/site/content/docs/elements/ai.mdx +0 -385
  438. package/site/content/docs/elements/channel.mdx +0 -346
  439. package/site/content/docs/elements/clock.mdx +0 -244
  440. package/site/content/docs/elements/flow.mdx +0 -420
  441. package/site/content/docs/elements/gate.mdx +0 -436
  442. package/site/content/docs/elements/signal.mdx +0 -380
  443. package/site/content/docs/elements/store.mdx +0 -1099
  444. package/site/content/docs/elements/vault.mdx +0 -405
  445. package/site/content/docs/get-started/basic-usage.mdx +0 -173
  446. package/site/content/docs/get-started/index.mdx +0 -43
  447. package/site/content/docs/get-started/installation.mdx +0 -220
  448. package/site/content/docs/get-started/introduction.mdx +0 -144
  449. package/site/content/docs/get-started/meta.json +0 -13
  450. package/site/content/docs/get-started/project-structure.mdx +0 -925
  451. package/site/content/docs/get-started/testing.mdx +0 -328
  452. package/site/content/docs/get-started/why.mdx +0 -114
  453. package/site/content/docs/recipes/llama-cpp.mdx +0 -151
  454. package/site/content/docs/recipes/ollama.mdx +0 -142
  455. package/site/content/docs/recipes/sglang.mdx +0 -105
  456. package/site/content/docs/recipes/vllm.mdx +0 -106
  457. package/site/content/docs/reference/cli.md +0 -232
  458. package/site/content/docs/reference/client.mdx +0 -469
  459. package/site/content/docs/reference/security.md +0 -74
  460. package/src/cli/ai-setup/detect-ollama.ts +0 -213
  461. package/src/cli/ai-setup/recommend.test.ts +0 -225
  462. package/src/cli/ai-setup/recommend.ts +0 -225
  463. package/src/cli/dev-controls.test.ts +0 -122
  464. package/src/cli/dev-controls.ts +0 -164
  465. package/src/cli/tui/DevLive.tsx +0 -126
  466. package/src/console/ui-next/dist/assets/access-page-3-EFj-2G.js +0 -4
  467. package/src/console/ui-next/dist/assets/agent-disclosure-BP0Y0Sux.js +0 -1
  468. package/src/console/ui-next/dist/assets/cache-glyph-B5X-NM0-.js +0 -1
  469. package/src/console/ui-next/dist/assets/call-pii-button-ChAUiLo9.js +0 -1
  470. package/src/console/ui-next/dist/assets/collapsible-DGnOM2ph.js +0 -1
  471. package/src/console/ui-next/dist/assets/copy-inline-button-DBHhgHKP.js +0 -1
  472. package/src/console/ui-next/dist/assets/dagre.esm-ZwcdTuZZ.js +0 -1
  473. package/src/console/ui-next/dist/assets/detail-header-DhHM1iaZ.js +0 -1
  474. package/src/console/ui-next/dist/assets/dropdown-menu-_NjTEo5_.js +0 -1
  475. package/src/console/ui-next/dist/assets/duration-tone-sC3lGABz.js +0 -9
  476. package/src/console/ui-next/dist/assets/element-icons-BVXtRyd3.js +0 -1
  477. package/src/console/ui-next/dist/assets/explorer-empty-2uIhBu0_.js +0 -1
  478. package/src/console/ui-next/dist/assets/flows-page-cVFnA4HH.js +0 -1
  479. package/src/console/ui-next/dist/assets/highlighted-json-Awq7gYdu.js +0 -154
  480. package/src/console/ui-next/dist/assets/http-method-_UHM2ODJ.js +0 -1
  481. package/src/console/ui-next/dist/assets/index-CMIgUbD0.js +0 -66
  482. package/src/console/ui-next/dist/assets/index-Ck88Jmv8.css +0 -2
  483. package/src/console/ui-next/dist/assets/link-BX6Vqztd.js +0 -1
  484. package/src/console/ui-next/dist/assets/observability-page-CQ3p34ip.js +0 -4
  485. package/src/console/ui-next/dist/assets/preload-helper-oH4irX4C.js +0 -1
  486. package/src/console/ui-next/dist/assets/react-D8E3mtu1.js +0 -1
  487. package/src/console/ui-next/dist/assets/react-dom-Bph1y7z7.js +0 -9
  488. package/src/console/ui-next/dist/assets/replica-lag-DKRrbvdo.js +0 -18
  489. package/src/console/ui-next/dist/assets/request-meta-DV0ywz7t.js +0 -1
  490. package/src/console/ui-next/dist/assets/shortcut-JQIZlWfm.js +0 -1
  491. package/src/console/ui-next/dist/assets/shortcut-keys-DKxNTe_m.js +0 -1
  492. package/src/console/ui-next/dist/assets/skeleton-D-czQJT6.js +0 -1
  493. package/src/console/ui-next/dist/assets/store-page-02xOiqIK.js +0 -41
  494. package/src/console/ui-next/dist/assets/trace-detail-sheet-B09O8rA6.js +0 -2
  495. package/src/console/ui-next/dist/assets/tree-expand-toggle-BtyhmWb4.js +0 -55
  496. package/src/console/ui-next/dist/assets/units-page-l8FeKfnP.js +0 -1
  497. package/src/console/ui-next/dist/assets/useMutation-B8EO02Ej.js +0 -1
  498. package/src/console/ui-next/dist/assets/useRender-BE2A9BWC.js +0 -1
  499. package/src/console/ui-next/dist/assets/vault-page-CL-d_mLE.js +0 -2
  500. package/src/console/ui-next/dist/assets/xyflow-D7n4g6go.js +0 -7
  501. package/src/console/ui-next/dist/assets/xyflow-DZ0Ws1xk.css +0 -1
  502. package/src/docker/ollama-pull.ts +0 -232
  503. package/src/docker/recipes/llama-cpp.ts +0 -298
  504. package/src/docker/recipes/ollama.ts +0 -44
  505. package/src/docker/recipes/sglang.ts +0 -55
  506. package/src/docker/recipes/vllm.ts +0 -44
  507. package/src/drivers/ai-ollama-tools.integration.test.ts +0 -109
  508. package/src/drivers/ai-ollama.integration.test.ts +0 -184
  509. package/src/drivers/ai-ollama.ts +0 -389
  510. package/src/drivers/ollama.ts +0 -14
@@ -8,6 +8,14 @@
8
8
 
9
9
  import { parseSync } from "oxc-parser";
10
10
 
11
+ import {
12
+ AI_NATIVE_DRIVER_IDS,
13
+ formatAiProviderTier2Warn,
14
+ getAiProviderEntry,
15
+ } from "../elements/ai/providers.ts";
16
+ import { buildCronExpression, type CronField } from "../elements/clock/cron-fields.ts";
17
+ import { SearchConfigError } from "../elements/store/search-errors.ts";
18
+
11
19
  import type {
12
20
  Ai,
13
21
  AiAgent,
@@ -230,7 +238,21 @@ export async function extractManifest(options: ExtractManifestOptions = {}): Pro
230
238
  if (Object.keys(journeys).length > 0) manifest.journeys = journeys;
231
239
  if (Object.keys(drivers).length > 0) manifest.drivers = drivers;
232
240
  if (scope.tenancy) manifest.tenancy = scope.tenancy;
233
- if (scope.store && scope.store.live === true) manifest.store = { live: true };
241
+ if (scope.store) {
242
+ const storeManifest: NonNullable<Manifest["store"]> = {};
243
+ if (scope.store.live === true) storeManifest.live = true;
244
+ if (scope.store.search?.embed) {
245
+ storeManifest.search = {
246
+ embed: {
247
+ model: scope.store.search.embed.model,
248
+ dims: scope.store.search.embed.dims,
249
+ },
250
+ };
251
+ }
252
+ if (storeManifest.live === true || storeManifest.search?.embed) {
253
+ manifest.store = storeManifest;
254
+ }
255
+ }
234
256
  if (scope.i18n) manifest.i18n = scope.i18n;
235
257
  if (scope.topology) manifest.topology = scope.topology;
236
258
  if (scope.images) manifest.images = scope.images;
@@ -238,6 +260,8 @@ export async function extractManifest(options: ExtractManifestOptions = {}): Pro
238
260
  applyTenancyDefaults(manifest);
239
261
  assertTenantSafeSchema(manifest);
240
262
  applyLiveDefaults(manifest, scope);
263
+ resolveSearchEmbedDefaults(manifest);
264
+ assertSearchConfig(manifest);
241
265
 
242
266
  return manifest;
243
267
  }
@@ -295,6 +319,7 @@ function collectDeclarations(filePath: string, program: AstNode, scope: ProjectS
295
319
  if (name) scope.app = name;
296
320
  applyOkeTenantConfig(opts, scope);
297
321
  applyOkeStoreLiveConfig(opts, scope);
322
+ applyOkeStoreSearchConfig(opts, scope);
298
323
  });
299
324
  }
300
325
 
@@ -495,16 +520,35 @@ function tableFromDeclared(t: DeclaredSchemaTable): {
495
520
  policies?: Record<string, TablePolicy>;
496
521
  tenantScoped?: boolean;
497
522
  live?: boolean;
523
+ search?: { engines: Array<"bm25" | "lsh"> };
498
524
  } {
525
+ const engines = searchEnginesFromColumns(t.columns);
499
526
  return {
500
527
  columns: t.columns,
501
528
  ...(t.rls ? { rls: true } : {}),
502
529
  ...(t.policies ? { policies: t.policies } : {}),
503
530
  ...(t.tenantScoped === false ? { tenantScoped: false } : {}),
504
531
  ...(t.live === false ? { live: false } : {}),
532
+ ...(engines ? { search: { engines } } : {}),
505
533
  };
506
534
  }
507
535
 
536
+ function searchEnginesFromColumns(
537
+ columns: Record<string, DeclaredColumn>,
538
+ ): Array<"bm25" | "lsh"> | undefined {
539
+ let bm25 = false;
540
+ let lsh = false;
541
+ for (const col of Object.values(columns)) {
542
+ if (col.searchable) bm25 = true;
543
+ if (col.embed) lsh = true;
544
+ }
545
+ if (!bm25 && !lsh) return undefined;
546
+ const engines: Array<"bm25" | "lsh"> = [];
547
+ if (bm25) engines.push("bm25");
548
+ if (lsh) engines.push("lsh");
549
+ return engines;
550
+ }
551
+
508
552
  function parseSchemaTableExtras(
509
553
  node: AstNode | undefined,
510
554
  ): Pick<DeclaredSchemaTable, "rls" | "policies" | "tenantScoped" | "live"> {
@@ -678,11 +722,19 @@ const FIELD_SQL_TYPES: ReadonlySet<string> = new Set([
678
722
  "cidr",
679
723
  "macaddr",
680
724
  "macaddr8",
725
+ // Prepared text-id factories (`field.id()` / `field.okid()` ≡ text + defaultFn).
726
+ "id",
727
+ "okid",
681
728
  ]);
682
729
 
730
+ /** Prepared text-id heads that imply a dynamic default (same as `.okid()` / `.defaultFn(id)`). */
731
+ const FIELD_ID_HEADS: ReadonlySet<string> = new Set(["id", "okid"]);
732
+
683
733
  /** Manifest primitive for a field head (`decimal` collapses into `numeric`). */
684
734
  function manifestFieldType(head: string): DeclaredColumn["type"] {
685
- return head === "decimal" ? "numeric" : (head as DeclaredColumn["type"]);
735
+ if (head === "decimal") return "numeric";
736
+ if (FIELD_ID_HEADS.has(head)) return "text";
737
+ return head as DeclaredColumn["type"];
686
738
  }
687
739
 
688
740
  /**
@@ -719,7 +771,7 @@ function parseFieldChain(node: AstNode, key: string): DeclaredColumn | undefined
719
771
  if (method === "describe") {
720
772
  description = stringArg(call.arguments[0]) ?? description;
721
773
  }
722
- if (method === "defaultFn" || method === "okid" || method === "now") {
774
+ if (method === "defaultFn" || method === "okid" || method === "now" || method === "id") {
723
775
  hasDefault = true;
724
776
  defaultValue = defaultValue ?? null;
725
777
  }
@@ -755,11 +807,17 @@ function parseFieldChain(node: AstNode, key: string): DeclaredColumn | undefined
755
807
  const member = cur as AstNode & { object: AstNode; property: AstNode };
756
808
  if (identifierName(member.object) === "field") {
757
809
  const t = identifierName(member.property);
758
- if (t && FIELD_SQL_TYPES.has(t)) sqlType = manifestFieldType(t);
810
+ if (t && FIELD_SQL_TYPES.has(t)) {
811
+ sqlType = manifestFieldType(t);
812
+ if (FIELD_ID_HEADS.has(t)) {
813
+ hasDefault = true;
814
+ defaultValue = defaultValue ?? null;
815
+ }
816
+ }
759
817
  }
760
818
  }
761
819
 
762
- // Also: field.text() ends as CallExpression with callee MemberExpression field.text
820
+ // Also: field.text() / field.id() ends as CallExpression with callee MemberExpression
763
821
  if (!sqlType && node.type === "CallExpression") {
764
822
  let probe: AstNode | undefined = node;
765
823
  while (probe && probe.type === "CallExpression") {
@@ -770,6 +828,10 @@ function parseFieldChain(node: AstNode, key: string): DeclaredColumn | undefined
770
828
  const head = identifierName(member.property);
771
829
  if (identifierName(member.object) === "field" && head && FIELD_SQL_TYPES.has(head)) {
772
830
  sqlType = manifestFieldType(head);
831
+ if (FIELD_ID_HEADS.has(head)) {
832
+ hasDefault = true;
833
+ defaultValue = defaultValue ?? null;
834
+ }
773
835
  const arg0 = call.arguments[0];
774
836
  if (arg0?.type === "ObjectExpression") optionsNode = arg0;
775
837
  break;
@@ -796,6 +858,86 @@ function parseFieldChain(node: AstNode, key: string): DeclaredColumn | undefined
796
858
  ...(methods.has("sensitive") ? { sensitive: true } : {}),
797
859
  ...parseFieldOptions(optionsNode),
798
860
  };
861
+ if (methods.has("searchable")) {
862
+ let weight = 1;
863
+ let probe: AstNode | undefined = node;
864
+ while (probe && probe.type === "CallExpression") {
865
+ const call = probe as CallExpression;
866
+ const callee = call.callee;
867
+ if (callee.type === "MemberExpression") {
868
+ const member = callee as AstNode & { object: AstNode; property: AstNode };
869
+ if (identifierName(member.property) === "searchable") {
870
+ const arg0 = call.arguments[0];
871
+ if (arg0?.type === "ObjectExpression") {
872
+ const w = numberProp(arg0, "weight");
873
+ if (w !== undefined) weight = w;
874
+ }
875
+ break;
876
+ }
877
+ probe = member.object;
878
+ continue;
879
+ }
880
+ break;
881
+ }
882
+ col.searchable = { weight };
883
+ }
884
+ if (methods.has("embed")) {
885
+ if (!methods.has("searchable")) {
886
+ throw new Error(
887
+ `extract: .embed() on column "${key}" requires a prior .searchable() — weight is free SQL math; embed is an async AI pipeline`,
888
+ );
889
+ }
890
+ let dims: number | undefined;
891
+ let model: string | undefined;
892
+ let probe: AstNode | undefined = node;
893
+ while (probe && probe.type === "CallExpression") {
894
+ const call = probe as CallExpression;
895
+ const callee = call.callee;
896
+ if (callee.type === "MemberExpression") {
897
+ const member = callee as AstNode & { object: AstNode; property: AstNode };
898
+ if (identifierName(member.property) === "embed") {
899
+ const arg0 = call.arguments[0];
900
+ if (arg0?.type === "ObjectExpression") {
901
+ const d = numberProp(arg0, "dims");
902
+ if (d !== undefined) dims = d;
903
+ const modelNode = objectProp(arg0, "model");
904
+ if (modelNode) {
905
+ const lit = stringArg(modelNode);
906
+ if (lit) model = lit;
907
+ else {
908
+ const bindingName = identifierName(modelNode);
909
+ if (bindingName) model = bindingName;
910
+ else if (modelNode.type === "MemberExpression") {
911
+ const m = modelNode as AstNode & { property: AstNode };
912
+ const prop = identifierName(m.property);
913
+ if (prop === "name") {
914
+ const obj = (modelNode as AstNode & { object: AstNode }).object;
915
+ model = identifierName(obj) ?? model;
916
+ }
917
+ }
918
+ }
919
+ }
920
+ }
921
+ break;
922
+ }
923
+ probe = member.object;
924
+ continue;
925
+ }
926
+ break;
927
+ }
928
+ if (dims !== undefined && !(Number.isInteger(dims) && dims > 0)) {
929
+ throw new Error(
930
+ `extract: .embed() on column "${key}" has invalid dims (need positive integer)`,
931
+ );
932
+ }
933
+ // Stamp partial embed; resolveSearchEmbedDefaults fills model/dims from
934
+ // oke({ store: { search: { embed } } }) before assertSearchConfig.
935
+ // Cast: Manifest DeclaredColumn requires concrete dims after resolution.
936
+ (col as { embed?: { model?: string; dims?: number } }).embed = {
937
+ ...(dims !== undefined ? { dims } : {}),
938
+ ...(model !== undefined ? { model } : {}),
939
+ };
940
+ }
799
941
  if (methods.has("retain")) {
800
942
  // retain("7y") — find the call in the original chain by re-walk
801
943
  let probe: AstNode | undefined = node;
@@ -1013,7 +1155,7 @@ function visitDeclarationCall(call: CallExpression, program: AstNode, scope: Pro
1013
1155
  const name = stringArg(call.arguments[0]);
1014
1156
  const opts = objectArg(call.arguments[1]);
1015
1157
  if (name) {
1016
- scope.clocks[name] = {
1158
+ recordClockDecl(scope, program, call, name, {
1017
1159
  ...(stringProp(opts, "every") ? { every: stringProp(opts, "every") } : {}),
1018
1160
  ...(stringProp(opts, "cron") ? { cron: stringProp(opts, "cron") } : {}),
1019
1161
  ...(boolProp(opts, "overridable") !== undefined
@@ -1023,7 +1165,61 @@ function visitDeclarationCall(call: CallExpression, program: AstNode, scope: Pro
1023
1165
  ? { description: stringProp(opts, "description") }
1024
1166
  : {}),
1025
1167
  perTenant: true,
1026
- };
1168
+ });
1169
+ }
1170
+ }
1171
+
1172
+ // clock.every("name", "30s", opts?)
1173
+ if (obj === "clock" && prop === "every") {
1174
+ const name = stringArg(call.arguments[0]);
1175
+ const duration = stringArg(call.arguments[1]);
1176
+ const opts = objectArg(call.arguments[2]);
1177
+ if (name && duration) {
1178
+ recordClockDecl(scope, program, call, name, {
1179
+ every: duration,
1180
+ ...clockHelperOptsFromAst(opts),
1181
+ });
1182
+ }
1183
+ }
1184
+
1185
+ // clock.cron("name", "0 6 * * *" | fields, opts?)
1186
+ if (obj === "clock" && prop === "cron") {
1187
+ const name = stringArg(call.arguments[0]);
1188
+ if (name) {
1189
+ const expr = stringArg(call.arguments[1]);
1190
+ const fieldsObj = objectArg(call.arguments[1]);
1191
+ if (expr) {
1192
+ recordClockDecl(scope, program, call, name, {
1193
+ cron: expr,
1194
+ ...clockHelperOptsFromAst(objectArg(call.arguments[2])),
1195
+ });
1196
+ } else if (fieldsObj) {
1197
+ const built = cronExpressionFromFieldsAst(fieldsObj);
1198
+ if (built) {
1199
+ recordClockDecl(scope, program, call, name, {
1200
+ cron: built,
1201
+ ...clockHelperOptsFromAst(fieldsObj),
1202
+ });
1203
+ }
1204
+ }
1205
+ }
1206
+ }
1207
+
1208
+ // clock.daily / hourly / weekly / monthly
1209
+ if (
1210
+ obj === "clock" &&
1211
+ (prop === "daily" || prop === "hourly" || prop === "weekly" || prop === "monthly")
1212
+ ) {
1213
+ const name = stringArg(call.arguments[0]);
1214
+ const opts = objectArg(call.arguments[1]);
1215
+ if (name) {
1216
+ const cron = cronFromPresetHelper(prop, opts);
1217
+ if (cron) {
1218
+ recordClockDecl(scope, program, call, name, {
1219
+ cron,
1220
+ ...clockHelperOptsFromAst(opts),
1221
+ });
1222
+ }
1027
1223
  }
1028
1224
  }
1029
1225
 
@@ -1143,13 +1339,23 @@ function visitDeclarationCall(call: CallExpression, program: AstNode, scope: Pro
1143
1339
  const modelName = stringArg(call.arguments[0]);
1144
1340
  const opts = objectArg(call.arguments[1]);
1145
1341
  if (modelName) {
1342
+ const provider = stringProp(opts, "provider");
1343
+ const driverId = stringProp(opts, "driverId");
1344
+ const baseUrl = stringProp(opts, "baseUrl");
1146
1345
  const model: AiModel = {
1147
- ...(stringProp(opts, "provider") ? { provider: stringProp(opts, "provider") } : {}),
1346
+ ...(provider ? { provider } : {}),
1148
1347
  ...(stringProp(opts, "tier") ? { tier: stringProp(opts, "tier") } : {}),
1149
- ...(stringProp(opts, "driverId") ? { driverId: stringProp(opts, "driverId") } : {}),
1348
+ ...(driverId ? { driverId } : {}),
1150
1349
  };
1151
1350
  scope.ai.models = scope.ai.models ?? {};
1152
1351
  scope.ai.models[modelName] = model;
1352
+ // Limited OpenAI-compat caveat — same honesty as declare-time warn.
1353
+ if (provider && !baseUrl && !(driverId && AI_NATIVE_DRIVER_IDS.has(driverId))) {
1354
+ const entry = getAiProviderEntry(provider);
1355
+ if (entry?.tier === 2 && entry.caveat) {
1356
+ console.warn(formatAiProviderTier2Warn(provider, entry.caveat, "[oke extract] warn"));
1357
+ }
1358
+ }
1153
1359
  const bindingName = enclosingConstName(call, program);
1154
1360
  if (bindingName) {
1155
1361
  scope.bindings.set(bindingName, {
@@ -1265,15 +1471,13 @@ function visitDeclarationCall(call: CallExpression, program: AstNode, scope: Pro
1265
1471
  }
1266
1472
  }
1267
1473
  }
1268
- }
1269
1474
 
1270
- // signal("name", { delivery, })
1271
- if (identifierName(callee) === "signal") {
1272
- const signalName = stringArg(call.arguments[0]);
1273
- const opts = objectArg(call.arguments[1]);
1274
- if (signalName) {
1275
- const delivery = stringProp(opts, "delivery") as SignalDelivery | undefined;
1276
- if (delivery) {
1475
+ // signal.once("name", opts?) / .broadcast / .live
1476
+ if (obj === "signal" && (prop === "once" || prop === "broadcast" || prop === "live")) {
1477
+ const signalName = stringArg(call.arguments[0]);
1478
+ const opts = objectArg(call.arguments[1]);
1479
+ if (signalName) {
1480
+ const delivery = prop as SignalDelivery;
1277
1481
  const retentionObj = objectProp(opts, "retention");
1278
1482
  const maxAge = retentionObj ? stringProp(retentionObj, "maxAge") : undefined;
1279
1483
  const maxCount = retentionObj ? numberProp(retentionObj, "maxCount") : undefined;
@@ -1314,7 +1518,7 @@ function visitDeclarationCall(call: CallExpression, program: AstNode, scope: Pro
1314
1518
  const name = stringArg(call.arguments[0]);
1315
1519
  const opts = objectArg(call.arguments[1]);
1316
1520
  if (name && opts) {
1317
- scope.clocks[name] = {
1521
+ recordClockDecl(scope, program, call, name, {
1318
1522
  ...(stringProp(opts, "every") ? { every: stringProp(opts, "every") } : {}),
1319
1523
  ...(stringProp(opts, "cron") ? { cron: stringProp(opts, "cron") } : {}),
1320
1524
  ...(boolProp(opts, "overridable") !== undefined
@@ -1324,7 +1528,7 @@ function visitDeclarationCall(call: CallExpression, program: AstNode, scope: Pro
1324
1528
  ? { description: stringProp(opts, "description") }
1325
1529
  : {}),
1326
1530
  ...(boolProp(opts, "perTenant") === true ? { perTenant: true } : {}),
1327
- };
1531
+ });
1328
1532
  }
1329
1533
  }
1330
1534
 
@@ -1582,8 +1786,8 @@ function collectFlows(file: SourceFile, program: AstNode, scope: ProjectScope):
1582
1786
  return;
1583
1787
  }
1584
1788
 
1585
- if (callee === "flow") {
1586
- // Bare flow — skip when this call is the second arg of an on().
1789
+ if (callee === "flow" || callee === "call") {
1790
+ // Bare flow / call — skip when this call is the second arg of an on().
1587
1791
  if (isOnFlowArgument(call, program)) return;
1588
1792
  const exportName = enclosingConstName(call, program);
1589
1793
  registerFlow({
@@ -1621,12 +1825,9 @@ function isOnFlowArgument(flowCall: CallExpression, program: AstNode): boolean {
1621
1825
  }
1622
1826
 
1623
1827
  function unwrapFlowCall(node: AstNode): CallExpression | undefined {
1624
- if (
1625
- node.type === "CallExpression" &&
1626
- identifierName((node as CallExpression).callee) === "flow"
1627
- ) {
1628
- return node as CallExpression;
1629
- }
1828
+ if (node.type !== "CallExpression") return undefined;
1829
+ const name = identifierName((node as CallExpression).callee);
1830
+ if (name === "flow" || name === "call") return node as CallExpression;
1630
1831
  return undefined;
1631
1832
  }
1632
1833
 
@@ -1709,12 +1910,17 @@ function registerFlow(args: {
1709
1910
  }
1710
1911
  if (gates && gates.length > 0) flow.gates = gates;
1711
1912
 
1712
- const inSchema = schemaProp(opts, "in", args.scope, args.file.path);
1913
+ const inSchema =
1914
+ schemaProp(opts, "in", args.scope, args.file.path) ??
1915
+ exposureSchemaProp(trigger?.contract, "in", args.scope, args.file.path);
1713
1916
  if (inSchema !== undefined) flow.in = inSchema;
1714
- const outSchema = schemaProp(opts, "out", args.scope, args.file.path);
1917
+ const outSchema =
1918
+ schemaProp(opts, "out", args.scope, args.file.path) ??
1919
+ exposureSchemaProp(trigger?.contract, "out", args.scope, args.file.path);
1715
1920
  if (outSchema !== undefined) flow.out = outSchema;
1716
1921
 
1717
- const errors = parseErrors(objectProp(opts, "errors"));
1922
+ const errors =
1923
+ parseErrors(objectProp(opts, "errors")) ?? parseErrors(objectProp(trigger?.contract, "errors"));
1718
1924
  if (errors) flow.errors = errors;
1719
1925
 
1720
1926
  if (effects) flow.effects = effects;
@@ -1738,7 +1944,7 @@ function registerFlow(args: {
1738
1944
  };
1739
1945
  }
1740
1946
  }
1741
- if (boolProp(opts, "breaking")) flow.breaking = true;
1947
+ if (boolProp(opts, "breaking") || boolProp(trigger?.contract, "breaking")) flow.breaking = true;
1742
1948
 
1743
1949
  const slo = parseSlo(objectProp(opts, "slo"));
1744
1950
  if (slo) flow.slo = slo;
@@ -2146,6 +2352,8 @@ interface ParsedTrigger {
2146
2352
  live?: string;
2147
2353
  /** Physical table name when `.live(tableBinding)` — manual live surface. */
2148
2354
  liveTable?: string;
2355
+ /** Invoke contract bag authored on `http.*` / `mcp.tool`. */
2356
+ contract?: AstNode;
2149
2357
  }
2150
2358
 
2151
2359
  function parseTrigger(
@@ -2153,13 +2361,22 @@ function parseTrigger(
2153
2361
  scope: ProjectScope,
2154
2362
  filePath?: string,
2155
2363
  ): ParsedTrigger | undefined {
2156
- // every("10m")
2157
2364
  if (node.type === "CallExpression") {
2158
2365
  const call = node as CallExpression;
2159
2366
  const name = identifierName(call.callee);
2160
- if (name === "every") {
2161
- const interval = stringArg(call.arguments[0]);
2162
- if (interval) return { trigger: { every: interval } };
2367
+ if (name === "clock") {
2368
+ const clockName = stringArg(call.arguments[0]);
2369
+ const opts = objectArg(call.arguments[1]);
2370
+ const cron = stringProp(opts, "cron");
2371
+ const every = stringProp(opts, "every");
2372
+ if (cron) return { trigger: { cron } };
2373
+ if (every) return { trigger: { every } };
2374
+ if (clockName) {
2375
+ const clockDef = scope.clocks[clockName];
2376
+ if (clockDef?.cron) return { trigger: { cron: clockDef.cron } };
2377
+ if (clockDef?.every) return { trigger: { every: clockDef.every } };
2378
+ return { trigger: { cron: clockName } };
2379
+ }
2163
2380
  }
2164
2381
  if (name === "internal") {
2165
2382
  return { trigger: {} };
@@ -2173,8 +2390,8 @@ function parseTrigger(
2173
2390
  const http = parseHttpTrigger(call, scope, filePath);
2174
2391
  if (http) return http;
2175
2392
 
2176
- // table("orders").changed("status") / db.table(orders).changed(...)
2177
- const cdc = parseCdcTrigger(call);
2393
+ // db.table(orders).changed(...) / <storeName>.table(orders).changed(...)
2394
+ const cdc = parseCdcTrigger(call, scope);
2178
2395
  if (cdc) return { trigger: { cdc } };
2179
2396
 
2180
2397
  // on(signalHandle, …) — Identifier referring to a signal binding
@@ -2191,6 +2408,18 @@ function parseTrigger(
2191
2408
  const id = (node as Identifier).name;
2192
2409
  if (id === "internal") return { trigger: {} };
2193
2410
  const binding = scope.bindings.get(id);
2411
+ if (binding?.kind === "clock") {
2412
+ const clockDef = scope.clocks[binding.ref];
2413
+ if (clockDef?.cron) return { trigger: { cron: clockDef.cron } };
2414
+ if (clockDef?.every) return { trigger: { every: clockDef.every } };
2415
+ return { trigger: { cron: binding.ref } };
2416
+ }
2417
+ if (scope.clocks[id]) {
2418
+ const clockDef = scope.clocks[id];
2419
+ if (clockDef?.cron) return { trigger: { cron: clockDef.cron } };
2420
+ if (clockDef?.every) return { trigger: { every: clockDef.every } };
2421
+ return { trigger: { cron: id } };
2422
+ }
2194
2423
  if (binding?.kind === "signal") {
2195
2424
  return { trigger: { signal: binding.ref } };
2196
2425
  }
@@ -2237,6 +2466,7 @@ function parseHttpTrigger(
2237
2466
  let live: string | undefined;
2238
2467
  /** Table name when `.live(tableBinding)` — a manual live query surface. */
2239
2468
  let liveTable: string | undefined;
2469
+ let contract: AstNode | undefined;
2240
2470
  const gateNames: string[] = [];
2241
2471
 
2242
2472
  for (const node of chain) {
@@ -2269,7 +2499,14 @@ function parseHttpTrigger(
2269
2499
  continue;
2270
2500
  }
2271
2501
  method = prop.toUpperCase();
2272
- path = stringArg(c.arguments[0]);
2502
+ const pathArg = c.arguments[0];
2503
+ const contractArg = c.arguments[1];
2504
+ if (stringArg(pathArg) !== undefined) {
2505
+ path = stringArg(pathArg);
2506
+ if (isBoundaryContractObject(contractArg)) contract = contractArg;
2507
+ } else if (isBoundaryContractObject(pathArg)) {
2508
+ contract = pathArg;
2509
+ }
2273
2510
  continue;
2274
2511
  }
2275
2512
 
@@ -2323,10 +2560,11 @@ function parseHttpTrigger(
2323
2560
  gates: gateNames.length > 0 ? gateNames : undefined,
2324
2561
  ...(live !== undefined ? { live } : {}),
2325
2562
  ...(liveTable !== undefined ? { liveTable } : {}),
2563
+ ...(contract !== undefined ? { contract } : {}),
2326
2564
  };
2327
2565
  }
2328
2566
 
2329
- function parseCdcTrigger(call: CallExpression): Trigger["cdc"] | undefined {
2567
+ function parseCdcTrigger(call: CallExpression, scope?: ProjectScope): Trigger["cdc"] | undefined {
2330
2568
  // *.changed("col") or *.changed()
2331
2569
  const callee = call.callee;
2332
2570
  if (callee.type !== "MemberExpression") return undefined;
@@ -2334,21 +2572,22 @@ function parseCdcTrigger(call: CallExpression): Trigger["cdc"] | undefined {
2334
2572
  if (identifierName(member.property) !== "changed") return undefined;
2335
2573
 
2336
2574
  const column = stringArg(call.arguments[0]);
2337
- // object is table("orders") or db.table(orders) or table(orders)
2575
+ // object is db.table(orders) or <storeName>.table(orders)
2338
2576
  const obj = member.object;
2339
2577
  if (obj.type === "CallExpression") {
2340
2578
  const inner = obj as CallExpression;
2341
2579
  const innerCallee = inner.callee;
2342
- if (identifierName(innerCallee) === "table") {
2343
- const tableName = stringArg(inner.arguments[0]) ?? identifierName(inner.arguments[0]);
2344
- if (!tableName) return undefined;
2345
- return column ? { table: tableName, column } : { table: tableName };
2346
- }
2347
2580
  if (
2348
2581
  innerCallee.type === "MemberExpression" &&
2349
2582
  identifierName((innerCallee as AstNode & { property: AstNode }).property) === "table"
2350
2583
  ) {
2351
- const tableName = stringArg(inner.arguments[0]) ?? identifierName(inner.arguments[0]);
2584
+ const argId = identifierName(inner.arguments[0]);
2585
+ const resolvedName = argId
2586
+ ? scope?.bindings.get(argId)?.kind === "table"
2587
+ ? scope.bindings.get(argId)?.ref
2588
+ : argId
2589
+ : undefined;
2590
+ const tableName = stringArg(inner.arguments[0]) ?? resolvedName;
2352
2591
  if (!tableName) return undefined;
2353
2592
  return column ? { table: tableName, column } : { table: tableName };
2354
2593
  }
@@ -2368,6 +2607,7 @@ function stripTriggerExtras(trigger: Trigger): Trigger {
2368
2607
  function parseMcpTrigger(leaf: AstNode, scope: ProjectScope): ParsedTrigger | undefined {
2369
2608
  const chain = flattenMemberCallChain(leaf);
2370
2609
  let toolName: string | undefined;
2610
+ let contract: AstNode | undefined;
2371
2611
  const gateNames: string[] = [];
2372
2612
 
2373
2613
  for (const node of chain) {
@@ -2383,6 +2623,7 @@ function parseMcpTrigger(leaf: AstNode, scope: ProjectScope): ParsedTrigger | un
2383
2623
  (member.object as Identifier).name === "mcp"
2384
2624
  ) {
2385
2625
  toolName = stringArg(c.arguments[0]);
2626
+ if (isBoundaryContractObject(c.arguments[1])) contract = c.arguments[1];
2386
2627
  continue;
2387
2628
  }
2388
2629
  if (prop === "gate") {
@@ -2396,6 +2637,7 @@ function parseMcpTrigger(leaf: AstNode, scope: ProjectScope): ParsedTrigger | un
2396
2637
  return {
2397
2638
  trigger: { mcp: { name: toolName } },
2398
2639
  gates: gateNames.length > 0 ? gateNames : undefined,
2640
+ ...(contract !== undefined ? { contract } : {}),
2399
2641
  };
2400
2642
  }
2401
2643
 
@@ -2407,15 +2649,19 @@ function parseEffectsObject(node: AstNode | undefined): Effects | undefined {
2407
2649
  const emits = stringArrayProp(node, "emits");
2408
2650
  const sends = stringArrayProp(node, "sends");
2409
2651
  const asks = stringArrayProp(node, "asks");
2652
+ const embeds = stringArrayProp(node, "embeds");
2410
2653
  const secrets = stringArrayProp(node, "secrets");
2411
2654
  const calls = stringArrayProp(node, "calls");
2655
+ const fetches = stringArrayProp(node, "fetches");
2412
2656
  if (reads) effects.reads = reads as Effects["reads"];
2413
2657
  if (writes) effects.writes = writes as Effects["writes"];
2414
2658
  if (emits) effects.emits = emits;
2415
2659
  if (sends) effects.sends = sends;
2416
2660
  if (asks) effects.asks = asks;
2661
+ if (embeds) effects.embeds = embeds;
2417
2662
  if (secrets) effects.secrets = secrets;
2418
2663
  if (calls) effects.calls = calls;
2664
+ if (fetches) effects.fetches = fetches;
2419
2665
  return effects;
2420
2666
  }
2421
2667
 
@@ -2526,6 +2772,26 @@ function schemaProp(
2526
2772
  return "…";
2527
2773
  }
2528
2774
 
2775
+ /** Read invoke contract fields from an exposure bag (`http.*` / `mcp.tool`). */
2776
+ function exposureSchemaProp(
2777
+ contract: AstNode | undefined,
2778
+ key: string,
2779
+ scope: ProjectScope,
2780
+ filePath: string,
2781
+ ): JsonSchema | undefined {
2782
+ return schemaProp(contract, key, scope, filePath);
2783
+ }
2784
+
2785
+ /** True when an AST node looks like a {@link BoundaryContract} bag (not a path). */
2786
+ function isBoundaryContractObject(node: AstNode | undefined): node is AstNode {
2787
+ if (!node || node.type !== "ObjectExpression") return false;
2788
+ for (const prop of objectProperties(node)) {
2789
+ const key = propKey(prop);
2790
+ if (key === "in" || key === "out" || key === "errors" || key === "breaking") return true;
2791
+ }
2792
+ return false;
2793
+ }
2794
+
2529
2795
  // ── AST helpers ────────────────────────────────────────────────────────────
2530
2796
 
2531
2797
  /** Leaf-to-root walk of `http.get("/x").public().live(signal)`. */
@@ -2648,10 +2914,148 @@ function objectArg(node: AstNode | undefined): AstNode | undefined {
2648
2914
  return undefined;
2649
2915
  }
2650
2916
 
2917
+ /** Shared helper options (`timezone` is runtime-only — Manifest Clock has no zone field). */
2918
+ function clockHelperOptsFromAst(opts: AstNode | undefined): {
2919
+ readonly overridable?: boolean;
2920
+ readonly description?: string;
2921
+ readonly perTenant?: boolean;
2922
+ } {
2923
+ return {
2924
+ ...(boolProp(opts, "overridable") !== undefined
2925
+ ? { overridable: boolProp(opts, "overridable") }
2926
+ : {}),
2927
+ ...(stringProp(opts, "description") ? { description: stringProp(opts, "description") } : {}),
2928
+ ...(boolProp(opts, "perTenant") === true ? { perTenant: true } : {}),
2929
+ };
2930
+ }
2931
+
2932
+ /**
2933
+ * Record a clock into the extract scope + binding map.
2934
+ *
2935
+ * @param scope - Project scope
2936
+ * @param program - AST program
2937
+ * @param call - Declaration call
2938
+ * @param name - Clock name
2939
+ * @param entry - Partial Manifest clock
2940
+ */
2941
+ function recordClockDecl(
2942
+ scope: ProjectScope,
2943
+ program: AstNode,
2944
+ call: CallExpression,
2945
+ name: string,
2946
+ entry: Clock,
2947
+ ): void {
2948
+ scope.clocks[name] = entry;
2949
+ const bindingName = enclosingConstName(call, program);
2950
+ if (bindingName) {
2951
+ scope.bindings.set(bindingName, {
2952
+ kind: "clock",
2953
+ ref: name,
2954
+ });
2955
+ }
2956
+ }
2957
+
2958
+ /**
2959
+ * Read a structured cron field from an AST node (literal or array of literals).
2960
+ *
2961
+ * @param node - Property value
2962
+ */
2963
+ function cronFieldFromAst(node: AstNode | undefined): CronField | undefined {
2964
+ if (!node) return undefined;
2965
+ if (node.type === "Literal") {
2966
+ const v = (node as Literal).value;
2967
+ if (typeof v === "number" || typeof v === "string") return v;
2968
+ return undefined;
2969
+ }
2970
+ if (node.type === "ArrayExpression") {
2971
+ const els = (node as AstNode & { elements?: AstNode[] }).elements ?? [];
2972
+ const out: Array<number | string> = [];
2973
+ for (const el of els) {
2974
+ if (!el || el.type !== "Literal") return undefined;
2975
+ const v = (el as Literal).value;
2976
+ if (typeof v !== "number" && typeof v !== "string") return undefined;
2977
+ out.push(v);
2978
+ }
2979
+ return out;
2980
+ }
2981
+ return undefined;
2982
+ }
2983
+
2984
+ /**
2985
+ * Build a cron expression from a structured fields object literal.
2986
+ *
2987
+ * @param obj - ObjectExpression with CronFields keys
2988
+ */
2989
+ function cronExpressionFromFieldsAst(obj: AstNode): string | undefined {
2990
+ const fields: {
2991
+ minute?: CronField;
2992
+ hour?: CronField;
2993
+ dayOfMonth?: CronField;
2994
+ month?: CronField;
2995
+ dayOfWeek?: CronField;
2996
+ at?: string;
2997
+ } = {};
2998
+ const minute = cronFieldFromAst(objectProp(obj, "minute"));
2999
+ const hour = cronFieldFromAst(objectProp(obj, "hour"));
3000
+ const dayOfMonth = cronFieldFromAst(objectProp(obj, "dayOfMonth"));
3001
+ const month = cronFieldFromAst(objectProp(obj, "month"));
3002
+ const dayOfWeek = cronFieldFromAst(objectProp(obj, "dayOfWeek"));
3003
+ const at = stringProp(obj, "at");
3004
+ if (minute !== undefined) fields.minute = minute;
3005
+ if (hour !== undefined) fields.hour = hour;
3006
+ if (dayOfMonth !== undefined) fields.dayOfMonth = dayOfMonth;
3007
+ if (month !== undefined) fields.month = month;
3008
+ if (dayOfWeek !== undefined) fields.dayOfWeek = dayOfWeek;
3009
+ if (at !== undefined) fields.at = at;
3010
+ try {
3011
+ return buildCronExpression(fields);
3012
+ } catch {
3013
+ return undefined;
3014
+ }
3015
+ }
3016
+
3017
+ /**
3018
+ * Compile a preset helper (`daily` / `hourly` / `weekly` / `monthly`) to cron.
3019
+ *
3020
+ * @param prop - Helper name
3021
+ * @param opts - Options object literal
3022
+ */
3023
+ function cronFromPresetHelper(
3024
+ prop: "daily" | "hourly" | "weekly" | "monthly",
3025
+ opts: AstNode | undefined,
3026
+ ): string | undefined {
3027
+ try {
3028
+ if (prop === "daily") {
3029
+ return buildCronExpression({ at: stringProp(opts, "at") ?? "00:00" });
3030
+ }
3031
+ if (prop === "hourly") {
3032
+ const minute = numberProp(opts, "minute") ?? 0;
3033
+ return buildCronExpression({ minute });
3034
+ }
3035
+ if (prop === "weekly") {
3036
+ const on = cronFieldFromAst(objectProp(opts, "on"));
3037
+ if (on === undefined) return undefined;
3038
+ return buildCronExpression({
3039
+ at: stringProp(opts, "at") ?? "00:00",
3040
+ dayOfWeek: on,
3041
+ });
3042
+ }
3043
+ const on = cronFieldFromAst(objectProp(opts, "on"));
3044
+ if (on === undefined) return undefined;
3045
+ return buildCronExpression({
3046
+ at: stringProp(opts, "at") ?? "00:00",
3047
+ dayOfMonth: on,
3048
+ });
3049
+ } catch {
3050
+ return undefined;
3051
+ }
3052
+ }
3053
+
2651
3054
  /**
2652
- * `flow("name", { do })` options live in arg1; `flow({ do })` in arg0.
3055
+ * `flow("name", { do })` / `call("name", { do })` options live in arg1;
3056
+ * `flow({ do })` in arg0.
2653
3057
  *
2654
- * @param flowCall - `flow(...)` AST node
3058
+ * @param flowCall - `flow(...)` / `call(...)` AST node
2655
3059
  */
2656
3060
  function flowOptionsArg(flowCall: CallExpression): AstNode | undefined {
2657
3061
  const named = objectArg(flowCall.arguments[1]);
@@ -2787,6 +3191,54 @@ function applyOkeStoreLiveConfig(opts: AstNode | undefined, scope: ProjectScope)
2787
3191
  scope.store = { ...scope.store, live: true };
2788
3192
  }
2789
3193
 
3194
+ /**
3195
+ * Stamp project default for field `.embed()` from
3196
+ * `oke({ store: { search: { embed: { model, dims } } } })`.
3197
+ *
3198
+ * @param opts - `oke({…})` options object
3199
+ * @param scope - Project scope
3200
+ */
3201
+ function applyOkeStoreSearchConfig(opts: AstNode | undefined, scope: ProjectScope): void {
3202
+ const store = objectProp(opts, "store");
3203
+ const search = objectProp(store, "search");
3204
+ const embed = objectProp(search, "embed");
3205
+ if (!embed) return;
3206
+ const dims = numberProp(embed, "dims");
3207
+ if (dims === undefined || !(Number.isInteger(dims) && dims > 0)) {
3208
+ throw new Error(
3209
+ `extract: oke({ store: { search: { embed } } }) requires dims: positive integer`,
3210
+ );
3211
+ }
3212
+ const modelNode = objectProp(embed, "model");
3213
+ let model: string | undefined = stringArg(modelNode);
3214
+ if (!model && modelNode) {
3215
+ const bindingName = identifierName(modelNode);
3216
+ if (bindingName) {
3217
+ const binding = scope.bindings.get(bindingName);
3218
+ model = binding?.ref ?? bindingName;
3219
+ } else if (modelNode.type === "MemberExpression") {
3220
+ const prop = identifierName((modelNode as AstNode & { property: AstNode }).property);
3221
+ if (prop === "name") {
3222
+ const obj = (modelNode as AstNode & { object: AstNode }).object;
3223
+ const objName = identifierName(obj);
3224
+ if (objName) {
3225
+ const binding = scope.bindings.get(objName);
3226
+ model = binding?.ref ?? objName;
3227
+ }
3228
+ }
3229
+ }
3230
+ }
3231
+ if (!model) {
3232
+ throw new Error(
3233
+ `extract: oke({ store: { search: { embed } } }) requires model (ai.model handle or name string)`,
3234
+ );
3235
+ }
3236
+ scope.store = {
3237
+ ...scope.store,
3238
+ search: { embed: { model, dims } },
3239
+ };
3240
+ }
3241
+
2790
3242
  /**
2791
3243
  * Default every SQL table to live when `oke({ store: { live: true } })` is
2792
3244
  * on, unless the table explicitly opted out with `store.schema.live(false)`.
@@ -2881,6 +3333,72 @@ function assertTenantSafeSchema(manifest: Manifest): void {
2881
3333
  }
2882
3334
  }
2883
3335
 
3336
+ /**
3337
+ * Inherit `oke({ store: { search: { embed } } })` onto every column that
3338
+ * called `.embed()`, then stamp concrete `{ model, dims }` on the Manifest.
3339
+ * Per-field values win when set.
3340
+ */
3341
+ function resolveSearchEmbedDefaults(manifest: Manifest): void {
3342
+ const project = manifest.store?.search?.embed;
3343
+ for (const store of Object.values(manifest.stores ?? {})) {
3344
+ if (store.facet !== "sql" || !store.tables) continue;
3345
+ for (const [tableName, table] of Object.entries(store.tables)) {
3346
+ for (const [colName, col] of Object.entries(table.columns ?? {})) {
3347
+ if (!col || typeof col !== "object") continue;
3348
+ const declared = col as DeclaredColumn;
3349
+ // `embed: {}` is stamped when `.embed()` was called with no options.
3350
+ if (declared.embed === undefined) continue;
3351
+ const model = declared.embed.model ?? project?.model;
3352
+ const dims = declared.embed.dims ?? project?.dims;
3353
+ if (!model || dims === undefined || !(Number.isInteger(dims) && dims > 0)) {
3354
+ throw new SearchConfigError(
3355
+ tableName,
3356
+ colName,
3357
+ `.embed() needs model and dims — set oke({ store: { search: { embed: { model, dims } } } }) or pass them on .embed({ model, dims })`,
3358
+ );
3359
+ }
3360
+ declared.embed = { model, dims };
3361
+ }
3362
+ }
3363
+ }
3364
+ }
3365
+
3366
+ /**
3367
+ * Fail-loud hybrid-search config: `.embed()` requires an `ai` element;
3368
+ * `.embed()` without `searchable` is already rejected in parseFieldChain.
3369
+ * Call after {@link resolveSearchEmbedDefaults} so columns carry concrete dims.
3370
+ */
3371
+ function assertSearchConfig(manifest: Manifest): void {
3372
+ const hasAi =
3373
+ manifest.ai !== undefined &&
3374
+ (Object.keys(manifest.ai.models ?? {}).length > 0 ||
3375
+ Object.keys(manifest.ai.prompts ?? {}).length > 0 ||
3376
+ Object.keys(manifest.ai.agents ?? {}).length > 0);
3377
+ for (const store of Object.values(manifest.stores ?? {})) {
3378
+ if (store.facet !== "sql" || !store.tables) continue;
3379
+ for (const [tableName, table] of Object.entries(store.tables)) {
3380
+ for (const [colName, col] of Object.entries(table.columns ?? {})) {
3381
+ if (!col || typeof col !== "object") continue;
3382
+ const declared = col as DeclaredColumn;
3383
+ if (declared.embed && !hasAi) {
3384
+ throw new SearchConfigError(
3385
+ tableName,
3386
+ colName,
3387
+ `.embed() requires a configured ai element (ai.model / ai.embed). Remove .embed() for BM25-only search, or declare an embedding model.`,
3388
+ );
3389
+ }
3390
+ if (declared.embed && !declared.searchable) {
3391
+ throw new SearchConfigError(
3392
+ tableName,
3393
+ colName,
3394
+ `.embed() requires a prior .searchable() on the same field`,
3395
+ );
3396
+ }
3397
+ }
3398
+ }
3399
+ }
3400
+ }
3401
+
2884
3402
  function sortRecord<T>(record: Record<string, T>): Record<string, T> {
2885
3403
  const out: Record<string, T> = {};
2886
3404
  for (const key of Object.keys(record).sort()) {