okengine 0.18.5 → 0.19.1

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 (512) 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/skills.mdx +4 -1
  6. package/site/content/docs/client/auth.mdx +208 -0
  7. package/site/content/docs/client/calling.mdx +451 -0
  8. package/site/content/docs/client/index.mdx +208 -0
  9. package/site/content/docs/client/live.mdx +234 -0
  10. package/site/content/docs/client/meta.json +5 -0
  11. package/site/content/docs/client/react.mdx +249 -0
  12. package/site/content/docs/elements/ai/agents.mdx +235 -0
  13. package/site/content/docs/elements/ai/index.mdx +342 -0
  14. package/site/content/docs/elements/ai/mcp.mdx +275 -0
  15. package/site/content/docs/elements/ai/meta.json +5 -0
  16. package/site/content/docs/elements/ai/models.mdx +283 -0
  17. package/site/content/docs/elements/ai/prompts.mdx +257 -0
  18. package/site/content/docs/elements/channel/email.mdx +269 -0
  19. package/site/content/docs/elements/channel/index.mdx +344 -0
  20. package/site/content/docs/elements/channel/meta.json +5 -0
  21. package/site/content/docs/elements/channel/push.mdx +218 -0
  22. package/site/content/docs/elements/channel/receipts.mdx +206 -0
  23. package/site/content/docs/elements/channel/sms.mdx +264 -0
  24. package/site/content/docs/elements/channel/whatsapp.mdx +220 -0
  25. package/site/content/docs/elements/clock/index.mdx +301 -0
  26. package/site/content/docs/elements/clock/meta.json +5 -0
  27. package/site/content/docs/elements/clock/schedules.mdx +508 -0
  28. package/site/content/docs/elements/clock/sleep.mdx +448 -0
  29. package/site/content/docs/elements/flow/consumers.mdx +707 -0
  30. package/site/content/docs/elements/flow/http.mdx +1080 -0
  31. package/site/content/docs/elements/flow/index.mdx +618 -0
  32. package/site/content/docs/elements/flow/meta.json +5 -0
  33. package/site/content/docs/elements/flow/routing.mdx +559 -0
  34. package/site/content/docs/elements/flow/workflows.mdx +640 -0
  35. package/site/content/docs/elements/gate/auth.mdx +371 -0
  36. package/site/content/docs/elements/gate/authorization.mdx +298 -0
  37. package/site/content/docs/elements/gate/index.mdx +418 -0
  38. package/site/content/docs/elements/gate/meta.json +5 -0
  39. package/site/content/docs/elements/gate/rate-limits.mdx +365 -0
  40. package/site/content/docs/elements/gate/rls.mdx +303 -0
  41. package/site/content/docs/elements/gate/tenancy.mdx +375 -0
  42. package/site/content/docs/elements/index.mdx +20 -10
  43. package/site/content/docs/elements/signal/broadcast.mdx +519 -0
  44. package/site/content/docs/elements/signal/index.mdx +352 -0
  45. package/site/content/docs/elements/signal/live.mdx +591 -0
  46. package/site/content/docs/elements/signal/meta.json +5 -0
  47. package/site/content/docs/elements/signal/once.mdx +597 -0
  48. package/site/content/docs/elements/store/files.mdx +667 -0
  49. package/site/content/docs/elements/store/index.mdx +313 -0
  50. package/site/content/docs/elements/store/kv.mdx +637 -0
  51. package/site/content/docs/elements/store/meta.json +5 -0
  52. package/site/content/docs/elements/store/search.mdx +950 -0
  53. package/site/content/docs/elements/store/sql.mdx +941 -0
  54. package/site/content/docs/elements/vault/config.mdx +259 -0
  55. package/site/content/docs/elements/vault/index.mdx +356 -0
  56. package/site/content/docs/elements/vault/meta.json +5 -0
  57. package/site/content/docs/elements/vault/rotation.mdx +276 -0
  58. package/site/content/docs/elements/vault/secrets.mdx +288 -0
  59. package/site/content/docs/index.mdx +52 -40
  60. package/site/content/docs/meta.json +8 -7
  61. package/site/content/docs/plugins/cors.mdx +2 -1
  62. package/site/content/docs/plugins/csrf.mdx +4 -1
  63. package/site/content/docs/plugins/headers.mdx +2 -1
  64. package/site/content/docs/plugins/ip-allowlist.mdx +2 -1
  65. package/site/content/docs/plugins/magic-link.mdx +6 -0
  66. package/site/content/docs/plugins/maintenance-mode.mdx +2 -1
  67. package/site/content/docs/plugins/oauth.mdx +1 -1
  68. package/site/content/docs/plugins/otp.mdx +12 -1
  69. package/site/content/docs/plugins/passkey.mdx +39 -15
  70. package/site/content/docs/plugins/two-factor.mdx +91 -34
  71. package/site/content/docs/plugins/username.mdx +1 -1
  72. package/site/content/docs/recipes/caddy.mdx +3 -3
  73. package/site/content/docs/recipes/index.mdx +18 -28
  74. package/site/content/docs/recipes/mailpit.mdx +1 -1
  75. package/site/content/docs/recipes/meilisearch.mdx +3 -3
  76. package/site/content/docs/recipes/meta.json +1 -4
  77. package/site/content/docs/recipes/nginx.mdx +2 -2
  78. package/site/content/docs/recipes/openrouter.mdx +283 -0
  79. package/site/content/docs/recipes/pgdog.mdx +3 -3
  80. package/site/content/docs/recipes/postgres.mdx +1 -2
  81. package/site/content/docs/recipes/rustfs.mdx +2 -2
  82. package/site/content/docs/recipes/traefik.mdx +3 -3
  83. package/site/content/docs/reference/cli.mdx +231 -0
  84. package/site/content/docs/reference/configuration.mdx +30 -10
  85. package/site/content/docs/reference/environment-variables.mdx +93 -20
  86. package/site/content/docs/reference/errors.mdx +132 -29
  87. package/site/content/docs/reference/fx.mdx +138 -73
  88. package/site/content/docs/reference/index.mdx +13 -21
  89. package/site/content/docs/reference/meta.json +4 -5
  90. package/site/content/docs/reference/okid.mdx +42 -17
  91. package/site/content/docs/reference/plugins.mdx +4 -3
  92. package/site/content/docs/reference/security.mdx +197 -0
  93. package/site/content/docs/understand/meta.json +5 -0
  94. package/site/content/docs/understand/the-architecture.mdx +264 -0
  95. package/site/content/docs/understand/try-it.mdx +58 -0
  96. package/src/auth/api-keys.ts +2 -1
  97. package/src/auth/bindings.ts +64 -16
  98. package/src/auth/gate-auth.test.ts +6 -1
  99. package/src/auth/identity.ts +152 -5
  100. package/src/auth/index.ts +29 -0
  101. package/src/auth/invites.ts +2 -1
  102. package/src/auth/method-context.ts +41 -0
  103. package/src/auth/oauth-as/crypto.ts +2 -1
  104. package/src/auth/oauth-as/stores.ts +2 -1
  105. package/src/auth/operator.ts +2 -1
  106. package/src/auth/sessions-jwt.test.ts +77 -0
  107. package/src/auth/sessions.ts +20 -4
  108. package/src/auth/tenants.ts +3 -2
  109. package/src/auth/two-factor-challenge.test.ts +61 -0
  110. package/src/auth/two-factor-challenge.ts +284 -0
  111. package/src/auth/verification.ts +5 -0
  112. package/src/bench/REPORT.md +80 -0
  113. package/src/bench/g03-signal-once.bench.ts +1 -1
  114. package/src/bench/g10-observability-contention.bench.ts +1 -1
  115. package/src/bench/g17-hybrid-search.bench.ts +572 -0
  116. package/src/bench/load-app.ts +2 -10
  117. package/src/cli/ai-setup/ai-setup.test.ts +331 -42
  118. package/src/cli/ai-setup/apply.ts +294 -52
  119. package/src/cli/ai-setup/catalog.ts +238 -1342
  120. package/src/cli/ai-setup/index.ts +14 -99
  121. package/src/cli/ai-setup/prompts.ts +60 -443
  122. package/src/cli/ask-seed.test.ts +96 -0
  123. package/src/cli/ask-seed.ts +82 -0
  124. package/src/cli/ask-vault-gaps.test.ts +84 -0
  125. package/src/cli/ask-vault-gaps.ts +146 -0
  126. package/src/cli/build.ts +2 -2
  127. package/src/cli/client-add.test.ts +26 -1
  128. package/src/cli/client-add.ts +106 -27
  129. package/src/cli/db-seed.ts +2 -0
  130. package/src/cli/db.ts +150 -11
  131. package/src/cli/dev.ts +119 -211
  132. package/src/cli/docker-clean.ts +2 -2
  133. package/src/cli/docker-cli.test.ts +1 -1
  134. package/src/cli/doctor-diff.ts +4 -2
  135. package/src/cli/doctor-pii.test.ts +1 -1
  136. package/src/cli/doctor.ts +14 -1
  137. package/src/cli/hero-meta.test.ts +4 -2
  138. package/src/cli/load-config.images.test.ts +8 -10
  139. package/src/cli/load-config.ts +1 -1
  140. package/src/cli/registry.ts +22 -10
  141. package/src/cli/replay.ts +3 -1
  142. package/src/cli/start.ts +1 -1
  143. package/src/cli/tui/keys.ts +2 -3
  144. package/src/client/auth/cookies.ts +74 -0
  145. package/src/client/auth/create-auth-client.ts +636 -0
  146. package/src/client/auth/denials.ts +99 -0
  147. package/src/client/auth/session.ts +283 -0
  148. package/src/client/auth.test.ts +251 -0
  149. package/src/client/auth.ts +39 -114
  150. package/src/client/create-with-session.ts +245 -0
  151. package/src/client/create.ts +51 -5
  152. package/src/client/index.ts +11 -1
  153. package/src/client/live.ts +11 -79
  154. package/src/client/notes-contract.test.ts +5 -10
  155. package/src/client/sse.ts +137 -0
  156. package/src/client/stream.ts +147 -0
  157. package/src/client/transport.test.ts +32 -0
  158. package/src/client/transport.ts +123 -26
  159. package/src/client/types.ts +118 -14
  160. package/src/client-react/index.ts +185 -24
  161. package/src/client-react/use-live-query.ts +13 -3
  162. package/src/compiler/aot.test.ts +7 -4
  163. package/src/compiler/effects-embed.test.ts +56 -0
  164. package/src/compiler/effects-fetch.test.ts +43 -0
  165. package/src/compiler/effects-infer.ts +58 -2
  166. package/src/compiler/extract.test.ts +257 -43
  167. package/src/compiler/extract.ts +566 -48
  168. package/src/compiler/fixtures/skyport/src/flows/bookings/index.ts +7 -4
  169. package/src/compiler/fixtures/skyport/src/flows/bookings/signals.ts +2 -8
  170. package/src/compiler/fixtures/skyport.expected.json +4 -4
  171. package/src/compiler/fixtures/triggers/five-triggers.ts +14 -10
  172. package/src/compiler/response.ts +2 -2
  173. package/src/compiler/search-writer-isolation.test.ts +40 -0
  174. package/src/config/index.ts +11 -0
  175. package/src/console/server/app.ts +17 -23
  176. package/src/console/server/bind.ts +4 -0
  177. package/src/console/server/console.test.ts +2 -2
  178. package/src/console/server/flows-invoke.test.ts +50 -34
  179. package/src/console/server/flows.ts +436 -255
  180. package/src/console/server/runs-ingest.test.ts +6 -3
  181. package/src/console/server/serve.ts +1 -1
  182. package/src/console/server/signals.test.ts +1 -5
  183. package/src/console/server/signals.ts +8 -6
  184. package/src/console/server/state.ts +6 -1
  185. package/src/console/server/store.ts +1 -1
  186. package/src/console/ui-next/dist/assets/FileExportIcon-Ck-5od4R.js +1 -0
  187. package/src/console/ui-next/dist/assets/MoreHorizontalCircle01Icon-gMNGsE37.js +1 -0
  188. package/src/console/ui-next/dist/assets/PlusSignIcon-CwG3nxfu.js +1 -0
  189. package/src/console/ui-next/dist/assets/UnavailableIcon-D9cvHVPr.js +1 -0
  190. package/src/console/ui-next/dist/assets/UserIcon-DaE2PB5_.js +1 -0
  191. package/src/console/ui-next/dist/assets/access-page-C_qLDhTq.js +4 -0
  192. package/src/console/ui-next/dist/assets/agent-disclosure-BHVqr3TN.js +1 -0
  193. package/src/console/ui-next/dist/assets/cache-glyph-CKe92lRQ.js +1 -0
  194. package/src/console/ui-next/dist/assets/call-pii-button-DEwTl8ZX.js +1 -0
  195. package/src/console/ui-next/dist/assets/collapsible-BCBtDrCt.js +1 -0
  196. package/src/console/ui-next/dist/assets/copy-inline-button-CAYD18cr.js +1 -0
  197. package/src/console/ui-next/dist/assets/dagre.esm-B1_XeuLP.js +1 -0
  198. package/src/console/ui-next/dist/assets/detail-header-DVWNjWjg.js +1 -0
  199. package/src/console/ui-next/dist/assets/dropdown-menu-4h2LOVXM.js +1 -0
  200. package/src/console/ui-next/dist/assets/duration-tone-JroqeuCp.js +9 -0
  201. package/src/console/ui-next/dist/assets/element-icons-BI8cJgdh.js +1 -0
  202. package/src/console/ui-next/dist/assets/explorer-empty-CJs5A-wm.js +1 -0
  203. package/src/console/ui-next/dist/assets/flows-page-CVHa0RTt.js +1 -0
  204. package/src/console/ui-next/dist/assets/highlighted-json-DjJW6hqe.js +154 -0
  205. package/src/console/ui-next/dist/assets/http-method-DC5HBdLU.js +1 -0
  206. package/src/console/ui-next/dist/assets/index-CYjiZ3WO.js +63 -0
  207. package/src/console/ui-next/dist/assets/index-D0zS5rKO.css +2 -0
  208. package/src/console/ui-next/dist/assets/observability-page-CAYMyKb3.js +4 -0
  209. package/src/console/ui-next/dist/assets/react-dom-Ddte4I-Q.js +9 -0
  210. package/src/console/ui-next/dist/assets/replica-lag-yAQYLv75.js +18 -0
  211. package/src/console/ui-next/dist/assets/request-meta-D0yusGxJ.js +1 -0
  212. package/src/console/ui-next/dist/assets/shortcut-keys-3ILd8oGn.js +1 -0
  213. package/src/console/ui-next/dist/assets/{sql-BskegiFM.js → sql-BsFa4tDR.js} +1 -1
  214. package/src/console/ui-next/dist/assets/store-page-BTKJeJ02.js +41 -0
  215. package/src/console/ui-next/dist/assets/trace-detail-sheet-Bp-Yygs5.js +2 -0
  216. package/src/console/ui-next/dist/assets/tree-expand-toggle-DoaVDfAM.js +55 -0
  217. package/src/console/ui-next/dist/assets/units-page-BRz7xyYL.js +1 -0
  218. package/src/console/ui-next/dist/assets/{use-vault-list-uk4WVboC.js → use-vault-list-CT4-gajj.js} +1 -1
  219. package/src/console/ui-next/dist/assets/vault-page-3jQt-bOJ.js +2 -0
  220. package/src/console/ui-next/dist/assets/xyflow-CSyC6ryz.css +1 -0
  221. package/src/console/ui-next/dist/assets/xyflow-yApv7D4e.js +7 -0
  222. package/src/console/ui-next/dist/index.html +6 -13
  223. package/src/console/ui-next/seed-invoke-host.ts +21 -18
  224. package/src/console/ui-next/src/client.ts +7 -2
  225. package/src/console/ui-next/src/features/flows/graph/element-map.test.ts +1 -1
  226. package/src/console/ui-next/src/features/flows/graph/element-map.ts +5 -3
  227. package/src/console/ui-next/src/features/flows/traces/effect-kind.ts +12 -2
  228. package/src/console/ui-next/src/features/flows/traces/effect-summary.ts +4 -0
  229. package/src/console/ui-next/src/features/flows/traces/trace-detail-sheet.tsx +8 -0
  230. package/src/console/ui-next/src/features/flows/traces/trace-detail.test.ts +4 -5
  231. package/src/console/ui-next/src/features/flows/traces/trace-gates.ts +3 -6
  232. package/src/console/ui-next/src/features/flows/traces/waterfall-bars.ts +4 -0
  233. package/src/console/ui-next/src/features/flows/traces/waterfall-tooltip.ts +9 -3
  234. package/src/console/ui-next/src/features/store/detail/reveal-cell.tsx +7 -11
  235. package/src/console/ui-next/src/features/store/detail/store-row-detail-sheet.tsx +2 -2
  236. package/src/console/ui-next/src/features/store/lib/fields-from-table.ts +2 -0
  237. package/src/console/ui-next/src/features/store/lib/grid-model.test.ts +25 -1
  238. package/src/console/ui-next/src/features/store/lib/grid-model.ts +36 -1
  239. package/src/console/ui-next/src/features/store/query/query-results.tsx +3 -3
  240. package/src/console/ui-next/src/features/units/detail/effects-summary.tsx +2 -0
  241. package/src/console/ui-next/src/features/units/lib/call-read-safe.ts +3 -1
  242. package/src/console/ui-next/src/features/units/lib/fields-from-schema.ts +6 -0
  243. package/src/docker/ai-model-status.test.ts +1 -12
  244. package/src/docker/ai-model-status.ts +10 -38
  245. package/src/docker/compose-up.test.ts +100 -0
  246. package/src/docker/compose-up.ts +292 -0
  247. package/src/docker/compose.ts +4 -14
  248. package/src/docker/derive.ts +9 -34
  249. package/src/docker/docker.test.ts +16 -261
  250. package/src/docker/images-config.test.ts +13 -25
  251. package/src/docker/index.ts +1 -24
  252. package/src/docker/recipes/index.ts +0 -23
  253. package/src/docker/recipes/pgdog.ts +6 -4
  254. package/src/docker/stack-id.test.ts +2 -2
  255. package/src/docker/stack-id.ts +0 -1
  256. package/src/docker/types.ts +2 -2
  257. package/src/drivers/ai-anthropic.ts +10 -0
  258. package/src/drivers/ai-openai-compatible.ts +34 -7
  259. package/src/drivers/ai-providers.test.ts +0 -107
  260. package/src/drivers/ai-stream.test.ts +0 -34
  261. package/src/drivers/ai-types.ts +17 -8
  262. package/src/drivers/channel-fcm.ts +6 -1
  263. package/src/drivers/channel-msegat.ts +6 -1
  264. package/src/drivers/channel-resend.ts +5 -1
  265. package/src/drivers/channel-smtp.ts +5 -1
  266. package/src/drivers/channel-sndr.ts +10 -1
  267. package/src/drivers/channel-taqnyat-mail.ts +5 -1
  268. package/src/drivers/channel-taqnyat-whatsapp.ts +6 -1
  269. package/src/drivers/channel-taqnyat.ts +6 -1
  270. package/src/drivers/channel-types.ts +10 -0
  271. package/src/drivers/channel-unifonic.ts +6 -1
  272. package/src/drivers/channel-wa-cloud.ts +6 -1
  273. package/src/drivers/channel-webpush.ts +6 -1
  274. package/src/drivers/external.ts +33 -0
  275. package/src/drivers/index.ts +0 -10
  276. package/src/drivers/meilisearch.ts +6 -0
  277. package/src/drivers/oauth-types.ts +4 -0
  278. package/src/drivers/postgres.test.ts +74 -3
  279. package/src/drivers/postgres.ts +175 -6
  280. package/src/drivers/signal-types.ts +5 -5
  281. package/src/drivers/types.ts +7 -0
  282. package/src/drivers/vault-types.ts +5 -0
  283. package/src/elements/ai/declare.ts +17 -4
  284. package/src/elements/ai/eval.ts +1 -1
  285. package/src/elements/ai/mcp-http.ts +1 -1
  286. package/src/elements/ai/pii.ts +1 -1
  287. package/src/elements/ai/providers.test.ts +289 -0
  288. package/src/elements/ai/providers.ts +169 -0
  289. package/src/elements/ai/runtime.ts +68 -5
  290. package/src/elements/ai/schema.ts +2 -2
  291. package/src/elements/ai.test.ts +7 -7
  292. package/src/elements/ai.ts +17 -1
  293. package/src/elements/channel/runtime.ts +105 -12
  294. package/src/elements/clock/cron-fields.test.ts +144 -0
  295. package/src/elements/clock/cron-fields.ts +185 -0
  296. package/src/elements/clock/declare.ts +247 -3
  297. package/src/elements/clock.ts +21 -1
  298. package/src/elements/gate/declare.ts +1 -1
  299. package/src/elements/gate.ts +1 -1
  300. package/src/elements/index.ts +15 -0
  301. package/src/elements/signal/chaos-child.ts +1 -2
  302. package/src/elements/signal/declare.ts +65 -37
  303. package/src/elements/signal/delivery-modes.test.ts +11 -31
  304. package/src/elements/signal/dry-run-replay.test.ts +1 -7
  305. package/src/elements/signal/dry-run-write-isolation.test.ts +1 -7
  306. package/src/elements/signal/key-ordering.test.ts +6 -26
  307. package/src/elements/signal/lease-reclaim.test.ts +2 -6
  308. package/src/elements/signal/optional-emit.test.ts +6 -9
  309. package/src/elements/signal/order-lifecycle.test.ts +4 -17
  310. package/src/elements/signal/orphan-messages.test.ts +4 -15
  311. package/src/elements/signal/reconcile.test.ts +2 -2
  312. package/src/elements/signal/runtime.ts +1 -1
  313. package/src/elements/signal/schema-emit.test.ts +4 -8
  314. package/src/elements/signal.test.ts +24 -49
  315. package/src/elements/signal.ts +9 -2
  316. package/src/elements/store/domain-ddl.test.ts +1 -1
  317. package/src/elements/store/live-http.test.ts +9 -4
  318. package/src/elements/store/prepare-row.test.ts +123 -0
  319. package/src/elements/store/resource.ts +43 -12
  320. package/src/elements/store/schema-decl.ts +117 -0
  321. package/src/elements/store/search-backfill.ts +205 -0
  322. package/src/elements/store/search-bind.ts +66 -0
  323. package/src/elements/store/search-bm25.ts +78 -0
  324. package/src/elements/store/search-ddl.ts +156 -0
  325. package/src/elements/store/search-embed-flow.ts +141 -0
  326. package/src/elements/store/search-errors.ts +34 -0
  327. package/src/elements/store/search-fusion.ts +112 -0
  328. package/src/elements/store/search-lsh.ts +214 -0
  329. package/src/elements/store/search-runtime.pglite.test.ts +186 -0
  330. package/src/elements/store/search-runtime.ts +314 -0
  331. package/src/elements/store/search.test.ts +226 -0
  332. package/src/elements/store/sql-session.test.ts +1 -0
  333. package/src/elements/store/sql-session.ts +83 -3
  334. package/src/elements/store/table.ts +23 -2
  335. package/src/elements/store/upsert-app.test.ts +1 -3
  336. package/src/elements/store.ts +52 -0
  337. package/src/full.ts +4 -2
  338. package/src/http.ts +4 -1
  339. package/src/i18n/catalogs/ar.ts +14 -6
  340. package/src/i18n/catalogs/en.ts +14 -6
  341. package/src/index.ts +6 -1
  342. package/src/kernel/adopt-barrel-fresh.test.ts +7 -7
  343. package/src/kernel/adopt-routes.ts +9 -1
  344. package/src/kernel/app-auth.ts +5 -0
  345. package/src/kernel/app.ts +102 -9
  346. package/src/kernel/auto-cache.test.ts +6 -12
  347. package/src/kernel/auto-registry.test.ts +3 -3
  348. package/src/kernel/boot-bind/ai.test.ts +22 -19
  349. package/src/kernel/boot-bind/ai.ts +5 -43
  350. package/src/kernel/boot-bind/clock.ts +23 -5
  351. package/src/kernel/boot-bind/honor-config.test.ts +4 -4
  352. package/src/kernel/boot.test.ts +11 -6
  353. package/src/kernel/boot.ts +66 -16
  354. package/src/kernel/boundary-contract.ts +93 -0
  355. package/src/kernel/budget.test.ts +1 -1
  356. package/src/kernel/call.ts +89 -0
  357. package/src/kernel/capability.ts +6 -0
  358. package/src/kernel/client-descriptor.ts +123 -0
  359. package/src/kernel/clock-timezone.test.ts +80 -0
  360. package/src/kernel/correlation.test.ts +1 -1
  361. package/src/kernel/dry-run.ts +11 -8
  362. package/src/kernel/effects-stamping.test.ts +42 -9
  363. package/src/kernel/effects.test.ts +4 -2
  364. package/src/kernel/effects.ts +51 -9
  365. package/src/kernel/errors-channel.ts +17 -0
  366. package/src/kernel/errors-live-resume.ts +3 -2
  367. package/src/kernel/errors-tenant.ts +7 -4
  368. package/src/kernel/errors.registry-helpers.ts +96 -0
  369. package/src/kernel/errors.registry.test.ts +139 -25
  370. package/src/kernel/errors.ts +102 -23
  371. package/src/kernel/external-effects.test.ts +196 -0
  372. package/src/kernel/flow.test.ts +13 -4
  373. package/src/kernel/flow.ts +69 -71
  374. package/src/kernel/fx-ask-telemetry.test.ts +2 -2
  375. package/src/kernel/fx-dead-letters.test.ts +3 -3
  376. package/src/kernel/fx-fetch.ts +55 -0
  377. package/src/kernel/fx-live-stream.ts +2 -2
  378. package/src/kernel/fx-live.test.ts +8 -8
  379. package/src/kernel/fx.test.ts +17 -11
  380. package/src/kernel/fx.ts +180 -57
  381. package/src/kernel/horizontal-child.ts +1 -1
  382. package/src/kernel/http-query.test.ts +3 -6
  383. package/src/kernel/index.ts +13 -3
  384. package/src/kernel/instance-id.ts +1 -1
  385. package/src/kernel/instances.test.ts +40 -0
  386. package/src/kernel/instances.ts +47 -9
  387. package/src/kernel/live-http.test.ts +6 -7
  388. package/src/kernel/live-http.ts +6 -2
  389. package/src/kernel/live-resume.test.ts +1 -1
  390. package/src/kernel/mcp-tool.test.ts +3 -3
  391. package/src/kernel/on.ts +98 -4
  392. package/src/kernel/pipeline.test.ts +12 -15
  393. package/src/kernel/plugin-elements.test.ts +1 -1
  394. package/src/kernel/stamp-http.test.ts +3 -3
  395. package/src/kernel/triggers.ts +210 -95
  396. package/src/kernel-entry.ts +0 -1
  397. package/src/manifest/diff.ts +11 -1
  398. package/src/manifest/fixtures/skyport.excerpt.json +1 -1
  399. package/src/manifest/types.ts +46 -2
  400. package/src/mcp/docs-index.ts +3 -3
  401. package/src/mcp/docs-mcp.test.ts +2 -2
  402. package/src/mcp/docs-tools.ts +2 -1
  403. package/src/okid.test.ts +56 -0
  404. package/src/okid.ts +55 -11
  405. package/src/plugins/anonymous.ts +8 -4
  406. package/src/plugins/auth/shared.ts +27 -3
  407. package/src/plugins/auth-delivery.mailpit.integration.test.ts +1 -1
  408. package/src/plugins/auth-methods.security.test.ts +561 -23
  409. package/src/plugins/config-source.ts +1 -1
  410. package/src/plugins/csrf.test.ts +65 -0
  411. package/src/plugins/index.ts +2 -0
  412. package/src/plugins/magic-link.ts +36 -16
  413. package/src/plugins/oauth/shared.ts +3 -1
  414. package/src/plugins/oauth.ts +22 -14
  415. package/src/plugins/otp.ts +37 -21
  416. package/src/plugins/passkey-webauthn.ts +9 -6
  417. package/src/plugins/passkey.ts +98 -26
  418. package/src/plugins/pre-account-hijack.test.ts +223 -0
  419. package/src/plugins/two-factor.ts +466 -37
  420. package/src/plugins/username.ts +19 -7
  421. package/src/release/build-lib.ts +1 -0
  422. package/src/release/limits.ts +2 -2
  423. package/src/release/measure.ts +1 -1
  424. package/src/runs/collect.test.ts +0 -2
  425. package/src/runtime/json-code-block.test.ts +250 -20
  426. package/src/runtime/json-code-block.ts +1261 -41
  427. package/src/term.test.ts +1 -1
  428. package/src/test/create-test-app.test.ts +12 -12
  429. package/src/test/live-signals.test.ts +6 -6
  430. package/src/test/provisions.integration.test.ts +10 -14
  431. package/src/test/reset-element-registries.ts +1 -1
  432. package/src/test/tenant-isolation.test.ts +12 -6
  433. package/site/content/docs/deployment/docker-swarm.mdx +0 -164
  434. package/site/content/docs/deployment/docker.mdx +0 -227
  435. package/site/content/docs/deployment/index.mdx +0 -83
  436. package/site/content/docs/deployment/kubernetes.mdx +0 -176
  437. package/site/content/docs/deployment/meta.json +0 -5
  438. package/site/content/docs/deployment/reverse-proxy.mdx +0 -234
  439. package/site/content/docs/elements/ai.mdx +0 -385
  440. package/site/content/docs/elements/channel.mdx +0 -346
  441. package/site/content/docs/elements/clock.mdx +0 -244
  442. package/site/content/docs/elements/flow.mdx +0 -420
  443. package/site/content/docs/elements/gate.mdx +0 -436
  444. package/site/content/docs/elements/signal.mdx +0 -380
  445. package/site/content/docs/elements/store.mdx +0 -1099
  446. package/site/content/docs/elements/vault.mdx +0 -405
  447. package/site/content/docs/get-started/basic-usage.mdx +0 -173
  448. package/site/content/docs/get-started/index.mdx +0 -43
  449. package/site/content/docs/get-started/installation.mdx +0 -220
  450. package/site/content/docs/get-started/introduction.mdx +0 -144
  451. package/site/content/docs/get-started/meta.json +0 -13
  452. package/site/content/docs/get-started/project-structure.mdx +0 -925
  453. package/site/content/docs/get-started/testing.mdx +0 -328
  454. package/site/content/docs/get-started/why.mdx +0 -114
  455. package/site/content/docs/recipes/llama-cpp.mdx +0 -151
  456. package/site/content/docs/recipes/ollama.mdx +0 -142
  457. package/site/content/docs/recipes/sglang.mdx +0 -105
  458. package/site/content/docs/recipes/vllm.mdx +0 -106
  459. package/site/content/docs/reference/cli.md +0 -232
  460. package/site/content/docs/reference/client.mdx +0 -469
  461. package/site/content/docs/reference/security.md +0 -74
  462. package/src/cli/ai-setup/detect-ollama.ts +0 -213
  463. package/src/cli/ai-setup/recommend.test.ts +0 -225
  464. package/src/cli/ai-setup/recommend.ts +0 -225
  465. package/src/cli/dev-controls.test.ts +0 -122
  466. package/src/cli/dev-controls.ts +0 -164
  467. package/src/cli/tui/DevLive.tsx +0 -126
  468. package/src/console/ui-next/dist/assets/access-page-3-EFj-2G.js +0 -4
  469. package/src/console/ui-next/dist/assets/agent-disclosure-BP0Y0Sux.js +0 -1
  470. package/src/console/ui-next/dist/assets/cache-glyph-B5X-NM0-.js +0 -1
  471. package/src/console/ui-next/dist/assets/call-pii-button-ChAUiLo9.js +0 -1
  472. package/src/console/ui-next/dist/assets/collapsible-DGnOM2ph.js +0 -1
  473. package/src/console/ui-next/dist/assets/copy-inline-button-DBHhgHKP.js +0 -1
  474. package/src/console/ui-next/dist/assets/dagre.esm-ZwcdTuZZ.js +0 -1
  475. package/src/console/ui-next/dist/assets/detail-header-DhHM1iaZ.js +0 -1
  476. package/src/console/ui-next/dist/assets/dropdown-menu-_NjTEo5_.js +0 -1
  477. package/src/console/ui-next/dist/assets/duration-tone-sC3lGABz.js +0 -9
  478. package/src/console/ui-next/dist/assets/element-icons-BVXtRyd3.js +0 -1
  479. package/src/console/ui-next/dist/assets/explorer-empty-2uIhBu0_.js +0 -1
  480. package/src/console/ui-next/dist/assets/flows-page-cVFnA4HH.js +0 -1
  481. package/src/console/ui-next/dist/assets/highlighted-json-Awq7gYdu.js +0 -154
  482. package/src/console/ui-next/dist/assets/http-method-_UHM2ODJ.js +0 -1
  483. package/src/console/ui-next/dist/assets/index-CMIgUbD0.js +0 -66
  484. package/src/console/ui-next/dist/assets/index-Ck88Jmv8.css +0 -2
  485. package/src/console/ui-next/dist/assets/link-BX6Vqztd.js +0 -1
  486. package/src/console/ui-next/dist/assets/observability-page-CQ3p34ip.js +0 -4
  487. package/src/console/ui-next/dist/assets/preload-helper-oH4irX4C.js +0 -1
  488. package/src/console/ui-next/dist/assets/react-D8E3mtu1.js +0 -1
  489. package/src/console/ui-next/dist/assets/react-dom-Bph1y7z7.js +0 -9
  490. package/src/console/ui-next/dist/assets/replica-lag-DKRrbvdo.js +0 -18
  491. package/src/console/ui-next/dist/assets/request-meta-DV0ywz7t.js +0 -1
  492. package/src/console/ui-next/dist/assets/shortcut-JQIZlWfm.js +0 -1
  493. package/src/console/ui-next/dist/assets/shortcut-keys-DKxNTe_m.js +0 -1
  494. package/src/console/ui-next/dist/assets/skeleton-D-czQJT6.js +0 -1
  495. package/src/console/ui-next/dist/assets/store-page-02xOiqIK.js +0 -41
  496. package/src/console/ui-next/dist/assets/trace-detail-sheet-B09O8rA6.js +0 -2
  497. package/src/console/ui-next/dist/assets/tree-expand-toggle-BtyhmWb4.js +0 -55
  498. package/src/console/ui-next/dist/assets/units-page-l8FeKfnP.js +0 -1
  499. package/src/console/ui-next/dist/assets/useMutation-B8EO02Ej.js +0 -1
  500. package/src/console/ui-next/dist/assets/useRender-BE2A9BWC.js +0 -1
  501. package/src/console/ui-next/dist/assets/vault-page-CL-d_mLE.js +0 -2
  502. package/src/console/ui-next/dist/assets/xyflow-D7n4g6go.js +0 -7
  503. package/src/console/ui-next/dist/assets/xyflow-DZ0Ws1xk.css +0 -1
  504. package/src/docker/ollama-pull.ts +0 -232
  505. package/src/docker/recipes/llama-cpp.ts +0 -298
  506. package/src/docker/recipes/ollama.ts +0 -44
  507. package/src/docker/recipes/sglang.ts +0 -55
  508. package/src/docker/recipes/vllm.ts +0 -44
  509. package/src/drivers/ai-ollama-tools.integration.test.ts +0 -109
  510. package/src/drivers/ai-ollama.integration.test.ts +0 -184
  511. package/src/drivers/ai-ollama.ts +0 -389
  512. package/src/drivers/ollama.ts +0 -14
