okengine 0.2.5 → 0.2.8

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 (584) hide show
  1. package/AGENTS.md +24 -23
  2. package/README.md +52 -346
  3. package/docs/spec/console.md +165 -142
  4. package/docs/spec/example.md +490 -312
  5. package/docs/spec/four-applications.md +495 -314
  6. package/docs/spec/unified-theory.md +184 -175
  7. package/package.json +46 -38
  8. package/spec/manifest.v1.schema.json +40 -9
  9. package/src/auth/api-keys.ts +2 -5
  10. package/src/auth/attenuation.ts +2 -6
  11. package/src/auth/auth.test.ts +53 -31
  12. package/src/auth/invoke-as.ts +1 -3
  13. package/src/auth/operator.ts +8 -26
  14. package/src/auth/planes.ts +2 -7
  15. package/src/auth/plugin.ts +4 -7
  16. package/src/auth/roles.ts +1 -5
  17. package/src/auth/sessions.ts +10 -39
  18. package/src/cli/args.ts +1 -5
  19. package/src/cli/ask-dev-mode.ts +40 -0
  20. package/src/cli/build.ts +1 -2
  21. package/src/cli/client-add.test.ts +8 -15
  22. package/src/cli/client-add.ts +5 -14
  23. package/src/cli/completion.test.ts +1 -5
  24. package/src/cli/completion.ts +4 -17
  25. package/src/cli/db-auto-push.test.ts +64 -0
  26. package/src/cli/db-auto-push.ts +72 -0
  27. package/src/cli/db.test.ts +189 -0
  28. package/src/cli/db.ts +386 -0
  29. package/src/cli/dev-app-runner.ts +2 -8
  30. package/src/cli/dev-db-push.test.ts +163 -0
  31. package/src/cli/dev-mode.test.ts +45 -0
  32. package/src/cli/dev-mode.ts +78 -0
  33. package/src/cli/dev.test.ts +148 -21
  34. package/src/cli/dev.ts +208 -134
  35. package/src/cli/doc-drift.test.ts +3 -2
  36. package/src/cli/doc-drift.ts +21 -50
  37. package/src/cli/doc-staleness.test.ts +69 -0
  38. package/src/cli/docker-cli.test.ts +7 -8
  39. package/src/cli/docker.ts +1 -3
  40. package/src/cli/doctor-diff-examples.ts +1 -3
  41. package/src/cli/doctor-diff.test.ts +1 -5
  42. package/src/cli/doctor-diff.ts +17 -46
  43. package/src/cli/doctor-pii.ts +2 -7
  44. package/src/cli/doctor.test.ts +26 -4
  45. package/src/cli/doctor.ts +35 -11
  46. package/src/cli/eval.ts +4 -14
  47. package/src/cli/gates-list.ts +1 -4
  48. package/src/cli/hero-meta.test.ts +17 -19
  49. package/src/cli/hero-meta.ts +20 -30
  50. package/src/cli/images.ts +3 -8
  51. package/src/cli/index.ts +11 -2
  52. package/src/cli/load-config.images.test.ts +14 -14
  53. package/src/cli/load-config.ts +41 -47
  54. package/src/cli/mcp-from-console.ts +1 -4
  55. package/src/cli/mode.ts +45 -0
  56. package/src/cli/ports.test.ts +4 -13
  57. package/src/cli/ports.ts +1 -3
  58. package/src/cli/privacy-erase.ts +1 -3
  59. package/src/cli/registry.ts +75 -25
  60. package/src/cli/schema.ts +3 -10
  61. package/src/cli/stack.ts +2 -7
  62. package/src/cli/start.ts +4 -15
  63. package/src/cli/upgrade.ts +1 -4
  64. package/src/client/budget-entry.ts +1 -6
  65. package/src/client/create.test.ts +6 -12
  66. package/src/client/create.ts +2 -8
  67. package/src/client/errors.ts +1 -4
  68. package/src/client/index.ts +1 -6
  69. package/src/client/notes-contract.test.ts +4 -13
  70. package/src/client/transport.ts +9 -30
  71. package/src/client/types.ts +8 -29
  72. package/src/compiler/aot.test.ts +2 -9
  73. package/src/compiler/aot.ts +1 -4
  74. package/src/compiler/differential.test.ts +3 -11
  75. package/src/compiler/dynamic.ts +1 -4
  76. package/src/compiler/effects-infer.ts +8 -34
  77. package/src/compiler/emit.ts +1 -3
  78. package/src/compiler/extract.test.ts +96 -30
  79. package/src/compiler/extract.ts +362 -156
  80. package/src/compiler/fixtures/skyport/oke.config.ts +1 -1
  81. package/src/compiler/fixtures/skyport/src/flows/bookings/index.ts +7 -3
  82. package/src/compiler/fixtures/skyport/src/flows/payments/index.ts +1 -3
  83. package/src/compiler/fixtures/skyport/src/flows/support/index.ts +4 -1
  84. package/src/compiler/fixtures/skyport.expected.json +24 -83
  85. package/src/compiler/fixtures/triggers/five-triggers.ts +1 -2
  86. package/src/compiler/http-parse.ts +2 -9
  87. package/src/compiler/index.ts +1 -5
  88. package/src/compiler/response.ts +4 -7
  89. package/src/compiler/sucrose.ts +11 -31
  90. package/src/config/define-config.test.ts +57 -34
  91. package/src/config/index.ts +181 -37
  92. package/src/config/resolve-driver.test.ts +28 -17
  93. package/src/console/budget.test.ts +1 -4
  94. package/src/console/index.ts +1 -4
  95. package/src/console/server/access.test.ts +15 -43
  96. package/src/console/server/access.ts +19 -49
  97. package/src/console/server/ai.test.ts +3 -11
  98. package/src/console/server/ai.ts +23 -46
  99. package/src/console/server/app.ts +14 -45
  100. package/src/console/server/auth-rate.test.ts +4 -12
  101. package/src/console/server/bind.ts +1 -5
  102. package/src/console/server/channels.test.ts +2 -6
  103. package/src/console/server/channels.ts +10 -35
  104. package/src/console/server/claim.ts +2 -8
  105. package/src/console/server/clock.test.ts +2 -6
  106. package/src/console/server/clock.ts +8 -26
  107. package/src/console/server/console.test.ts +1 -3
  108. package/src/console/server/diff.test.ts +5 -17
  109. package/src/console/server/diff.ts +10 -31
  110. package/src/console/server/dry-run.audit.test.ts +2 -8
  111. package/src/console/server/flows.ts +64 -233
  112. package/src/console/server/gates.test.ts +14 -50
  113. package/src/console/server/gates.ts +13 -48
  114. package/src/console/server/index.ts +2 -9
  115. package/src/console/server/live.ts +3 -12
  116. package/src/console/server/operator-db.test.ts +4 -15
  117. package/src/console/server/operator-db.ts +13 -32
  118. package/src/console/server/plugin.ts +50 -53
  119. package/src/console/server/plugins.test.ts +2 -6
  120. package/src/console/server/plugins.ts +17 -56
  121. package/src/console/server/runs-pii.ts +2 -7
  122. package/src/console/server/security.gate.test.ts +18 -40
  123. package/src/console/server/serve.ts +10 -25
  124. package/src/console/server/signals.test.ts +1 -3
  125. package/src/console/server/signals.ts +17 -40
  126. package/src/console/server/state.ts +21 -59
  127. package/src/console/server/store.test.ts +20 -22
  128. package/src/console/server/store.ts +31 -76
  129. package/src/console/server/vault.test.ts +12 -29
  130. package/src/console/server/vault.ts +23 -60
  131. package/src/console/ui/access/AccessA11yView.tsx +6 -21
  132. package/src/console/ui/access/a11y.test.tsx +3 -12
  133. package/src/console/ui/access/acknowledgement.test.ts +1 -4
  134. package/src/console/ui/access/acknowledgement.ts +1 -3
  135. package/src/console/ui/access/blast-radius.ts +1 -3
  136. package/src/console/ui/access/confirmation.test.ts +1 -5
  137. package/src/console/ui/access/confirmation.ts +1 -4
  138. package/src/console/ui/access/fixture.ts +1 -5
  139. package/src/console/ui/access/grantable.test.ts +1 -3
  140. package/src/console/ui/access/grantable.ts +2 -7
  141. package/src/console/ui/access/index.ts +2 -9
  142. package/src/console/ui/access/provenance.test.ts +1 -3
  143. package/src/console/ui/access/provenance.ts +2 -6
  144. package/src/console/ui/access/search.test.ts +1 -5
  145. package/src/console/ui/access/search.ts +2 -6
  146. package/src/console/ui/ai/AiA11yView.tsx +6 -17
  147. package/src/console/ui/ai/a11y.test.tsx +2 -5
  148. package/src/console/ui/ai/fixture.ts +2 -7
  149. package/src/console/ui/ai/format.ts +1 -3
  150. package/src/console/ui/ai/group.test.ts +1 -3
  151. package/src/console/ui/ai/group.ts +4 -12
  152. package/src/console/ui/ai/index.ts +2 -9
  153. package/src/console/ui/ai/promotion.test.ts +2 -7
  154. package/src/console/ui/ai/promotion.ts +4 -13
  155. package/src/console/ui/ai/search.test.ts +2 -11
  156. package/src/console/ui/ai/search.ts +3 -13
  157. package/src/console/ui/ai/types.ts +1 -8
  158. package/src/console/ui/architecture/ArchitectureA11yView.tsx +8 -29
  159. package/src/console/ui/architecture/a11y.test.tsx +1 -2
  160. package/src/console/ui/architecture/declared.ts +1 -5
  161. package/src/console/ui/architecture/index.ts +7 -35
  162. package/src/console/ui/architecture/layout.ts +1 -3
  163. package/src/console/ui/architecture/pathologies.ts +10 -19
  164. package/src/console/ui/architecture/search.ts +6 -15
  165. package/src/console/ui/architecture/traffic.ts +4 -14
  166. package/src/console/ui/architecture/types.ts +2 -10
  167. package/src/console/ui/architecture/view.test.ts +7 -19
  168. package/src/console/ui/architecture/view.ts +9 -36
  169. package/src/console/ui/channels/ChannelsA11yView.tsx +4 -13
  170. package/src/console/ui/channels/a11y.test.tsx +1 -2
  171. package/src/console/ui/channels/confirmation.test.ts +1 -4
  172. package/src/console/ui/channels/confirmation.ts +1 -4
  173. package/src/console/ui/channels/findings.ts +1 -5
  174. package/src/console/ui/channels/index.ts +2 -8
  175. package/src/console/ui/channels/locale.test.ts +1 -3
  176. package/src/console/ui/channels/locale.ts +2 -8
  177. package/src/console/ui/channels/search.ts +4 -13
  178. package/src/console/ui/channels/taxonomy.ts +1 -3
  179. package/src/console/ui/clock/ClockA11yView.tsx +9 -26
  180. package/src/console/ui/clock/a11y.test.tsx +2 -5
  181. package/src/console/ui/clock/confirmation.test.ts +1 -3
  182. package/src/console/ui/clock/confirmation.ts +1 -4
  183. package/src/console/ui/clock/findings.ts +1 -4
  184. package/src/console/ui/clock/health.ts +2 -7
  185. package/src/console/ui/clock/index.ts +2 -10
  186. package/src/console/ui/clock/search.test.ts +1 -3
  187. package/src/console/ui/clock/search.ts +2 -6
  188. package/src/console/ui/clock/timeline.test.ts +1 -3
  189. package/src/console/ui/clock/waiting-on.test.ts +1 -5
  190. package/src/console/ui/clock/waiting-on.ts +2 -7
  191. package/src/console/ui/diff/DiffA11yView.tsx +8 -17
  192. package/src/console/ui/diff/a11y.test.tsx +2 -5
  193. package/src/console/ui/diff/group.test.ts +3 -13
  194. package/src/console/ui/diff/group.ts +2 -8
  195. package/src/console/ui/diff/index.ts +2 -8
  196. package/src/console/ui/diff/search.test.ts +1 -5
  197. package/src/console/ui/diff/search.ts +4 -16
  198. package/src/console/ui/diff/summary.ts +1 -2
  199. package/src/console/ui/dist/assets/{index-Dy4jht9P.js → index-BWo8R7NR.js} +2 -2
  200. package/src/console/ui/dist/assets/{panel-access-Dd37LU2c.js → panel-access-C0J2D-a2.js} +1 -1
  201. package/src/console/ui/dist/assets/{panel-ai-CC7LR6-J.js → panel-ai-D_m6WQI8.js} +1 -1
  202. package/src/console/ui/dist/assets/{panel-architecture-B5b3iKCz.js → panel-architecture-CKnXFyUx.js} +1 -1
  203. package/src/console/ui/dist/assets/{panel-channels-CeNjTKXp.js → panel-channels-BOmQ-onL.js} +1 -1
  204. package/src/console/ui/dist/assets/{panel-clock-DgFTLoHV.js → panel-clock-giAq0Ccv.js} +1 -1
  205. package/src/console/ui/dist/assets/{panel-diff-DxehccqB.js → panel-diff-cdonmH8c.js} +1 -1
  206. package/src/console/ui/dist/assets/{panel-flows-BtrVn-Eg.js → panel-flows-DlCU5zjA.js} +2 -2
  207. package/src/console/ui/dist/assets/{panel-gates-Z9MKRGdH.js → panel-gates-XclZxWD5.js} +1 -1
  208. package/src/console/ui/dist/assets/{panel-overview-Dt_AeXgd.js → panel-overview-BznEOTnb.js} +1 -1
  209. package/src/console/ui/dist/assets/{panel-plugins-DWd0TowH.js → panel-plugins-CcGM1g64.js} +1 -1
  210. package/src/console/ui/dist/assets/{panel-runs-DGstFHeq.js → panel-runs-CGWNHLR4.js} +1 -1
  211. package/src/console/ui/dist/assets/{panel-signals-DzEa2Fnt.js → panel-signals-CNywkdak.js} +1 -1
  212. package/src/console/ui/dist/assets/{panel-store-BJkbNgxx.js → panel-store-KmTbFHMH.js} +1 -1
  213. package/src/console/ui/dist/assets/{panel-traces-BaRVO3gM.js → panel-traces-DBLx2ilD.js} +1 -1
  214. package/src/console/ui/dist/assets/panel-vault-CEnFc0dk.js +1 -0
  215. package/src/console/ui/dist/index.html +1 -1
  216. package/src/console/ui/flows/FlowsA11yView.tsx +7 -22
  217. package/src/console/ui/flows/a11y.test.tsx +2 -8
  218. package/src/console/ui/flows/buffer.test.ts +3 -11
  219. package/src/console/ui/flows/buffer.ts +4 -11
  220. package/src/console/ui/flows/confirmation.ts +1 -3
  221. package/src/console/ui/flows/contract.test.ts +3 -7
  222. package/src/console/ui/flows/contract.ts +12 -36
  223. package/src/console/ui/flows/graph.test.ts +3 -11
  224. package/src/console/ui/flows/graph.ts +5 -15
  225. package/src/console/ui/flows/index.ts +1 -6
  226. package/src/console/ui/flows/save-as-test.ts +1 -3
  227. package/src/console/ui/flows/search.ts +8 -25
  228. package/src/console/ui/flows/tiers.ts +2 -9
  229. package/src/console/ui/gates/GatesA11yView.tsx +4 -13
  230. package/src/console/ui/gates/a11y.test.tsx +2 -5
  231. package/src/console/ui/gates/audit.ts +1 -3
  232. package/src/console/ui/gates/denial.test.ts +1 -3
  233. package/src/console/ui/gates/denial.ts +4 -12
  234. package/src/console/ui/gates/findings.ts +1 -3
  235. package/src/console/ui/gates/group.test.ts +3 -3
  236. package/src/console/ui/gates/group.ts +3 -11
  237. package/src/console/ui/gates/index.ts +2 -10
  238. package/src/console/ui/gates/search.ts +3 -10
  239. package/src/console/ui/overview/OverviewA11yView.tsx +9 -27
  240. package/src/console/ui/overview/a11y.test.tsx +1 -2
  241. package/src/console/ui/overview/busiest.ts +1 -3
  242. package/src/console/ui/overview/compose.ts +1 -13
  243. package/src/console/ui/overview/cost.ts +34 -38
  244. package/src/console/ui/overview/fixture.ts +21 -26
  245. package/src/console/ui/overview/golden.ts +4 -14
  246. package/src/console/ui/overview/index.ts +2 -9
  247. package/src/console/ui/overview/rank.test.ts +3 -10
  248. package/src/console/ui/overview/rank.ts +2 -7
  249. package/src/console/ui/overview/slo.test.ts +5 -9
  250. package/src/console/ui/overview/slo.ts +6 -23
  251. package/src/console/ui/overview/verdict.ts +3 -10
  252. package/src/console/ui/plugins/PluginsA11yView.tsx +8 -18
  253. package/src/console/ui/plugins/a11y.test.tsx +1 -2
  254. package/src/console/ui/plugins/fixture.ts +2 -4
  255. package/src/console/ui/plugins/group.test.ts +1 -3
  256. package/src/console/ui/plugins/group.ts +1 -6
  257. package/src/console/ui/plugins/index.ts +3 -13
  258. package/src/console/ui/plugins/search.ts +7 -19
  259. package/src/console/ui/runs/RunsA11yView.tsx +5 -20
  260. package/src/console/ui/runs/a11y.test.tsx +2 -5
  261. package/src/console/ui/runs/explain.ts +2 -6
  262. package/src/console/ui/runs/group.ts +3 -12
  263. package/src/console/ui/runs/histogram.test.ts +2 -11
  264. package/src/console/ui/runs/histogram.ts +1 -4
  265. package/src/console/ui/runs/index.ts +4 -18
  266. package/src/console/ui/runs/project.ts +2 -6
  267. package/src/console/ui/runs/query.test.ts +1 -3
  268. package/src/console/ui/runs/query.ts +12 -35
  269. package/src/console/ui/runs/search.ts +5 -19
  270. package/src/console/ui/runs/trace-link.test.ts +2 -6
  271. package/src/console/ui/runs/trace-link.ts +4 -16
  272. package/src/console/ui/runs/types.ts +1 -3
  273. package/src/console/ui/shell/App.tsx +2 -10
  274. package/src/console/ui/shell/client.ts +22 -115
  275. package/src/console/ui/shell/components/ui.tsx +1 -4
  276. package/src/console/ui/shell/layout/Shell.tsx +2 -4
  277. package/src/console/ui/shell/main.tsx +31 -96
  278. package/src/console/ui/shell/panels/access/AccessPanel.tsx +34 -98
  279. package/src/console/ui/shell/panels/ai/AiPanel.tsx +35 -106
  280. package/src/console/ui/shell/panels/architecture/ArchitecturePanel.tsx +18 -66
  281. package/src/console/ui/shell/panels/channels/ChannelsPanel.tsx +20 -57
  282. package/src/console/ui/shell/panels/clock/ClockPanel.tsx +22 -54
  283. package/src/console/ui/shell/panels/diff/DiffPanel.tsx +20 -61
  284. package/src/console/ui/shell/panels/flows/ContractEditor.tsx +6 -23
  285. package/src/console/ui/shell/panels/flows/FlowDrawer.tsx +13 -28
  286. package/src/console/ui/shell/panels/flows/FlowsPanel.tsx +9 -29
  287. package/src/console/ui/shell/panels/gates/GatesPanel.tsx +24 -80
  288. package/src/console/ui/shell/panels/overview/OverviewPanel.tsx +20 -58
  289. package/src/console/ui/shell/panels/plugins/PluginsPanel.tsx +16 -41
  290. package/src/console/ui/shell/panels/runs/RunsPanel.tsx +31 -99
  291. package/src/console/ui/shell/panels/signals/SignalsPanel.tsx +38 -101
  292. package/src/console/ui/shell/panels/store/StorePanel.tsx +27 -66
  293. package/src/console/ui/shell/panels/traces/TracesPanel.tsx +28 -89
  294. package/src/console/ui/shell/panels/vault/VaultPanel.tsx +22 -53
  295. package/src/console/ui/shell/setup/Wizard.tsx +3 -5
  296. package/src/console/ui/shell/styles.css +1 -2
  297. package/src/console/ui/signals/SignalsA11yView.tsx +9 -33
  298. package/src/console/ui/signals/a11y.test.tsx +2 -5
  299. package/src/console/ui/signals/confirmation.test.ts +1 -5
  300. package/src/console/ui/signals/confirmation.ts +2 -6
  301. package/src/console/ui/signals/dry-run.ts +1 -3
  302. package/src/console/ui/signals/durable.ts +1 -3
  303. package/src/console/ui/signals/findings.ts +1 -3
  304. package/src/console/ui/signals/group.test.ts +2 -8
  305. package/src/console/ui/signals/group.ts +2 -10
  306. package/src/console/ui/signals/index.ts +1 -4
  307. package/src/console/ui/signals/monitor.ts +2 -7
  308. package/src/console/ui/signals/schema-form.test.ts +3 -14
  309. package/src/console/ui/signals/schema-form.ts +1 -4
  310. package/src/console/ui/signals/search.ts +4 -13
  311. package/src/console/ui/store/StoreA11yView.tsx +3 -9
  312. package/src/console/ui/store/a11y.test.tsx +2 -5
  313. package/src/console/ui/store/confirmation.test.ts +1 -3
  314. package/src/console/ui/store/confirmation.ts +1 -4
  315. package/src/console/ui/store/dry-run.ts +2 -5
  316. package/src/console/ui/store/fixture.ts +1 -2
  317. package/src/console/ui/store/group.test.ts +1 -6
  318. package/src/console/ui/store/group.ts +1 -4
  319. package/src/console/ui/store/index.ts +1 -5
  320. package/src/console/ui/store/search.ts +2 -6
  321. package/src/console/ui/store/will-not-fire.ts +1 -2
  322. package/src/console/ui/traces/TracesA11yView.tsx +10 -29
  323. package/src/console/ui/traces/a11y.test.tsx +1 -2
  324. package/src/console/ui/traces/chain.test.ts +5 -19
  325. package/src/console/ui/traces/chain.ts +1 -3
  326. package/src/console/ui/traces/critical-path.test.ts +1 -3
  327. package/src/console/ui/traces/critical-path.ts +2 -6
  328. package/src/console/ui/traces/filter.test.ts +2 -6
  329. package/src/console/ui/traces/filter.ts +5 -13
  330. package/src/console/ui/traces/fold.test.ts +4 -12
  331. package/src/console/ui/traces/fold.ts +1 -3
  332. package/src/console/ui/traces/mini.ts +1 -3
  333. package/src/console/ui/traces/replay.test.ts +1 -3
  334. package/src/console/ui/traces/replay.ts +1 -3
  335. package/src/console/ui/traces/sampling.ts +1 -4
  336. package/src/console/ui/traces/search.ts +4 -17
  337. package/src/console/ui/traces/tier.ts +1 -3
  338. package/src/console/ui/vault/VaultA11yView.tsx +5 -17
  339. package/src/console/ui/vault/a11y.test.tsx +1 -2
  340. package/src/console/ui/vault/blast-radius.ts +1 -3
  341. package/src/console/ui/vault/confirmation.test.ts +1 -5
  342. package/src/console/ui/vault/confirmation.ts +1 -4
  343. package/src/console/ui/vault/dormant.test.ts +1 -4
  344. package/src/console/ui/vault/export-safe.test.ts +1 -5
  345. package/src/console/ui/vault/export-safe.ts +1 -4
  346. package/src/console/ui/vault/fixture.ts +3 -3
  347. package/src/console/ui/vault/group.ts +1 -2
  348. package/src/console/ui/vault/index.ts +2 -10
  349. package/src/console/ui/vault/search.ts +2 -6
  350. package/src/console/ui/vault/types.ts +1 -1
  351. package/src/console/xss.gate.test.ts +1 -3
  352. package/src/docker/compose.ts +112 -20
  353. package/src/docker/derive.ts +5 -19
  354. package/src/docker/docker.test.ts +15 -15
  355. package/src/docker/dockerfile.integration.test.ts +12 -21
  356. package/src/docker/helpers.ts +5 -4
  357. package/src/docker/index.ts +4 -11
  358. package/src/docker/pin.ts +1 -9
  359. package/src/docker/recipes/index.ts +5 -6
  360. package/src/docker/recipes/mailpit.ts +22 -0
  361. package/src/docker/recipes/postgres.ts +17 -6
  362. package/src/docker/recipes/redis.ts +6 -1
  363. package/src/docker/recipes/rustfs.ts +33 -0
  364. package/src/docker/stack-id.test.ts +33 -0
  365. package/src/docker/stack-id.ts +25 -13
  366. package/src/docker/stack.integration.test.ts +6 -21
  367. package/src/docker/types.ts +13 -4
  368. package/src/drivers/ai-anthropic.ts +5 -14
  369. package/src/drivers/ai-mock.ts +2 -5
  370. package/src/drivers/ai-openai-compatible.ts +4 -10
  371. package/src/drivers/ai-providers.test.ts +6 -13
  372. package/src/drivers/bun-native-completeness.test.ts +6 -15
  373. package/src/drivers/channel-console.ts +6 -7
  374. package/src/drivers/channel-fcm.ts +20 -30
  375. package/src/drivers/channel-resend.ts +1 -3
  376. package/src/drivers/channel-smtp.ts +2 -6
  377. package/src/drivers/channel-types.ts +2 -12
  378. package/src/drivers/channel-unifonic.ts +2 -6
  379. package/src/drivers/channel-wa-cloud.ts +2 -6
  380. package/src/drivers/channel-webpush.ts +21 -60
  381. package/src/drivers/conformance.test.ts +2 -10
  382. package/src/drivers/conformance.ts +8 -21
  383. package/src/drivers/fs.ts +2 -8
  384. package/src/drivers/index.ts +12 -56
  385. package/src/drivers/memory.ts +14 -41
  386. package/src/drivers/pgvector.ts +7 -11
  387. package/src/drivers/postgres.ts +7 -13
  388. package/src/drivers/redis.ts +13 -37
  389. package/src/drivers/s3.ts +5 -17
  390. package/src/drivers/signal-engine.ts +12 -49
  391. package/src/drivers/signal-memory.ts +2 -8
  392. package/src/drivers/signal-nats.ts +4 -9
  393. package/src/drivers/signal-postgres.ts +62 -129
  394. package/src/drivers/signal-redis.ts +7 -21
  395. package/src/drivers/signal-types.ts +1 -4
  396. package/src/drivers/sqlite.ts +3 -12
  397. package/src/drivers/types.ts +4 -23
  398. package/src/drivers/vault-env.ts +2 -6
  399. package/src/drivers/vault-infisical.ts +1 -5
  400. package/src/drivers/vault-managed.ts +1 -5
  401. package/src/drivers/vault-memory.ts +1 -5
  402. package/src/drivers/vault-openbao.ts +1 -5
  403. package/src/drivers/vault-sops.ts +10 -35
  404. package/src/drivers/vault-types.ts +1 -7
  405. package/src/elements/ai/declare.ts +4 -14
  406. package/src/elements/ai/eval.ts +2 -6
  407. package/src/elements/ai/pii.ts +1 -3
  408. package/src/elements/ai/runtime.ts +12 -50
  409. package/src/elements/ai/schema.ts +4 -15
  410. package/src/elements/ai.test.ts +13 -24
  411. package/src/elements/ai.ts +1 -5
  412. package/src/elements/channel/consent.ts +3 -12
  413. package/src/elements/channel/costs.ts +4 -15
  414. package/src/elements/channel/declare.ts +1 -4
  415. package/src/elements/channel/email-auth.ts +1 -4
  416. package/src/elements/channel/locale.ts +2 -10
  417. package/src/elements/channel/mask.ts +1 -3
  418. package/src/elements/channel/mime.ts +2 -12
  419. package/src/elements/channel/outcomes.test.ts +1 -5
  420. package/src/elements/channel/outcomes.ts +5 -15
  421. package/src/elements/channel/receipts.ts +2 -7
  422. package/src/elements/channel/runtime.ts +28 -74
  423. package/src/elements/channel/suppression.ts +8 -14
  424. package/src/elements/channel.test.ts +8 -31
  425. package/src/elements/channel.ts +3 -14
  426. package/src/elements/clock/actions.ts +6 -18
  427. package/src/elements/clock/declare.ts +1 -3
  428. package/src/elements/clock/dst.ts +1 -5
  429. package/src/elements/clock/durable.ts +4 -16
  430. package/src/elements/clock/health.ts +2 -7
  431. package/src/elements/clock/leader.ts +2 -9
  432. package/src/elements/clock/reconcile.ts +4 -15
  433. package/src/elements/clock/runtime.ts +2 -5
  434. package/src/elements/clock/schedule.ts +4 -20
  435. package/src/elements/clock.test.ts +2 -7
  436. package/src/elements/clock.ts +5 -23
  437. package/src/elements/gate/permissions.ts +1 -4
  438. package/src/elements/gate/runtime.ts +7 -28
  439. package/src/elements/gate.ts +1 -5
  440. package/src/elements/index.ts +9 -7
  441. package/src/elements/signal/declare.ts +2 -7
  442. package/src/elements/signal/reconcile.test.ts +1 -4
  443. package/src/elements/signal/runtime.ts +1 -3
  444. package/src/elements/signal.test.ts +3 -15
  445. package/src/elements/signal.ts +2 -8
  446. package/src/elements/store/cache.test.ts +46 -0
  447. package/src/elements/store/cache.ts +3 -11
  448. package/src/elements/store/classify.ts +5 -22
  449. package/src/elements/store/declare.ts +8 -17
  450. package/src/elements/store/domain-ddl.test.ts +86 -0
  451. package/src/elements/store/emit-drizzle.ts +363 -0
  452. package/src/elements/store/files-policy.test.ts +1 -5
  453. package/src/elements/store/files-policy.ts +4 -12
  454. package/src/elements/store/load-plugin-tables.ts +103 -0
  455. package/src/elements/store/missing-relation.test.ts +83 -0
  456. package/src/elements/store/missing-relation.ts +38 -0
  457. package/src/elements/store/replica.ts +1 -4
  458. package/src/elements/store/runtime.ts +25 -28
  459. package/src/elements/store/schema-decl.test.ts +227 -0
  460. package/src/elements/store/schema-decl.ts +516 -0
  461. package/src/elements/store/sql-condition.ts +11 -21
  462. package/src/elements/store/sql-session.test.ts +50 -0
  463. package/src/elements/store/sql-session.ts +60 -63
  464. package/src/elements/store/table.ts +35 -19
  465. package/src/elements/store.test.ts +17 -39
  466. package/src/elements/store.ts +37 -31
  467. package/src/elements/vault/chain.ts +34 -0
  468. package/src/elements/vault/declare.ts +22 -32
  469. package/src/elements/vault/fingerprint.ts +2 -7
  470. package/src/elements/vault/redact.ts +2 -6
  471. package/src/elements/vault/runtime.ts +18 -32
  472. package/src/elements/vault.test.ts +12 -28
  473. package/src/elements/vault.ts +8 -12
  474. package/src/index.ts +5 -2
  475. package/src/kernel/adopt-routes.ts +15 -29
  476. package/src/kernel/app.ts +34 -94
  477. package/src/kernel/auth-resolve.ts +2 -9
  478. package/src/kernel/boot-bind/ai.ts +1 -4
  479. package/src/kernel/boot-bind/channel.ts +2 -8
  480. package/src/kernel/boot-bind/clock.ts +2 -6
  481. package/src/kernel/boot-bind/gate.ts +2 -8
  482. package/src/kernel/boot-bind/runs.ts +1 -3
  483. package/src/kernel/boot-bind/signal.ts +3 -10
  484. package/src/kernel/boot-bind/store.test.ts +17 -17
  485. package/src/kernel/boot-bind/store.ts +44 -50
  486. package/src/kernel/boot-bind/vault.ts +2 -8
  487. package/src/kernel/boot.test.ts +4 -9
  488. package/src/kernel/boot.ts +29 -76
  489. package/src/kernel/budget.test.ts +1 -3
  490. package/src/kernel/capability.ts +3 -10
  491. package/src/kernel/dry-run.test.ts +1 -4
  492. package/src/kernel/dry-run.ts +3 -11
  493. package/src/kernel/effects.test.ts +1 -5
  494. package/src/kernel/effects.ts +4 -11
  495. package/src/kernel/errors.registry.test.ts +2 -8
  496. package/src/kernel/errors.ts +23 -32
  497. package/src/kernel/flow.test.ts +12 -25
  498. package/src/kernel/flow.ts +5 -25
  499. package/src/kernel/fx.test.ts +15 -44
  500. package/src/kernel/fx.ts +27 -79
  501. package/src/kernel/hook-timing.test.ts +3 -6
  502. package/src/kernel/hook-timing.ts +5 -14
  503. package/src/kernel/hooks.test.ts +10 -10
  504. package/src/kernel/hooks.ts +3 -12
  505. package/src/kernel/index.ts +4 -17
  506. package/src/kernel/journal.ts +7 -31
  507. package/src/kernel/on.ts +1 -6
  508. package/src/kernel/pipeline.test.ts +10 -19
  509. package/src/kernel/pipeline.ts +5 -17
  510. package/src/kernel/plug.ts +4 -11
  511. package/src/kernel/plugin/conflicts.test.ts +13 -25
  512. package/src/kernel/plugin/decorate.test.ts +8 -13
  513. package/src/kernel/plugin/scoping.test.ts +4 -11
  514. package/src/kernel/plugin.ts +48 -24
  515. package/src/kernel/registry.ts +80 -29
  516. package/src/kernel/router.test.ts +15 -13
  517. package/src/kernel/router.ts +9 -29
  518. package/src/kernel/routing-budget.test.ts +1 -3
  519. package/src/kernel/run-telemetry.ts +1 -3
  520. package/src/kernel/triggers.ts +4 -21
  521. package/src/manifest/diff.test.ts +26 -91
  522. package/src/manifest/diff.ts +95 -225
  523. package/src/manifest/index.ts +2 -9
  524. package/src/manifest/types.ts +29 -5
  525. package/src/manifest/undeclared.test.ts +4 -12
  526. package/src/manifest/undeclared.ts +1 -2
  527. package/src/manifest/validate.test.ts +2 -6
  528. package/src/manifest/validate.ts +4 -14
  529. package/src/mcp/authorization.ts +8 -27
  530. package/src/mcp/confirmation.ts +3 -10
  531. package/src/mcp/data.ts +2 -6
  532. package/src/mcp/docs-index.ts +4 -7
  533. package/src/mcp/docs-mcp.test.ts +3 -12
  534. package/src/mcp/docs-server.ts +7 -27
  535. package/src/mcp/docs-tools.ts +2 -6
  536. package/src/mcp/index.ts +1 -6
  537. package/src/mcp/injection.gate.test.ts +4 -18
  538. package/src/mcp/mcp.test.ts +18 -49
  539. package/src/mcp/protocol.ts +3 -10
  540. package/src/mcp/server.ts +11 -52
  541. package/src/mcp/session.ts +4 -14
  542. package/src/mcp/tools.ts +14 -50
  543. package/src/plugins/catalogue.test.ts +3 -7
  544. package/src/plugins/catalogue.ts +1 -3
  545. package/src/plugins/node-import-scan.ts +2 -5
  546. package/src/plugins/supply-chain.ts +5 -17
  547. package/src/release/exports.ts +3 -8
  548. package/src/release/measure.exports.test.ts +1 -3
  549. package/src/release/measure.ts +15 -43
  550. package/src/runs/bench.test.ts +1 -3
  551. package/src/runs/collect.ts +8 -25
  552. package/src/runs/drivers/clickhouse.ts +2 -9
  553. package/src/runs/drivers/files.ts +9 -31
  554. package/src/runs/drivers/memory.ts +5 -22
  555. package/src/runs/drivers/postgres.ts +8 -20
  556. package/src/runs/duckdb.ts +2 -8
  557. package/src/runs/index.ts +3 -14
  558. package/src/runs/outlier.ts +3 -10
  559. package/src/runs/parquet.ts +8 -26
  560. package/src/runs/privacy.ts +2 -8
  561. package/src/runs/runs.test.ts +3 -9
  562. package/src/runs/runtime.ts +7 -32
  563. package/src/runs/shred.ts +10 -29
  564. package/src/runs/types.ts +1 -4
  565. package/src/runtime/bun.ts +4 -15
  566. package/src/runtime/cold-start.bench.ts +1 -3
  567. package/src/runtime/dev-request-log.test.ts +1 -4
  568. package/src/runtime/dev-request-log.ts +1 -4
  569. package/src/runtime/index.ts +1 -6
  570. package/src/runtime/primitives.ts +5 -4
  571. package/src/runtime/security.ts +3 -13
  572. package/src/runtime/serve.test.ts +4 -7
  573. package/src/runtime/types.ts +1 -4
  574. package/src/runtime/web-standard.ts +3 -15
  575. package/src/term.test.ts +4 -6
  576. package/src/term.ts +49 -98
  577. package/src/test/create-test-app.test.ts +4 -14
  578. package/src/test/create-test-app.ts +13 -31
  579. package/src/test/provisions.integration.test.ts +3 -11
  580. package/src/upgrade/codemods.test.ts +25 -6
  581. package/src/upgrade/codemods.ts +116 -3
  582. package/src/validation/standard-schema.test.ts +2 -5
  583. package/src/validation/standard-schema.ts +13 -33
  584. package/src/console/ui/dist/assets/panel-vault-BbfWdox0.js +0 -1
