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