okengine 0.19.0 → 0.19.2

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 (110) hide show
  1. package/package.json +1 -1
  2. package/site/content/docs/ai/meta.json +1 -1
  3. package/site/content/docs/ai/skills.mdx +1 -1
  4. package/site/content/docs/client/index.mdx +1 -2
  5. package/site/content/docs/elements/ai/agents.mdx +5 -2
  6. package/site/content/docs/elements/ai/index.mdx +4 -3
  7. package/site/content/docs/elements/ai/prompts.mdx +3 -2
  8. package/site/content/docs/elements/channel/email.mdx +5 -2
  9. package/site/content/docs/elements/channel/index.mdx +1 -1
  10. package/site/content/docs/elements/clock/index.mdx +23 -12
  11. package/site/content/docs/elements/clock/schedules.mdx +6 -3
  12. package/site/content/docs/elements/clock/sleep.mdx +10 -7
  13. package/site/content/docs/elements/flow/consumers.mdx +96 -58
  14. package/site/content/docs/elements/flow/index.mdx +15 -15
  15. package/site/content/docs/elements/flow/routing.mdx +11 -2
  16. package/site/content/docs/elements/gate/tenancy.mdx +5 -2
  17. package/site/content/docs/elements/signal/broadcast.mdx +6 -4
  18. package/site/content/docs/elements/signal/index.mdx +14 -1
  19. package/site/content/docs/elements/signal/live.mdx +3 -2
  20. package/site/content/docs/elements/signal/once.mdx +3 -2
  21. package/site/content/docs/elements/store/files.mdx +24 -16
  22. package/site/content/docs/elements/store/index.mdx +11 -8
  23. package/site/content/docs/elements/store/kv.mdx +24 -16
  24. package/site/content/docs/elements/store/search.mdx +18 -14
  25. package/site/content/docs/elements/store/sql.mdx +16 -12
  26. package/site/content/docs/elements/vault/config.mdx +5 -2
  27. package/site/content/docs/elements/vault/rotation.mdx +5 -2
  28. package/site/content/docs/elements/vault/secrets.mdx +5 -2
  29. package/site/content/docs/index.mdx +4 -19
  30. package/site/content/docs/plugins/anonymous.mdx +1 -1
  31. package/site/content/docs/plugins/cors.mdx +1 -1
  32. package/site/content/docs/plugins/csrf.mdx +1 -1
  33. package/site/content/docs/plugins/headers.mdx +1 -1
  34. package/site/content/docs/plugins/ip-allowlist.mdx +1 -1
  35. package/site/content/docs/plugins/maintenance-mode.mdx +1 -1
  36. package/site/content/docs/reference/cli.mdx +1 -1
  37. package/site/content/docs/reference/errors.mdx +1 -0
  38. package/site/content/docs/reference/fx.mdx +9 -8
  39. package/site/content/docs/reference/okid.mdx +1 -1
  40. package/site/content/docs/reference/plugins.mdx +1 -1
  41. package/site/content/docs/reference/security.mdx +2 -2
  42. package/site/content/docs/understand/meta.json +1 -1
  43. package/site/content/docs/understand/the-architecture.mdx +264 -0
  44. package/src/bench/REPORT.md +48 -17
  45. package/src/bench/g17-hybrid-search.bench.ts +13 -13
  46. package/src/compiler/extract.test.ts +76 -1
  47. package/src/compiler/extract.ts +106 -7
  48. package/src/compiler/search-writer-isolation.test.ts +0 -1
  49. package/src/console/ui-next/dist/assets/{access-page-DFeymU07.js → access-page-DceEWH9u.js} +1 -1
  50. package/src/console/ui-next/dist/assets/{agent-disclosure-U1rdfblp.js → agent-disclosure-tC9s2VFd.js} +1 -1
  51. package/src/console/ui-next/dist/assets/{cache-glyph-BeFJeqBG.js → cache-glyph-C-naNQSR.js} +1 -1
  52. package/src/console/ui-next/dist/assets/{call-pii-button-CVAONPii.js → call-pii-button-DnZ_MlDn.js} +1 -1
  53. package/src/console/ui-next/dist/assets/{collapsible-D2A6NJ-3.js → collapsible-RekgR6Qz.js} +1 -1
  54. package/src/console/ui-next/dist/assets/{duration-tone-Cgk_h5ja.js → duration-tone-DugtWBS0.js} +1 -1
  55. package/src/console/ui-next/dist/assets/flows-page-DVmn1ZuQ.js +1 -0
  56. package/src/console/ui-next/dist/assets/{highlighted-json-MYZQtRnw.js → highlighted-json-CvBGaiSD.js} +1 -1
  57. package/src/console/ui-next/dist/assets/{http-method-Jrh39p7A.js → http-method-D7_OXbdC.js} +1 -1
  58. package/src/console/ui-next/dist/assets/{index-DXP2dBIF.js → index-DH0K2f6N.js} +3 -3
  59. package/src/console/ui-next/dist/assets/{observability-page-HvolXxTI.js → observability-page-qJzF2nSN.js} +1 -1
  60. package/src/console/ui-next/dist/assets/{replica-lag-CSh2dzrb.js → replica-lag-Vqk0pUBA.js} +1 -1
  61. package/src/console/ui-next/dist/assets/request-meta-BtShi4sG.js +1 -0
  62. package/src/console/ui-next/dist/assets/{store-page-eiKiHnNe.js → store-page-CMsYH_vH.js} +1 -1
  63. package/src/console/ui-next/dist/assets/{trace-detail-sheet-CZkMeKS-.js → trace-detail-sheet-ycFB2uua.js} +1 -1
  64. package/src/console/ui-next/dist/assets/{tree-expand-toggle-CW8y5A2h.js → tree-expand-toggle-iG1jcWgU.js} +1 -1
  65. package/src/console/ui-next/dist/assets/{units-page-B_RWJrEO.js → units-page-Bavchke4.js} +1 -1
  66. package/src/console/ui-next/dist/assets/{vault-page-CWrg-A68.js → vault-page-DxUFhiZI.js} +1 -1
  67. package/src/console/ui-next/dist/index.html +1 -1
  68. package/src/console/ui-next/src/features/flows/graph/element-map.test.ts +1 -1
  69. package/src/console/ui-next/src/features/flows/graph/element-map.ts +3 -3
  70. package/src/console/ui-next/src/features/flows/traces/trace-detail.test.ts +4 -5
  71. package/src/console/ui-next/src/features/flows/traces/trace-gates.ts +3 -6
  72. package/src/elements/gate/declare.ts +1 -1
  73. package/src/elements/gate.ts +1 -1
  74. package/src/elements/store/live-default.test.ts +8 -0
  75. package/src/elements/store/search-embed-flow.ts +2 -2
  76. package/src/elements/store/search-lsh.ts +35 -0
  77. package/src/elements/store/search-runtime.pglite.test.ts +186 -0
  78. package/src/elements/store/search-runtime.ts +30 -16
  79. package/src/elements/store/search.test.ts +83 -1
  80. package/src/elements/store.ts +2 -0
  81. package/src/full.ts +3 -0
  82. package/src/http.ts +3 -0
  83. package/src/index.ts +3 -0
  84. package/src/kernel/app.ts +30 -10
  85. package/src/kernel/boot.ts +1 -1
  86. package/src/kernel/cdc-payload.test.ts +224 -0
  87. package/src/kernel/cdc-payload.ts +146 -0
  88. package/src/kernel/errors-flow-name.ts +17 -0
  89. package/src/kernel/errors.registry.test.ts +6 -4
  90. package/src/kernel/flow-name.test.ts +104 -0
  91. package/src/kernel/flow.ts +3 -2
  92. package/src/kernel/fx-emit-types.test.ts +31 -0
  93. package/src/kernel/fx.test.ts +2 -1
  94. package/src/kernel/fx.ts +13 -3
  95. package/src/kernel/index.ts +2 -0
  96. package/src/kernel/on.ts +5 -0
  97. package/src/kernel/stamp-http.test.ts +13 -0
  98. package/src/kernel/stamp-http.ts +21 -3
  99. package/src/kernel/unit.ts +4 -2
  100. package/src/kernel-entry.ts +3 -0
  101. package/src/mcp/docs-index.ts +3 -3
  102. package/src/mcp/docs-mcp.test.ts +2 -2
  103. package/src/mcp/docs-tools.ts +2 -1
  104. package/site/content/docs/understand/the-anatomy.mdx +0 -132
  105. package/site/content/docs/understand/the-model.mdx +0 -32
  106. package/site/content/docs/understand/the-problem.mdx +0 -74
  107. package/site/content/docs/understand/the-vocabulary.mdx +0 -26
  108. package/src/console/ui-next/dist/assets/flows-page-Dss7941e.js +0 -1
  109. package/src/console/ui-next/dist/assets/request-meta-BatF8KrK.js +0 -1
  110. /package/site/content/docs/{ai → understand}/try-it.mdx +0 -0
