okengine 0.3.5 → 0.4.3

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 (144) hide show
  1. package/AGENTS.md +6 -0
  2. package/manifest.v1.schema.json +43 -1
  3. package/package.json +27 -12
  4. package/site/content/docs/ai/skills.mdx +11 -7
  5. package/site/content/docs/console/vault.mdx +4 -0
  6. package/site/content/docs/elements/ai.mdx +2 -0
  7. package/site/content/docs/elements/channel.mdx +7 -4
  8. package/site/content/docs/elements/clock.mdx +2 -4
  9. package/site/content/docs/elements/flow.mdx +19 -10
  10. package/site/content/docs/elements/gate.mdx +18 -11
  11. package/site/content/docs/elements/signal.mdx +9 -12
  12. package/site/content/docs/elements/store.mdx +69 -8
  13. package/site/content/docs/elements/vault.mdx +11 -12
  14. package/site/content/docs/get-started/basic-usage.mdx +76 -41
  15. package/site/content/docs/get-started/installation.mdx +95 -43
  16. package/site/content/docs/get-started/introduction.mdx +128 -75
  17. package/site/content/docs/get-started/meta.json +1 -1
  18. package/site/content/docs/get-started/why.mdx +141 -0
  19. package/site/content/docs/plugins/ip-allowlist.mdx +1 -2
  20. package/site/content/docs/plugins/security-headers.mdx +1 -1
  21. package/site/content/docs/reference/configuration.mdx +2 -2
  22. package/site/content/docs/reference/environment-variables.mdx +10 -0
  23. package/site/content/docs/reference/fx.mdx +56 -3
  24. package/site/content/docs/reference/plugins.mdx +18 -18
  25. package/src/cli/competitor-mention-removal.test.ts +117 -0
  26. package/src/cli/dev.ts +20 -0
  27. package/src/cli/meilisearch-local.test.ts +69 -0
  28. package/src/cli/meilisearch-local.ts +188 -0
  29. package/src/compiler/extract.test.ts +63 -0
  30. package/src/compiler/extract.ts +36 -15
  31. package/src/compiler/fixtures/skyport/src/flows/payments/index.ts +5 -1
  32. package/src/console/server/channels.ts +2 -0
  33. package/src/console/server/clock.ts +3 -0
  34. package/src/console/server/flows.ts +13 -0
  35. package/src/console/server/gates.ts +2 -0
  36. package/src/console/server/plugins.ts +20 -1
  37. package/src/console/server/signals.ts +5 -0
  38. package/src/console/server/store.test.ts +17 -0
  39. package/src/console/server/store.ts +35 -1
  40. package/src/console/ui/channels/types.ts +1 -0
  41. package/src/console/ui/clock/types.ts +1 -0
  42. package/src/console/ui/display.test.ts +14 -0
  43. package/src/console/ui/display.ts +9 -0
  44. package/src/console/ui/dist/assets/index-CjxwRGVv.js +10 -0
  45. package/src/console/ui/dist/assets/panel-access-BGv45snf.js +64 -0
  46. package/src/console/ui/dist/assets/{panel-ai-D_m6WQI8.js → panel-ai-B2S7LEii.js} +1 -1
  47. package/src/console/ui/dist/assets/{panel-architecture-CKnXFyUx.js → panel-architecture-D7UJh91v.js} +1 -1
  48. package/src/console/ui/dist/assets/panel-channels-9T3ybqRu.js +1 -0
  49. package/src/console/ui/dist/assets/panel-clock-Cb1UXGRQ.js +1 -0
  50. package/src/console/ui/dist/assets/{panel-diff-cdonmH8c.js → panel-diff-DmYbKWmN.js} +1 -1
  51. package/src/console/ui/dist/assets/panel-flows-PiHwT55z.js +48 -0
  52. package/src/console/ui/dist/assets/panel-gates-BQGYXvjT.js +1 -0
  53. package/src/console/ui/dist/assets/panel-overview-BBnRO18l.js +1 -0
  54. package/src/console/ui/dist/assets/panel-plugins-D0PsmVw2.js +1 -0
  55. package/src/console/ui/dist/assets/panel-runs-CWuRDe0r.js +1 -0
  56. package/src/console/ui/dist/assets/panel-signals-Bbg4ewpP.js +1 -0
  57. package/src/console/ui/dist/assets/panel-store-CPCbsDRa.js +1 -0
  58. package/src/console/ui/dist/assets/panel-traces-DVAzuA_S.js +1 -0
  59. package/src/console/ui/dist/assets/panel-vault-D1_MvOmo.js +1 -0
  60. package/src/console/ui/dist/assets/{rolldown-runtime-CNC7AqOf.js → rolldown-runtime-B0Z9INg1.js} +1 -1
  61. package/src/console/ui/dist/index.html +2 -2
  62. package/src/console/ui/gates/types.ts +1 -0
  63. package/src/console/ui/plugins/fixture.ts +7 -0
  64. package/src/console/ui/plugins/types.ts +3 -0
  65. package/src/console/ui/shell/client.ts +3 -0
  66. package/src/console/ui/shell/panels/channels/ChannelsPanel.tsx +7 -2
  67. package/src/console/ui/shell/panels/clock/ClockPanel.tsx +9 -2
  68. package/src/console/ui/shell/panels/gates/GatesPanel.tsx +12 -5
  69. package/src/console/ui/shell/panels/plugins/PluginsPanel.tsx +18 -4
  70. package/src/console/ui/shell/panels/signals/SignalsPanel.tsx +13 -2
  71. package/src/console/ui/shell/panels/store/StorePanel.tsx +11 -4
  72. package/src/console/ui/shell/panels/vault/VaultPanel.tsx +9 -3
  73. package/src/console/ui/signals/types.ts +1 -0
  74. package/src/console/ui/store/fixture.ts +5 -0
  75. package/src/console/ui/store/types.ts +2 -0
  76. package/src/docker/compose.ts +5 -0
  77. package/src/docker/docker.test.ts +41 -0
  78. package/src/docker/recipes/index.ts +10 -2
  79. package/src/docker/recipes/meilisearch.ts +31 -0
  80. package/src/drivers/conformance.test.ts +26 -0
  81. package/src/drivers/conformance.ts +40 -3
  82. package/src/drivers/drizzle-dialect.test.ts +4 -0
  83. package/src/drivers/drizzle-dialect.ts +8 -4
  84. package/src/drivers/index.ts +18 -2
  85. package/src/drivers/libsql.ts +179 -0
  86. package/src/drivers/meilisearch.integration.test.ts +77 -0
  87. package/src/drivers/meilisearch.test.ts +181 -0
  88. package/src/drivers/meilisearch.ts +208 -0
  89. package/src/drivers/memory.ts +4 -4
  90. package/src/drivers/pglite.ts +79 -0
  91. package/src/drivers/pgvector.ts +60 -25
  92. package/src/drivers/types.ts +106 -16
  93. package/src/drivers/vault-driver-removal.test.ts +6 -0
  94. package/src/drivers/vault-types.ts +4 -4
  95. package/src/elements/ai/runtime.ts +6 -0
  96. package/src/elements/ai.test.ts +22 -0
  97. package/src/elements/channel/declare.ts +5 -0
  98. package/src/elements/clock/declare.ts +5 -0
  99. package/src/elements/clock/durable.ts +7 -1
  100. package/src/elements/gate/declare.ts +28 -5
  101. package/src/elements/gate.ts +1 -0
  102. package/src/elements/signal/declare.ts +5 -0
  103. package/src/elements/store/declare.ts +10 -2
  104. package/src/elements/store/index-boot.test.ts +299 -0
  105. package/src/elements/store/runtime.ts +110 -17
  106. package/src/elements/store/schema-decl.ts +7 -0
  107. package/src/elements/store.ts +2 -0
  108. package/src/elements/vault.test.ts +27 -4
  109. package/src/elements/vault.ts +1 -1
  110. package/src/index.ts +4 -0
  111. package/src/kernel/abort-scope.ts +116 -0
  112. package/src/kernel/app.ts +12 -2
  113. package/src/kernel/boot-bind/store.test.ts +68 -1
  114. package/src/kernel/boot-bind/store.ts +92 -2
  115. package/src/kernel/concurrency.test.ts +294 -0
  116. package/src/kernel/concurrency.ts +220 -0
  117. package/src/kernel/flow.ts +9 -0
  118. package/src/kernel/fx.test.ts +23 -2
  119. package/src/kernel/fx.ts +80 -4
  120. package/src/kernel/index.ts +23 -0
  121. package/src/kernel/journal.ts +9 -0
  122. package/src/kernel/plugin/capabilities.test.ts +18 -0
  123. package/src/kernel/plugin.ts +11 -3
  124. package/src/kernel/redacted.ts +74 -0
  125. package/src/kernel/registry.ts +29 -6
  126. package/src/kernel/router.ts +3 -3
  127. package/src/manifest/types.ts +21 -0
  128. package/src/release/measure.ts +4 -0
  129. package/src/test/provisions.integration.test.ts +1 -1
  130. package/site/content/docs/get-started/comparison.mdx +0 -65
  131. package/src/console/ui/dist/assets/index-BWo8R7NR.js +0 -10
  132. package/src/console/ui/dist/assets/panel-access-C0J2D-a2.js +0 -64
  133. package/src/console/ui/dist/assets/panel-channels-BOmQ-onL.js +0 -1
  134. package/src/console/ui/dist/assets/panel-clock-giAq0Ccv.js +0 -1
  135. package/src/console/ui/dist/assets/panel-flows-DlCU5zjA.js +0 -45
  136. package/src/console/ui/dist/assets/panel-gates-XclZxWD5.js +0 -1
  137. package/src/console/ui/dist/assets/panel-overview-BznEOTnb.js +0 -1
  138. package/src/console/ui/dist/assets/panel-plugins-CcGM1g64.js +0 -1
  139. package/src/console/ui/dist/assets/panel-runs-CGWNHLR4.js +0 -1
  140. package/src/console/ui/dist/assets/panel-signals-CNywkdak.js +0 -1
  141. package/src/console/ui/dist/assets/panel-store-KmTbFHMH.js +0 -1
  142. package/src/console/ui/dist/assets/panel-traces-DBLx2ilD.js +0 -1
  143. package/src/console/ui/dist/assets/panel-vault-CEnFc0dk.js +0 -1
  144. package/src/drivers/vault-infisical.ts +0 -57
