okengine 0.19.0 → 0.19.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) 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 +3 -2
  11. package/site/content/docs/elements/clock/sleep.mdx +10 -7
  12. package/site/content/docs/elements/flow/index.mdx +3 -3
  13. package/site/content/docs/elements/flow/routing.mdx +1 -1
  14. package/site/content/docs/elements/gate/tenancy.mdx +5 -2
  15. package/site/content/docs/elements/signal/broadcast.mdx +6 -4
  16. package/site/content/docs/elements/signal/index.mdx +1 -1
  17. package/site/content/docs/elements/signal/live.mdx +3 -2
  18. package/site/content/docs/elements/signal/once.mdx +3 -2
  19. package/site/content/docs/elements/store/files.mdx +24 -16
  20. package/site/content/docs/elements/store/index.mdx +11 -8
  21. package/site/content/docs/elements/store/kv.mdx +24 -16
  22. package/site/content/docs/elements/store/search.mdx +18 -14
  23. package/site/content/docs/elements/store/sql.mdx +14 -10
  24. package/site/content/docs/elements/vault/config.mdx +5 -2
  25. package/site/content/docs/elements/vault/rotation.mdx +5 -2
  26. package/site/content/docs/elements/vault/secrets.mdx +5 -2
  27. package/site/content/docs/index.mdx +4 -19
  28. package/site/content/docs/reference/cli.mdx +1 -1
  29. package/site/content/docs/reference/fx.mdx +3 -2
  30. package/site/content/docs/reference/security.mdx +2 -2
  31. package/site/content/docs/understand/meta.json +1 -1
  32. package/site/content/docs/understand/the-architecture.mdx +264 -0
  33. package/src/bench/REPORT.md +48 -17
  34. package/src/bench/g17-hybrid-search.bench.ts +13 -13
  35. package/src/console/ui-next/dist/assets/{access-page-DFeymU07.js → access-page-C_qLDhTq.js} +1 -1
  36. package/src/console/ui-next/dist/assets/{agent-disclosure-U1rdfblp.js → agent-disclosure-BHVqr3TN.js} +1 -1
  37. package/src/console/ui-next/dist/assets/{cache-glyph-BeFJeqBG.js → cache-glyph-CKe92lRQ.js} +1 -1
  38. package/src/console/ui-next/dist/assets/{call-pii-button-CVAONPii.js → call-pii-button-DEwTl8ZX.js} +1 -1
  39. package/src/console/ui-next/dist/assets/{collapsible-D2A6NJ-3.js → collapsible-BCBtDrCt.js} +1 -1
  40. package/src/console/ui-next/dist/assets/{duration-tone-Cgk_h5ja.js → duration-tone-JroqeuCp.js} +1 -1
  41. package/src/console/ui-next/dist/assets/flows-page-CVHa0RTt.js +1 -0
  42. package/src/console/ui-next/dist/assets/{highlighted-json-MYZQtRnw.js → highlighted-json-DjJW6hqe.js} +1 -1
  43. package/src/console/ui-next/dist/assets/{http-method-Jrh39p7A.js → http-method-DC5HBdLU.js} +1 -1
  44. package/src/console/ui-next/dist/assets/{index-DXP2dBIF.js → index-CYjiZ3WO.js} +3 -3
  45. package/src/console/ui-next/dist/assets/{observability-page-HvolXxTI.js → observability-page-CAYMyKb3.js} +1 -1
  46. package/src/console/ui-next/dist/assets/{replica-lag-CSh2dzrb.js → replica-lag-yAQYLv75.js} +1 -1
  47. package/src/console/ui-next/dist/assets/request-meta-D0yusGxJ.js +1 -0
  48. package/src/console/ui-next/dist/assets/{store-page-eiKiHnNe.js → store-page-BTKJeJ02.js} +1 -1
  49. package/src/console/ui-next/dist/assets/{trace-detail-sheet-CZkMeKS-.js → trace-detail-sheet-Bp-Yygs5.js} +1 -1
  50. package/src/console/ui-next/dist/assets/{tree-expand-toggle-CW8y5A2h.js → tree-expand-toggle-DoaVDfAM.js} +1 -1
  51. package/src/console/ui-next/dist/assets/{units-page-B_RWJrEO.js → units-page-BRz7xyYL.js} +1 -1
  52. package/src/console/ui-next/dist/assets/{vault-page-CWrg-A68.js → vault-page-3jQt-bOJ.js} +1 -1
  53. package/src/console/ui-next/dist/index.html +1 -1
  54. package/src/console/ui-next/src/features/flows/graph/element-map.test.ts +1 -1
  55. package/src/console/ui-next/src/features/flows/graph/element-map.ts +3 -3
  56. package/src/console/ui-next/src/features/flows/traces/trace-detail.test.ts +4 -5
  57. package/src/console/ui-next/src/features/flows/traces/trace-gates.ts +3 -6
  58. package/src/elements/gate/declare.ts +1 -1
  59. package/src/elements/gate.ts +1 -1
  60. package/src/elements/store/search-lsh.ts +35 -0
  61. package/src/elements/store/search-runtime.pglite.test.ts +186 -0
  62. package/src/elements/store/search-runtime.ts +30 -16
  63. package/src/elements/store/search.test.ts +83 -1
  64. package/src/elements/store.ts +2 -0
  65. package/src/mcp/docs-index.ts +3 -3
  66. package/src/mcp/docs-mcp.test.ts +2 -2
  67. package/src/mcp/docs-tools.ts +2 -1
  68. package/site/content/docs/understand/the-anatomy.mdx +0 -132
  69. package/site/content/docs/understand/the-model.mdx +0 -32
  70. package/site/content/docs/understand/the-problem.mdx +0 -74
  71. package/site/content/docs/understand/the-vocabulary.mdx +0 -26
  72. package/src/console/ui-next/dist/assets/flows-page-Dss7941e.js +0 -1
  73. package/src/console/ui-next/dist/assets/request-meta-BatF8KrK.js +0 -1
  74. /package/site/content/docs/{ai → understand}/try-it.mdx +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "okengine",