@@ -1,21 +1,23 @@
1
1
  # OKE — Four Applications
2
+
2
3
  ### A progressive path from one flow to a full system
3
4
 
4
5
  **Package:** `okengine` · **CLI:** `oke`
5
6
 
6
7
  Four complete, runnable applications. Each one introduces the smallest possible set of new ideas, and each ends by naming the limitation that motivates the next. Read them in order and the eight elements arrive one or two at a time instead of all at once.
7
8
 
8
- | | App | Teaches | Elements | Exports | Files |
9
- |---|---|---|---|---|---|
10
- | **1 · Basic** | Notes | the one law · contracts · typed errors · the client | Flow · Store | 4 / 10 | 5 |
11
- | **2 · Intermediate** | Linkly | one species, many triggers · delivery physics · transactional emit | + Signal · Clock · Gate | 7 / 10 | 11 |
12
- | **3 · Advanced** | Provisions | durability · reaching humans · live queries · plugins | + Vault · Channel | 10 / 10 | 18 |
13
- | **4 · Complex** | Skyport | AI and agents · tenancy · SLOs · distributed topology | all eight | all ten | 24 |
9
+ | | App | Teaches | Elements | Exports | Files |
10
+ | -------------------- | ---------- | ------------------------------------------------------------------ | ----------------------- | ------- | ----- |
11
+ | **1 · Basic** | Notes | the one law · contracts · typed errors · the client | Flow · Store | 4 / 10 | 5 |
12
+ | **2 · Intermediate** | Linkly | one species, many triggers · delivery physics · transactional emit | + Signal · Clock · Gate | 7 / 10 | 11 |
13
+ | **3 · Advanced** | Provisions | durability · reaching humans · live queries · plugins | + Vault · Channel | 10 / 10 | 18 |
14
+ | **4 · Complex** | Skyport | AI and agents · tenancy · SLOs · distributed topology | all eight | all ten | 24 |
14
15
 