@@ -0,0 +1,314 @@
1
+ /**
2
+ * Built-in hybrid search runtime — BM25F ± LSH ± RRF/weighted fusion,
3
+ * composing {@link parseListQuery} for ordinary filters / pagination.
4
+ */
5
+
6
+ import type { PromptRef } from "../../manifest/types.ts";
7
+ import type { SqlConnection, SqlRow } from "../../drivers/types.ts";
8
+ import { parseListQuery, resolveListScope, type ListOptions } from "./list-query.ts";
9
+ import { bm25fScore, termFrequencies, tokenize } from "./search-bm25.ts";
10
+ import { SearchConfigError, LSH_DEFAULT_K } from "./search-errors.ts";
11
+ import { fuseLists, type FuseOptions } from "./search-fusion.ts";
12
+ import {
13
+ cosineSimilarity,
14
+ deserializePlanes,
15
+ lshBucket,
16
+ lshBucketToSql,
17
+ lshHammingSql,
18
+ } from "./search-lsh.ts";
19
+ import {
20
+ embColumn,
21
+ lshColumn,
22
+ OKE_SEARCH_DF,
23
+ OKE_SEARCH_PLANES,
24
+ OKE_SEARCH_STATS,
25
+ OKE_TSV_COL,
26
+ } from "./search-ddl.ts";
27
+ import { compileWhere } from "./sql-condition.ts";
28
+ import { resolveTableName, type TableHandle } from "./table.ts";
29
+
30
+ /** Search options — `query` / `fuse` / `rerank` are search-specific; rest is list grammar. */
31
+ export interface SqlSearchOptions extends ListOptions {
32
+ /** Hybrid relevance string (BM25 ± semantic). Not the same as list `?search=` LIKE. */
33
+ readonly query: string;
34
+ readonly fuse?: FuseOptions;
35
+ /** Default `false` — never silently invokes fx.ask. */
36
+ readonly rerank?: false | { readonly model: PromptRef };
37
+ /** Extra PostgREST-shaped filter input (same as list / live). */
38
+ readonly filterInput?: unknown;
39
+ }
40
+
41
+ export interface SqlSearchResult {
42
+ readonly data: SqlRow[];
43
+ readonly meta: {
44
+ readonly engine: Array<"bm25" | "lsh">;
45
+ readonly fusedBy?: "rrf" | "weighted";
46
+ readonly rrfK?: number;
47
+ readonly limit: number;
48
+ };
49
+ }
50
+
51
+ export interface SearchColumnMeta {
52
+ readonly key: string;
53
+ readonly sqlName: string;
54
+ readonly weight: number;
55
+ readonly embed?: { readonly dims: number; readonly model?: string };
56
+ }
57
+
58
+ export interface RunSqlSearchDeps {
59
+ readonly conn: SqlConnection;
60
+ readonly table: TableHandle | unknown;
61
+ readonly columns: readonly SearchColumnMeta[];
62
+ readonly pkSqlName: string;
63
+ readonly options: SqlSearchOptions;
64
+ /** Optional embed for the query string when LSH fields exist. */
65
+ readonly embedQuery?: (text: string, model?: string) => Promise<readonly number[]>;
66
+ /** Optional rerank via fx.ask — only when options.rerank is set. */
67
+ readonly askRerank?: (
68
+ model: PromptRef,
69
+ input: {
70
+ query: string;
71
+ docs: Array<{ id: string; text: string; score: number }>;
72
+ },
73
+ ) => Promise<{ rankedIds?: string[] }>;
74
+ }
75
+
76
+ /**
77
+ * Execute hybrid search against PostgreSQL using GIN lexical candidates UNION
78
+ * SimHash Hamming-rank (K=64 `bit_count` ORDER BY LIMIT), then BM25F / cosine
79
+ * / fusion in-process (correct, testable math).
80
+ *
81
+ * @param deps - Connection, table meta, options
82
+ */
83
+ export async function runSqlSearch(deps: RunSqlSearchDeps): Promise<SqlSearchResult> {
84
+ const { conn, options, columns, pkSqlName } = deps;
85
+ const tableName = resolveTableName(deps.table);
86
+ if (!tableName) throw new Error("search(): table name could not be resolved");
87
+
88
+ const searchable = columns.filter((c) => c.weight > 0);
89
+ if (searchable.length === 0) {
90
+ throw new SearchConfigError(tableName, "*", "no .searchable() columns on this table");
91
+ }
92
+
93
+ const { query, fuse, rerank, filterInput, ...listOpts } = options;
94
+ const scope = resolveListScope(deps.table, listOpts);
95
+ const parsed = parseListQuery(filterInput ?? {}, scope.query);
96
+ if (!parsed.ok) {
97
+ throw parsed.failure instanceof Error
98
+ ? parsed.failure
99
+ : new Error(`search(): invalid list filters (${String(parsed.failure)})`);
100
+ }
101
+
102
+ const limit = parsed.page.limit ?? scope.limit;
103
+ const engines: Array<"bm25" | "lsh"> = ["bm25"];
104
+ const embedFields = searchable.filter((c) => c.embed);
105
+ if (embedFields.length > 0) engines.push("lsh");
106
+
107
+ let queryVec: readonly number[] | undefined;
108
+ const hamExprs: string[] = [];
109
+ const hamParams: unknown[] = [];
110
+
111
+ if (embedFields.length > 0 && deps.embedQuery) {
112
+ const model = embedFields[0]?.embed?.model;
113
+ queryVec = await deps.embedQuery(query, model);
114
+ for (const field of embedFields) {
115
+ const dims = field.embed!.dims;
116
+ if (queryVec.length !== dims) {
117
+ throw new SearchConfigError(
118
+ tableName,
119
+ field.sqlName,
120
+ `query embedding length ${queryVec.length} !== declared dims ${dims}`,
121
+ );
122
+ }
123
+ const planesRow = await conn.query(
124
+ `SELECT k, planes FROM ${OKE_SEARCH_PLANES} WHERE table_name = ? AND column_name = ?`,
125
+ [tableName, field.sqlName],
126
+ );
127
+ const row = planesRow[0];
128
+ if (!row) {
129
+ throw new SearchConfigError(
130
+ tableName,
131
+ field.sqlName,
132
+ `missing hyperplanes — run oke db search-backfill or ensure push applied search DDL`,
133
+ );
134
+ }
135
+ const k = Number(row["k"] ?? LSH_DEFAULT_K);
136
+ const planesBuf = row["planes"] as Buffer;
137
+ const planes = deserializePlanes(Buffer.from(planesBuf), k);
138
+ const bucket = lshBucket(queryVec, planes);
139
+ hamExprs.push(lshHammingSql(lshColumn(field.sqlName), "?"));
140
+ hamParams.push(lshBucketToSql(bucket));
141
+ }
142
+ }
143
+
144
+ let filterClause = "";
145
+ const filterParams: unknown[] = [];
146
+ if (parsed.page.where) {
147
+ const compiled = compileWhere(parsed.page.where);
148
+ if (compiled.clause) {
149
+ filterClause = ` AND (${compiled.clause})`;
150
+ filterParams.push(...compiled.params);
151
+ }
152
+ }
153
+
154
+ const selectCols = [
155
+ quoteIdent(pkSqlName),
156
+ ...searchable.map((c) => quoteIdent(c.sqlName)),
157
+ ...embedFields.map((c) => quoteIdent(embColumn(c.sqlName))),
158
+ ].join(", ");
159
+ const tableIdent = quoteIdent(tableName);
160
+
161
+ // Oversample candidates for fusion then truncate.
162
+ const fetchLimit = Math.min(Math.max(limit * 5, 50), 500);
163
+ const lexicalSql = `SELECT ${selectCols} FROM ${tableIdent} WHERE ${OKE_TSV_COL} @@ plainto_tsquery('english', ?)${filterClause} LIMIT ${fetchLimit}`;
164
+
165
+ // SimHash k-NN: rank by Hamming over the K=64 bit pack, then LIMIT.
166
+ // Hamming-1 equality (`= ANY(65 buckets)`) is not a candidate set — true
167
+ // near-neighbors sit at distance ~5–16, so that probe recovered ~0 rows.
168
+ let candidateSql: string;
169
+ const params: unknown[] = [];
170
+ if (hamExprs.length > 0) {
171
+ const lshPred = embedFields.map((f) => `${lshColumn(f.sqlName)} IS NOT NULL`).join(" OR ");
172
+ const orderExpr = hamExprs.length === 1 ? hamExprs[0]! : `LEAST(${hamExprs.join(", ")})`;
173
+ const lshSql = `SELECT ${selectCols} FROM ${tableIdent} WHERE (${lshPred})${filterClause} ORDER BY ${orderExpr} ASC NULLS LAST LIMIT ${fetchLimit}`;
174
+ candidateSql = `(${lexicalSql}) UNION (${lshSql})`;
175
+ params.push(query, ...filterParams, ...hamParams, ...filterParams);
176
+ } else {
177
+ candidateSql = lexicalSql;
178
+ params.push(query, ...filterParams);
179
+ }
180
+ const candidates = await conn.query(candidateSql, params);
181
+
182
+ const statsRows = await conn.query(
183
+ `SELECT n, avgdl FROM ${OKE_SEARCH_STATS} WHERE table_name = ?`,
184
+ [tableName],
185
+ );
186
+ const N = Number(statsRows[0]?.["n"] ?? candidates.length) || candidates.length;
187
+ const avgdl = Number(statsRows[0]?.["avgdl"] ?? 50) || 50;
188
+
189
+ const dfRows = await conn.query(`SELECT term, df FROM ${OKE_SEARCH_DF} WHERE table_name = ?`, [
190
+ tableName,
191
+ ]);
192
+ const df = new Map<string, number>();
193
+ for (const r of dfRows) {
194
+ df.set(String(r["term"]), Number(r["df"] ?? 0));
195
+ }
196
+
197
+ const qTerms = tokenize(query);
198
+ const bm25Hits: Array<{ id: string; score: number; rank: number; row: SqlRow }> = [];
199
+ const vecHits: Array<{ id: string; score: number; rank: number; row: SqlRow }> = [];
200
+
201
+ for (const row of candidates) {
202
+ const id = String(row[pkSqlName] ?? row["id"] ?? "");
203
+ const fields = searchable.map((c) => {
204
+ const text = String(row[c.sqlName] ?? "");
205
+ return { weight: c.weight, tf: termFrequencies(tokenize(text)) };
206
+ });
207
+ let docLen = 0;
208
+ for (const f of fields) {
209
+ for (const n of f.tf.values()) docLen += n;
210
+ }
211
+ const score = bm25fScore(qTerms, fields, docLen, avgdl, N, df);
212
+ bm25Hits.push({ id, score, rank: 0, row });
213
+ }
214
+ bm25Hits.sort((a, b) => b.score - a.score);
215
+ bm25Hits.forEach((h, i) => {
216
+ h.rank = i + 1;
217
+ });
218
+
219
+ if (queryVec && embedFields.length > 0) {
220
+ for (const row of candidates) {
221
+ const id = String(row[pkSqlName] ?? row["id"] ?? "");
222
+ let best = -1;
223
+ for (const field of embedFields) {
224
+ const emb = parseRealArray(row[embColumn(field.sqlName)]);
225
+ if (!emb) continue;
226
+ if (emb.length !== field.embed!.dims) {
227
+ throw new SearchConfigError(
228
+ tableName,
229
+ field.sqlName,
230
+ `stored embedding length ${emb.length} !== declared dims ${field.embed!.dims}`,
231
+ );
232
+ }
233
+ const sim = cosineSimilarity(queryVec, emb);
234
+ if (sim > best) best = sim;
235
+ }
236
+ if (best >= 0) vecHits.push({ id, score: best, rank: 0, row });
237
+ }
238
+ vecHits.sort((a, b) => b.score - a.score);
239
+ vecHits.forEach((h, i) => {
240
+ h.rank = i + 1;
241
+ });
242
+ }
243
+
244
+ const rowById = new Map<string, SqlRow>();
245
+ for (const h of bm25Hits) rowById.set(h.id, h.row);
246
+ for (const h of vecHits) rowById.set(h.id, h.row);
247
+
248
+ let orderedIds: string[];
249
+ let fusedBy: "rrf" | "weighted" | undefined;
250
+ let rrfK: number | undefined;
251
+
252
+ if (vecHits.length > 0) {
253
+ const lists = new Map([
254
+ ["bm25", bm25Hits.map(({ id, score, rank }) => ({ id, score, rank }))],
255
+ ["vector", vecHits.map(({ id, score, rank }) => ({ id, score, rank }))],
256
+ ]);
257
+ const fused = fuseLists(lists, fuse);
258
+ orderedIds = fused.hits.map((h) => h.id);
259
+ fusedBy = fused.strategy;
260
+ rrfK = fused.k;
261
+ } else {
262
+ orderedIds = bm25Hits.map((h) => h.id);
263
+ }
264
+
265
+ let data = orderedIds
266
+ .slice(0, limit)
267
+ .map((id) => rowById.get(id))
268
+ .filter((r): r is SqlRow => r !== undefined);
269
+
270
+ if (rerank && typeof rerank === "object" && rerank.model && deps.askRerank) {
271
+ const docs = data.map((row) => {
272
+ const id = String(row[pkSqlName] ?? row["id"] ?? "");
273
+ const text = searchable.map((c) => String(row[c.sqlName] ?? "")).join("\n");
274
+ return { id, text, score: 0 };
275
+ });
276
+ const out = await deps.askRerank(rerank.model, { query, docs });
277
+ if (out.rankedIds && out.rankedIds.length > 0) {
278
+ const byId = new Map(data.map((r) => [String(r[pkSqlName] ?? r["id"]), r]));
279
+ data = out.rankedIds.map((id) => byId.get(id)).filter((r): r is SqlRow => r !== undefined);
280
+ }
281
+ }
282
+
283
+ return {
284
+ data,
285
+ meta: {
286
+ engine: engines,
287
+ ...(fusedBy ? { fusedBy } : {}),
288
+ ...(rrfK !== undefined ? { rrfK } : {}),
289
+ limit,
290
+ },
291
+ };
292
+ }
293
+
294
+ function quoteIdent(name: string): string {
295
+ if (!/^[a-z_][a-z0-9_]*$/i.test(name)) {
296
+ return `"${name.replaceAll('"', '""')}"`;
297
+ }
298
+ return name;
299
+ }
300
+
301
+ /**
302
+ * Coerce a Postgres `real[]` driver value into a number array.
303
+ *
304
+ * @param value - Array or `{1,2,3}` text
305
+ */
306
+ function parseRealArray(value: unknown): number[] | undefined {
307
+ if (Array.isArray(value)) return value.map((x) => Number(x));
308
+ if (typeof value === "string") {
309
+ const inner = value.trim().replace(/^\{/, "").replace(/\}$/, "");
310
+ if (inner.length === 0) return [];
311
+ return inner.split(",").map((x) => Number(x.trim()));
312
+ }
313
+ return undefined;
314
+ }
@@ -0,0 +1,226 @@
1
+ /**
2
+ * Built-in hybrid search — unit correctness (BM25F, LSH, RRF, searchable API).
3
+ */
4
+
5
+ import { describe, expect, test } from "bun:test";
6
+ import { createHash } from "node:crypto";
7
+ import { field, store } from "../store.ts";
8
+ import { bm25fScore, termFrequencies, tokenize } from "./search-bm25.ts";
9
+ import { fuseLists, fuseRrf, fuseWeighted } from "./search-fusion.ts";
10
+ import { LSH_DEFAULT_K, RRF_DEFAULT_K, SearchConfigError } from "./search-errors.ts";
11
+ import {
12
+ cosineSimilarity,
13
+ deserializePlanes,
14
+ generateHyperplanes,
15
+ hammingDistance,
16
+ hyperplaneSeed,
17
+ lshBucket,
18
+ lshBucketFromSql,
19
+ lshBucketToSql,
20
+ lshHammingSql,
21
+ neighborBuckets,
22
+ serializePlanes,
23
+ } from "./search-lsh.ts";
24
+
25
+ describe("field.searchable / .embed", () => {
26
+ test("searchable weight defaults to 1; embed requires prior searchable", () => {
27
+ const articles = store.schema.table("articles", {
28
+ id: field.text().primaryKey(),
29
+ title: field.text().searchable({ weight: 2 }).notNull(),
30
+ body: field.text().searchable().embed({ dims: 8, model: "embedder" }),
31
+ });
32
+ expect(articles.columns.title.search).toEqual({ searchable: true, weight: 2 });
33
+ expect(articles.columns.body.search?.embed).toEqual({ dims: 8, model: "embedder" });
34
+ expect(() => field.text().embed({ dims: 8 })).toThrow(/prior \.searchable/);
35
+ });
36
+
37
+ test("bare .embed() and partial options are allowed at declare time", () => {
38
+ const bare = field.text().searchable().embed();
39
+ const modelOnly = field.text().searchable().embed({ model: "embedder" });
40
+ expect(bare).toBeDefined();
41
+ expect(modelOnly).toBeDefined();
42
+ const table = store.schema.table("articles", {
43
+ id: field.text().primaryKey(),
44
+ body: field.text().searchable().embed(),
45
+ caption: field.text().searchable().embed({ model: "other" }),
46
+ });
47
+ expect(table.columns.body.search?.embed).toEqual({});
48
+ expect(table.columns.caption.search?.embed).toEqual({ model: "other" });
49
+ });
50
+ });
51
+
52
+ describe("BM25F", () => {
53
+ test("field weight applied before saturation matches hand reference", () => {
54
+ const query = ["cat"];
55
+ const df = new Map([["cat", 1]]);
56
+ const N = 2;
57
+ const avgdl = 2;
58
+ // Doc A: title has cat once (weight 2); Doc B: body has cat once (weight 1)
59
+ const scoreA = bm25fScore(
60
+ query,
61
+ [{ weight: 2, tf: termFrequencies(["cat"]) }],
62
+ 1,
63
+ avgdl,
64
+ N,
65
+ df,
66
+ );
67
+ const scoreB = bm25fScore(
68
+ query,
69
+ [{ weight: 1, tf: termFrequencies(["cat"]) }],
70
+ 1,
71
+ avgdl,
72
+ N,
73
+ df,
74
+ );
75
+ expect(scoreA).toBeGreaterThan(scoreB);
76
+ // Hand: weightedTf=2 vs 1 with same IDF → A ranks higher
77
+ expect(tokenize("Hello, CAT!")).toEqual(["hello", "cat"]);
78
+ });
79
+ });
80
+
81
+ describe("LSH", () => {
82
+ test("planes are stable for the same seed; near vectors collide", () => {
83
+ const seed = hyperplaneSeed("articles", "body", 8, 16);
84
+ const a = generateHyperplanes(seed, 8, 16);
85
+ const b = generateHyperplanes(seed, 8, 16);
86
+ expect(a[0]![0]).toBe(b[0]![0]);
87
+ const v = [1, 0, 0, 0, 0, 0, 0, 0];
88
+ const near = [0.99, 0.01, 0, 0, 0, 0, 0, 0];
89
+ const ba = lshBucket(v, a);
90
+ const bn = lshBucket(near, a);
91
+ // Exact or Hamming-1 neighbor should include near for this orientation
92
+ const neighbors = new Set(neighborBuckets(ba, 16).map(String));
93
+ expect(neighbors.has(String(bn)) || ba === bn).toBe(true);
94
+ expect(cosineSimilarity(v, near)).toBeGreaterThan(0.9);
95
+ });
96
+
97
+ test("dimension mismatch fails loud", () => {
98
+ const planes = generateHyperplanes("s", 4, 8);
99
+ expect(() => lshBucket([1, 2], planes)).toThrow(/length/);
100
+ });
101
+
102
+ test("lshBucketToSql round-trips high bit into signed bigint range", () => {
103
+ const high = 1n << 63n;
104
+ const sql = lshBucketToSql(high);
105
+ expect(BigInt(sql) < 0n).toBe(true);
106
+ expect(lshBucketFromSql(sql)).toBe(high);
107
+ expect(lshBucketToSql(42n)).toBe("42");
108
+ });
109
+
110
+ test("write-time and query-time buckets are identical for the same vector", () => {
111
+ const seed = hyperplaneSeed("g17_docs", "body", 32, LSH_DEFAULT_K);
112
+ const planes = generateHyperplanes(seed, 32, LSH_DEFAULT_K);
113
+ const reloaded = deserializePlanes(serializePlanes(planes), LSH_DEFAULT_K);
114
+ const vector = synthHashBag(
115
+ "refund policy shipping delay. Document 0 discusses refund policy shipping delay.",
116
+ );
117
+ const write = lshBucket(vector, planes);
118
+ const query = lshBucket(vector, reloaded);
119
+ expect(write).toBe(query);
120
+ expect(lshBucketToSql(write)).toBe(lshBucketToSql(query));
121
+ expect(neighborBuckets(write, LSH_DEFAULT_K)).toHaveLength(LSH_DEFAULT_K + 1);
122
+ });
123
+
124
+ test("K=64 Hamming-1 misses G17-like neighbors; Hamming-rank recovers them", () => {
125
+ // Same deterministic hash-bag generator G17 uses — 40 docs, 8 topics.
126
+ const topics = [
127
+ "refund policy shipping delay",
128
+ "password reset two factor auth",
129
+ "invoice payment stripe webhook",
130
+ "search ranking bm25 hybrid",
131
+ "postgres gin btree index plan",
132
+ "tenant isolation row level security",
133
+ "durable flow journal resume",
134
+ "live query fanout subscriber",
135
+ ];
136
+ const planes = generateHyperplanes(hyperplaneSeed("g17_docs", "body", 32, 64), 32, 64);
137
+ const docs = topics.flatMap((topic, t) =>
138
+ [0, 1, 2, 3, 4].map((n) => {
139
+ const i = t + n * 8;
140
+ const body = `${topic}. Document ${i} discusses ${topic} with enough tokens for BM25.`;
141
+ const vec = synthHashBag(body);
142
+ return { i, vec, bucket: lshBucket(vec, planes) };
143
+ }),
144
+ );
145
+ const qVec = synthHashBag(topics[0]!);
146
+ const qBucket = lshBucket(qVec, planes);
147
+ const scored = docs.map((d) => ({
148
+ i: d.i,
149
+ cos: cosineSimilarity(qVec, d.vec),
150
+ ham: hammingDistance(qBucket, d.bucket),
151
+ }));
152
+ const exact = [...scored].sort((a, b) => b.cos - a.cos).slice(0, 10);
153
+ const exactSet = new Set(exact.map((x) => x.i));
154
+ const ham1 = scored.filter((s) => s.ham <= 1);
155
+ const ham1Hits = ham1.filter((s) => exactSet.has(s.i)).length;
156
+ const ranked = [...scored].sort((a, b) => a.ham - b.ham || b.cos - a.cos).slice(0, 50);
157
+ const rankedTop = [...ranked].sort((a, b) => b.cos - a.cos).slice(0, 10);
158
+ const rankedHits = rankedTop.filter((s) => exactSet.has(s.i)).length;
159
+ // Hamming-1 is the broken probe (G17 P@10 ≈ 0). Hamming-rank of the K=64
160
+ // pack recovers the exact cosine top-10 on this 40-row hand case.
161
+ expect(ham1Hits).toBe(0);
162
+ expect(rankedHits).toBe(10);
163
+ expect(exact.every((e) => e.ham > 1)).toBe(true);
164
+ expect(lshHammingSql("__oke_lsh_body")).toContain("bit_count");
165
+ });
166
+ });
167
+
168
+ /**
169
+ * G17 synthetic embedding — deterministic SHA-256 hash bag, L2-normalized.
170
+ *
171
+ * @param text - Source text
172
+ */
173
+ function synthHashBag(text: string, dims = 32): number[] {
174
+ const v = Array.from({ length: dims }, () => 0);
175
+ const tokens = text.toLowerCase().split(/\W+/).filter(Boolean);
176
+ for (const t of tokens) {
177
+ const h = createHash("sha256").update(t).digest();
178
+ for (let i = 0; i < dims; i++) {
179
+ v[i]! += (h[i % h.length]! / 255) * 2 - 1;
180
+ }
181
+ }
182
+ let norm = 0;
183
+ for (const x of v) norm += x * x;
184
+ norm = Math.sqrt(norm) || 1;
185
+ return v.map((x) => x / norm);
186
+ }
187
+
188
+ describe("fusion", () => {
189
+ test("RRF default k=60 matches Cormack hand ranks", () => {
190
+ expect(RRF_DEFAULT_K).toBe(60);
191
+ const lists = new Map([
192
+ [
193
+ "bm25",
194
+ [
195
+ { id: "a", score: 10, rank: 1 },
196
+ { id: "b", score: 5, rank: 2 },
197
+ ],
198
+ ],
199
+ [
200
+ "vector",
201
+ [
202
+ { id: "b", score: 0.9, rank: 1 },
203
+ { id: "a", score: 0.5, rank: 2 },
204
+ ],
205
+ ],
206
+ ]);
207
+ const hits = fuseRrf(lists, 60);
208
+ // a: 1/61 + 1/62; b: 1/62 + 1/61 — equal contributions swapped → tied-ish; b wins on equal?
209
+ // a = 1/61 + 1/62; b = 1/62 + 1/61 — identical
210
+ expect(hits[0]!.score).toBeCloseTo(hits[1]!.score, 10);
211
+ const weighted = fuseWeighted(lists, { bm25: 1, vector: 0 });
212
+ expect(weighted[0]!.id).toBe("a");
213
+ const fused = fuseLists(lists);
214
+ expect(fused.strategy).toBe("rrf");
215
+ expect(fused.k).toBe(60);
216
+ });
217
+ });
218
+
219
+ describe("SearchConfigError", () => {
220
+ test("names table and column", () => {
221
+ const err = new SearchConfigError("notes", "body", "missing ai");
222
+ expect(err.name).toBe("SearchConfigError");
223
+ expect(err.table).toBe("notes");
224
+ expect(err.message).toContain("notes.body");
225
+ });
226
+ });
@@ -71,6 +71,7 @@ describe("SqlStoreHandle — no relational query surface (path b)", () => {
71
71
  "count",
72
72
  "page",
73
73
  "ensureTable",
74
+ "search",
74
75
  ].sort(),
75
76
  );