3
- "version": "0.19.0",
3
+ "version": "0.19.1",
4
4
  "description": "One law. Eight elements. One contract. The backend model stays small; operational surfaces are derived from it instead of maintained separately.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "title": "AI Resources",
3
3
  "icon": "Bot",
4
- "pages": ["try-it", "index", "mcp", "skills", "llms-txt"]
4
+ "pages": ["index", "mcp", "skills", "llms-txt"]
5
5
  }
@@ -46,7 +46,7 @@ Contracts teach the _vocabulary_; [MCP](/docs/ai/mcp) grants the _hands_. An age
46
46
  ## Learn more
47
47
 
48
48
  - [MCP](/docs/ai/mcp) — the runtime surface these contracts pair with
49
- - [The Model](/docs/understand/the-model) — the eight elements in human terms
49
+ - [The Architecture](/docs/understand/the-architecture) — the eight elements in human terms
50
50
  - [llms.txt](/docs/ai/llms-txt) — `/llms.txt`, `/llms.json`, `/llms/agents`, per-page markdown
51
51
 
52
52
  ## Next
@@ -184,8 +184,7 @@ dev so `createClient("")` stays same-origin behind the Vite proxy.
184
184
 
185
185
  - [Routing](/docs/elements/flow/routing) — folders are the URL; `$routes` without `.adopt()`
186
186
  - [HTTP](/docs/elements/flow/http) — verbs, `http.resource`, live SSE
187
- - [The Anatomy](/docs/understand/the-anatomy) — generated barrel → client → test loop
188
- - [The Model](/docs/understand/the-model) — same contract for Client, Console, MCP
187
+ - [The Architecture](/docs/understand/the-architecture) — generated barrel → client → test loop; same contract for Client, Console, MCP
189
188
  - [Errors](/docs/reference/errors) — framework codes vs failure values
190
189
 
191
190
  ## Next
@@ -54,9 +54,12 @@ import { supportAgent } from "@/core/ai";
54
54
  import { member } from "@/core/gate";
55
55
 