15
16
  **The ten exports:** `on · flow · signal · store · clock · gate · vault · channel · ai · plugin`
16
17
  **The one law:** `on(Trigger) → Effects`
17
18
 
18
19
  ---
20
+
19
21
  ---
20
22
 
21
23
  # 1 · BASIC — Notes
@@ -45,8 +47,8 @@ export default defineConfig({
45
47
  drivers: {
46
48
  store: {
47
49
  sql: {
48
- dev: "sqlite",
49
- stack: "postgres",
50
+ local: "sqlite",
51
+ docker: "postgres",
50
52
  test: "memory",
51
53
  prod: "postgres",
52
54
  },
@@ -64,9 +66,9 @@ import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
64
66
  import { id, now } from "okengine/store";
65
67
 
66
68
  export const notes = sqliteTable("notes", {
67
- id: text("id").primaryKey().$defaultFn(id),
68
- title: text("title").notNull(),
69
- body: text("body").notNull(),
69
+ id: text("id").primaryKey().$defaultFn(id),
70
+ title: text("title").notNull(),
71
+ body: text("body").notNull(),
70
72
  createdAt: integer("created_at").notNull().$defaultFn(now),
71
73
  });
72
74
  ```
@@ -94,43 +96,57 @@ import { db } from "../../core";
94
96
  import { notes } from "../../schema";
95
97
 
96
98
  // Contracts derived from the schema — one source of truth, refined where the API is stricter
97
- const NewNote = createInsertSchema(notes, { title: (s) => s.min(1).max(120) })
98
- .omit({ id: true, createdAt: true });
99
- const Note = createSelectSchema(notes);
100
- const NoteId = z.object({ id: z.string() });
99
+ const NewNote = createInsertSchema(notes, { title: (s) => s.min(1).max(120) }).omit({
100
+ id: true,
101
+ createdAt: true,
102
+ });
103
+ const Note = createSelectSchema(notes);
104
+ const NoteId = z.object({ id: z.string() });
101
105
  const NotFound = z.object({});
102
106
 
103
- export const create = on(http.post("/notes"), flow({
104
- in: NewNote,
105
- out: NoteId,
106
- do: async (input, fx) => {
107
- const [note] = await fx.store(db).insert(notes).values(input).returning();
108
- return { id: note.id };
109
- },
110
- }));
107
+ export const create = on(
108
+ http.post("/notes"),
109
+ flow({
110
+ in: NewNote,
111
+ out: NoteId,
112
+ do: async (input, fx) => {
113
+ const [note] = await fx.store(db).insert(notes).values(input).returning();
114
+ return { id: note.id };
115
+ },
116
+ }),
117
+ );
111
118
  // effects → writes[sql:notes]
112
119
 
113
- export const list = on(http.get("/notes"), flow({
114
- out: Note.array(),
115
- do: (_, fx) => fx.store(db).select().from(notes),
116
- }));
120
+ export const list = on(
121
+ http.get("/notes"),
122
+ flow({
123
+ out: Note.array(),
124
+ do: (_, fx) => fx.store(db).select().from(notes),
125
+ }),
126
+ );
117
127
  // effects → reads[sql:notes]
118
128
 
119
- export const get = on(http.get("/notes/:id"), flow({
120
- in: NoteId,
121
- out: Note,
122
- errors: { NotFound },
123
- do: async ({ id }, fx) => (await fx.store(db).findById(notes, id)) ?? fx.fail("NotFound", {}),
124
- }));
125
-
126
- export const remove = on(http.delete("/notes/:id"), flow({
127
- in: NoteId,
128
- errors: { NotFound },
129
- do: async ({ id }, fx) => {
130
- const deleted = await fx.store(db).delete(notes, id);
131
- if (!deleted) return fx.fail("NotFound", {});
132
- },
133
- }));
129
+ export const get = on(
130
+ http.get("/notes/:id"),
131
+ flow({
132
+ in: NoteId,
133
+ out: Note,
134
+ errors: { NotFound },
135
+ do: async ({ id }, fx) => (await fx.store(db).findById(notes, id)) ?? fx.fail("NotFound", {}),
136
+ }),
137
+ );
138
+
139
+ export const remove = on(
140
+ http.delete("/notes/:id"),
141
+ flow({
142
+ in: NoteId,
143
+ errors: { NotFound },
144
+ do: async ({ id }, fx) => {
145
+ const deleted = await fx.store(db).delete(notes, id);
146
+ if (!deleted) return fx.fail("NotFound", {});
147
+ },
148
+ }),
149
+ );
134
150
  ```
135
151
 
136
152
  **Four things to notice.**
@@ -151,7 +167,7 @@ import * as notes from "./flows/notes";
151
167
 
152
168
  export const app = oke({ name: "notes" }).adopt({ notes });
153
169
 
154
- export type App = typeof app; // ← the client needs nothing else
170
+ export type App = typeof app; // ← the client needs nothing else
155
171
  ```
156
172
 
157
173
  `on()` still registers each flow with the router and the Manifest — `.adopt()` exists so the type of `app` accumulates every contract in `notes`, which is what lets the client below need no hand-written types and no separate codegen step. The namespace key (`notes`) becomes the client's namespace; each export becomes a method.
@@ -169,7 +185,7 @@ const api = createClient<App>("http://localhost:6530", { $routes: app.$routes })
169
185
  const { data, error } = await api.notes.get({ id: "n_1" });
170
186
 
171
187
  if (error?.code === "NotFound") show("gone");
172
- else console.log(data.title); // ← typed, no codegen ✅
188
+ else console.log(data.title); // ← typed, no codegen ✅
173
189
  // GET /notes/n_1 — the method and path are derived from the flow's own trigger,
174
190
  // not from a separate RPC convention.
175
191
  ```
@@ -182,7 +198,7 @@ import { createTestApp } from "okengine/test";
182
198
  import { app } from "../src/app";
183
199
 
184
200
  test("create then read", async () => {
185
- const t = await createTestApp(app); // memory driver, automatic
201
+ const t = await createTestApp(app); // memory driver, automatic
186
202
  const { data } = await t.api.notes.create({ title: "First", body: "Hello" });
187
203
  const { data: note } = await t.api.notes.get({ id: data!.id });
188
204
  expect(note!.title).toBe("First");
@@ -205,9 +221,10 @@ Four exports (`oke`, `on`, `flow`, `store`), two elements, a typed client, autom
205
221
 
206
222
  ### What is missing
207
223
 
208
- Everything here is synchronous. A real application needs work that happens *later* — after the response, on a schedule, or in reaction to something. That is the next app.
224
+ Everything here is synchronous. A real application needs work that happens _later_ — after the response, on a schedule, or in reaction to something. That is the next app.
209
225
 
210
226
  ---
227
+
211
228
  ---
212
229
 
213
230
  # 2 · INTERMEDIATE — Linkly
@@ -223,6 +240,8 @@ linkly/
223
240
  │ ├── app.ts
224
241
  │ ├── core.ts
225
242
  │ ├── gates.ts
243
+ │ ├── schema.decl.ts
244
+ │ ├── schema.generated.ts
226
245
  │ ├── schema.ts
227
246
  │ └── flows/
228
247
  │ ├── links/
@@ -240,10 +259,12 @@ import { defineConfig } from "okengine/config";
240
259
 
241
260
  export default defineConfig({
242
261
  drivers: {
243
- store: { sql: { dev: "sqlite", test: "memory", prod: "postgres" },
244
- kv: { dev: "memory", test: "memory", prod: "redis" } },
245
- signal: { dev: "memory", test: "memory", prod: "postgres" },
246
- clock: { dev: "memory", test: "frozen", prod: "postgres" },
262
+ store: {
263
+ sql: { local: "sqlite", test: "memory", prod: "postgres" },
264
+ kv: { local: "memory", test: "memory", prod: "redis" },
265
+ },
266
+ signal: { local: "memory", test: "memory", prod: "postgres" },
267
+ clock: { local: "memory", test: "frozen", prod: "postgres" },
247
268
  },
248
269
  });
249
270
  ```
@@ -258,36 +279,60 @@ import { gate } from "okengine";
258
279
  export const member = gate.policy("member", ({ auth }) => !!auth?.verified);
259
280
 
260
281
  export const fair = gate.rate({
261
- strategy: "sliding-window-counter", // near-exact, two keys, no boundary bursts
262
- max: 60, per: "1m", keyBy: "ip",
282
+ strategy: "sliding-window-counter", // near-exact, two keys, no boundary bursts
283
+ max: 60,
284
+ per: "1m",
285
+ keyBy: "ip",
263
286
  });
264
287
  ```
265
288
 
266
289
  Five strategies exist (`fixed-window`, `sliding-log`, `token-bucket`, `leaky-bucket`); this one is the default because it has the best accuracy-to-cost ratio.
267
290
 
268
- ### `src/schema.ts`
291
+ ### `src/schema.decl.ts`
269
292
 
270
293
  ```typescript
271
- import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
272
-
273
- export const links = sqliteTable("links", {
274
- id: text("id").primaryKey(), // `increment` targets this column
275
- code: text("code").notNull().unique(), // the short, human-facing key
276
- url: text("url").notNull(),
277
- userId: text("user_id").notNull(),
278
- clicks: integer("clicks").notNull().default(0),
279
- createdAt: integer("created_at").notNull(),
294
+ import { store, field } from "okengine";
295
+
296
+ /** Short links `code` is the human-facing unique key. */
297
+ export const links = store.schema.table("links", {
298
+ id: field.text().primaryKey(), // `increment` targets this column
299
+ code: field.text().notNull().unique(), // the short, human-facing key
300
+ url: field.text().notNull(),
301
+ userId: field.text().notNull(),
302
+ clicks: field.integer().notNull().default(0),
303
+ createdAt: field.integer().notNull(),
280
304
  });
281
305
 
282
- export const daily = sqliteTable("daily", {
283
- id: text("id").primaryKey(),
284
- code: text("code").notNull(),
285
- day: text("day").notNull(), // "YYYY-MM-DD"
286
- clicks: integer("clicks").notNull().default(0),
306
+ /** Per-link daily click counts — FK to `links.code`. */
307
+ export const daily = store.schema.table("daily", {
308
+ id: field.text().primaryKey(),
309
+ code: field
310
+ .text()
311
+ .notNull()
312
+ .references(() => links.code),
313
+ day: field.text().notNull(), // "YYYY-MM-DD"
314
+ clicks: field.integer().notNull().default(0),
287
315
  });
316
+
317
+ /** links ↔ daily — one link has many daily rows. */
318
+ export const relations = store.schema.relations({ links, daily }, (r) => ({
319
+ links: {
320
+ daily: r.many.daily({
321
+ from: r.links.code,
322
+ to: r.daily.code,
323
+ }),
324
+ },
325
+ daily: {
326
+ link: r.one.links({
327
+ from: r.daily.code,
328
+ to: r.links.code,
329
+ optional: false,
330
+ }),
331
+ },
332
+ }));
288
333
  ```
289
334
 
290
- A generated `id` plus a separate unique `code` is the standard shape for a shortener: `increment` — see below — is a store-level primitive that targets a row by its primary key, so every table it touches needs one.
335
+ A generated `id` plus a separate unique `code` is the standard shape for a shortener: `increment` — see below — is a store-level primitive that targets a row by its primary key, so every table it touches needs one. `daily.code` references `links.code`; `store.schema.relations` mirrors Drizzle’s `defineRelations`.
291
336
 
292
337
  ### `src/flows/links/signals.ts`
293
338
 
@@ -297,13 +342,14 @@ import { z } from "zod";
297
342
 
298
343
  export const linkClicked = signal("link-clicked", {
299
344
  schema: z.object({ code: z.string(), at: z.number(), referrer: z.string().optional() }),
300
- delivery: "once", // queue physics: one consumer, retries, DLQ
301
- retries: 3, deadLetter: true,
345
+ delivery: "once", // queue physics: one consumer, retries, DLQ
346
+ retries: 3,
347
+ deadLetter: true,
302
348
  });
303
349
 
304
350
  export const linkStats = signal("link-stats", {
305
351
  schema: z.object({ code: z.string(), clicks: z.number() }),
306
- delivery: "live", // stream physics: clients subscribe
352
+ delivery: "live", // stream physics: clients subscribe
307
353
  });
308
354
  ```
309
355
 
@@ -321,52 +367,75 @@ import { NewLink, LinkCode, Link, NotFound, Taken } from "./shapes";
321
367
  import { links } from "../../schema";
322
368
 
323
369
  // ① HTTP — "an endpoint"
324
- export const shorten = on(http.post("/links").gate(member, fair), flow({
325
- in: NewLink, out: LinkCode, errors: { Taken },
326
- do: async ({ url, code }, fx) => {
327
- if (await fx.store(db).exists(links, { code })) return fx.fail("Taken", {});
328
- const id = fx.id();
329
- await fx.store(db).insert(links).values(
330
- { id, code, url, userId: fx.auth.userId, clicks: 0, createdAt: Date.now() });
331
- return { code };
332
- },
333
- }));
370
+ export const shorten = on(
371
+ http.post("/links").gate(member, fair),
372
+ flow({
373
+ in: NewLink,
374
+ out: LinkCode,
375
+ errors: { Taken },
376
+ do: async ({ url, code }, fx) => {
377
+ if (await fx.store(db).exists(links, { code })) return fx.fail("Taken", {});
378
+ const id = fx.id();
379
+ await fx
380
+ .store(db)
381
+ .insert(links)
382
+ .values({ id, code, url, userId: fx.auth.userId, clicks: 0, createdAt: Date.now() });
383
+ return { code };
384
+ },
385
+ }),
386
+ );
334
387
 
335
388
  // ② HTTP — the hot path
336
- export const redirect = on(http.get("/:code").gate(fair), flow({
337
- in: LinkCode, out: Link, errors: { NotFound },
338
- do: async ({ code }, fx) => {
339
- const [link] = await fx.store(db).select().from(links).where(eq(links.code, code)).limit(1);
340
- if (!link) return fx.fail("NotFound", {});
341
-
342
- await fx.emit(linkClicked, { code, at: Date.now() }); // same transaction as any write
343
- return link;
344
- },
345
- }));
389
+ export const redirect = on(
390
+ http.get("/:code").gate(fair),
391
+ flow({
392
+ in: LinkCode,
393
+ out: Link,
394
+ errors: { NotFound },
395
+ do: async ({ code }, fx) => {
396
+ const [link] = await fx.store(db).select().from(links).where(eq(links.code, code)).limit(1);
397
+ if (!link) return fx.fail("NotFound", {});
398
+
399
+ await fx.emit(linkClicked, { code, at: Date.now() }); // same transaction as any write
400
+ return link;
401
+ },
402
+ }),
403
+ );
346
404
 
347
405
  // ③ SIGNAL — "a queue consumer", and the same species as ① and ②
348
- on(linkClicked, flow({
349
- do: async ({ code }, fx) => {
350
- const [link] = await fx.store(db).select().from(links).where(eq(links.code, code)).limit(1);
351
- const clicks = await fx.store(db).increment(links, link.id, "clicks");
352
- await fx.emit(linkStats, { code, clicks }); // live: pushed to subscribers
353
- },
354
- }));
406
+ on(
407
+ linkClicked,
408
+ flow({
409
+ do: async ({ code }, fx) => {
410
+ const [link] = await fx.store(db).select().from(links).where(eq(links.code, code)).limit(1);
411
+ const clicks = await fx.store(db).increment(links, link.id, "clicks");
412
+ await fx.emit(linkStats, { code, clicks }); // live: pushed to subscribers
413
+ },
414
+ }),
415
+ );
355
416
 
356
417
  // ④ CLOCK — "a cron job", and still the same species
357
- on(every("1h"), flow({
358
- do: (_, fx) => {
359
- const cutoff = Date.now() - 30 * 24 * 60 * 60 * 1000; // 30 days
360
- return fx.store(db).delete(links).where(lt(links.createdAt, cutoff));
361
- },
362
- }));
418
+ on(
419
+ every("1h"),
420
+ flow({
421
+ do: (_, fx) => {
422
+ const cutoff = Date.now() - 30 * 24 * 60 * 60 * 1000; // 30 days
423
+ return fx.store(db).delete(links).where(lt(links.createdAt, cutoff));
424
+ },
425
+ }),
426
+ );
363
427
 
364
428
  // ⑤ A plain flow with no trigger — callable, not "private"
365
429
  export const stats = flow({
366
- in: LinkCode, out: z.object({ clicks: z.number() }),
430
+ in: LinkCode,
431
+ out: z.object({ clicks: z.number() }),
367
432
  do: async ({ code }, fx) => {
368
- const [link] = await fx.store(db).select({ clicks: links.clicks })
369
- .from(links).where(eq(links.code, code)).limit(1);
433
+ const [link] = await fx
434
+ .store(db)
435
+ .select({ clicks: links.clicks })
436
+ .from(links)
437
+ .where(eq(links.code, code))
438
+ .limit(1);
370
439
  return link ?? { clicks: 0 };
371
440
  },
372
441
  });
@@ -385,22 +454,37 @@ import { db } from "../../core";
385
454
  import { member } from "../../gates";
386
455
  import { daily } from "../../schema";
387
456
 
388
- on(linkClicked, flow({ // a second consumer of the same signal
389
- do: async ({ code, at }, fx) => {
390
- const day = new Date(at).toISOString().slice(0, 10);
391
- const [row] = await fx.store(db).select().from(daily)
392
- .where(and(eq(daily.code, code), eq(daily.day, day))).limit(1);
393
-
394
- if (row) await fx.store(db).increment(daily, row.id, "clicks");
395
- else await fx.store(db).insert(daily).values({ id: fx.id(), code, day, clicks: 1 });
396
- },
397
- }));
398
-
399
- export const report = on(http.get("/links/:code/report").gate(member), flow({
400
- out: z.array(z.object({ day: z.string(), clicks: z.number() })),
401
- do: ({ code }, fx) => fx.store(db).select({ day: daily.day, clicks: daily.clicks })
402
- .from(daily).where(eq(daily.code, code)),
403
- }));
457
+ on(
458
+ linkClicked,
459
+ flow({
460
+ // a second consumer of the same signal
461
+ do: async ({ code, at }, fx) => {
462
+ const day = new Date(at).toISOString().slice(0, 10);
463
+ const [row] = await fx
464
+ .store(db)
465
+ .select()
466
+ .from(daily)
467
+ .where(and(eq(daily.code, code), eq(daily.day, day)))
468
+ .limit(1);
469
+
470
+ if (row) await fx.store(db).increment(daily, row.id, "clicks");
471
+ else await fx.store(db).insert(daily).values({ id: fx.id(), code, day, clicks: 1 });
472
+ },
473
+ }),
474
+ );
475
+
476
+ export const report = on(
477
+ http.get("/links/:code/report").gate(member),
478
+ flow({
479
+ out: z.array(z.object({ day: z.string(), clicks: z.number() })),
480
+ do: ({ code }, fx) =>
481
+ fx
482
+ .store(db)
483
+ .select({ day: daily.day, clicks: daily.clicks })
484
+ .from(daily)
485
+ .where(eq(daily.code, code)),
486
+ }),
487
+ );
404
488
  ```
405
489
 
406
490
  This unit never imports anything from `links` except the signal declaration. Decoupling is structural, not a discipline.
@@ -429,18 +513,18 @@ api.signals.linkStats.subscribe(({ code, clicks }) => paint(code, clicks));
429
513
  ### `tests/linkly.test.ts`
430
514
 
431
515
  ```typescript
432
- const t = await createTestApp(app); // memory drivers, frozen clock
516
+ const t = await createTestApp(app); // memory drivers, frozen clock
433
517
  const u = await t.auth.loginAs({});
434
518
 
435
519
  await t.api.links.shorten({ url: "https://example.com", code: "sa" }, { as: u });
436
520
  await t.api.links.redirect({ code: "sa" });
437
- await t.signals.drain(); // run queued work deterministically
521
+ await t.signals.drain(); // run queued work deterministically
438
522
 
439
523
  const { data } = await t.api.links.report({ code: "sa" }, { as: u });
440
524
  expect(data![0].clicks).toBe(1);
441
525
 
442
526
  await t.clock.advance("31d");
443
- await t.cron.run("1h"); // time travel
527
+ await t.cron.run("1h"); // time travel
444
528
  ```
445
529
 
446
530
  ### What you have
@@ -452,6 +536,7 @@ Seven exports, five elements, and five different trigger kinds — all of them t
452
536
  Nothing here survives a deploy. A payment that must wait two minutes for confirmation, an email that must actually reach a person, an order page that updates itself — none of that is expressible yet.
453
537
 
454
538
  ---
539
+
455
540
  ---
456
541
 
457
542
  # 3 · ADVANCED — Provisions
@@ -479,7 +564,7 @@ provisions/
479
564
  └── tests/orders.test.ts
480
565
  ```
481
566
 
482
- **The layout rule from here on: whoever *produces* an element declares it; consumers import it.** Shared concerns (the database, shared gates, secrets, channels) sit at the root. The framework never forces this — the Manifest is built from the import graph — but the tree teaches the vocabulary.
567
+ **The layout rule from here on: whoever _produces_ an element declares it; consumers import it.** Shared concerns (the database, shared gates, secrets, channels) sit at the root. The framework never forces this — the Manifest is built from the import graph — but the tree teaches the vocabulary.
483
568
 
484
569
  ### `src/schema.ts`
485
570
 
@@ -487,17 +572,17 @@ provisions/
487
572
  import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
488
573
 
489
574
  export const products = sqliteTable("products", {
490
- sku: text("sku").primaryKey(),
491
- name: text("name").notNull(),
575
+ sku: text("sku").primaryKey(),
576
+ name: text("name").notNull(),
492
577
  stock: integer("stock").notNull().default(0),
493
578
  });
494
579
 
495
580
  export const orders = sqliteTable("orders", {
496
- id: text("id").primaryKey(),
497
- userId: text("user_id").notNull(),
498
- sku: text("sku").notNull(),
499
- qty: integer("qty").notNull(),
500
- status: text("status").notNull().default("pending"),
581
+ id: text("id").primaryKey(),
582
+ userId: text("user_id").notNull(),
583
+ sku: text("sku").notNull(),
584
+ qty: integer("qty").notNull(),
585
+ status: text("status").notNull().default("pending"),
501
586
  createdAt: integer("created_at").notNull(),
502
587
  });
503
588
  ```
@@ -509,7 +594,7 @@ import { vault } from "okengine";
509
594
  import { z } from "zod";
510
595
 
511
596
  // A declaration is a CONTRACT, not a value.
512
- // Resolution: process.env → .env.local → .env.stack → vault driver → dev fallback
597
+ // Resolution: process.env → .env.local → docker/.env.docker → vault driver → dev fallback
513
598
  export const stripeKey = vault.secret("STRIPE_KEY", {
514
599
  schema: z.string().startsWith("sk_"),
515
600
  description: "Payments gateway key",
@@ -519,7 +604,7 @@ export const stripeKey = vault.secret("STRIPE_KEY", {
519
604
 
520
605
  export const dbUrl = vault.secret("DATABASE_URL", {
521
606
  schema: z.string().url(),
522
- dev: vault.fromStack("store.sql"), // generated by `oke dev --stack` — zero manual setup
607
+ dev: vault.fromDocker("store.sql"), // generated by `oke dev --docker` — zero manual setup
523
608
  });
524
609
  ```
525
610
 
@@ -532,14 +617,15 @@ import { channel } from "okengine";
532
617
  import { z } from "zod";
533
618
 
534
619
  export const mail = channel.email({ from: "Provisions <no-reply@provisions.sa>" });
535
- export const sms = channel.sms({ sender: "PROVISIONS" });
536
- export const wa = channel.whatsapp();
620
+ export const sms = channel.sms({ sender: "PROVISIONS" });
621
+ export const wa = channel.whatsapp();
537
622
 
538
623
  export const orderConfirmed = mail.template("order-confirmed", {
539
624
  schema: z.object({ name: z.string(), orderId: z.string(), total: z.number() }),
540
625
  });
541
626
 
542
- export const otpCode = channel.template("otp-code", { // medium-agnostic
627
+ export const otpCode = channel.template("otp-code", {
628
+ // medium-agnostic
543
629
  schema: z.object({ code: z.string() }),
544
630
  });
545
631
  ```
@@ -560,31 +646,49 @@ import { orders, products } from "../../schema";
560
646
 
561
647
  const canOrder = gate.policy("order:create", ({ auth }) => auth.scopes.has("order:create"));
562
648
 
563
- export const create = on(http.post("/orders").gate(member, canOrder), flow({
564
- in: NewOrder, out: OrderId, errors: { OutOfStock },
565
- do: async (input, fx) => {
566
- const [product] = await fx.store(db).select({ stock: products.stock })
567
- .from(products).where(eq(products.sku, input.sku)).limit(1);
568
- if (!product || product.stock < input.qty) return fx.fail("OutOfStock",
569
- { left: product?.stock ?? 0 },
570
- { message: fx.t("order.outOfStock", { left: product?.stock ?? 0 }) });
571
-
572
- const id = fx.id();
573
- await fx.store(db).insert(orders).values(
574
- { id, userId: fx.auth.userId, ...input, status: "pending", createdAt: Date.now() });
575
- await fx.emit(orderPlaced, { orderId: id });
576
- return { id };
577
- },
578
- }));
649
+ export const create = on(
650
+ http.post("/orders").gate(member, canOrder),
651
+ flow({
652
+ in: NewOrder,
653
+ out: OrderId,
654
+ errors: { OutOfStock },
655
+ do: async (input, fx) => {
656
+ const [product] = await fx
657
+ .store(db)
658
+ .select({ stock: products.stock })
659
+ .from(products)
660
+ .where(eq(products.sku, input.sku))
661
+ .limit(1);
662
+ if (!product || product.stock < input.qty)
663
+ return fx.fail(
664
+ "OutOfStock",
665
+ { left: product?.stock ?? 0 },
666
+ { message: fx.t("order.outOfStock", { left: product?.stock ?? 0 }) },
667
+ );
668
+
669
+ const id = fx.id();
670
+ await fx
671
+ .store(db)
672
+ .insert(orders)
673
+ .values({ id, userId: fx.auth.userId, ...input, status: "pending", createdAt: Date.now() });
674
+ await fx.emit(orderPlaced, { orderId: id });
675
+ return { id };
676
+ },
677
+ }),
678
+ );
579
679
 
580
680
  // LIVE QUERY — realtime and auto-caching from one flag
581
- export const mine = on(http.get("/orders").gate(member).live(), flow({
582
- out: OrderRow.array(),
583
- do: (_, fx) => fx.store(db).select().from(orders).where(eq(orders.userId, fx.auth.userId)),
584
- }));
681
+ export const mine = on(
682
+ http.get("/orders").gate(member).live(),
683
+ flow({
684
+ out: OrderRow.array(),
685
+ do: (_, fx) => fx.store(db).select().from(orders).where(eq(orders.userId, fx.auth.userId)),
686
+ }),
687
+ );
585
688
 
586
689
  export const getOrder = flow({
587
- in: OrderId, out: OrderRow,
690
+ in: OrderId,
691
+ out: OrderRow,
588
692
  do: async ({ id }, fx) => {
589
693
  const [order] = await fx.store(db).select().from(orders).where(eq(orders.id, id)).limit(1);
590
694
  return order;
@@ -592,19 +696,28 @@ export const getOrder = flow({
592
696
  });
593
697
 
594
698
  // SIGNAL consumer
595
- on(orderPlaced, flow({
596
- do: async ({ orderId }, fx) => {
597
- const paid = await fx.call(chargeOrder, { orderId });
598
- await fx.store(db).update(orders).set({ status: paid ? "confirmed" : "failed" })
599
- .where(eq(orders.id, orderId));
600
- await fx.emit(orderNews, { orderId, status: paid ? "confirmed" : "failed" });
601
- },
602
- }));
699
+ on(
700
+ orderPlaced,
701
+ flow({
702
+ do: async ({ orderId }, fx) => {
703
+ const paid = await fx.call(chargeOrder, { orderId });
704
+ await fx
705
+ .store(db)
706
+ .update(orders)
707
+ .set({ status: paid ? "confirmed" : "failed" })
708
+ .where(eq(orders.id, orderId));
709
+ await fx.emit(orderNews, { orderId, status: paid ? "confirmed" : "failed" });
710
+ },
711
+ }),
712
+ );
603
713
 
604
714
  // CHANGE trigger — CDC, built in
605
- on(db.table(orders).changed("status"), flow({
606
- do: ({ before, after }, fx) => fx.log.info("status", { from: before.status, to: after.status }),
607
- }));
715
+ on(
716
+ db.table(orders).changed("status"),
717
+ flow({
718
+ do: ({ before, after }, fx) => fx.log.info("status", { from: before.status, to: after.status }),
719
+ }),
720
+ );
608
721
  ```
609
722
 
610
723
  **`.live()` is the whole of realtime.** The result is cached, invalidated by exactly the writes that touch those rows, and pushed to subscribed clients on exactly those writes. No cache code, no socket code.
@@ -618,13 +731,16 @@ import { stripeKey } from "../../vault";
618
731
  import { OrderRef } from "./shapes";
619
732
 
620
733
  export const chargeOrder = flow({
621
- durable: true, // every fx call below is journaled
622
- in: OrderRef, out: z.boolean(),
734
+ durable: true, // every fx call below is journaled
735
+ in: OrderRef,
736
+ out: z.boolean(),
623
737
  do: async ({ orderId }, fx) => {
624
- const intent = await fx.step("create-intent", () => // never re-runs on replay
625
- stripe(fx.vault(stripeKey)).create(orderId));
738
+ const intent = await fx.step("create-intent", () =>
739
+ // never re-runs on replay
740
+ stripe(fx.vault(stripeKey)).create(orderId),
741
+ );
626
742
 
627
- await fx.clock.sleep("verify-window", "2m"); // survives restart and deploy
743
+ await fx.clock.sleep("verify-window", "2m"); // survives restart and deploy
628
744
 
629
745
  return fx.step("confirm", () => stripe(fx.vault(stripeKey)).confirm(intent));
630
746
  },
@@ -642,13 +758,19 @@ import { orderNews } from "../orders/signals";
642
758
  import { getOrder } from "../orders";
643
759
  import { orderConfirmed, otpCode, wa, sms } from "../../channels";
644
760
 
645
- on(orderNews, flow({
646
- do: async ({ orderId, status }, fx) => {
647
- if (status !== "confirmed") return;
648
- const o = await fx.call(getOrder, { id: orderId });
649
- await fx.send(orderConfirmed, { to: o.userId, data: { name: o.userName, orderId, total: o.total } });
650
- },
651
- }));
761
+ on(
762
+ orderNews,
763
+ flow({
764
+ do: async ({ orderId, status }, fx) => {
765
+ if (status !== "confirmed") return;
766
+ const o = await fx.call(getOrder, { id: orderId });
767
+ await fx.send(orderConfirmed, {
768
+ to: o.userId,
769
+ data: { name: o.userName, orderId, total: o.total },
770
+ });
771
+ },
772
+ }),
773
+ );
652
774
 
653
775
  export const sendOtp = flow({
654
776
  in: z.object({ userId: z.string(), code: z.string() }),
@@ -690,11 +812,11 @@ import * as notifications from "./flows/notifications";
690
812
 
691
813
  export const app = oke({ name: "provisions" })
692
814
  .adopt({ orders, payments, notifications })
693
- .plug(auth()) // zero ceremony: uses your configured store
694
- .plug(audit) // app-wide
815
+ .plug(auth()) // zero ceremony: uses your configured store
816
+ .plug(audit) // app-wide
695
817
  .hook("onError", (ctx, err, fx) => fx.log.error(err));
696
818
 
697
- app.unit("orders").plug(rateLimit({ max: 30 })); // this unit only
819
+ app.unit("orders").plug(rateLimit({ max: 30 })); // this unit only
698
820
 
699
821
  export type App = typeof app;
700
822
  ```
@@ -725,16 +847,17 @@ Typed keys — a missing key is a compile error. Locale resolves per request: us
725
847
  ### `tests/orders.test.ts`
726
848
 
727
849
  ```typescript
728
- const t = await createTestApp(app); // memory drivers, frozen clock
850
+ const t = await createTestApp(app); // memory drivers, frozen clock
729
851
  const u = await t.auth.loginAs({ scopes: ["order:create"] });
730
852
 
731
853
  const { data } = await t.api.orders.create({ sku: "COFFEE", qty: 2 }, { as: u });
732
854
  await t.signals.drain();
733
- await t.clock.advance("2m"); // the durable sleep elapses instantly
855
+ await t.clock.advance("2m"); // the durable sleep elapses instantly
734
856
  await t.signals.drain();
735
857
 
736
858
  expect(t.channels.sent()).toContainEqual(
737
- expect.objectContaining({ template: "order-confirmed", to: u.id, locale: "ar" }));
859
+ expect.objectContaining({ template: "order-confirmed", to: u.id, locale: "ar" }),
860
+ );
738
861
  ```
739
862
 
740
863
  ### What you have
@@ -746,6 +869,7 @@ All ten exports and seven of the eight elements. Durable execution, human-facing
746
869
  The system serves one customer, treats every user the same, and has no way to state what "working" means. It also cannot reason about anything.
747
870
 
748
871
  ---
872
+
749
873
  ---
750
874
 
751
875
  # 4 · COMPLEX — Skyport
@@ -782,41 +906,44 @@ export default defineConfig({
782
906
  // (Bun.sql, bun:sqlite, Bun.redis, Bun.S3) — zero npm client dependencies.
783
907
  drivers: {
784
908
  store: {
785
- sql: { dev: "sqlite", test: "memory",
786
- prod: { driver: "postgres", url: dbUrl, pool: { max: 20 },
787
- replicas: [dbReplica1] } }, // read-only flows auto-route here
788
- kv: { dev: "memory", test: "memory", prod: "redis" }, // Redis · Valkey · Dragonfly
789
- files: { dev: "fs", test: "memory", prod: "s3" }, // S3 · R2 · SeaweedFS · MinIO
790
- index: { dev: "pgvector", test: "memory", prod: "pgvector" },
909
+ sql: {
910
+ local: "sqlite",
911
+ test: "memory",
912
+ prod: { driver: "postgres", url: dbUrl, pool: { max: 20 }, replicas: [dbReplica1] },
913
+ }, // read-only flows auto-route here
914
+ kv: { local: "memory", test: "memory", prod: "redis" }, // Redis · Valkey · Dragonfly
915
+ files: { local: "fs", test: "memory", prod: "s3" }, // S3 · R2 · SeaweedFS · MinIO
916
+ index: { local: "pgvector", test: "memory", prod: "pgvector" },
791
917
  },
792
- signal: { dev: "memory", test: "memory", prod: "postgres" },
793
- clock: { dev: "memory", test: "frozen", prod: "postgres" },
794
- vault: { dev: "dotenv", test: "memory", prod: "sops" }, // SOPS/age — committable
795
- runs: { dev: "files", test: "memory", prod: "files" }, // Parquet + DuckDB
918
+ signal: { local: "memory", test: "memory", prod: "postgres" },
919
+ clock: { local: "memory", test: "frozen", prod: "postgres" },
920
+ vault: { local: "dotenv", test: "memory", prod: "sops" }, // SOPS/age — committable
921
+ runs: { local: "files", test: "memory", prod: "files" }, // Parquet + DuckDB
796
922
  channel: {
797
- email: { dev: "console", prod: "smtp" },
798
- sms: { dev: "console", prod: "unifonic" },
799
- whatsapp: { dev: "console", prod: "wa-cloud" },
800
- push: { dev: "console", prod: "fcm" },
923
+ email: { local: "console", prod: "smtp" },
924
+ sms: { local: "console", prod: "unifonic" },
925
+ whatsapp: { local: "console", prod: "wa-cloud" },
926
+ push: { local: "console", prod: "fcm" },
801
927
  },
802
928
  ai: {
803
- dev: "mock", // deterministic — tests never call out
929
+ local: "mock", // deterministic — tests never call out
804
930
  prod: { driver: "anthropic", key: anthropicKey },
805
931
  // no prod default: model choice is never guessed.
806
932
  // "openai-compatible" covers vLLM · Groq · Together · LM Studio · most self-hosted
807
933
  },
808
934
  },
809
935
 
810
- images: { // vendor choice, keyed by ROLE
936
+ images: {
937
+ // vendor choice, keyed by ROLE
811
938
  "store.sql": "pgvector/pgvector:pg17",
812
- "store.kv": "valkey/valkey:8-alpine",
939
+ "store.kv": "valkey/valkey:8-alpine",
813
940
  },
814
941
 
815
- i18n: { locales: ["en", "ar"], default: "ar", dir: { ar: "rtl" } },
816
- tenancy: { resolve: (ctx) => ctx.auth.orgId, isolation: "row" },
817
- topology: "monolith", // flip to "services" — code unchanged
818
- ports: { app: 6530, console: 6533, mcp: 6535 }, // O·K·E = 6·5·3
819
- console: { prod: { enabled: true, auth: "required" } },
942
+ i18n: { locales: ["en", "ar"], default: "ar", dir: { ar: "rtl" } },
943
+ tenancy: { resolve: (ctx) => ctx.auth.orgId, isolation: "row" },
944
+ topology: "monolith", // flip to "services" — code unchanged
945
+ ports: { app: 6530, console: 6533, mcp: 6535 }, // O·K·E = 6·5·3
946
+ console: { prod: { enabled: true, auth: "required" } },
820
947
  });
821
948
  ```
822
949
 
@@ -827,22 +954,25 @@ import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
827
954
  import { id } from "okengine/store";
828
955
 
829
956
  export const bookings = sqliteTable("bookings", {
830
- id: text("id").primaryKey().$defaultFn(id),
831
- userId: text("user_id").notNull(),
832
- flightId: text("flight_id").notNull(),
833
- seats: integer("seats").notNull(),
834
- status: text("status").notNull().default("pending"),
957
+ id: text("id").primaryKey().$defaultFn(id),
958
+ userId: text("user_id").notNull(),
959
+ flightId: text("flight_id").notNull(),
960
+ seats: integer("seats").notNull(),
961
+ status: text("status").notNull().default("pending"),
835
962
  createdAt: integer("created_at").notNull(),
836
963
  });
837
964
 
838
965
  export const flights = sqliteTable("flights", {
839
- id: text("id").primaryKey(),
966
+ id: text("id").primaryKey(),
840
967
  seatsAvailable: integer("seats_available").notNull(),
841
968
  });
842
969
 
843
970
  export const tickets = sqliteTable("tickets", {
844
- id: text("id").primaryKey(), subject: text("subject").notNull(),
845
- body: text("body").notNull(), urgency: text("urgency"), team: text("team"),
971
+ id: text("id").primaryKey(),
972
+ subject: text("subject").notNull(),
973
+ body: text("body").notNull(),
974
+ urgency: text("urgency"),
975
+ team: text("team"),
846
976
  summary: text("summary"),
847
977
  });
848
978
  ```
@@ -852,10 +982,10 @@ export const tickets = sqliteTable("tickets", {
852
982
  ```typescript
853
983
  import { z } from "zod";
854
984
 
855
- export const NewBooking = z.object({ flightId: z.string(), seats: z.number().min(1).max(9) });
856
- export const BookingId = z.object({ id: z.string() });
857
- export const BookingRow = z.object({ id: z.string(), status: z.string(), seats: z.number() });
858
- export const FlightFull = z.object({ seatsLeft: z.number() });
985
+ export const NewBooking = z.object({ flightId: z.string(), seats: z.number().min(1).max(9) });
986
+ export const BookingId = z.object({ id: z.string() });
987
+ export const BookingRow = z.object({ id: z.string(), status: z.string(), seats: z.number() });
988
+ export const FlightFull = z.object({ seatsLeft: z.number() });
859
989
  ```
860
990
 
861
991
  ### `src/flows/bookings/signals.ts`
@@ -865,10 +995,14 @@ import { signal } from "okengine";
865
995
  import { z } from "zod";
866
996
 
867
997
  export const orderPlaced = signal("order-placed", {
868
- schema: z.object({ orderId: z.string() }), delivery: "once", retries: 5, deadLetter: true,
998
+ schema: z.object({ orderId: z.string() }),
999
+ delivery: "once",
1000
+ retries: 5,
1001
+ deadLetter: true,
869
1002
  });
870
1003
  export const seatFeed = signal("seat-feed", {
871
- schema: z.object({ flightId: z.string(), left: z.number() }), delivery: "live",
1004
+ schema: z.object({ flightId: z.string(), left: z.number() }),
1005
+ delivery: "live",
872
1006
  });
873
1007
  ```
874
1008
 
@@ -883,32 +1017,54 @@ import { orderPlaced, seatFeed } from "./signals";
883
1017
  import { NewBooking, BookingId, BookingRow, FlightFull } from "./shapes";
884
1018
  import { bookings, flights } from "../../schema";
885
1019
 
886
- export const canBook = gate.policy("booking:create", ({ auth }) => auth.scopes.has("booking:create"));
887
-
888
- export const create = on(http.post("/bookings").gate(member, canBook, fair), flow({
889
- slo: { availability: "99.9%", latency: { p99: "200ms" } },
890
- in: NewBooking, out: BookingId, errors: { FlightFull },
891
- do: async ({ flightId, seats }, fx) => {
892
- const [flight] = await fx.store(db).select().from(flights).where(eq(flights.id, flightId)).limit(1);
893
- if (!flight || flight.seatsAvailable < seats)
894
- return fx.fail("FlightFull", { seatsLeft: flight?.seatsAvailable ?? 0 });
895
-
896
- const id = fx.id();
897
- await fx.store(db).insert(bookings).values(
898
- { id, userId: fx.auth.userId, flightId, seats, status: "pending", createdAt: Date.now() });
899
- await fx.emit(orderPlaced, { orderId: id });
900
- await fx.emit(seatFeed, { flightId, left: flight.seatsAvailable - seats });
901
- return { id };
902
- },
903
- }));
1020
+ export const canBook = gate.policy("booking:create", ({ auth }) =>
1021
+ auth.scopes.has("booking:create"),
1022
+ );
1023
+
1024
+ export const create = on(
1025
+ http.post("/bookings").gate(member, canBook, fair),
1026
+ flow({
1027
+ slo: { availability: "99.9%", latency: { p99: "200ms" } },
1028
+ in: NewBooking,
1029
+ out: BookingId,
1030
+ errors: { FlightFull },
1031
+ do: async ({ flightId, seats }, fx) => {
1032
+ const [flight] = await fx
1033
+ .store(db)
1034
+ .select()
1035
+ .from(flights)
1036
+ .where(eq(flights.id, flightId))
1037
+ .limit(1);
1038
+ if (!flight || flight.seatsAvailable < seats)
1039
+ return fx.fail("FlightFull", { seatsLeft: flight?.seatsAvailable ?? 0 });
1040
+
1041
+ const id = fx.id();
1042
+ await fx.store(db).insert(bookings).values({
1043
+ id,
1044
+ userId: fx.auth.userId,
1045
+ flightId,
1046
+ seats,
1047
+ status: "pending",
1048
+ createdAt: Date.now(),
1049
+ });
1050
+ await fx.emit(orderPlaced, { orderId: id });
1051
+ await fx.emit(seatFeed, { flightId, left: flight.seatsAvailable - seats });
1052
+ return { id };
1053
+ },
1054
+ }),
1055
+ );
904
1056
 
905
- export const mine = on(http.get("/bookings").gate(member).live(), flow({
906
- out: BookingRow.array(),
907
- do: (_, fx) => fx.store(db).select().from(bookings).where(eq(bookings.userId, fx.auth.userId)),
908
- }));
1057
+ export const mine = on(
1058
+ http.get("/bookings").gate(member).live(),
1059
+ flow({
1060
+ out: BookingRow.array(),
1061
+ do: (_, fx) => fx.store(db).select().from(bookings).where(eq(bookings.userId, fx.auth.userId)),
1062
+ }),
1063
+ );
909
1064
 
910
1065
  export const getBooking = flow({
911
- in: BookingId, out: BookingRow,
1066
+ in: BookingId,
1067
+ out: BookingRow,
912
1068
  do: async ({ id }, fx) => {
913
1069
  const [b] = await fx.store(db).select().from(bookings).where(eq(bookings.id, id)).limit(1);
914
1070
  return b;
@@ -918,7 +1074,8 @@ export const getBooking = flow({
918
1074
  // The agent's second tool — refunding is a distinct, gated capability, never the same
919
1075
  // permission as reading a booking, since the agent's tool list is exactly its authority.
920
1076
  export const refundBooking = flow({
921
- in: BookingId, out: BookingRow,
1077
+ in: BookingId,
1078
+ out: BookingRow,
922
1079
  do: async ({ id }, fx) => {
923
1080
  await fx.store(db).update(bookings).set({ status: "refunded" }).where(eq(bookings.id, id));
924
1081
  const [b] = await fx.store(db).select().from(bookings).where(eq(bookings.id, id)).limit(1);
@@ -935,15 +1092,15 @@ import { z } from "zod";
935
1092
  import { getBooking, refundBooking } from "./flows/bookings";
936
1093
 
937
1094
  export const smart = ai.model("smart", { provider: "anthropic", tier: "opus" });
938
- export const fast = ai.model("fast", { provider: "anthropic", tier: "haiku" });
1095
+ export const fast = ai.model("fast", { provider: "anthropic", tier: "haiku" });
939
1096
 
940
1097
  // A prompt is a VERSIONED ARTIFACT with a validated output shape — not a string in a handler
941
1098
  export const triage = smart.prompt("ticket-triage", {
942
- in: z.object({ subject: z.string(), body: z.string() }),
1099
+ in: z.object({ subject: z.string(), body: z.string() }),
943
1100
  out: z.object({ urgency: z.enum(["low", "high"]), team: z.string(), summary: z.string() }),
944
1101
  version: 3,
945
- evals: "./evals/triage.jsonl", // regression-gated in CI via `oke eval`
946
- budget: { maxCostPerCall: 0.02 }, // cost is a first-class dimension
1102
+ evals: "./evals/triage.jsonl", // regression-gated in CI via `oke eval`
1103
+ budget: { maxCostPerCall: 0.02 }, // cost is a first-class dimension
947
1104
  });
948
1105
 
949
1106
  export const embed = ai.embed("docs", { model: fast, into: store.index("kb") });
@@ -968,36 +1125,48 @@ import { db } from "../../core";
968
1125
  import { tickets } from "../../schema";
969
1126
 
970
1127
  // ① A prompt call with a provider fallback chain and a validated result
971
- export const createTicket = on(http.post("/tickets").gate(member), flow({
972
- in: z.object({ subject: z.string(), body: z.string() }),
973
- out: z.object({ id: z.string(), urgency: z.string() }),
974
- do: async (input, fx) => {
975
- const t = await fx.ask(triage, input, { via: [smart, fast] });
976
- const id = fx.id();
977
- await fx.store(db).insert(tickets).values({ id, ...input, ...t });
978
- return { id, urgency: t.urgency };
979
- },
980
- }));
1128
+ export const createTicket = on(
1129
+ http.post("/tickets").gate(member),
1130
+ flow({
1131
+ in: z.object({ subject: z.string(), body: z.string() }),
1132
+ out: z.object({ id: z.string(), urgency: z.string() }),
1133
+ do: async (input, fx) => {
1134
+ const t = await fx.ask(triage, input, { via: [smart, fast] });
1135
+ const id = fx.id();
1136
+ await fx
1137
+ .store(db)
1138
+ .insert(tickets)
1139
+ .values({ id, ...input, ...t });
1140
+ return { id, urgency: t.urgency };
1141
+ },
1142
+ }),
1143
+ );
981
1144
  // effects → writes[sql:tickets] asks[ticket-triage v3] cost[~$0.01] nondeterministic
982
1145
 
983
1146
  // ② RAG — retrieve, then answer with streaming tokens
984
- export const askDocs = on(http.post("/ask").gate(member).live(), flow({
985
- in: z.object({ question: z.string() }),
986
- do: async ({ question }, fx) => {
987
- const context = await fx.search(embed, question, { topK: 5 });
988
- return fx.stream(smart, { prompt: "answer-with-context", data: { question, context } });
989
- // streaming reaches the client through the Signal element — no separate socket layer
990
- },
991
- }));
1147
+ export const askDocs = on(
1148
+ http.post("/ask").gate(member).live(),
1149
+ flow({
1150
+ in: z.object({ question: z.string() }),
1151
+ do: async ({ question }, fx) => {
1152
+ const context = await fx.search(embed, question, { topK: 5 });
1153
+ return fx.stream(smart, { prompt: "answer-with-context", data: { question, context } });
1154
+ // streaming reaches the client through the Signal element — no separate socket layer
1155
+ },
1156
+ }),
1157
+ );
992
1158
 
993
1159
  // ③ A durable, bounded agent
994
- export const supportAgent = on(http.post("/support").gate(member), flow({
995
- durable: true, // nondeterministic calls are ALWAYS journaled
996
- in: z.object({ message: z.string() }),
997
- do: ({ message }, fx) => fx.run(support, { message }),
998
- // the agent can only call getBooking and refundBooking, and only within THIS user's
999
- // gates and tenant scope it cannot exceed what the code declares
1000
- }));
1160
+ export const supportAgent = on(
1161
+ http.post("/support").gate(member),
1162
+ flow({
1163
+ durable: true, // nondeterministic calls are ALWAYS journaled
1164
+ in: z.object({ message: z.string() }),
1165
+ do: ({ message }, fx) => fx.run(support, { message }),
1166
+ // the agent can only call getBooking and refundBooking, and only within THIS user's
1167
+ // gates and tenant scope — it cannot exceed what the code declares
1168
+ }),
1169
+ );
1001
1170
  ```
1002
1171
 
1003
1172
  **What the compiler enforces here, for free:**
@@ -1005,7 +1174,7 @@ export const supportAgent = on(http.post("/support").gate(member), flow({
1005
1174
  - A field tagged `pii` in the schema **cannot reach a third-party model** — the build fails unless the flow masks it or declares `allowPii` explicitly.
1006
1175
  - `nondeterministic` forces journaling: on replay, a model is never re-called; the recorded answer is reused.
1007
1176
  - Automatic caching is disabled for AI flows unless a semantic cache is explicitly enabled.
1008
- - Cost accumulates per flow, per tenant and per release — visible in the Console and in Manifest Diff *before* deploy.
1177
+ - Cost accumulates per flow, per tenant and per release — visible in the Console and in Manifest Diff _before_ deploy.
1009
1178
 
1010
1179
  ### Declaring what "working" means
1011
1180
 
@@ -1024,7 +1193,7 @@ journey("book-a-flight", {
1024
1193
  });
1025
1194
  ```
1026
1195
 
1027
- Forty services at 99.9% in sequence yield 96.1% for the user. Because the causal chain is known, **the compiler rejects the impossible**: *"this path composes to 99.4% but declares 99.5%."* And because the objective lives in the Manifest, lowering a target is a code change that passes through Manifest Diff and team review — not a silent dashboard edit.
1196
+ Forty services at 99.9% in sequence yield 96.1% for the user. Because the causal chain is known, **the compiler rejects the impossible**: _"this path composes to 99.4% but declares 99.5%."_ And because the objective lives in the Manifest, lowering a target is a code change that passes through Manifest Diff and team review — not a silent dashboard edit.
1028
1197
 
1029
1198
  ### Multi-tenancy as a dimension of `fx`
1030
1199
 
@@ -1032,11 +1201,11 @@ Forty services at 99.9% in sequence yield 96.1% for the user. Because the causal
1032
1201
 
1033
1202
  ### The three scaling axes, never conflated
1034
1203
 
1035
- | Axis | Question | Mechanism |
1036
- |---|---|---|
1037
- | **Split** (`topology`) | one deployable, or one per unit? | `monolith` = in-process calls · `services` = a container per unit, `fx.call` becomes network — code unchanged |
1038
- | **Clone** (horizontal) | how many copies of the app? | run N instances: `once` signals deliver to exactly one, crons leader-elect, live queries fan out. `oke docker --prod` emits `deploy.replicas` |
1039
- | **Data replicas** | how many copies of the data? | `replicas:` on the driver; read-only flows auto-route, derived from effects |
1204
+ | Axis | Question | Mechanism |
1205
+ | ---------------------- | -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
1206
+ | **Split** (`topology`) | one deployable, or one per unit? | `monolith` = in-process calls · `services` = a container per unit, `fx.call` becomes network — code unchanged |
1207
+ | **Clone** (horizontal) | how many copies of the app? | run N instances: `once` signals deliver to exactly one, crons leader-elect, live queries fan out. `oke docker --prod` emits `deploy.replicas` |
1208
+ | **Data replicas** | how many copies of the data? | `replicas:` on the driver; read-only flows auto-route, derived from effects |
1040
1209
 
1041
1210
  ### `src/app.ts`
1042
1211
 
@@ -1071,7 +1240,7 @@ const u = await t.auth.loginAs({});
1071
1240
  const { data } = await t.api.support.createTicket({ subject: "…", body: "…" }, { as: u });
1072
1241
 
1073
1242
  expect(data!.urgency).toBe("high");
1074
- expect(t.ai.cost()).toBeLessThan(0.02); // budgets are assertable
1243
+ expect(t.ai.cost()).toBeLessThan(0.02); // budgets are assertable
1075
1244
  ```
1076
1245
 
1077
1246
  ### What you have
@@ -1079,6 +1248,7 @@ expect(t.ai.cost()).toBeLessThan(0.02); // budgets are assertable
1079
1248
  All eight elements, all ten exports, and one law.
1080
1249
 
1081
1250
  ---
1251
+
1082
1252
  ---
1083
1253
 
1084
1254
  # REFERENCE
@@ -1093,19 +1263,24 @@ All eight elements, all ten exports, and one law.
1093
1263
  "bookings.create": {
1094
1264
  "trigger": { "http": { "method": "POST", "path": "/bookings" } },
1095
1265
  "gates": ["member", "booking:create", "rate:sliding-window-counter:300/1m"],
1096
- "in": "…", "out": "…", "errors": ["FlightFull"],
1266
+ "in": "…",
1267
+ "out": "…",
1268
+ "errors": ["FlightFull"],
1097
1269
  "effects": {
1098
- "reads": ["sql:bookings"],
1270
+ "reads": ["sql:bookings"],
1099
1271
  "writes": ["sql:bookings"],
1100
- "emits": ["order-placed", "seat-feed"],
1272
+ "emits": ["order-placed", "seat-feed"],
1101
1273
  "secrets": []
1102
1274
  },
1103
1275
  "slo": { "availability": "99.9%", "latency": { "p99": "200ms" } },
1104
1276
  "source": "src/flows/bookings/index.ts:18"
1105
1277
  },
1106
1278
  "bookings.mine": { "live": true, "cacheKeys": "computed:sql:bookings/userId" },
1107
- "payments.chargeBooking": { "durable": true, "steps": ["create-intent", "confirm"],
1108
- "effects": { "secrets": ["STRIPE_KEY"] } },
1279
+ "payments.chargeBooking": {
1280
+ "durable": true,
1281
+ "steps": ["create-intent", "confirm"],
1282
+ "effects": { "secrets": ["STRIPE_KEY"] }
1283
+ },
1109
1284
  "support.createTicket": {
1110
1285
  "effects": { "writes": ["sql:tickets"], "asks": ["ticket-triage@3"] },
1111
1286
  "nondeterministic": true,
@@ -1115,14 +1290,15 @@ All eight elements, all ten exports, and one law.
1115
1290
  },
1116
1291
  "signals": {
1117
1292
  "order-placed": { "delivery": "once", "retries": 5, "deadLetter": true },
1118
- "seat-feed": { "delivery": "live" }
1293
+ "seat-feed": { "delivery": "live" }
1119
1294
  },
1120
1295
  "channels": { "booking-confirmed": { "medium": "email", "locales": ["en", "ar"] } },
1121
1296
  "ai": {
1122
- "models": { "smart": { "provider": "anthropic", "tier": "opus" } },
1297
+ "models": { "smart": { "provider": "anthropic", "tier": "opus" } },
1123
1298
  "prompts": { "ticket-triage": { "version": 3, "evals": "./evals/triage.jsonl" } },
1124
- "agents": { "support": { "tools": ["bookings.getBooking", "bookings.refundBooking"],
1125
- "maxSteps": 6 } }
1299
+ "agents": {
1300
+ "support": { "tools": ["bookings.getBooking", "bookings.refundBooking"], "maxSteps": 6 }
1301
+ }
1126
1302
  },
1127
1303
  "journeys": { "book-a-flight": { "slo": { "availability": "99.5%" }, "composes": "99.6%" } },
1128
1304
  "drivers": { "prod": ["postgres", "redis", "s3", "smtp", "sops", "anthropic", "pgvector"] },
@@ -1138,15 +1314,21 @@ From this one file OKE derives: the typed client · OpenAPI + AsyncAPI · the Co
1138
1314
  bun add okengine # ONE package
1139
1315
 
1140
1316
  oke dev # watch · hot reload · Console :6533 · app :6530 · MCP :6535
1141
- # → also auto-syncs client types on every save
1142
- oke dev --stack # -s infra compose under docker/ (no app container; host Bun)
1143
- oke dev -s store.sql,signal # partial: only these roles get real backends
1317
+ # → client types + domain schema push on save (local)
1318
+ oke dev --local # -l session-only local (in-memory; never writes .oke/mode)
1319
+ oke dev --no-db-push # opt out of auto oke db push on schema change
1320
+ oke dev --docker # -d infra compose under docker/ (no app container; host Bun)
1321
+ oke dev -d store.sql,signal # partial: only these roles get real backends
1322
+ oke mode local|docker # get/set default oke dev mode (saved in .oke/mode)
1144
1323
 
1145
1324
  oke start # runs exactly what production runs (this is the Docker CMD)
1146
1325
  oke doctor # verify secrets, ports, drivers, tenancy, schema drift
1147
1326
  oke stack # preview resolved images/tags/ports — writes nothing
1148
1327
 
1149
- oke schema generate # core + plugin tables → schema/oke.ts (--check in CI)
1328
+ oke schema generate # core + plugin stubs → schema/oke.ts (--check in CI)
1329
+ oke db push # domain schema.ts → live local DB (dev; drizzle-kit)
1330
+ oke db generate # versioned SQL under drizzle/ (review)
1331
+ oke db migrate # apply migrations (explicit; never auto in prod)
1150
1332
  oke vault set STRIPE_KEY # also: list · import .env · key rotate
1151
1333
  oke client add <url> # types for a separate frontend repo
1152
1334
 
@@ -1156,7 +1338,6 @@ oke images pin # tags → digests in oke.images.lock
1156
1338
 
1157
1339
  oke build --target edge # < 15 kB kernel profile
1158
1340
  oke eval # run prompt eval sets; fails CI on regression
1159
- oke ai cost --since 7d # cost per flow, per tenant, per prompt version
1160
1341
  oke branch prod --at "yesterday" # fork journaled state into a sandbox
1161
1342
  oke privacy erase --subject <id> # crypto-shredding: deletes the key, not the terabytes
1162
1343
  oke upgrade # run codemods for a breaking change, print the diff
@@ -1172,23 +1353,23 @@ Runtime actions execute directly; structural changes arrive as reviewable diffs
1172
1353
 
1173
1354
  **Why Drizzle is a required peer dependency, not an abstraction.** The framework commits to one query builder rather than supporting several, because the effect inferencer performs real static analysis on Drizzle's own shapes — a table object, `.select().from(t)`, `.insert(t).values()` — to derive `reads`/`writes`/PII classification with no annotation from you. Supporting N ORMs would mean either analysing N different query builders (and getting it wrong for the ones nobody tests) or falling back to hints, which is exactly the annotation burden the effect system exists to remove. One committed ORM is what makes automatic cache invalidation and least-privilege capability tokens possible at all.
1174
1355
 
1175
- **When you still need `fx.id()` despite `$defaultFn(id)`.** The schema default fills the `id` column at insert time — fine when nothing in the flow needs the value beforehand, as in Notes' `create` (the id is only read back from `.returning()`). Generate it explicitly with `fx.id()`, and pass it into `.values({ id, … })` yourself, whenever the flow needs the same id *before or alongside* the insert — to reference it in an emitted signal payload, to use it as a foreign key in a second insert in the same flow, or to return it without a round-trip. Linkly's `shorten` is the pattern: `const id = fx.id()` because the row and any signal about it need to agree on the same identifier within one flow body.
1356
+ **When you still need `fx.id()` despite `$defaultFn(id)`.** The schema default fills the `id` column at insert time — fine when nothing in the flow needs the value beforehand, as in Notes' `create` (the id is only read back from `.returning()`). Generate it explicitly with `fx.id()`, and pass it into `.values({ id, … })` yourself, whenever the flow needs the same id _before or alongside_ the insert — to reference it in an emitted signal payload, to use it as a foreign key in a second insert in the same flow, or to return it without a round-trip. Linkly's `shorten` is the pattern: `const id = fx.id()` because the row and any signal about it need to agree on the same identifier within one flow body.
1176
1357
 
1177
1358
  ## Element checklist across the four applications
1178
1359
 
1179
- | Element | First appears | The unification it proves |
1180
- |---|---|---|
1181
- | **Flow** | Basic | endpoint = consumer = cron = CDC = workflow — one species |
1182
- | **Store** | Basic | sql · kv · files · index; cache and replica routing derived from effects |
1183
- | **Signal** | Intermediate | queue = pub/sub = stream; delivery is a property, not three ecosystems |
1184
- | **Clock** | Intermediate | cron = delay = durable time |
1185
- | **Gate** | Intermediate | auth = ABAC = rate limit, composable at the trigger |
1186
- | **Vault** | Advanced | typed contracts, boot-time validation, per-flow read capability |
1187
- | **Channel** | Advanced | email = sms = whatsapp = push; consent, locale and fallback built in |
1188
- | **AI** | Complex | prompts versioned · agents bounded by your own flows · cost and PII enforced by the compiler |
1189
- | **`fx` door** | Basic | journaling · tests · least privilege · transactions · tenancy · i18n · cost |
1190
- | **Manifest** | Basic | client · docs · console · security · bundle · infrastructure |
1191
- | **Plugin** | Advanced | extends through the same law; built-ins have no private API |
1360
+ | Element | First appears | The unification it proves |
1361
+ | ------------- | ------------- | -------------------------------------------------------------------------------------------- |
1362
+ | **Flow** | Basic | endpoint = consumer = cron = CDC = workflow — one species |
1363
+ | **Store** | Basic | sql · kv · files · index; cache and replica routing derived from effects |
1364
+ | **Signal** | Intermediate | queue = pub/sub = stream; delivery is a property, not three ecosystems |
1365
+ | **Clock** | Intermediate | cron = delay = durable time |
1366
+ | **Gate** | Intermediate | auth = ABAC = rate limit, composable at the trigger |
1367
+ | **Vault** | Advanced | typed contracts, boot-time validation, per-flow read capability |
1368
+ | **Channel** | Advanced | email = sms = whatsapp = push; consent, locale and fallback built in |
1369
+ | **AI** | Complex | prompts versioned · agents bounded by your own flows · cost and PII enforced by the compiler |
1370
+ | **`fx` door** | Basic | journaling · tests · least privilege · transactions · tenancy · i18n · cost |
1371
+ | **Manifest** | Basic | client · docs · console · security · bundle · infrastructure |
1372
+ | **Plugin** | Advanced | extends through the same law; built-ins have no private API |
1192
1373
 
1193
1374
  **Ten exports — `on, flow, signal, store, clock, gate, vault, channel, ai, plugin` — one law.**
1194
1375