76
77
 
@@ -19,8 +19,8 @@ import { maskRows, tableFromSql } from "./classify.ts";
19
19
  import { isMissingDomainRelationError } from "./missing-relation.ts";
20
20
  import {
21
21
  andWhere,
22
- compileOrderBy,
23
22
  compileWhere,
23
+ compileOrderBy,
24
24
  resolveSelectColumns,
25
25
  type WhereMap,
26
26
  } from "./sql-condition.ts";
@@ -32,6 +32,9 @@ import {
32
32
  resolveTableName,
33
33
  type TableHandle,
34
34
  } from "./table.ts";
35
+ import { isSchemaTableDecl } from "./schema-decl.ts";
36
+ import { SearchConfigError } from "./search-errors.ts";
37
+ import { runSqlSearch, type SearchColumnMeta, type SqlSearchOptions } from "./search-runtime.ts";
35
38
 
36
39
  export type { WhereMap } from "./sql-condition.ts";
37
40
 
@@ -204,6 +207,11 @@ export interface SqlSessionOptions {
204
207
  readonly domainDdl?: DomainDdlMode;
205
208
  /** Gate identity — stamps `oke.*` on postgres / pglite. */
206
209
  readonly rls?: RlsIdentity;
210
+ /**
211
+ * Optional query-time embedding for hybrid LSH (wired from `fx.embed` at boot).
212
+ * When omitted, search is BM25-only even if columns declare `.embed()`.
213
+ */
214
+ readonly embedQuery?: (text: string, model?: string) => Promise<readonly number[]>;
207
215
  }