56
56
  export const assist = on(
57
- http.post().gate(member),
57
+ http
58
+ .post({
59
+ in: z.object({ query: z.string().min(1) }),
60
+ })
61
+ .gate(member),
58
62
  flow({
59
- in: z.object({ query: z.string().min(1) }),
60
63
  do: async ({ query }, fx) => {
61
64
  return await fx.run(supportAgent, {
62
65
  message: `Help the member: ${query}`,
@@ -60,9 +60,10 @@ import { z } from "zod";
60
60
  import { triage } from "@/core/ai";
61
61
 
62
62
  export const classify = on(
63
- http.post(),
64
- flow({
63
+ http.post({
65
64
  in: z.object({ message: z.string().min(1) }),
65
+ }),
66
+ flow({
66
67
  do: async ({ message }, fx) => {
67
68
  return await fx.ask(triage, { message });
68
69
  },
@@ -336,6 +337,6 @@ Compose does not pin inference — BYO URL + keys, or [OpenRouter](/docs/recipes
336
337
  <Card
337
338
  title="The Model"
338
339
  description="Eight elements overview."
339
- href="/docs/understand/the-model"
340
+ href="/docs/understand/the-architecture"
340
341
  />
341
342
  </Cards>
@@ -56,9 +56,10 @@ import { z } from "zod";
56
56
  import { classifyTicket } from "@/core/ai";
57
57
 
58
58
  export const classify = on(
59
- http.post(),
60
- flow({
59
+ http.post({
61
60
  in: z.object({ message: z.string().min(1) }),
61
+ }),
62
+ flow({
62
63
  do: async ({ message }, fx) => {
63
64
  return await fx.ask(classifyTicket, { message });
64
65
  },
@@ -66,9 +66,12 @@ import { z } from "zod";
66
66
  import { passwordReset } from "@/core/channel";
67
67
 
68
68
  export const reset = on(
69
- http.post().public(),
69
+ http
70
+ .post({
71
+ in: z.object({ email: z.string().email() }),
72
+ })
73
+ .public(),
70
74
  flow({
71
- in: z.object({ email: z.string().email() }),
72
75
  do: async ({ email }, fx) => {
73
76
  await fx.send(passwordReset, {
74
77
  to: email,
@@ -339,6 +339,6 @@ Env knobs: [Environment variables](/docs/reference/environment-variables). Local
339
339
  <Card
340
340
  title="The Model"
341
341
  description="Eight elements overview."
342
- href="/docs/understand/the-model"
342
+ href="/docs/understand/the-architecture"
343
343
  />
344
344
  </Cards>
@@ -140,10 +140,11 @@ import { on, flow, http } from "okengine";
140
140
  import { z } from "zod";
141
141
 
142
142
  export const start = on(
143
- http.post(),
143
+ http.post({
144
+ in: z.object({ email: z.string().email() }),
145
+ }),
144
146
  flow({
145
147
  durable: true,
146
- in: z.object({ email: z.string().email() }),
147
148
  do: async ({ email }, fx) => {
148
149
  await fx.step("mark-trial", async () => {
149
150
  await fx.call(startTrial, { email });
@@ -30,10 +30,11 @@ import { on, flow, http } from "okengine";
30
30
  import { z } from "zod";
31
31
 
32
32
  export const start = on(
33
- http.post(),
33
+ http.post({
34
+ in: z.object({ email: z.string().email() }),
35
+ }),
34
36
  flow({
35
37
  durable: true,
36
- in: z.object({ email: z.string().email() }),
37
38
  do: async ({ email }, fx) => {
38
39
  await fx.step("mark-trial", async () => {
39
40
  await fx.call(startTrial, { email });
@@ -101,10 +102,11 @@ import { on, flow, http } from "okengine";
101
102
  import { z } from "zod";
102
103
 
103
104
  export const create = on(
104
- http.post(),
105
+ http.post({
106
+ in: z.object({ email: z.string().email() }),
107
+ }),
105
108
  flow({
106
109
  durable: true,
107
- in: z.object({ email: z.string().email() }),
108
110
  do: async ({ email }, fx) => {
109
111
  await fx.step("create", async () => {
110
112
  await fx.call(createWorkspace, { email });
@@ -256,11 +258,12 @@ import { on, flow, http } from "okengine";
256
258
  import { z } from "zod";
257
259
 
258
260
  export const create = on(
259
- http.post(),
260
- flow({
261
- durable: true,
261
+ http.post({
262
262
  in: z.object({ email: z.string().email() }),
263
263
  out: z.object({ trialId: z.string() }),
264
+ }),
265
+ flow({
266
+ durable: true,
264
267
  do: async ({ email }, fx) => {
265
268
  const trialId = await fx.step("create", async () => {
266
269
  return await fx.call(createTrial, { email });
@@ -582,7 +582,7 @@ Default is `true` once `gate.auth.tenant` is on.
582
582
 
583
583
  ## Learn more
584
584
 
585
- - [The Anatomy](/docs/understand/the-anatomy) — `on`, trigger, `flow`, `do`, `fx`
585
+ - [The Architecture](/docs/understand/the-architecture) — `on`, trigger, `flow`, `do`, `fx`
586
586
  - [HTTP](/docs/elements/flow/http) — verbs, envelopes, resources, live SSE
587
587
  - [Routing](/docs/elements/flow/routing) — file-tree stamps, barrels, OKE1030 · OKE1040–1045
588
588
  - [Consumers](/docs/elements/flow/consumers) — Signal / Clock / CDC
@@ -611,8 +611,8 @@ Default is `true` once `gate.auth.tenant` is on.
611
611
  href="/docs/elements/flow/workflows"
612
612
  />
613
613
  <Card
614
- title="The Anatomy"
614
+ title="The Architecture"
615
615
  description="Five pieces behind on(trigger, flow)."
616
- href="/docs/understand/the-anatomy"
616
+ href="/docs/understand/the-architecture"
617
617
  />
618
618
  </Cards>
@@ -535,7 +535,7 @@ plus a handwritten `http.get("/notes")`.
535
535
  - [HTTP](/docs/elements/flow/http) — verbs, envelopes, `http.resource`, live SSE
536
536
  - [Client](/docs/client/calling) — `api.notes.get`, REST vs RPC, `$routes`
537
537
  - [Errors](/docs/reference/errors) — OKE1040 · OKE1030 · OKE1041 · OKE1045
538
- - [The Model](/docs/understand/the-model) — derived routes, no hand-written table
538
+ - [The Architecture](/docs/understand/the-architecture) — derived routes, no hand-written table
539
539
  - [Gate](/docs/elements/gate) — `.gate(...)` / `.public()` on the same trigger
540
540
 
541
541
  ## Next
@@ -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 });
@@ -347,6 +347,6 @@ export default defineConfig({
347
347
  <Card
348
348
  title="The Model"
349
349
  description="Eight elements overview."
350
- href="/docs/understand/the-model"
350
+ href="/docs/understand/the-architecture"
351
351
  />
352
352
  </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