package/AGENTS.md CHANGED
@@ -92,3 +92,9 @@ Engine: Bun `>=1.3`.
92
92
  | Manifest | `manifest.v1.schema.json` |
93
93
 
94
94
  **If the documentation is silent, stop and ask.**
95
+
96
+ ## After every implementation
97
+
98
+ Before claiming work done: run [`.agents/skills/oke-ship`](.agents/skills/oke-ship/SKILL.md) — append notes to `changelog.md` under `## Unreleased` (never under a shipped `## v…` section), and update site docs via [`.agents/skills/oke-docs`](.agents/skills/oke-docs/SKILL.md) for any user-facing surface. Version bump is separate: `bun run bump` promotes Unreleased into the next `## vX.Y.Z`.
99
+
100
+ Dependency bumps (one package, one `package.json`, or all): [`.agents/skills/oke-deps`](.agents/skills/oke-deps/SKILL.md).
@@ -321,6 +321,10 @@
321
321
  "type": "string",
322
322
  "enum": ["once", "broadcast", "live"]
323
323
  },
324
+ "description": {
325
+ "description": "Optional human description; consumers fall back to the signal map key.",
326
+ "type": "string"
327
+ },
324
328
  "retries": { "type": "integer", "minimum": 0 },
325
329
  "deadLetter": { "type": "boolean" },
326
330
  "schema": { "$ref": "#/$defs/JsonSchema" },