@@ -267,9 +267,12 @@ import { z } from "zod";
267
267
  import { member } from "@/core/gate";
268
268
 
269
269
  export const switchTenant = on(
270
- http.post().gate(member),
270
+ http
271
+ .post({
272
+ in: z.object({ tenantId: z.string() }),
273
+ })
274
+ .gate(member),
271
275
  flow({
272
- in: z.object({ tenantId: z.string() }),
273
276
  do: async ({ tenantId }, fx) => fx.auth.switchTenant(tenantId),
274
277
  }),
275
278
  );
@@ -65,9 +65,10 @@ import { z } from "zod";
65
65
  import { cacheInvalidated } from "@/signals/cache";
66
66
 
67
67
  export const update = on(
68
- http.patch(),
69
- flow({
68
+ http.patch({
70
69
  in: z.object({ sku: z.string(), title: z.string().min(1) }),
70
+ }),
71
+ flow({
71
72
  do: async ({ sku, title }, fx) => {
72
73
  // … persist the change …
73
74
  await fx.emit(cacheInvalidated, { key: `sku:${sku}` });
@@ -284,9 +285,10 @@ import { z } from "zod";
284
285
  import { catalogChanged } from "@/signals/catalog";
285
286
 
286
287
  export const create = on(
287
- http.post(),
288
- flow({
288
+ http.post({
289
289
  in: z.object({ sku: z.string(), title: z.string().min(1) }),
290
+ }),
291
+ flow({
290
292
  do: async ({ sku, title }, fx) => {
291
293
  // … write …
292
294
  await fx.emit(catalogChanged, { sku });
@@ -80,6 +80,19 @@ for the worker.
80
80
  competing consumer.
81
81
  </Callout>
82
82
 
83
+ ## Inline or named export
84
+
85
+ | Style | When |
86
+ | --------------------------------------------- | --------------------------------------------------------------------------- |
87
+ | `on(signal.once("name", opts), flow({ do }))` | Self-contained — nothing else emits to this Signal |
88
+ | `export const x = signal.once<Payload>(…)` | Another file needs `fx.emit(x, payload)` with compile-time payload checking |
89
+
90
+ A string `fx.emit("name", payload)` still runs (runtime `schema` still applies) but does not
91
+ type-check the payload.
92
+
93
+ Nameless `flow({ do })` inherits the Signal name; file-tree `unit.export` and explicit
94
+ `flow("…")` win. Two nameless inheritances of the same name fail **OKE1070**.
95
+
83
96
  ## Progressive Patterns
84
97
 
85
98
  Same helpers + `fx.emit` from a queue job to a fan-out and a browser feed:
@@ -347,6 +360,6 @@ export default defineConfig({
347
360
  <Card
348
361
  title="The Model"
349
362
  description="Eight elements overview."
350
- href="/docs/understand/the-model"
363
+ href="/docs/understand/the-architecture"
351
364
  />
352
365
  </Cards>
@@ -342,9 +342,10 @@ import { z } from "zod";
342
342
  import { orderStatus } from "@/signals/orders";
343
343
 
344
344
  export const ship = on(
345
- http.post(),
346
- flow({
345
+ http.post({
347
346
  in: z.object({ id: z.string() }),
347
+ }),
348
+ flow({
348
349
  do: async ({ id }, fx) => {
349
350
  await fx.emit(orderStatus, { orderId: id, status: "shipped" });
350
351
  return { id, status: "shipped" };
@@ -311,9 +311,10 @@ import { z } from "zod";
311
311
  import { orderPlaced } from "@/signals/orders";
312
312
 
313
313
  export const create = on(
314
- http.post(),
315
- flow({
314
+ http.post({
316
315
  in: z.object({ userId: z.string(), amount: z.number() }),
316
+ }),
317
+ flow({
317
318
  do: async ({ userId, amount }, fx) => {
318
319
  const orderId = fx.id();
319
320
  await fx.emit(orderPlaced, { orderId, amount, userId }, { key: userId });
@@ -38,13 +38,14 @@ import { z } from "zod";
38
38
  import { uploads } from "@/core";
39
39
 
40
40
  export const create = on(
41
- http.post(),
42
- flow({
41
+ http.post({
43
42
  in: z.object({
44
43
  name: z.string().min(1),
45
44
  bytes: z.string(), // base64 for the demo
46
45
  }),
47
46
  out: z.object({ key: z.string() }),
47
+ }),
48
+ flow({
48
49
  do: async ({ name, bytes }, fx) => {
49
50
  const key = `uploads/${fx.id()}-${name}`;
50
51
  const data = Uint8Array.from(atob(bytes), (c) => c.charCodeAt(0));
@@ -123,9 +124,10 @@ import { z } from "zod";
123
124
  import { uploads } from "@/core";
124
125
 
125
126
  export const cleanup = on(
126
- http.delete(),
127
- flow({
127
+ http.delete({
128
128
  in: z.object({ prefix: z.string().default("avatars/") }),
129
+ }),
130
+ flow({
129
131
  do: async ({ prefix }, fx) => {
130
132
  const keys = await fx.store(uploads).list(prefix);
131
133
  let removed = 0;
@@ -154,9 +156,10 @@ import { z } from "zod";
154
156
  import { uploads } from "@/core";
155
157
 
156
158
  export const create = on(
157
- http.post(),
158
- flow({
159
+ http.post({
159
160
  in: z.object({ bytes: z.string() }),
161
+ }),
162
+ flow({
160
163
  do: async ({ bytes }, fx) => {
161
164
  const key = `photos/${fx.id()}.jpg`;
162
165
  const data = Uint8Array.from(atob(bytes), (c) => c.charCodeAt(0));
@@ -195,9 +198,10 @@ import { z } from "zod";
195
198
  import { uploads } from "@/core";
196
199
 
197
200
  export const card = on(
198
- http.post(),
199
- flow({
201
+ http.post({
200
202
  in: z.object({ sourceKey: z.string() }),
203
+ }),
204
+ flow({
201
205
  do: async ({ sourceKey }, fx) => {
202
206
  const outKey = sourceKey.replace(/\.[^.]+$/, ".card.webp");
203
207
  await fx
@@ -282,13 +286,14 @@ import { z } from "zod";
282
286
  import { uploads } from "@/core";
283
287
 
284
288
  export const create = on(
285
- http.post(),
286
- flow({
289
+ http.post({
287
290
  in: z.object({
288
291
  id: z.string(),
289
292
  name: z.string().min(1),
290
293
  bytes: z.string(),
291
294
  }),
295
+ }),
296
+ flow({
292
297
  do: async ({ id, name, bytes }, fx) => {
293
298
  const key = `notes/${id}/${name}`;
294
299
  const data = Uint8Array.from(atob(bytes), (c) => c.charCodeAt(0));
@@ -311,10 +316,11 @@ import { z } from "zod";
311
316
  import { uploads } from "@/core";
312
317
 
313
318
  export const get = on(
314
- http.get(),
315
- flow({
319
+ http.get({
316
320
  in: z.object({ id: z.string(), name: z.string() }),
317
321
  errors: { NotFound: z.object({ key: z.string() }) },
322
+ }),
323
+ flow({
318
324
  do: async ({ id, name }, fx) => {
319
325
  const key = `notes/${id}/${name}`;
320
326
  const bytes = await fx.store(uploads).get(key);
@@ -337,9 +343,10 @@ import { z } from "zod";
337
343
  import { uploads } from "@/core";
338
344
 
339
345
  export const remove = on(
340
- http.delete(),
341
- flow({
346
+ http.delete({
342
347
  in: z.object({ id: z.string(), name: z.string() }),
348
+ }),
349
+ flow({
343
350
  do: async ({ id, name }, fx) => {
344
351
  const key = `notes/${id}/${name}`;
345
352
  const removed = await fx.store(uploads).delete(key);
@@ -362,9 +369,10 @@ import { z } from "zod";
362
369
  import { uploads } from "@/core";
363
370
 
364
371
  export const attachments = on(
365
- http.get(),
366
- flow({
372
+ http.get({
367
373
  in: z.object({ id: z.string() }),
374
+ }),
375
+ flow({
368
376
  do: async ({ id }, fx) => {
369
377
  const keys = await fx.store(uploads).list(`notes/${id}/`);
370
378
  return { keys };
@@ -47,9 +47,10 @@ import { z } from "zod";
47
47
  import { db, notes } from "@/schema";
48
48
 
49
49
  export const create = on(
50
- http.post(),
51
- flow({
50
+ http.post({
52
51
  in: z.object({ title: z.string().min(1) }),
52
+ }),
53
+ flow({
53
54
  do: async ({ title }, fx) => {
54
55
  const id = fx.id();
55
56
  await fx.store(db).insert(notes).values({ id, title });
@@ -106,10 +107,11 @@ import { eq } from "drizzle-orm";
106
107
  import { db, notes } from "@/schema";
107
108
 
108
109
  export const get = on(
109
- http.get(),
110
- flow({
110
+ http.get({
111
111
  in: z.object({ id: z.string() }),
112
112
  errors: { NotFound: z.object({ id: z.string() }) },
113
+ }),
114
+ flow({
113
115
  do: async ({ id }, fx) => {
114
116
  const [note] = await fx.store(db).select().from(notes).where(eq(notes.id, id));
115
117
  if (!note) return fx.fail("NotFound", { id });
@@ -128,11 +130,11 @@ Prefer [`store.resource`](/docs/elements/store/sql#resources) when you want five
128
130
  Pass the **declaration** into `fx.store` — there is no `fx.store.kv` namespace. TTL is a duration string:
129
131
 
130
132
  ```typescript title="src/flows/sessions/put.ts"
131
- import { flow } from "okengine";
133
+ import { call } from "okengine";
132
134
  import { z } from "zod";
133
135
  import { sessions } from "@/core";
134
136
 
135
- export const putSession = flow("sessions.put", {
137
+ export const putSession = call("sessions.put", {
136
138
  in: z.object({ token: z.string(), userId: z.string() }),
137
139
  do: async ({ token, userId }, fx) => {
138
140
  await fx.store(sessions).set(`session:${token}`, userId, "1h");
@@ -159,9 +161,10 @@ import { z } from "zod";
159
161
  import { uploads } from "@/core";
160
162
 
161
163
  export const create = on(
162
- http.post(),
163
- flow({
164
+ http.post({
164
165
  in: z.object({ bytes: z.string() }),
166
+ }),
167
+ flow({
165
168
  do: async ({ bytes }, fx) => {
166
169
  const key = `avatars/${fx.id()}.png`;
167
170
  const data = Uint8Array.from(atob(bytes), (c) => c.charCodeAt(0));
@@ -39,9 +39,10 @@ import { z } from "zod";
39
39
  import { sessions } from "@/core";
40
40
 
41
41
  export const create = on(
42
- http.post(),
43
- flow({
42
+ http.post({
44
43
  in: z.object({ token: z.string(), userId: z.string() }),
44
+ }),
45
+ flow({
45
46
  do: async ({ token, userId }, fx) => {
46
47
  await fx.store(sessions).set(`session:${token}`, userId, "1h");
47
48
  const cached = await fx.store(sessions).get(`session:${token}`);
@@ -97,9 +98,10 @@ import { z } from "zod";
97
98
  import { sessions } from "@/core";
98
99
 
99
100
  export const get = on(
100
- http.get(),
101
- flow({
101
+ http.get({
102
102
  in: z.object({ userId: z.string() }),
103
+ }),
104
+ flow({
103
105
  do: async ({ userId }, fx) => {
104
106
  const prefs = await fx.store(sessions).get(`user:${userId}:prefs`);
105
107
  return { prefs: prefs ?? null };
@@ -122,9 +124,10 @@ import { z } from "zod";
122
124
  import { sessions } from "@/core";
123
125
 
124
126
  export const create = on(
125
- http.post(),
126
- flow({
127
+ http.post({
127
128
  in: z.object({ userId: z.string() }),
129
+ }),
130
+ flow({
128
131
  do: async ({ userId }, fx) => {
129
132
  const code = "482193";
130
133
  await fx.store(sessions).set(`otp:${userId}`, code, "5m");
@@ -251,10 +254,11 @@ import { z } from "zod";
251
254
  import { sessions } from "@/core";
252
255
 
253
256
  export const get = on(
254
- http.get(),
255
- flow({
257
+ http.get({
256
258
  in: z.object({ token: z.string() }),
257
259
  errors: { NotFound: z.object({ token: z.string() }) },
260
+ }),
261
+ flow({
258
262
  do: async ({ token }, fx) => {
259
263
  const userId = await fx.store(sessions).get(`session:${token}`);
260
264
  if (userId === undefined || userId === null) {
@@ -278,9 +282,10 @@ import { z } from "zod";
278
282
  import { sessions } from "@/core";
279
283
 
280
284
  export const create = on(
281
- http.post(),
282
- flow({
285
+ http.post({
283
286
  in: z.object({ token: z.string(), userId: z.string() }),
287
+ }),
288
+ flow({
284
289
  do: async ({ token, userId }, fx) => {
285
290
  await fx.store(sessions).set(`session:${token}`, userId, "1h");
286
291
  return { ok: true as const };
@@ -304,9 +309,10 @@ import { z } from "zod";
304
309
  import { sessions } from "@/core";
305
310
 
306
311
  export const remove = on(
307
- http.delete(),
308
- flow({
312
+ http.delete({
309
313
  in: z.object({ token: z.string() }),
314
+ }),
315
+ flow({
310
316
  do: async ({ token }, fx) => {
311
317
  const removed = await fx.store(sessions).delete(`session:${token}`);
312
318
  return { removed };
@@ -358,9 +364,10 @@ import { z } from "zod";
358
364
  import { sessions } from "@/core";
359
365
 
360
366
  export const ttl = on(
361
- http.get(),
362
- flow({
367
+ http.get({
363
368
  in: z.object({ userId: z.string() }),
369
+ }),
370
+ flow({
364
371
  do: async ({ userId }, fx) => {
365
372
  const ttlMs = await fx.store(sessions).ttlMs(`otp:${userId}`);
366
373
  return { ttlMs };
@@ -409,13 +416,14 @@ import { z } from "zod";
409
416
  import { drafts } from "@/core";
410
417
 
411
418
  export const save = on(
412
- http.put("/drafts/:id"),
413
- flow({
419
+ http.put("/drafts/:id", {
414
420
  in: z.object({
415
421
  id: z.string().min(1),
416
422
  title: z.string().min(1),
417
423
  body: z.string().optional(),
418
424
  }),
425
+ }),
426
+ flow({
419
427
  do: async ({ id, title, body }, fx) => {
420
428
  await fx.store(drafts).set(id, { title, body: body ?? "" }, "7d");
421
429
  return { id };
@@ -42,9 +42,10 @@ import { z } from "zod";
42
42
  import { articles, db } from "@/schema";
43
43
 
44
44
  export const search = on(
45
- http.get(),
46
- flow({
45
+ http.get({
47
46
  in: z.object({ q: z.string() }),
47
+ }),
48
+ flow({
48
49
  do: async ({ q }, fx) => {
49
50
  const result = await fx.store(db).search(articles, {
50
51
  query: q,
@@ -399,9 +400,10 @@ import { z } from "zod";
399
400
  import { articles, db } from "@/schema";
400
401
 
401
402
  export const search = on(
402
- http.get(),
403
- flow({
403
+ http.get({
404
404
  in: z.object({ q: z.string().min(1) }),
405
+ }),
406
+ flow({
405
407
  do: async ({ q }, fx) => {
406
408
  const result = await fx.store(db).search(articles, {
407
409
  query: q,
@@ -430,12 +432,13 @@ import { z } from "zod";
430
432
  import { articles, db } from "@/schema";
431
433
 
432
434
  export const search = on(
433
- http.get(),
434
- flow({
435
+ http.get({
435
436
  in: z.object({
436
437
  q: z.string().min(1),
437
438
  status: z.string().optional(),
438
439
  }),
440
+ }),
441
+ flow({
439
442
  do: async ({ q, status }, fx) => {
440
443
  const result = await fx.store(db).search(articles, {
441
444
  query: q,
@@ -539,7 +542,7 @@ intentional. Do not poll `fx.embed` from the writer to “close” it.
539
542
  | ------------------------------------- | ---------------------------------------------------------- |
540
543
  | Generated `tsvector` + GIN | BM25 candidate retrieval (`plainto_tsquery('english', …)`) |
541
544
  | `real[]` embedding column | Stored vector per `.embed()` field |
542
- | `bigint` LSH column + B-tree | Bucket lookup (Hamming-1 neighbors included) |
545
+ | `bigint` LSH column + B-tree | Stored SimHash pack (query ranks by Hamming, not equality) |
543
546
  | Corpus stats / DF / hyperplane tables | IDF, average length, stable LSH planes |
544
547
 
545
548
  Hyperplanes insert once (`ON CONFLICT DO NOTHING`) and are **never** regenerated.
@@ -619,8 +622,9 @@ await fx.store(db).search(articles, {
619
622
  BM25F uses Robertson–Zaragoza saturation: **k1 = 1.2**, **b = 0.75**. Field
620
623
  `weight` multiplies term frequency *before* that saturation.
621
624
 
622
- LSH uses **64** hyperplanes (fits a `bigint` bit pack). Query-time lookup includes
623
- the exact bucket and Hamming-1 neighbors.
625
+ LSH uses **64** hyperplanes packed into a `bigint`. Query-time retrieval ranks
626
+ rows by Hamming distance (`bit_count` of XOR) and keeps the oversampled nearest
627
+ (`max(limit × 5, 50)`, cap 500), then cosine-reranks in process.
624
628
 
625
629
  </Accordion>
626
630
 
@@ -800,13 +804,13 @@ Headline numbers from the live-Postgres G17 gate (`OKE_TEST_POSTGRES=1`, Bun 1.4
800
804
 
801
805
  | Corpus size | BM25 (text) p50 | LSH/hybrid p50 | LSH precision@10 vs exact cosine | Guidance |
802
806
  | ----------- | --------------- | -------------- | -------------------------------- | ---------------------------------------------------------------------------------------- |
803
- | ≤10k | ~1–4 ms | ~1–6 ms | ≈0 on this gate | Built-in hybrid is fine for ranking UX; do not market LSH recall |
804
- | ~100k | ~40 ms | ~50–60 ms | ≈0 | Expect tens of ms; re-`EXPLAIN` after `ANALYZE` |
805
- | ~1M | ~0.6–1.2 s | ~0.6–1.2 s | ≈0 | Prefer external `store.index` (pgvector / Meilisearch) for semantic recall at this scale |
807
+ | ≤10k | ~1–5 ms | ~1–9 ms | 0.10–0.17 vector | Built-in hybrid is fine for ranking UX; LSH is not HNSW |
808
+ | ~100k | ~21 ms | ~31–34 ms | 0.017 vector | Expect tens of ms; re-`EXPLAIN` after `ANALYZE` |
809
+ | ~1M | ~0.3 s | ~0.4 s | 0.017 vector / 0 hybrid | Prefer external `store.index` (pgvector / Meilisearch) for semantic recall at this scale |
806
810
 
807
- **Honest LSH note:** random-hyperplane LSH (K=64, Hamming-1, ≤50 candidates) did **not** match exact brute-force cosine top-10 on the G17 corpus (precision@10 mostly **0**). Same class of honesty as earlier LSH-vs-HNSW design notes — use LSH as a cheap candidate hint inside RRF, not as a recall guarantee. BM25-only needs no AI and remains the smallest path.
811
+ **Honest LSH note:** after the Hamming-rank fix (2026-09-11), vector precision@10 vs exact cosine is 0.17 at 1k, 0.10 at 10k, ~0.02 at 100k–1M on the G17 hash-bag corpus — a smooth drop, not the v0.19.0 zero collapse. Still **not** HNSW. Prefer BM25-only or `store.index` when semantic recall matters.
808
812
 
809
- **Query plan:** at N=100k, `EXPLAIN (ANALYZE, BUFFERS)` showed a **Seq Scan** with a tsvector/LSH Filter — not BitmapOr — despite GIN + B-tree indexes. Capture your own plan on production data before assuming an index path.
813
+ **Query plan:** at N=100k, `EXPLAIN (ANALYZE, BUFFERS)` is a UNION of GIN **Bitmap Index Scan** and a **Parallel Seq Scan** + top-N heapsort on Hamming distance (~15 ms). Hamming-rank cannot use the LSH B-tree. Capture your own plan on production data.
810
814
 
811
815
  **Backfill:** `oke db search-backfill` is interrupt-safe to re-run (G17 killed at 2k/50k embeds, resumed to completion in ~29 s on that table).
812
816
 
@@ -291,11 +291,12 @@ import { eq } from "drizzle-orm";
291
291
  import { db, notes } from "@/schema";
292
292
 
293
293
  export const get = on(
294
- http.get(),
295
- flow({
294
+ http.get({
296
295
  in: z.object({ id: z.string() }),
297
296
  out: z.object({ id: z.string(), title: z.string() }),
298
297
  errors: { NotFound: z.object({ id: z.string() }) },
298
+ }),
299
+ flow({
299
300
  do: async ({ id }, fx) => {
300
301
  const [note] = await fx.store(db).select().from(notes).where(eq(notes.id, id));
301
302
  if (!note) return fx.fail("NotFound", { id });
@@ -317,10 +318,11 @@ import { z } from "zod";
317
318
  import { db, notes } from "@/schema";
318
319
 
319
320
  export const create = on(
320
- http.post(),
321
- flow({
321
+ http.post({
322
322
  in: z.object({ title: z.string().min(1) }),
323
323
  out: z.object({ id: z.string(), title: z.string() }),
324
+ }),
325
+ flow({
324
326
  do: async ({ title }, fx) => {
325
327
  const id = fx.id();
326
328
  const [row] = await fx.store(db).insert(notes).values({ id, title }).returning();
@@ -344,13 +346,14 @@ import { eq } from "drizzle-orm";
344
346
  import { db, notes } from "@/schema";
345
347
 
346
348
  export const update = on(
347
- http.patch(),
348
- flow({
349
+ http.patch({
349
350
  in: z.object({
350
351
  id: z.string(),
351
352
  title: z.string().min(1).optional(),
352
353
  }),
353
354
  errors: { NotFound: z.object({ id: z.string() }) },
355
+ }),
356
+ flow({
354
357
  do: async ({ id, title }, fx) => {
355
358
  if (title !== undefined) {
356
359
  await fx.store(db).update(notes).set({ title }).where(eq(notes.id, id));
@@ -376,9 +379,10 @@ import { z } from "zod";
376
379
  import { db, notes } from "@/schema";
377
380
 
378
381
  export const remove = on(
379
- http.delete(),
380
- flow({
382
+ http.delete({
381
383
  in: z.object({ id: z.string() }),
384
+ }),
385
+ flow({
382
386
  do: async ({ id }, fx) => {
383
387
  await fx.store(db).delete(notes, id);
384
388
  return fx.json.empty();
@@ -422,11 +426,11 @@ prefer them for live feeds. Offset is fine for admin tables.
422
426
  Default is insert-once. Pass `{ onExisting: "update" }` to overwrite a match:
423
427
 
424
428
  ```typescript title="src/flows/notes/ensure.ts"
425
- import { flow } from "okengine";
429
+ import { call } from "okengine";
426
430
  import { z } from "zod";
427
431
  import { db, notes } from "@/schema";
428
432
 
429
- export const ensureWelcome = flow("notes.ensureWelcome", {
433
+ export const ensureWelcome = call("notes.ensureWelcome", {
430
434
  in: z.object({ title: z.string() }),
431
435
  do: async ({ title }, fx) => {
432
436
  const result = await fx.store(db).upsert(
@@ -803,8 +807,8 @@ Default is insert-once; pass `{ onExisting: "update" }` to overwrite.
803
807
  </Callout>
804
808
 
805
809
  `db.table(orders).changed(column?)` builds a CDC trigger for `on(…)`. Input is
806
- always `{ before, after }`. `changed("status")` stamps a **column** name on the
807
- Manifest — it is not an op filter.
810
+ `{ before, after }` plus `table` / `action` / `id`. `changed("status")` stamps a
811
+ **column** name on the Manifest — it is not an op filter.
808
812
 
809
813
  ```typescript title="src/flows/orders/on-status.ts"
810
814
  import { on, flow } from "okengine";
@@ -48,9 +48,12 @@ import { member } from "@/core/gate";
48
48
  import { publicAppUrl, noteCreatedMail } from "@/core";
49
49
 
50
50
  export const create = on(
51
- http.post().gate(member),
51
+ http
52
+ .post({
53
+ in: z.object({ email: z.string().email() }),
54
+ })
55
+ .gate(member),
52
56
  flow({
53
- in: z.object({ email: z.string().email() }),
54
57
  do: async ({ email }, fx) => {
55
58
  const origin = await fx.vault.get(publicAppUrl);
56
59
  await fx.send(noteCreatedMail, {
@@ -93,10 +93,13 @@ import { gate } from "okengine";
93
93
  const operator = gate.policy("operator", ({ operator: op }) => !!op);
94
94
 
95
95
  export const rotateStripe = on(
96
- http.post().gate(operator),
96
+ http
97
+ .post({
98
+ in: z.object({ value: z.string().min(1) }),
99
+ })
100
+ .gate(operator),
97
101
  flow({
98
102
  plane: "operator",
99
- in: z.object({ value: z.string().min(1) }),
100
103
  effects: { secrets: ["STRIPE_KEY"] },
101
104
  do: async ({ value }, fx) => {
102
105
  const result = await fx.vault.rotate("STRIPE_KEY", value);
@@ -51,9 +51,12 @@ import { member } from "@/core/gate";
51
51
  import { stripeKey } from "@/core/vault";
52
52
 
53
53
  export const charge = on(
54
- http.post().gate(member),
54
+ http
55
+ .post({
56
+ in: z.object({ amount: z.number().int().positive() }),
57
+ })
58
+ .gate(member),
55
59
  flow({
56
- in: z.object({ amount: z.number().int().positive() }),
57
60
  do: async ({ amount }, fx) => {
58
61
  const key = await fx.vault.get(stripeKey);
59
62
  const stripe = new Stripe(key.reveal());
@@ -23,29 +23,14 @@ Master the mental model before exploring features:
23
23
 
24
24
  <Cards>
25
25
  <Card
26
- title="The Problem"
27
- description="Three unrelated features that hit the exact same wall, and the timeline that shows why."
28
- href="/docs/understand/the-problem"
29
- />
30
- <Card
31
- title="The Model"
32
- description="The one rule that removes the disagreement between systems — stated plainly, in two parts."
33
- href="/docs/understand/the-model"
34
- />
35
- <Card
36
- title="The Vocabulary"
37
- description="The eight things the door recognizes — what each one replaces, and why nothing else made the cut."
38
- href="/docs/understand/the-vocabulary"
39
- />
40
- <Card
41
- title="The Anatomy"
42
- description="The five pieces behind on(trigger, flow) — on, trigger, flow, do, and fx — explained one at a time."
43
- href="/docs/understand/the-anatomy"
26
+ title="The Architecture"
27
+ description="Why backends drift apart, the one rule that stops it, and the five pieces behind every Flow."
28
+ href="/docs/understand/the-architecture"
44
29
  />
45
30
  <Card
46
31
  title="Try It"
47
32
  description="From an empty folder to a Flow running in the Console — one sitting, minimal detour."
48
- href="/docs/ai/try-it"
33
+ href="/docs/understand/try-it"
49
34
  />
50
35
  </Cards>
51
36
 
@@ -38,7 +38,7 @@ export const app = oke({
38
38
 
39
39
  ```typescript
40
40
  const { data } = await api.auth.signInAnonymous();
41
- // data.userId is a fresh UUID; store tokens like any other session
41
+ // data.userId is a fresh OKID; store tokens like any other session
42
42
  ```
43
43
 
44
44
  `POST /auth/sign-in/anonymous` — no body.
@@ -71,7 +71,7 @@ const origins = configSource({
71
71
  db: { store: db },
72
72
  kv: cache,
73
73
  });
74
- const corsSyncClock = clock("cors.sync", { every: "30s" });
74
+ const corsSyncClock = clock.every("cors.sync", "30s");
75
75
  on(corsSyncClock, origins.sync());
76
76
  export const app = oke({ name: "shop", env: "dev" }).plug(cors(origins));
77
77
  ```
@@ -75,7 +75,7 @@ const rules = configSource({
75
75
  db: { store: db },
76
76
  kv: cache,
77
77
  });
78
- const csrfSyncClock = clock("csrf.sync", { every: "30s" });
78
+ const csrfSyncClock = clock.every("csrf.sync", "30s");
79
79
  on(csrfSyncClock, rules.sync());
80
80
  export const app = oke({ name: "shop", env: "dev" }).plug(csrf(rules));
81
81
  ```
@@ -103,7 +103,7 @@ const headerConfig = configSource({
103
103
  db: { store: db },
104
104
  kv: cache,
105
105
  });
106
- const headerSyncClock = clock("headers.sync", { every: "30s" });
106
+ const headerSyncClock = clock.every("headers.sync", "30s");
107
107
  on(headerSyncClock, headerConfig.sync());
108
108
  export const app = oke({ name: "shop", env: "dev" }).plug(headers(headerConfig));
109
109
  ```
@@ -69,7 +69,7 @@ const rules = configSource({
69
69
  db: { store: db },
70
70
  kv: cache,
71
71
  });
72
- const ipRulesSyncClock = clock("ip-allowlist.sync", { every: "30s" });
72
+ const ipRulesSyncClock = clock.every("ip-allowlist.sync", "30s");
73
73
  on(ipRulesSyncClock, rules.sync());
74
74
  export const app = oke({ name: "shop", env: "dev" }).plug(ipAllowlist(rules));
75
75
  ```