208
216
 
209
217
  /**
@@ -363,6 +371,11 @@ export interface SqlStoreHandle {
363
371
  readonly routedRole: "primary" | "replica";
364
372
  /** Underlying driver id. */
365
373
  readonly driverId: SqlConnection["driverId"];
374
+ /**
375
+ * Egress identity when the SQL connection leaves the process.
376
+ * Omitted for in-process drivers (`pglite`, `memory`).
377
+ */
378
+ readonly external?: SqlConnection["external"];
366
379
  /**
367
380
  * Start a select. No-arg form infers the row from `.from(table)`.
368
381
  * A column map (`select({ clicks: links.clicks })`) stays {@link SqlRow}.
@@ -453,6 +466,18 @@ export interface SqlStoreHandle {
453
466
  * @param options - Page options
454
467
  */
455
468
  page(table: TableHandle | unknown, options: SqlPageOptions): Promise<SqlRow[]>;
469
+ /**
470
+ * Built-in hybrid search (BM25 ± LSH ± fusion) on `.searchable()` columns.
471
+ * Reuses {@link parseListQuery} for filters / limit / cursor; `query` is
472
+ * the relevance string (not list-grammar `?search=` LIKE).
473
+ *
474
+ * @param table - Table with searchable columns
475
+ * @param options - query + list filters + optional fuse / rerank
476
+ */
477
+ search(
478
+ table: TableHandle | unknown,
479
+ options: import("./search-runtime.ts").SqlSearchOptions,
480
+ ): Promise<import("./search-runtime.ts").SqlSearchResult>;
456
481
  /**
457
482
  * Ensure a simple table exists (test / bootstrap helper).
458
483
  *
@@ -507,7 +532,7 @@ export function createSqlStoreHandle(
507
532
  ref: `sql:${string}`,
508
533
  options: SqlSessionOptions,
509
534
  ): SqlStoreHandle {
510
- const { connection, classifications, revealPii, rls } = options;
535
+ const { connection, classifications, revealPii, rls, embedQuery } = options;
511
536
  const domainDdl: DomainDdlMode = options.domainDdl ?? "ensure";
512
537
 
513
538
  function mask(rows: SqlRow[], table?: string): SqlRow[] {
@@ -515,7 +540,7 @@ export function createSqlStoreHandle(
515
540
  }
516
541
 
517
542
  /**
518
- * Run a connection op; remap missing-relation driver errors to OKE1101
543
+ * Run a connection op; remap missing-relation driver errors to OKE1110
519
544
  * when domain auto-DDL is off.
520
545
  */