@@ -354,6 +358,10 @@
354
358
  ]
355
359
  },
356
360
  "sqlName": { "type": "string", "minLength": 1 },
361
+ "description": {
362
+ "description": "Optional human description; consumers fall back to the column map key.",
363
+ "type": "string"
364
+ },
357
365
  "pii": { "type": "boolean" },
358
366
  "sensitive": { "type": "boolean" },
359
367
  "retain": { "type": "string", "minLength": 1 },
@@ -408,6 +416,10 @@
408
416
  "type": "string",
409
417
  "enum": ["sql", "kv", "files", "index"]
410
418
  },
419
+ "description": {
420
+ "description": "Optional human description; consumers fall back to the store map key.",
421
+ "type": "string"
422
+ },
411
423
  "tables": {
412
424
  "type": "object",
413
425
  "propertyNames": { "type": "string", "minLength": 1 },
@@ -451,7 +463,11 @@
451
463
  "cron": { "type": "string", "minLength": 1 },
452
464
  "every": { "type": "string", "minLength": 1 },
453
465
  "timezone": { "type": "string", "minLength": 1 },
454
- "overridable": { "type": "boolean" }
466
+ "overridable": { "type": "boolean" },
467
+ "description": {
468
+ "description": "Optional human description; consumers fall back to the clock map key.",
469
+ "type": "string"
470
+ }
455
471
  }
456
472
  },
457
473
  "Gate": {
@@ -485,6 +501,10 @@
485
501
  "type": "array",
486
502
  "items": { "type": "string", "minLength": 1 },
487
503
  "uniqueItems": true
504
+ },
505
+ "description": {
506
+ "description": "Optional human description; consumers fall back to the gate map key.",
507
+ "type": "string"
488
508
  }
489
509
  }
490
510
  },
@@ -502,6 +522,10 @@
502
522
  "type": "object",
503
523
  "additionalProperties": false,
504
524
  "properties": {
525
+ "description": {
526
+ "description": "Optional human description; consumers fall back to the channel map key.",
527
+ "type": "string"
528
+ },
505
529
  "medium": {
506
530
  "type": "string",
507
531
  "enum": ["email", "sms", "whatsapp", "push", "any"]
@@ -580,6 +604,18 @@
580
604
  }
581
605
  }
582
606
  },
607
+ "PluginTable": {
608
+ "description": "Optional metadata for a plugin-contributed table.",
609
+ "type": "object",
610
+ "additionalProperties": false,
611
+ "properties": {
612
+ "plane": { "type": "string", "minLength": 1 },
613
+ "description": {
614
+ "description": "Optional human description; consumers fall back to the table name.",
615
+ "type": "string"
616
+ }
617
+ }
618
+ },
583
619
  "Plugin": {
584
620
  "type": "object",
585
621
  "additionalProperties": false,
@@ -600,6 +636,12 @@
600
636
  "type": "array",
601
637
  "items": { "type": "string", "minLength": 1 },
602
638
  "uniqueItems": true
639
+ },
640
+ "tables": {
641
+ "description": "Optional metadata for table:* contributions.",
642
+ "type": "object",
643
+ "propertyNames": { "type": "string", "minLength": 1 },
644
+ "additionalProperties": { "$ref": "#/$defs/PluginTable" }
603
645
  }
604
646
  }
605
647
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "okengine",
3
- "version": "0.3.5",
3
+ "version": "0.4.3",
4
4
  "description": "One law. Eight elements. Ten exports. One package. One manifest. Every backend need is derived, never added.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -56,7 +56,7 @@
56
56
  "prepack": "bun run build",
57
57
  "bench": "bun test ./src/runtime/cold-start.bench.ts src/client/budget.test.ts src/console/budget.test.ts src/kernel/budget.test.ts src/kernel/routing-budget.test.ts src/runs/bench.test.ts",
58
58
  "budgets": "bun src/release/publish.ts",
59
- "gate": "bun test src/cli/doc-staleness.test.ts src/drivers/vault-driver-removal.test.ts src/kernel/errors.registry.test.ts src/upgrade/codemods.test.ts",
59
+ "gate": "bun test src/cli/doc-staleness.test.ts src/cli/competitor-mention-removal.test.ts src/drivers/vault-driver-removal.test.ts src/kernel/errors.registry.test.ts src/upgrade/codemods.test.ts",
60
60
  "dev": "bun run --cwd site dev",
61
61
  "site:build": "bun run --cwd site build",
62
62
  "ci": "bun scripts/ci.ts",
@@ -66,10 +66,10 @@
66
66
  },
67
67
  "dependencies": {
68
68
  "@clack/prompts": "^1.7.0",
69
- "@duckdb/node-api": "^1.5.5-r.1",
69
+ "@duckdb/node-api": "^1.5.5-r.2",
70
70
  "ajv": "^8.20.0",
71
71
  "ajv-formats": "^3.0.1",
72
- "oxc-parser": "^0.141.0",
72
+ "oxc-parser": "^0.142.0",
73
73
  "sently": "^0.8.0"
74
74
  },
75
75
  "devDependencies": {
@@ -78,17 +78,20 @@
78
78
  "@codemirror/lang-json": "^6.0.2",
79
79
  "@codemirror/state": "^6.7.1",
80
80
  "@codemirror/theme-one-dark": "^6.1.3",
81
- "@codemirror/view": "^6.43.6",
82
- "@oxc-project/types": "^0.141.0",
83
- "@playwright/test": "^1.62.0",
81
+ "@codemirror/view": "^6.43.7",
82
+ "@electric-sql/pglite": "^0.5.4",
83
+ "@electric-sql/pglite-pgvector": "^0.0.5",
84
+ "@libsql/client": "^0.17.4",
85
+ "@oxc-project/types": "^0.142.0",
86
+ "@playwright/test": "^1.62.1",
84
87
  "@sinclair/typebox": "^0.34.52",
85
88
  "@tailwindcss/vite": "^4.3.3",
86
89
  "@tanstack/react-query": "^5.101.4",
87
90
  "@tanstack/react-router": "^1.170.18",
88
91
  "@types/bun": "latest",
89
- "@types/react": "^19.2.17",
90
- "@types/react-dom": "^19.2.3",
91
- "@vitejs/plugin-react": "^6.0.4",
92
+ "@types/react": "^19.2.18",
93
+ "@types/react-dom": "^19.2.4",
94
+ "@vitejs/plugin-react": "^6.0.5",
92
95
  "arktype": "^2.2.3",
93
96
  "axe-core": "^4.12.1",
94
97
  "clsx": "^2.1.1",
@@ -102,22 +105,34 @@
102
105
  "oxfmt": "^0.61.0",
103
106
  "oxlint": "^1.76.0",
104
107
  "oxlint-tsgolint": "^7.0.2001",
105
- "playwright": "^1.62.0",
108
+ "playwright": "^1.62.1",
106
109
  "react": "^19.2.8",
107
110
  "react-dom": "^19.2.8",
108
111
  "tailwindcss": "^4.3.3",
109
112
  "typescript": "^7.0.2",
110
113
  "valibot": "^1.4.2",
111
- "vite": "^8.1.5",
114
+ "vite": "^8.2.0",
112
115
  "zod": "^4.4.3"
113
116
  },
114
117
  "peerDependencies": {
118
+ "@electric-sql/pglite": ">=0.5.0",
119
+ "@electric-sql/pglite-pgvector": ">=0.0.5",
120
+ "@libsql/client": ">=0.15.0",
115
121
  "drizzle-kit": ">=1.0.0-rc.0",
116
122
  "drizzle-orm": ">=1.0.0-rc.0",
117
123
  "drizzle-seed": ">=0.3.0",
118
124
  "zod": ">=3.23.0"
119
125
  },