521
546
  async function withSchemaGuard<T>(fn: () => Promise<T>): Promise<T> {
@@ -744,6 +769,7 @@ export function createSqlStoreHandle(
744
769
  ref,
745
770
  routedRole: options.routedRole,
746
771
  driverId: connection.driverId,
772
+ ...(connection.external !== undefined ? { external: connection.external } : {}),
747
773
 
748
774
  select(columns?: unknown) {
749
775
  return {
@@ -1134,6 +1160,20 @@ export function createSqlStoreHandle(
1134
1160
  });
1135
1161
  },
1136
1162
 
1163
+ async search(table: TableHandle | unknown, searchOptions: SqlSearchOptions) {
1164
+ await ensureFromMeta(table);
1165
+ const columns = searchColumnsFromTable(table);
1166
+ const pk = resolvePkColumn(table);
1167
+ return runSqlSearch({
1168
+ conn: connection,
1169
+ table,
1170
+ columns,
1171
+ pkSqlName: pk,
1172
+ options: searchOptions,
1173
+ ...(embedQuery ? { embedQuery } : {}),
1174
+ });
1175
+ },
1176
+
1137
1177
  async ensureTable(table: TableHandle) {
1138
1178
  const cols = Object.values(table.columns);
1139
1179
  const pk = resolvePkColumn(table);
@@ -1169,6 +1209,46 @@ export function resolvePkColumn(table: unknown): string {
1169
1209
  return "id";
1170
1210
  }
1171
1211
 
1212
+ /**
1213
+ * Collect `.searchable()` / `.embed()` column meta from a schema table.
1214
+ *
1215
+ * @param table - Table handle
1216
+ */
1217
+ function searchColumnsFromTable(table: unknown): SearchColumnMeta[] {
1218
+ if (!isSchemaTableDecl(table)) {
1219
+ throw new Error(
1220
+ "search(): table must be a store.schema.table() declaration with .searchable() columns",
1221
+ );
1222
+ }
1223
+ const out: SearchColumnMeta[] = [];
1224
+ const tableName = resolveTableName(table);
1225
+ for (const [key, col] of Object.entries(table.columns)) {
1226
+ if (!col.search?.searchable) continue;
1227
+ let embed: SearchColumnMeta["embed"];
1228
+ if (col.search.embed) {
1229
+ const dims = col.search.embed.dims;
1230
+ if (dims === undefined) {
1231
+ throw new SearchConfigError(
1232
+ tableName,
1233
+ key,
1234
+ `.embed() needs dims — set oke({ store: { search: { embed: { model, dims } } } }) or pass dims on .embed({ dims })`,
1235
+ );
1236
+ }
1237
+ embed = {
1238
+ dims,
1239
+ ...(col.search.embed.model ? { model: col.search.embed.model } : {}),
1240
+ };
1241
+ }
1242
+ out.push({
1243
+ key,
1244
+ sqlName: col.sqlName,
1245
+ weight: col.search.weight,
1246
+ ...(embed ? { embed } : {}),
1247
+ });
1248
+ }
1249
+ return out;
1250
+ }
1251
+
1172
1252
  function asNumber(value: unknown, column: string): number {
1173
1253
  if (typeof value === "number") return value;
1174
1254
  if (typeof value === "bigint") return Number(value);