120
126
  "peerDependenciesMeta": {
127
+ "@electric-sql/pglite": {
128
+ "optional": true
129
+ },
130
+ "@electric-sql/pglite-pgvector": {
131
+ "optional": true
132
+ },
133
+ "@libsql/client": {
134
+ "optional": true
135
+ },
121
136
  "drizzle-kit": {
122
137
  "optional": true
123
138
  },
@@ -7,13 +7,15 @@ source: "docs/spec/unified-theory.md"
7
7
 
8
8
  Tools alone don't make a good operator — an agent also needs to know the _vocabulary_: what a Flow is, why `fx` is the only door, which driver ids are legal. OKE ships that knowledge as contracts and skills that agents load automatically, so sessions start aligned instead of drifting and getting corrected.
9
9
 
10
- ## The three layers
10
+ ## The layers
11
11
 
12
12
  | Layer | Path | Loaded when | Teaches |
13
13
  | ------------------ | -------------------------- | ------------------------------------------------- | --------------------------------------------------------------------- |
14
14
  | **Agent contract** | `AGENTS.md` (repo root) | Every agent session, automatically | The one law, eight elements, ten exports, the fx rule, ports, budgets |
15
15
  | **Element skill** | `.agents/skills/oke/` | Building or changing an okengine app | The element contract in depth — declaration patterns per element |
16
16
  | **Docs skill** | `.agents/skills/oke-docs/` | Writing or editing docs under `site/content/docs` | The documentation information-architecture standard and its gates |
17
+ | **Ship skill** | `.agents/skills/oke-ship/` | After any implementation, before claiming done | Changelog under `## Unreleased` + docs sync via `oke-docs` |
18
+ | **Deps skill** | `.agents/skills/oke-deps/` | Updating `package.json` dependencies | Scoped bumps, Bun install, pins (Drizzle RC, fumadocs alias, …) |
17
19
 
18
20
  ## AGENTS.md — the root contract
19
21
 
@@ -21,14 +23,16 @@ Every OKE app's repo carries an `AGENTS.md` that agents (Cursor, Claude Code, an
21
23
 
22
24
  ## Skills — installable know-how
23
25
 
24
- Skills are `SKILL.md` packages an agent loads when the work matches their description. The two OKE ships:
26
+ Skills are `SKILL.md` packages an agent loads when the work matches their description. OKE ships:
25
27
 
26
- | Skill | Use it for | Inside |
27
- | ---------- | ----------------------------------- | --------------------------------------------------------- |
28
- | `oke` | App work — flows, elements, drivers | The agent contract, element patterns, the fx invariants |
29
- | `oke-docs` | Docs work — new pages, rewrites | The page skeleton, verification sources, the density gate |
28
+ | Skill | Use it for | Inside |
29
+ | ---------- | -------------------------------------------- | ------------------------------------------------------------------------- |
30
+ | `oke` | App work — flows, elements, drivers | The agent contract, element patterns, the fx invariants |
31
+ | `oke-docs` | Docs work — new pages, rewrites | The page skeleton, verification sources, the density gate |
32
+ | `oke-ship` | Closing an implementation — changelog + docs | Append under `## Unreleased`; `bun run bump` promotes it into `## vX.Y.Z` |
33
+ | `oke-deps` | Dependency updates — one package or all | Scope map, `ncu` + Bun, reject downgrades / protect RC and aliases |
30
34
 
31
- Both live in the repo under `.agents/skills/`, so they travel with the code and stay versioned with what they describe.
35
+ All live in the repo under `.agents/skills/`, so they travel with the code and stay versioned with what they describe.
32
36
 
33
37
  ## How contracts compose with MCP
34
38
 
@@ -14,6 +14,10 @@ Answers: **secret contracts, who can read each, rotation due**
14
14
  ## What this panel shows
15
15
 
16
16
  <Cards>
17
+ <Card
18
+ title="Contracts"
19
+ description="Optional description as the human title; technical key stays visible."
20
+ />
17
21
  <Card
18
22
  title="Fingerprints"
19
23
  description="Salted hash per environment — rotation and drift without exposure."
@@ -86,6 +86,8 @@ export const triage = smart.prompt("ticket-triage", {
86
86
  | `evals` | string | Path to a `.jsonl` eval set, regression-gated via `oke eval` |
87
87
  | `budget` | object | `maxCostPerCall` — cost is a first-class dimension |
88
88
 
89
+ <AiGuardrails />
90
+
89
91
  ## Agents with real guardrails
90
92
 
91
93
  An agent's tools are your flows — each carrying its own gates, effects, and typed errors, so the agent can never do anything a flow couldn't:
@@ -71,13 +71,16 @@ Locally the `console` driver captures mail into an inbox instead of sending; in
71
71
  | `binder.template(name, opts)` | Typed template bound to that medium |
72
72
  | `channel.template(name, opts)` | Medium-agnostic template (one body, any medium) |
73
73
 
74
- | Template option | Type | Meaning |
75
- | --------------- | --------------------- | ------------------------------------------------- |
76
- | `schema` | zod / Standard Schema | The data the template may reference — typed sends |
77
- | `locales` | string[] | Languages this template is rendered in |
74
+ | Template option | Type | Meaning |
75
+ | --------------- | --------------------- | ---------------------------------------------------------- |
76
+ | `description` | string | Human title in the Console (falls back to the template id) |
77
+ | `schema` | zod / Standard Schema | The data the template may reference — typed sends |
78
+ | `locales` | string[] | Languages this template is rendered in |
78
79
 
79
80
  ## The human physics
80
81
 
82
+ <ChannelPhysics />
83
+
81
84
  ### Consent is checked before sending
82
85
 
83
86
  Opt-out is first-class: a subject who opted out of a medium is **suppressed** — the send resolves without contacting the provider, and the receipt says so. You never hand-roll "did they unsubscribe?" checks.
@@ -83,10 +83,7 @@ do: async (input, fx) => {
83
83
 
84
84
  ## Two kinds of schedules
85
85
 
86
- | Declaration | Shows in Console | Cron + timezone | Runtime-editable | Use for |
87
- | ------------------- | ---------------- | --------------- | ------------------ | --------------------------------- |
88
- | `every("1h")` | no | no | no | Simple fixed intervals |
89
- | `clock(name, opts)` | yes | yes | when `overridable` | Business schedules operators tune |
86
+ <ClockSchedules />
90
87
 
91
88
  Both are triggers consumed with the same `on(trigger, flow)` — the flow underneath does not know the difference.
92
89
 
@@ -98,6 +95,7 @@ Both are triggers consumed with the same `on(trigger, flow)` — the flow undern
98
95
  | `every` | string | — | Fixed interval: `"30s"` · `"10m"` · `"1h"` · `"7d"` |
99
96
  | `timezone` | string | `"UTC"` | IANA timezone for cron evaluation |
100
97
  | `overridable` | boolean | `false` | Allow the Console to edit / pause the schedule |
98
+ | `description` | string | — | Human title in the Console (falls back to the clock name) |
101
99
 
102
100
  ## Sleeping inside a flow
103
101
 
@@ -77,6 +77,7 @@ curl -X POST localhost:6530/orders -d '{"sku":"SKU-1","qty":2}' -H 'content-type
77
77
  | `in` | Input contract — validated before `do` runs; bad input is a 422 |
78
78
  | `out` | Output contract — the return value is checked against it |
79
79
  | `errors` | Typed failures — **returned** with `fx.fail`, never thrown |
80
+ | `retry` | Optional whole-`do` backoff on thrown errors (same journal) |
80
81
  | `do` | The work — every read, write, emit, and call goes through `fx` |
81
82
 
82
83
  Failures are values, not exceptions:
@@ -95,13 +96,7 @@ Every response follows one envelope — success `{ data, error: null }`, failure
95
96
 
96
97
  ## The five triggers
97
98
 
98
- | Trigger | Starts when | Replaces |
99
- | ---------------------------- | ---------------------------- | ------------------ |
100
- | `http.post("/orders")` | a request arrives | endpoint · handler |
101
- | `orderPlaced` (a signal) | another flow emits | queue consumer |
102
- | `every("1h")` | time passes | cron job |
103
- | `db.table(orders).changed()` | a row changes | CDC pipeline |
104
- | — none | another flow calls `fx.call` | "private" helper |
99
+ <FlowTriggers />
105
100
 
106
101
  ### http — a request arrives
107
102
 
@@ -191,10 +186,12 @@ Everything a flow may touch, on one object:
191
186
  | `fx.ask(prompt, input)` | ask | Call a versioned AI prompt |
192
187
  | `fx.run(agent, input)` | ask | Run a bounded agent |
193
188
  | `fx.call(flow, input)` | call | Invoke another flow |
194
- | `fx.vault(contract)` | read | Read a secret (redacted from logs) |
189
+ | `fx.vault(contract)` | read | Read a secret (`Redacted`; logs masked) |
195
190
  | `fx.clock.now()` / `.sleep(…)` | — | Injected time / durable sleep |
196
191
  | `fx.cache.get/set` | — | Shared cache with effect-aware invalidation |
197
192
  | `fx.step(name, fn)` | — | Named durable step — never re-runs on replay |
193
+ | `fx.all` / `fx.race` / `fx.retry` | — | Structured concurrency + backoff retry |
194
+ | `fx.signal` | — | Ambient `AbortSignal` for the current branch |
198
195
  | `fx.id()` · `fx.log` · `fx.t` | — | UUIDs, redacting logger, i18n |
199
196
  | `fx.auth` · `fx.operator` · `fx.tenant` | — | Who is calling (user / operator / tenant) |
200
197
 
@@ -215,18 +212,20 @@ export const chargeOrder = flow({
215
212
  out: z.boolean(),
216
213
  do: async ({ orderId }, fx) => {
217
214
  const intent = await fx.step("create-intent", () =>
218
- stripe(fx.vault(stripeKey)).create(orderId),
215
+ stripe(fx.vault(stripeKey).reveal()).create(orderId),
219
216
  );
220
217
 
221
218
  await fx.clock.sleep("verify-window", "2m"); // survives restart and deploy
222
219
 
223
- return fx.step("confirm", () => stripe(fx.vault(stripeKey)).confirm(intent));
220
+ return fx.step("confirm", () => stripe(fx.vault(stripeKey).reveal()).confirm(intent));
224
221
  },
225
222
  });
226
223
  ```
227
224
 
228
225
  **Consequence:** kill the process between the two steps and the run **resumes at `confirm`** — completed steps replay from the journal, so the card is not charged twice. This is verified by the engine's own test suite: after resume, `create-intent` has run exactly once.
229
226
 
227
+ For flaky sub-steps, wrap the work in `fx.retry` **inside** `fx.step` so a completed charge is never retried on resume. Coarse whole-body retry is also available as `flow({ retry: { retries, delay, backoff, jitter } })` and reuses the same journal session.
228
+
230
229
  ## Composition is just calls
231
230
 
232
231
  Wiring is values flowing between flows — declared in code, never configured in a dashboard:
@@ -251,6 +250,16 @@ That bypasses the effect graph: the Manifest can't see the call, cache keys miss
251
250
 
252
251
  `fx.fail(code, data)` for expected outcomes — anything in `errors`. A throw is a crash: the run fails with a 500-shaped error and no typed code for the client. Expected failures are values so clients can switch on `error.code`.
253
252
 
253
+ </Accordion>
254
+ <Accordion title="Promise.all left orphan work running after one branch failed">
255
+
256
+ Use `fx.all` (or `fx.race`) with thunks. The first rejection aborts siblings through `fx.signal`. Bare `Promise.all` / `Promise.race` do not cancel losers.
257
+
258
+ </Accordion>
259
+ <Accordion title="Durable flow retried a completed charge">
260
+
261
+ Put `fx.retry` **inside** `fx.step`, not around it. Completed steps replay from the journal and never re-run. `flow({ retry })` is for coarse whole-body retries on the same session.
262
+
254
263
  </Accordion>
255
264
  <Accordion title="How do I share logic between flows — a private function?">
256
265
 
@@ -72,12 +72,18 @@ A denied request never reaches `do`. It returns one of three typed failures, lik
72
72
 
73
73
  </Steps>
74
74
 
75
+ <GatePipeline />
76
+
75
77
  ## Two kinds of gates
76
78
 
77
- | Declaration | Question it answers | Evaluated against |
78
- | -------------------------- | -------------------------------------- | ---------------------------------- |
79
- | `gate.policy(name, check)` | Is this principal allowed? (ABAC) | auth / operator / request metadata |
80
- | `gate.rate(options)` | Is there budget left for this subject? | an atomic counter on the kv driver |
79
+ | Declaration | Question it answers | Evaluated against |
80
+ | ------------------------------------- | -------------------------------------- | ---------------------------------- |
81
+ | `gate.policy(name, check \| options)` | Is this principal allowed? (ABAC) | auth / operator / request metadata |
82
+ | `gate.rate(options)` | Is there budget left for this subject? | an atomic counter on the kv driver |
83
+
84
+ `gate.policy(name, check)` stays the terse form. Pass
85
+ `{ check, description }` when you want a human title in the Console
86
+ (falls back to the policy name).
81
87
 
82
88
  ### The policy context
83
89
 
@@ -97,13 +103,14 @@ A policy name containing `:` is also a `Module:Action` permission — the same d
97
103
 
98
104
  ### Rate options
99
105
 
100
- | Option | Type | Default | Meaning |
101
- | ------------- | ------------ | -------------------------- | -------------------------------------------- |
102
- | `max` | number | — (**required**) | Takes allowed within `per` |
103
- | `per` | string | — (**required**) | Window / refill period (`"1m"`, `"60s"`, …) |
104
- | `keyBy` | string | — | Subject dimension (`"ip"`, `"user"`, …) |
105
- | `strategy` | RateStrategy | `"sliding-window-counter"` | Algorithm (below) |
106
- | `overridable` | boolean | `false` | Allow the Console to retune `max`/`per` live |
106
+ | Option | Type | Default | Meaning |
107
+ | ------------- | ------------ | -------------------------- | -------------------------------------------------------- |
108
+ | `max` | number | — (**required**) | Takes allowed within `per` |
109
+ | `per` | string | — (**required**) | Window / refill period (`"1m"`, `"60s"`, …) |
110
+ | `keyBy` | string | — | Subject dimension (`"ip"`, `"user"`, …) |
111
+ | `strategy` | RateStrategy | `"sliding-window-counter"` | Algorithm (below) |
112
+ | `overridable` | boolean | `false` | Allow the Console to retune `max`/`per` live |
113
+ | `description` | string | — | Human title in the Console (falls back to the rate name) |
107
114
 
108
115
  | Strategy | Behavior |
109
116
  | ------------------------ | ------------------------------------------------------ |
@@ -76,23 +76,20 @@ That's the loop: declare → `fx.emit` → `on(signal, flow)`. The runtime handl
76
76
 
77
77
  ## The three delivery physics
78
78
 
79
- | `delivery` | Semantics | Use for |
80
- | ------------- | ------------------------------------------------------ | ---------------------------------------------------------- |
81
- | `"once"` | Queue: competing consumers, retries, dead-letter queue | Jobs that must happen exactly once — emails, payments sync |
82
- | `"broadcast"` | Pub/sub: **every** subscriber receives a copy | Cache invalidation, cross-service events |
83
- | `"live"` | Stream: client-subscribable and replayable | Live feeds, dashboards, progress updates |
79
+ <SignalDelivery />
84
80
 
85
81
  The declaration is identical in shape for all three — switching physics later is a one-word change, not a migration to another library.
86
82
 
87
83
  ### Options
88
84
 
89
- | Option | Type | Default | Meaning |
90
- | ------------ | --------------------------------- | ---------------- | -------------------------------------------------------------- |
91
- | `delivery` | `"once" \| "broadcast" \| "live"` | — (**required**) | Delivery physics |
92
- | `schema` | zod / Standard Schema | — | Payload contract; typed emits and Manifest docs |
93
- | `retries` | number | `3` | Max delivery attempts before dead-letter (`once`) |
94
- | `deadLetter` | boolean | `true` | Preserve exhausted messages in the DLQ (`once`) |
95
- | `optional` | boolean | `false` | Allow emitting while nobody subscribes (skip the orphan check) |
85
+ | Option | Type | Default | Meaning |
86
+ | ------------- | --------------------------------- | ---------------- | -------------------------------------------------------------- |
87
+ | `delivery` | `"once" \| "broadcast" \| "live"` | — (**required**) | Delivery physics |
88
+ | `description` | string | — | Human title in the Console (falls back to the signal name) |
89
+ | `schema` | zod / Standard Schema | | Payload contract; typed emits and Manifest docs |
90
+ | `retries` | number | `3` | Max delivery attempts before dead-letter (`once`) |
91
+ | `deadLetter` | boolean | `true` | Preserve exhausted messages in the DLQ (`once`) |
92
+ | `optional` | boolean | `false` | Allow emitting while nobody subscribes (skip the orphan check) |
96
93
 
97
94
  ## When delivery fails
98
95
 
@@ -90,17 +90,18 @@ export const createNote = on(
90
90
 
91
91
  Pick the facet that matches the physics of your data. Each declaration is one line; the runtime handle shows what flows can do with it.
92
92
 
93
- | Facet | Declares | Best for | `fx.store(…)` handle |
94
- | ------------------------- | ----------------- | ------------------------------------- | ------------------------------------------------------ |
95
- | `store.sql(name, opts)` | a SQL database | domain tables, relations, constraints | `select` · `insert` · `update` · `delete` · `findById` |
96
- | `store.kv(name)` | a key-value space | cache, sessions, rate limits | `get` · `set(key, value, ttl?)` · `delete` · `list` |
97
- | `store.files(name)` | a blob bucket | uploads, exports, attachments | `put` · `get` · `delete` · `list(prefix?)` |
98
- | `store.index(name, opts)` | a vector index | semantic search / RAG (`dims`) | `upsert` · `search(vector, topK?)` · `delete` |
93
+ <StoreFacets />
94
+
95
+ Every facet accepts optional `description` a human title in the Console
96
+ (falls back to the store name). `store.index` also takes `dims`.
99
97
 
100
98
  ```typescript
101
- export const cache = store.kv("sessions");
99
+ export const cache = store.kv("sessions", { description: "Session cache" });
102
100
  export const uploads = store.files("attachments");
103
- export const embeddings = store.index("docs", { dims: 1536 });
101
+ export const embeddings = store.index("docs", {
102
+ dims: 1536,
103
+ description: "Document embeddings",
104
+ });
104
105
  ```
105
106
 
106
107
  ## CRUD without boilerplate — `store.resource`
@@ -175,6 +176,7 @@ The recommended path: declare tables ORM-agnostically, then let `oke db` emit re
175
176
  | `.default(v)` · `.defaultFn(id \| now)` | defaults |
176
177
  | `.pii()` · `.sensitive()` · `.retain("30d")` | privacy classification |
177
178
  | `.as("sql_name")` | override the automatic `camelCase → snake_case` |
179
+ | `.describe("…")` | human title in the Console (falls back to key) |
178
180
  | `.references(() => col, { onDelete })` | foreign key |
179
181
 
180
182
  ### Foreign keys and relations
@@ -242,6 +244,65 @@ drivers: {
242
244
 
243
245
  Container images come from the `images` map — change the vendor by changing the pin, never the driver id.
244
246
 
247
+ ### Opt-in SQL drivers
248
+
249
+ Two more `store.sql` drivers cover specific parity needs. Both are optional peers — `bun add` them yourself; neither changes the `sqlite` local default.
250
+
251
+ | Driver | Install | Wire dialect | Choose for |
252
+ | -------- | -------------------------------------------------------- | ------------ | ------------------------------------------------------ |
253
+ | `libsql` | `@libsql/client` | sqlite | local vector search without Docker (`index: "libsql"`) |
254
+ | `pglite` | `@electric-sql/pglite` + `@electric-sql/pglite-pgvector` | postgresql | the real Postgres dialect + pgvector, in-process |
255
+
256
+ <Callout title="PGlite is not a latency play">
257
+ PGlite pays a one-time ~1 s WASM init, and warm CRUD is roughly 15× slower than `bun:sqlite`.
258
+ Choose it for dialect/pgvector parity with prod — never as a faster local default.
259
+ </Callout>
260
+
261
+ A SQL-backed index shares the sql facet's already-open connection, so configure the pair together:
262
+
263
+ ```typescript title="oke.config.ts"
264
+ drivers: {
265
+ store: {
266
+ sql: { local: "libsql" },
267
+ index: { local: "libsql" },
268
+ },
269
+ },
270
+ ```
271
+
272
+ | `store.index` id | Shares connection from | Real ANN via |
273
+ | ---------------- | -------------------------- | ------------------------------------ |
274
+ | `memory` | — (in-process, default) | — |
275
+ | `pgvector` | `postgres` or `pglite` sql | HNSW + `cosineDistance` |
276
+ | `libsql` | `libsql` sql | `libsql_vector_idx` + `vector_top_k` |
277
+
278
+ **Consequence:** a configured SQL-backed index that cannot reach its engine — missing peer, missing `vector` extension, wrong sql driver — fails loudly at first use. It never silently falls back to `memory`.
279
+
280
+ ### Full-text search — `meilisearch` (opt-in)
281
+
282
+ `meilisearch` is a fourth `store.index` backend, opt-in only — `memory` stays the default. It is a genuinely different capability from the vector ANN drivers: typo-tolerant, faceted full-text relevance, not cosine similarity. The two models are kept apart by a **discriminated union** on `driverId`, so TypeScript rejects a vector query against a text index (and vice versa) at compile time — never at runtime:
283
+
284
+ | `driverId` | `upsert` | `search` | `score` means |
285
+ | -------------------------------- | --------------------- | ---------------------------------------- | --------------------------- |
286
+ | `memory` / `pgvector` / `libsql` | `(id, vector, meta?)` | `search(vector, topK?)` → `IndexHit[]` | cosine similarity |
287
+ | `meilisearch` | `(id, document)` | `search(q, opts?)` → `{ hits, facets? }` | relevance (`_rankingScore`) |
288
+
289
+ ```typescript title="oke.config.ts"
290
+ drivers: {
291
+ store: {
292
+ index: { local: "meilisearch", docker: "meilisearch", prod: "meilisearch" },
293
+ },
294
+ },
295
+ images: {
296
+ "store.index": "getmeili/meilisearch:v1.37",
297
+ },
298
+ ```
299
+
300
+ - **Local mode** needs the `meilisearch` binary on `PATH` (a documented prerequisite, like Docker for `--docker` — OKE never auto-downloads binaries). `oke dev` spawns it with a generated master key persisted under `.oke/meilisearch/` (`0700`/`0600`).
301
+ - **Docker / prod** uses the image recipe; the app gets `OKE_STORE_INDEX_URL` + `OKE_STORE_INDEX_KEY` from Compose — a standalone HTTP service, never the shared `sqlUrl`.
302
+ - **Fail-loud:** a configured meilisearch that is unreachable or unhealthy throws `MeilisearchUnavailableError` — never a silent memory fallback.
303
+ - **Not for vectors:** `ai.embed` / `fx.search` stay vector-only; pointing an embed `into` a meilisearch index fails loud (embeddings don't apply).
304
+ - **Alpine caveat:** the raw glibc binary fails on Alpine/musl _hosts_; the official image is musl-clean. Install the binary for your platform (e.g. Homebrew / install script), or use `--docker`.
305
+
245
306
  ## Privacy built in
246
307
 
247
308
  Columns tagged `.pii()` or `.sensitive()` are masked at the store boundary — flows, logs, and the Console see a mask, not the value. Revealing cleartext PII requires an explicit `pii:reveal` gate on the flow, so access is a permission, not a convention.
@@ -55,7 +55,9 @@ oke vault set STRIPE_KEY sk_test_your_key_here
55
55
  <Step>
56
56
  ### Read it in a Flow
57
57
 
58
- Flows read secrets through `fx.vault` — never through `process.env`:
58
+ Flows read secrets through `fx.vault` — never through `process.env`. `fx.vault` returns a
59
+ `Redacted<string>`: printing, logging, or serializing it yields a placeholder, never the value.
60
+ Call `.reveal()` once at the boundary that needs the real credential:
59
61
 
60
62
  ```typescript title="src/flows/billing/charge.ts"
61
63
  export const charge = on(
@@ -64,14 +66,17 @@ export const charge = on(
64
66
  in: ChargeInput,
65
67
  out: z.object({ id: z.string() }),
66
68
  do: async (input, fx) => {
67
- const key = fx.vault(stripeKey); // cleartext, inside this flow only
68
- const intent = await stripe(key).create(input);
69
+ const key = fx.vault(stripeKey); // Redacted safe to pass to fx.log
70
+ const intent = await stripe(key.reveal()).create(input);
69
71
  return { id: intent.id };
70
72
  },
71
73
  }),
72
74
  );
73
75
  ```
74
76
 
77
+ `fx.log` masks any `Redacted` found in the logged data (nested included). A revealed string that
78
+ still reaches `fx.log` is scrubbed by the boot redactor as before.
79
+
75
80
  </Step>
76
81
 
77
82
  </Steps>
@@ -91,7 +96,7 @@ That's the whole loop: declare → set → read. Everything below is what OKE gu
91
96
 
92
97
  | Option | Type | Meaning |
93
98
  | ------------- | --------------------- | ------------------------------------------------------------------------ |
94
- | `description` | string | Shown in boot-gap listings and the Console |
99
+ | `description` | string | Human title in boot-gap listings and the Console (falls back to the key) |
95
100
  | `rotate` | string | Rotation hint (`"90d"`) — drives the rotation-due signal in the Console |
96
101
  | `schema` | zod / Standard Schema | Validated at boot; a bad value fails boot like a missing one |
97
102
  | `dev` | string | Local-only fallback when no source provides a value (never used in prod) |
@@ -111,13 +116,7 @@ export const dbUrl = vault.secret("DATABASE_URL", {
111
116
 
112
117
  At boot, each contract is resolved through this chain — **first hit wins**:
113
118
 
114
- | # | Source | Typical content |
115
- | --- | -------------------- | ------------------------------------------------- |
116
- | 1 | `process.env` | Real environment (CI, hosting platform) |
117
- | 2 | `.env.local` | Your machine's local overrides (gitignored) |
118
- | 3 | `docker/.env.docker` | Generated compose stack credentials |
119
- | 4 | vault driver | OpenBao in docker/prod mode |
120
- | 5 | `dev` fallback | The `dev:` option on the contract (never in prod) |
119
+ <VaultResolution />
121
120
 
122
121
  If every layer misses a contract, boot **fails** before any request is served:
123
122
 
@@ -167,7 +166,7 @@ vault: {
167
166
  | `openbao` | OpenBao (KV v2) | Docker + prod — durable, access-controlled |
168
167
  | `memory` | in-process map | Tests |
169
168
 
170
- Other drivers (`infisical`, `managed`) implement the same `VaultDriver` interface — adding one is a driver exercise, not an architecture change.
169
+ Other drivers (`managed`) implement the same `VaultDriver` interface — adding one is a driver exercise, not an architecture change.
171
170
 
172
171
  ## OpenBao in docker and prod
173
172