okengine 0.2.5 → 0.2.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +46 -38
- 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
package/docs/spec/example.md
CHANGED
|
@@ -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
|
|
@@ -44,11 +46,11 @@ import { defineConfig } from "okengine/config";
|
|
|
44
46
|
export default defineConfig({
|
|
45
47
|
drivers: {
|
|
46
48
|
// `stack` = `oke dev -s` (local server). Omitting it copies `prod`
|
|
47
|
-
// (defineConfig); vault `sops` becomes
|
|
49
|
+
// (defineConfig); vault `sops` becomes docker `dotenv` for `docker/.env.docker`.
|
|
48
50
|
store: {
|
|
49
51
|
sql: {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
+
local: "sqlite",
|
|
53
|
+
docker: "postgres",
|
|
52
54
|
test: "memory",
|
|
53
55
|
prod: "postgres",
|
|
54
56
|
},
|
|
@@ -66,9 +68,9 @@ import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
|
|
|
66
68
|
import { id, now } from "okengine/store";
|
|
67
69
|
|
|
68
70
|
export const notes = sqliteTable("notes", {
|
|
69
|
-
id:
|
|
70
|
-
title:
|
|
71
|
-
body:
|
|
71
|
+
id: text("id").primaryKey().$defaultFn(id),
|
|
72
|
+
title: text("title").notNull(),
|
|
73
|
+
body: text("body").notNull(),
|
|
72
74
|
createdAt: integer("created_at").notNull().$defaultFn(now),
|
|
73
75
|
});
|
|
74
76
|
```
|
|
@@ -96,43 +98,57 @@ import { db } from "../../core";
|
|
|
96
98
|
import { notes } from "../../schema";
|
|
97
99
|
|
|
98
100
|
// Contracts derived from the schema — one source of truth, refined where the API is stricter
|
|
99
|
-
const NewNote
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
101
|
+
const NewNote = createInsertSchema(notes, { title: (s) => s.min(1).max(120) }).omit({
|
|
102
|
+
id: true,
|
|
103
|
+
createdAt: true,
|
|
104
|
+
});
|
|
105
|
+
const Note = createSelectSchema(notes);
|
|
106
|
+
const NoteId = z.object({ id: z.string() });
|
|
103
107
|
const NotFound = z.object({});
|
|
104
108
|
|
|
105
|
-
export const create = on(
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
}
|
|
109
|
+
export const create = on(
|
|
110
|
+
http.post("/notes"),
|
|
111
|
+
flow({
|
|
112
|
+
in: NewNote,
|
|
113
|
+
out: NoteId,
|
|
114
|
+
do: async (input, fx) => {
|
|
115
|
+
const [note] = await fx.store(db).insert(notes).values(input).returning();
|
|
116
|
+
return { id: note.id };
|
|
117
|
+
},
|
|
118
|
+
}),
|
|
119
|
+
);
|
|
113
120
|
// effects → writes[sql:notes]
|
|
114
121
|
|
|
115
|
-
export const list = on(
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
122
|
+
export const list = on(
|
|
123
|
+
http.get("/notes"),
|
|
124
|
+
flow({
|
|
125
|
+
out: Note.array(),
|
|
126
|
+
do: (_, fx) => fx.store(db).select().from(notes),
|
|
127
|
+
}),
|
|
128
|
+
);
|
|
119
129
|
// effects → reads[sql:notes]
|
|
120
130
|
|
|
121
|
-
export const get = on(
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
}
|
|
131
|
+
export const get = on(
|
|
132
|
+
http.get("/notes/:id"),
|
|
133
|
+
flow({
|
|
134
|
+
in: NoteId,
|
|
135
|
+
out: Note,
|
|
136
|
+
errors: { NotFound },
|
|
137
|
+
do: async ({ id }, fx) => (await fx.store(db).findById(notes, id)) ?? fx.fail("NotFound", {}),
|
|
138
|
+
}),
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
export const remove = on(
|
|
142
|
+
http.delete("/notes/:id"),
|
|
143
|
+
flow({
|
|
144
|
+
in: NoteId,
|
|
145
|
+
errors: { NotFound },
|
|
146
|
+
do: async ({ id }, fx) => {
|
|
147
|
+
const deleted = await fx.store(db).delete(notes, id);
|
|
148
|
+
if (!deleted) return fx.fail("NotFound", {});
|
|
149
|
+
},
|
|
150
|
+
}),
|
|
151
|
+
);
|
|
136
152
|
```
|
|
137
153
|
|
|
138
154
|
**Four things to notice.**
|
|
@@ -153,7 +169,7 @@ import * as notes from "./flows/notes";
|
|
|
153
169
|
|
|
154
170
|
export const app = oke({ name: "notes" }).adopt({ notes });
|
|
155
171
|
|
|
156
|
-
export type App = typeof app;
|
|
172
|
+
export type App = typeof app; // ← the client needs nothing else
|
|
157
173
|
```
|
|
158
174
|
|
|
159
175
|
`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.
|
|
@@ -171,7 +187,7 @@ const api = createClient<App>("http://localhost:6530", { $routes: app.$routes })
|
|
|
171
187
|
const { data, error } = await api.notes.get({ id: "n_1" });
|
|
172
188
|
|
|
173
189
|
if (error?.code === "NotFound") show("gone");
|
|
174
|
-
else console.log(data.title);
|
|
190
|
+
else console.log(data.title); // ← typed, no codegen ✅
|
|
175
191
|
// GET /notes/n_1 — the method and path are derived from the flow's own trigger,
|
|
176
192
|
// not from a separate RPC convention.
|
|
177
193
|
```
|
|
@@ -184,7 +200,7 @@ import { createTestApp } from "okengine/test";
|
|
|
184
200
|
import { app } from "../src/app";
|
|
185
201
|
|
|
186
202
|
test("create then read", async () => {
|
|
187
|
-
const t = await createTestApp(app);
|
|
203
|
+
const t = await createTestApp(app); // memory driver, automatic
|
|
188
204
|
const { data } = await t.api.notes.create({ title: "First", body: "Hello" });
|
|
189
205
|
const { data: note } = await t.api.notes.get({ id: data!.id });
|
|
190
206
|
expect(note!.title).toBe("First");
|
|
@@ -207,9 +223,10 @@ Four exports (`oke`, `on`, `flow`, `store`), two elements, a typed client, autom
|
|
|
207
223
|
|
|
208
224
|
### What is missing
|
|
209
225
|
|
|
210
|
-
Everything here is synchronous. A real application needs work that happens
|
|
226
|
+
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.
|
|
211
227
|
|
|
212
228
|
---
|
|
229
|
+
|
|
213
230
|
---
|
|
214
231
|
|
|
215
232
|
# 2 · INTERMEDIATE — Linkly
|
|
@@ -225,6 +242,8 @@ linkly/
|
|
|
225
242
|
│ ├── app.ts
|
|
226
243
|
│ ├── core.ts
|
|
227
244
|
│ ├── gates.ts
|
|
245
|
+
│ ├── schema.decl.ts
|
|
246
|
+
│ ├── schema.generated.ts
|
|
228
247
|
│ ├── schema.ts
|
|
229
248
|
│ └── flows/
|
|
230
249
|
│ ├── links/
|
|
@@ -242,10 +261,12 @@ import { defineConfig } from "okengine/config";
|
|
|
242
261
|
|
|
243
262
|
export default defineConfig({
|
|
244
263
|
drivers: {
|
|
245
|
-
store:
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
264
|
+
store: {
|
|
265
|
+
sql: { local: "sqlite", test: "memory", prod: "postgres" },
|
|
266
|
+
kv: { local: "memory", test: "memory", prod: "redis" },
|
|
267
|
+
},
|
|
268
|
+
signal: { local: "memory", test: "memory", prod: "postgres" },
|
|
269
|
+
clock: { local: "memory", test: "frozen", prod: "postgres" },
|
|
249
270
|
},
|
|
250
271
|
});
|
|
251
272
|
```
|
|
@@ -260,36 +281,60 @@ import { gate } from "okengine";
|
|
|
260
281
|
export const member = gate.policy("member", ({ auth }) => !!auth?.verified);
|
|
261
282
|
|
|
262
283
|
export const fair = gate.rate({
|
|
263
|
-
strategy: "sliding-window-counter",
|
|
264
|
-
max: 60,
|
|
284
|
+
strategy: "sliding-window-counter", // near-exact, two keys, no boundary bursts
|
|
285
|
+
max: 60,
|
|
286
|
+
per: "1m",
|
|
287
|
+
keyBy: "ip",
|
|
265
288
|
});
|
|
266
289
|
```
|
|
267
290
|
|
|
268
291
|
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.
|
|
269
292
|
|
|
270
|
-
### `src/schema.ts`
|
|
293
|
+
### `src/schema.decl.ts`
|
|
271
294
|
|
|
272
295
|
```typescript
|
|
273
|
-
import {
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
296
|
+
import { store, field } from "okengine";
|
|
297
|
+
|
|
298
|
+
/** Short links — `code` is the human-facing unique key. */
|
|
299
|
+
export const links = store.schema.table("links", {
|
|
300
|
+
id: field.text().primaryKey(), // `increment` targets this column
|
|
301
|
+
code: field.text().notNull().unique(), // the short, human-facing key
|
|
302
|
+
url: field.text().notNull(),
|
|
303
|
+
userId: field.text().notNull(),
|
|
304
|
+
clicks: field.integer().notNull().default(0),
|
|
305
|
+
createdAt: field.integer().notNull(),
|
|
282
306
|
});
|
|
283
307
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
308
|
+
/** Per-link daily click counts — FK to `links.code`. */
|
|
309
|
+
export const daily = store.schema.table("daily", {
|
|
310
|
+
id: field.text().primaryKey(),
|
|
311
|
+
code: field
|
|
312
|
+
.text()
|
|
313
|
+
.notNull()
|
|
314
|
+
.references(() => links.code),
|
|
315
|
+
day: field.text().notNull(), // "YYYY-MM-DD"
|
|
316
|
+
clicks: field.integer().notNull().default(0),
|
|
289
317
|
});
|
|
318
|
+
|
|
319
|
+
/** links ↔ daily — one link has many daily rows. */
|
|
320
|
+
export const relations = store.schema.relations({ links, daily }, (r) => ({
|
|
321
|
+
links: {
|
|
322
|
+
daily: r.many.daily({
|
|
323
|
+
from: r.links.code,
|
|
324
|
+
to: r.daily.code,
|
|
325
|
+
}),
|
|
326
|
+
},
|
|
327
|
+
daily: {
|
|
328
|
+
link: r.one.links({
|
|
329
|
+
from: r.daily.code,
|
|
330
|
+
to: r.links.code,
|
|
331
|
+
optional: false,
|
|
332
|
+
}),
|
|
333
|
+
},
|
|
334
|
+
}));
|
|
290
335
|
```
|
|
291
336
|
|
|
292
|
-
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.
|
|
337
|
+
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`.
|
|
293
338
|
|
|
294
339
|
### `src/flows/links/signals.ts`
|
|
295
340
|
|
|
@@ -299,13 +344,14 @@ import { z } from "zod";
|
|
|
299
344
|
|
|
300
345
|
export const linkClicked = signal("link-clicked", {
|
|
301
346
|
schema: z.object({ code: z.string(), at: z.number(), referrer: z.string().optional() }),
|
|
302
|
-
delivery: "once",
|
|
303
|
-
retries: 3,
|
|
347
|
+
delivery: "once", // queue physics: one consumer, retries, DLQ
|
|
348
|
+
retries: 3,
|
|
349
|
+
deadLetter: true,
|
|
304
350
|
});
|
|
305
351
|
|
|
306
352
|
export const linkStats = signal("link-stats", {
|
|
307
353
|
schema: z.object({ code: z.string(), clicks: z.number() }),
|
|
308
|
-
delivery: "live",
|
|
354
|
+
delivery: "live", // stream physics: clients subscribe
|
|
309
355
|
});
|
|
310
356
|
```
|
|
311
357
|
|
|
@@ -323,52 +369,75 @@ import { NewLink, LinkCode, Link, NotFound, Taken } from "./shapes";
|
|
|
323
369
|
import { links } from "../../schema";
|
|
324
370
|
|
|
325
371
|
// ① HTTP — "an endpoint"
|
|
326
|
-
export const shorten = on(
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
372
|
+
export const shorten = on(
|
|
373
|
+
http.post("/links").gate(member, fair),
|
|
374
|
+
flow({
|
|
375
|
+
in: NewLink,
|
|
376
|
+
out: LinkCode,
|
|
377
|
+
errors: { Taken },
|
|
378
|
+
do: async ({ url, code }, fx) => {
|
|
379
|
+
if (await fx.store(db).exists(links, { code })) return fx.fail("Taken", {});
|
|
380
|
+
const id = fx.id();
|
|
381
|
+
await fx
|
|
382
|
+
.store(db)
|
|
383
|
+
.insert(links)
|
|
384
|
+
.values({ id, code, url, userId: fx.auth.userId, clicks: 0, createdAt: Date.now() });
|
|
385
|
+
return { code };
|
|
386
|
+
},
|
|
387
|
+
}),
|
|
388
|
+
);
|
|
336
389
|
|
|
337
390
|
// ② HTTP — the hot path
|
|
338
|
-
export const redirect = on(
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
391
|
+
export const redirect = on(
|
|
392
|
+
http.get("/:code").gate(fair),
|
|
393
|
+
flow({
|
|
394
|
+
in: LinkCode,
|
|
395
|
+
out: Link,
|
|
396
|
+
errors: { NotFound },
|
|
397
|
+
do: async ({ code }, fx) => {
|
|
398
|
+
const [link] = await fx.store(db).select().from(links).where(eq(links.code, code)).limit(1);
|
|
399
|
+
if (!link) return fx.fail("NotFound", {});
|
|
400
|
+
|
|
401
|
+
await fx.emit(linkClicked, { code, at: Date.now() }); // same transaction as any write
|
|
402
|
+
return link;
|
|
403
|
+
},
|
|
404
|
+
}),
|
|
405
|
+
);
|
|
348
406
|
|
|
349
407
|
// ③ SIGNAL — "a queue consumer", and the same species as ① and ②
|
|
350
|
-
on(
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
})
|
|
408
|
+
on(
|
|
409
|
+
linkClicked,
|
|
410
|
+
flow({
|
|
411
|
+
do: async ({ code }, fx) => {
|
|
412
|
+
const [link] = await fx.store(db).select().from(links).where(eq(links.code, code)).limit(1);
|
|
413
|
+
const clicks = await fx.store(db).increment(links, link.id, "clicks");
|
|
414
|
+
await fx.emit(linkStats, { code, clicks }); // live: pushed to subscribers
|
|
415
|
+
},
|
|
416
|
+
}),
|
|
417
|
+
);
|
|
357
418
|
|
|
358
419
|
// ④ CLOCK — "a cron job", and still the same species
|
|
359
|
-
on(
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
420
|
+
on(
|
|
421
|
+
every("1h"),
|
|
422
|
+
flow({
|
|
423
|
+
do: (_, fx) => {
|
|
424
|
+
const cutoff = Date.now() - 30 * 24 * 60 * 60 * 1000; // 30 days
|
|
425
|
+
return fx.store(db).delete(links).where(lt(links.createdAt, cutoff));
|
|
426
|
+
},
|
|
427
|
+
}),
|
|
428
|
+
);
|
|
365
429
|
|
|
366
430
|
// ⑤ A plain flow with no trigger — callable, not "private"
|
|
367
431
|
export const stats = flow({
|
|
368
|
-
in: LinkCode,
|
|
432
|
+
in: LinkCode,
|
|
433
|
+
out: z.object({ clicks: z.number() }),
|
|
369
434
|
do: async ({ code }, fx) => {
|
|
370
|
-
const [link] = await fx
|
|
371
|
-
.
|
|
435
|
+
const [link] = await fx
|
|
436
|
+
.store(db)
|
|
437
|
+
.select({ clicks: links.clicks })
|
|
438
|
+
.from(links)
|
|
439
|
+
.where(eq(links.code, code))
|
|
440
|
+
.limit(1);
|
|
372
441
|
return link ?? { clicks: 0 };
|
|
373
442
|
},
|
|
374
443
|
});
|
|
@@ -387,22 +456,37 @@ import { db } from "../../core";
|
|
|
387
456
|
import { member } from "../../gates";
|
|
388
457
|
import { daily } from "../../schema";
|
|
389
458
|
|
|
390
|
-
on(
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
}
|
|
459
|
+
on(
|
|
460
|
+
linkClicked,
|
|
461
|
+
flow({
|
|
462
|
+
// a second consumer of the same signal
|
|
463
|
+
do: async ({ code, at }, fx) => {
|
|
464
|
+
const day = new Date(at).toISOString().slice(0, 10);
|
|
465
|
+
const [row] = await fx
|
|
466
|
+
.store(db)
|
|
467
|
+
.select()
|
|
468
|
+
.from(daily)
|
|
469
|
+
.where(and(eq(daily.code, code), eq(daily.day, day)))
|
|
470
|
+
.limit(1);
|
|
471
|
+
|
|
472
|
+
if (row) await fx.store(db).increment(daily, row.id, "clicks");
|
|
473
|
+
else await fx.store(db).insert(daily).values({ id: fx.id(), code, day, clicks: 1 });
|
|
474
|
+
},
|
|
475
|
+
}),
|
|
476
|
+
);
|
|
477
|
+
|
|
478
|
+
export const report = on(
|
|
479
|
+
http.get("/links/:code/report").gate(member),
|
|
480
|
+
flow({
|
|
481
|
+
out: z.array(z.object({ day: z.string(), clicks: z.number() })),
|
|
482
|
+
do: ({ code }, fx) =>
|
|
483
|
+
fx
|
|
484
|
+
.store(db)
|
|
485
|
+
.select({ day: daily.day, clicks: daily.clicks })
|
|
486
|
+
.from(daily)
|
|
487
|
+
.where(eq(daily.code, code)),
|
|
488
|
+
}),
|
|
489
|
+
);
|
|
406
490
|
```
|
|
407
491
|
|
|
408
492
|
This unit never imports anything from `links` except the signal declaration. Decoupling is structural, not a discipline.
|
|
@@ -431,18 +515,18 @@ api.signals.linkStats.subscribe(({ code, clicks }) => paint(code, clicks));
|
|
|
431
515
|
### `tests/linkly.test.ts`
|
|
432
516
|
|
|
433
517
|
```typescript
|
|
434
|
-
const t = await createTestApp(app);
|
|
518
|
+
const t = await createTestApp(app); // memory drivers, frozen clock
|
|
435
519
|
const u = await t.auth.loginAs({});
|
|
436
520
|
|
|
437
521
|
await t.api.links.shorten({ url: "https://example.com", code: "sa" }, { as: u });
|
|
438
522
|
await t.api.links.redirect({ code: "sa" });
|
|
439
|
-
await t.signals.drain();
|
|
523
|
+
await t.signals.drain(); // run queued work deterministically
|
|
440
524
|
|
|
441
525
|
const { data } = await t.api.links.report({ code: "sa" }, { as: u });
|
|
442
526
|
expect(data![0].clicks).toBe(1);
|
|
443
527
|
|
|
444
528
|
await t.clock.advance("31d");
|
|
445
|
-
await t.cron.run("1h");
|
|
529
|
+
await t.cron.run("1h"); // time travel
|
|
446
530
|
```
|
|
447
531
|
|
|
448
532
|
### What you have
|
|
@@ -454,6 +538,7 @@ Seven exports, five elements, and five different trigger kinds — all of them t
|
|
|
454
538
|
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.
|
|
455
539
|
|
|
456
540
|
---
|
|
541
|
+
|
|
457
542
|
---
|
|
458
543
|
|
|
459
544
|
# 3 · ADVANCED — Provisions
|
|
@@ -481,7 +566,7 @@ provisions/
|
|
|
481
566
|
└── tests/orders.test.ts
|
|
482
567
|
```
|
|
483
568
|
|
|
484
|
-
**The layout rule from here on: whoever
|
|
569
|
+
**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.
|
|
485
570
|
|
|
486
571
|
### `src/schema.ts`
|
|
487
572
|
|
|
@@ -489,17 +574,17 @@ provisions/
|
|
|
489
574
|
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
|
|
490
575
|
|
|
491
576
|
export const products = sqliteTable("products", {
|
|
492
|
-
sku:
|
|
493
|
-
name:
|
|
577
|
+
sku: text("sku").primaryKey(),
|
|
578
|
+
name: text("name").notNull(),
|
|
494
579
|
stock: integer("stock").notNull().default(0),
|
|
495
580
|
});
|
|
496
581
|
|
|
497
582
|
export const orders = sqliteTable("orders", {
|
|
498
|
-
id:
|
|
499
|
-
userId:
|
|
500
|
-
sku:
|
|
501
|
-
qty:
|
|
502
|
-
status:
|
|
583
|
+
id: text("id").primaryKey(),
|
|
584
|
+
userId: text("user_id").notNull(),
|
|
585
|
+
sku: text("sku").notNull(),
|
|
586
|
+
qty: integer("qty").notNull(),
|
|
587
|
+
status: text("status").notNull().default("pending"),
|
|
503
588
|
createdAt: integer("created_at").notNull(),
|
|
504
589
|
});
|
|
505
590
|
```
|
|
@@ -511,7 +596,7 @@ import { vault } from "okengine";
|
|
|
511
596
|
import { z } from "zod";
|
|
512
597
|
|
|
513
598
|
// A declaration is a CONTRACT, not a value.
|
|
514
|
-
// Resolution: process.env → .env.local →
|
|
599
|
+
// Resolution: process.env → .env.local → docker/.env.docker → vault driver → dev fallback
|
|
515
600
|
export const stripeKey = vault.secret("STRIPE_KEY", {
|
|
516
601
|
schema: z.string().startsWith("sk_"),
|
|
517
602
|
description: "Payments gateway key",
|
|
@@ -521,7 +606,7 @@ export const stripeKey = vault.secret("STRIPE_KEY", {
|
|
|
521
606
|
|
|
522
607
|
export const dbUrl = vault.secret("DATABASE_URL", {
|
|
523
608
|
schema: z.string().url(),
|
|
524
|
-
dev: vault.
|
|
609
|
+
dev: vault.fromDocker("store.sql"), // generated by `oke dev --docker` — zero manual setup
|
|
525
610
|
});
|
|
526
611
|
```
|
|
527
612
|
|
|
@@ -534,14 +619,15 @@ import { channel } from "okengine";
|
|
|
534
619
|
import { z } from "zod";
|
|
535
620
|
|
|
536
621
|
export const mail = channel.email({ from: "Provisions <no-reply@provisions.sa>" });
|
|
537
|
-
export const sms
|
|
538
|
-
export const wa
|
|
622
|
+
export const sms = channel.sms({ sender: "PROVISIONS" });
|
|
623
|
+
export const wa = channel.whatsapp();
|
|
539
624
|
|
|
540
625
|
export const orderConfirmed = mail.template("order-confirmed", {
|
|
541
626
|
schema: z.object({ name: z.string(), orderId: z.string(), total: z.number() }),
|
|
542
627
|
});
|
|
543
628
|
|
|
544
|
-
export const otpCode = channel.template("otp-code", {
|
|
629
|
+
export const otpCode = channel.template("otp-code", {
|
|
630
|
+
// medium-agnostic
|
|
545
631
|
schema: z.object({ code: z.string() }),
|
|
546
632
|
});
|
|
547
633
|
```
|
|
@@ -562,31 +648,49 @@ import { orders, products } from "../../schema";
|
|
|
562
648
|
|
|
563
649
|
const canOrder = gate.policy("order:create", ({ auth }) => auth.scopes.has("order:create"));
|
|
564
650
|
|
|
565
|
-
export const create = on(
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
651
|
+
export const create = on(
|
|
652
|
+
http.post("/orders").gate(member, canOrder),
|
|
653
|
+
flow({
|
|
654
|
+
in: NewOrder,
|
|
655
|
+
out: OrderId,
|
|
656
|
+
errors: { OutOfStock },
|
|
657
|
+
do: async (input, fx) => {
|
|
658
|
+
const [product] = await fx
|
|
659
|
+
.store(db)
|
|
660
|
+
.select({ stock: products.stock })
|
|
661
|
+
.from(products)
|
|
662
|
+
.where(eq(products.sku, input.sku))
|
|
663
|
+
.limit(1);
|
|
664
|
+
if (!product || product.stock < input.qty)
|
|
665
|
+
return fx.fail(
|
|
666
|
+
"OutOfStock",
|
|
667
|
+
{ left: product?.stock ?? 0 },
|
|
668
|
+
{ message: fx.t("order.outOfStock", { left: product?.stock ?? 0 }) },
|
|
669
|
+
);
|
|
670
|
+
|
|
671
|
+
const id = fx.id();
|
|
672
|
+
await fx
|
|
673
|
+
.store(db)
|
|
674
|
+
.insert(orders)
|
|
675
|
+
.values({ id, userId: fx.auth.userId, ...input, status: "pending", createdAt: Date.now() });
|
|
676
|
+
await fx.emit(orderPlaced, { orderId: id });
|
|
677
|
+
return { id };
|
|
678
|
+
},
|
|
679
|
+
}),
|
|
680
|
+
);
|
|
581
681
|
|
|
582
682
|
// LIVE QUERY — realtime and auto-caching from one flag
|
|
583
|
-
export const mine = on(
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
683
|
+
export const mine = on(
|
|
684
|
+
http.get("/orders").gate(member).live(),
|
|
685
|
+
flow({
|
|
686
|
+
out: OrderRow.array(),
|
|
687
|
+
do: (_, fx) => fx.store(db).select().from(orders).where(eq(orders.userId, fx.auth.userId)),
|
|
688
|
+
}),
|
|
689
|
+
);
|
|
587
690
|
|
|
588
691
|
export const getOrder = flow({
|
|
589
|
-
in: OrderId,
|
|
692
|
+
in: OrderId,
|
|
693
|
+
out: OrderRow,
|
|
590
694
|
do: async ({ id }, fx) => {
|
|
591
695
|
const [order] = await fx.store(db).select().from(orders).where(eq(orders.id, id)).limit(1);
|
|
592
696
|
return order;
|
|
@@ -594,19 +698,28 @@ export const getOrder = flow({
|
|
|
594
698
|
});
|
|
595
699
|
|
|
596
700
|
// SIGNAL consumer
|
|
597
|
-
on(
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
.
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
701
|
+
on(
|
|
702
|
+
orderPlaced,
|
|
703
|
+
flow({
|
|
704
|
+
do: async ({ orderId }, fx) => {
|
|
705
|
+
const paid = await fx.call(chargeOrder, { orderId });
|
|
706
|
+
await fx
|
|
707
|
+
.store(db)
|
|
708
|
+
.update(orders)
|
|
709
|
+
.set({ status: paid ? "confirmed" : "failed" })
|
|
710
|
+
.where(eq(orders.id, orderId));
|
|
711
|
+
await fx.emit(orderNews, { orderId, status: paid ? "confirmed" : "failed" });
|
|
712
|
+
},
|
|
713
|
+
}),
|
|
714
|
+
);
|
|
605
715
|
|
|
606
716
|
// CHANGE trigger — CDC, built in
|
|
607
|
-
on(
|
|
608
|
-
|
|
609
|
-
|
|
717
|
+
on(
|
|
718
|
+
db.table(orders).changed("status"),
|
|
719
|
+
flow({
|
|
720
|
+
do: ({ before, after }, fx) => fx.log.info("status", { from: before.status, to: after.status }),
|
|
721
|
+
}),
|
|
722
|
+
);
|
|
610
723
|
```
|
|
611
724
|
|
|
612
725
|
**`.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.
|
|
@@ -620,13 +733,16 @@ import { stripeKey } from "../../vault";
|
|
|
620
733
|
import { OrderRef } from "./shapes";
|
|
621
734
|
|
|
622
735
|
export const chargeOrder = flow({
|
|
623
|
-
durable: true,
|
|
624
|
-
in: OrderRef,
|
|
736
|
+
durable: true, // every fx call below is journaled
|
|
737
|
+
in: OrderRef,
|
|
738
|
+
out: z.boolean(),
|
|
625
739
|
do: async ({ orderId }, fx) => {
|
|
626
|
-
const intent = await fx.step("create-intent", () =>
|
|
627
|
-
|
|
740
|
+
const intent = await fx.step("create-intent", () =>
|
|
741
|
+
// never re-runs on replay
|
|
742
|
+
stripe(fx.vault(stripeKey)).create(orderId),
|
|
743
|
+
);
|
|
628
744
|
|
|
629
|
-
await fx.clock.sleep("verify-window", "2m");
|
|
745
|
+
await fx.clock.sleep("verify-window", "2m"); // survives restart and deploy
|
|
630
746
|
|
|
631
747
|
return fx.step("confirm", () => stripe(fx.vault(stripeKey)).confirm(intent));
|
|
632
748
|
},
|
|
@@ -644,13 +760,19 @@ import { orderNews } from "../orders/signals";
|
|
|
644
760
|
import { getOrder } from "../orders";
|
|
645
761
|
import { orderConfirmed, otpCode, wa, sms } from "../../channels";
|
|
646
762
|
|
|
647
|
-
on(
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
763
|
+
on(
|
|
764
|
+
orderNews,
|
|
765
|
+
flow({
|
|
766
|
+
do: async ({ orderId, status }, fx) => {
|
|
767
|
+
if (status !== "confirmed") return;
|
|
768
|
+
const o = await fx.call(getOrder, { id: orderId });
|
|
769
|
+
await fx.send(orderConfirmed, {
|
|
770
|
+
to: o.userId,
|
|
771
|
+
data: { name: o.userName, orderId, total: o.total },
|
|
772
|
+
});
|
|
773
|
+
},
|
|
774
|
+
}),
|
|
775
|
+
);
|
|
654
776
|
|
|
655
777
|
export const sendOtp = flow({
|
|
656
778
|
in: z.object({ userId: z.string(), code: z.string() }),
|
|
@@ -692,11 +814,11 @@ import * as notifications from "./flows/notifications";
|
|
|
692
814
|
|
|
693
815
|
export const app = oke({ name: "provisions" })
|
|
694
816
|
.adopt({ orders, payments, notifications })
|
|
695
|
-
.plug(auth())
|
|
696
|
-
.plug(audit)
|
|
817
|
+
.plug(auth()) // zero ceremony: uses your configured store
|
|
818
|
+
.plug(audit) // app-wide
|
|
697
819
|
.hook("onError", (ctx, err, fx) => fx.log.error(err));
|
|
698
820
|
|
|
699
|
-
app.unit("orders").plug(rateLimit({ max: 30 }));
|
|
821
|
+
app.unit("orders").plug(rateLimit({ max: 30 })); // this unit only
|
|
700
822
|
|
|
701
823
|
export type App = typeof app;
|
|
702
824
|
```
|
|
@@ -727,16 +849,17 @@ Typed keys — a missing key is a compile error. Locale resolves per request: us
|
|
|
727
849
|
### `tests/orders.test.ts`
|
|
728
850
|
|
|
729
851
|
```typescript
|
|
730
|
-
const t = await createTestApp(app);
|
|
852
|
+
const t = await createTestApp(app); // memory drivers, frozen clock
|
|
731
853
|
const u = await t.auth.loginAs({ scopes: ["order:create"] });
|
|
732
854
|
|
|
733
855
|
const { data } = await t.api.orders.create({ sku: "COFFEE", qty: 2 }, { as: u });
|
|
734
856
|
await t.signals.drain();
|
|
735
|
-
await t.clock.advance("2m");
|
|
857
|
+
await t.clock.advance("2m"); // the durable sleep elapses instantly
|
|
736
858
|
await t.signals.drain();
|
|
737
859
|
|
|
738
860
|
expect(t.channels.sent()).toContainEqual(
|
|
739
|
-
expect.objectContaining({ template: "order-confirmed", to: u.id, locale: "ar" })
|
|
861
|
+
expect.objectContaining({ template: "order-confirmed", to: u.id, locale: "ar" }),
|
|
862
|
+
);
|
|
740
863
|
```
|
|
741
864
|
|
|
742
865
|
### What you have
|
|
@@ -748,6 +871,7 @@ All ten exports and seven of the eight elements. Durable execution, human-facing
|
|
|
748
871
|
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.
|
|
749
872
|
|
|
750
873
|
---
|
|
874
|
+
|
|
751
875
|
---
|
|
752
876
|
|
|
753
877
|
# 4 · COMPLEX — Skyport
|
|
@@ -784,41 +908,44 @@ export default defineConfig({
|
|
|
784
908
|
// (Bun.sql, bun:sqlite, Bun.redis, Bun.S3) — zero npm client dependencies.
|
|
785
909
|
drivers: {
|
|
786
910
|
store: {
|
|
787
|
-
sql: {
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
911
|
+
sql: {
|
|
912
|
+
local: "sqlite",
|
|
913
|
+
test: "memory",
|
|
914
|
+
prod: { driver: "postgres", url: dbUrl, pool: { max: 20 }, replicas: [dbReplica1] },
|
|
915
|
+
}, // read-only flows auto-route here
|
|
916
|
+
kv: { local: "memory", test: "memory", prod: "redis" }, // Redis · Valkey · Dragonfly
|
|
917
|
+
files: { local: "fs", test: "memory", prod: "s3" }, // S3 · R2 · SeaweedFS · MinIO
|
|
918
|
+
index: { local: "pgvector", test: "memory", prod: "pgvector" },
|
|
793
919
|
},
|
|
794
|
-
signal:
|
|
795
|
-
clock:
|
|
796
|
-
vault:
|
|
797
|
-
runs:
|
|
920
|
+
signal: { local: "memory", test: "memory", prod: "postgres" },
|
|
921
|
+
clock: { local: "memory", test: "frozen", prod: "postgres" },
|
|
922
|
+
vault: { local: "dotenv", test: "memory", prod: "sops" }, // SOPS/age — committable
|
|
923
|
+
runs: { local: "files", test: "memory", prod: "files" }, // Parquet + DuckDB
|
|
798
924
|
channel: {
|
|
799
|
-
email:
|
|
800
|
-
sms:
|
|
801
|
-
whatsapp: {
|
|
802
|
-
push:
|
|
925
|
+
email: { local: "console", prod: "smtp" },
|
|
926
|
+
sms: { local: "console", prod: "unifonic" },
|
|
927
|
+
whatsapp: { local: "console", prod: "wa-cloud" },
|
|
928
|
+
push: { local: "console", prod: "fcm" },
|
|
803
929
|
},
|
|
804
930
|
ai: {
|
|
805
|
-
|
|
931
|
+
local: "mock", // deterministic — tests never call out
|
|
806
932
|
prod: { driver: "anthropic", key: anthropicKey },
|
|
807
933
|
// no prod default: model choice is never guessed.
|
|
808
934
|
// "openai-compatible" covers vLLM · Groq · Together · LM Studio · most self-hosted
|
|
809
935
|
},
|
|
810
936
|
},
|
|
811
937
|
|
|
812
|
-
images: {
|
|
938
|
+
images: {
|
|
939
|
+
// vendor choice, keyed by ROLE
|
|
813
940
|
"store.sql": "pgvector/pgvector:pg17",
|
|
814
|
-
"store.kv":
|
|
941
|
+
"store.kv": "valkey/valkey:8-alpine",
|
|
815
942
|
},
|
|
816
943
|
|
|
817
|
-
i18n:
|
|
818
|
-
tenancy:
|
|
819
|
-
topology: "monolith",
|
|
820
|
-
ports:
|
|
821
|
-
console:
|
|
944
|
+
i18n: { locales: ["en", "ar"], default: "ar", dir: { ar: "rtl" } },
|
|
945
|
+
tenancy: { resolve: (ctx) => ctx.auth.orgId, isolation: "row" },
|
|
946
|
+
topology: "monolith", // flip to "services" — code unchanged
|
|
947
|
+
ports: { app: 6530, console: 6533, mcp: 6535 }, // O·K·E = 6·5·3
|
|
948
|
+
console: { prod: { enabled: true, auth: "required" } },
|
|
822
949
|
});
|
|
823
950
|
```
|
|
824
951
|
|
|
@@ -829,22 +956,25 @@ import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
|
|
|
829
956
|
import { id } from "okengine/store";
|
|
830
957
|
|
|
831
958
|
export const bookings = sqliteTable("bookings", {
|
|
832
|
-
id:
|
|
833
|
-
userId:
|
|
834
|
-
flightId:
|
|
835
|
-
seats:
|
|
836
|
-
status:
|
|
959
|
+
id: text("id").primaryKey().$defaultFn(id),
|
|
960
|
+
userId: text("user_id").notNull(),
|
|
961
|
+
flightId: text("flight_id").notNull(),
|
|
962
|
+
seats: integer("seats").notNull(),
|
|
963
|
+
status: text("status").notNull().default("pending"),
|
|
837
964
|
createdAt: integer("created_at").notNull(),
|
|
838
965
|
});
|
|
839
966
|
|
|
840
967
|
export const flights = sqliteTable("flights", {
|
|
841
|
-
id:
|
|
968
|
+
id: text("id").primaryKey(),
|
|
842
969
|
seatsAvailable: integer("seats_available").notNull(),
|
|
843
970
|
});
|
|
844
971
|
|
|
845
972
|
export const tickets = sqliteTable("tickets", {
|
|
846
|
-
id: text("id").primaryKey(),
|
|
847
|
-
|
|
973
|
+
id: text("id").primaryKey(),
|
|
974
|
+
subject: text("subject").notNull(),
|
|
975
|
+
body: text("body").notNull(),
|
|
976
|
+
urgency: text("urgency"),
|
|
977
|
+
team: text("team"),
|
|
848
978
|
summary: text("summary"),
|
|
849
979
|
});
|
|
850
980
|
```
|
|
@@ -854,10 +984,10 @@ export const tickets = sqliteTable("tickets", {
|
|
|
854
984
|
```typescript
|
|
855
985
|
import { z } from "zod";
|
|
856
986
|
|
|
857
|
-
export const NewBooking
|
|
858
|
-
export const BookingId
|
|
859
|
-
export const BookingRow
|
|
860
|
-
export const FlightFull
|
|
987
|
+
export const NewBooking = z.object({ flightId: z.string(), seats: z.number().min(1).max(9) });
|
|
988
|
+
export const BookingId = z.object({ id: z.string() });
|
|
989
|
+
export const BookingRow = z.object({ id: z.string(), status: z.string(), seats: z.number() });
|
|
990
|
+
export const FlightFull = z.object({ seatsLeft: z.number() });
|
|
861
991
|
```
|
|
862
992
|
|
|
863
993
|
### `src/flows/bookings/signals.ts`
|
|
@@ -867,10 +997,14 @@ import { signal } from "okengine";
|
|
|
867
997
|
import { z } from "zod";
|
|
868
998
|
|
|
869
999
|
export const orderPlaced = signal("order-placed", {
|
|
870
|
-
schema: z.object({ orderId: z.string() }),
|
|
1000
|
+
schema: z.object({ orderId: z.string() }),
|
|
1001
|
+
delivery: "once",
|
|
1002
|
+
retries: 5,
|
|
1003
|
+
deadLetter: true,
|
|
871
1004
|
});
|
|
872
1005
|
export const seatFeed = signal("seat-feed", {
|
|
873
|
-
schema: z.object({ flightId: z.string(), left: z.number() }),
|
|
1006
|
+
schema: z.object({ flightId: z.string(), left: z.number() }),
|
|
1007
|
+
delivery: "live",
|
|
874
1008
|
});
|
|
875
1009
|
```
|
|
876
1010
|
|
|
@@ -885,32 +1019,54 @@ import { orderPlaced, seatFeed } from "./signals";
|
|
|
885
1019
|
import { NewBooking, BookingId, BookingRow, FlightFull } from "./shapes";
|
|
886
1020
|
import { bookings, flights } from "../../schema";
|
|
887
1021
|
|
|
888
|
-
export const canBook = gate.policy("booking:create", ({ auth }) =>
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
1022
|
+
export const canBook = gate.policy("booking:create", ({ auth }) =>
|
|
1023
|
+
auth.scopes.has("booking:create"),
|
|
1024
|
+
);
|
|
1025
|
+
|
|
1026
|
+
export const create = on(
|
|
1027
|
+
http.post("/bookings").gate(member, canBook, fair),
|
|
1028
|
+
flow({
|
|
1029
|
+
slo: { availability: "99.9%", latency: { p99: "200ms" } },
|
|
1030
|
+
in: NewBooking,
|
|
1031
|
+
out: BookingId,
|
|
1032
|
+
errors: { FlightFull },
|
|
1033
|
+
do: async ({ flightId, seats }, fx) => {
|
|
1034
|
+
const [flight] = await fx
|
|
1035
|
+
.store(db)
|
|
1036
|
+
.select()
|
|
1037
|
+
.from(flights)
|
|
1038
|
+
.where(eq(flights.id, flightId))
|
|
1039
|
+
.limit(1);
|
|
1040
|
+
if (!flight || flight.seatsAvailable < seats)
|
|
1041
|
+
return fx.fail("FlightFull", { seatsLeft: flight?.seatsAvailable ?? 0 });
|
|
1042
|
+
|
|
1043
|
+
const id = fx.id();
|
|
1044
|
+
await fx.store(db).insert(bookings).values({
|
|
1045
|
+
id,
|
|
1046
|
+
userId: fx.auth.userId,
|
|
1047
|
+
flightId,
|
|
1048
|
+
seats,
|
|
1049
|
+
status: "pending",
|
|
1050
|
+
createdAt: Date.now(),
|
|
1051
|
+
});
|
|
1052
|
+
await fx.emit(orderPlaced, { orderId: id });
|
|
1053
|
+
await fx.emit(seatFeed, { flightId, left: flight.seatsAvailable - seats });
|
|
1054
|
+
return { id };
|
|
1055
|
+
},
|
|
1056
|
+
}),
|
|
1057
|
+
);
|
|
906
1058
|
|
|
907
|
-
export const mine = on(
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
1059
|
+
export const mine = on(
|
|
1060
|
+
http.get("/bookings").gate(member).live(),
|
|
1061
|
+
flow({
|
|
1062
|
+
out: BookingRow.array(),
|
|
1063
|
+
do: (_, fx) => fx.store(db).select().from(bookings).where(eq(bookings.userId, fx.auth.userId)),
|
|
1064
|
+
}),
|
|
1065
|
+
);
|
|
911
1066
|
|
|
912
1067
|
export const getBooking = flow({
|
|
913
|
-
in: BookingId,
|
|
1068
|
+
in: BookingId,
|
|
1069
|
+
out: BookingRow,
|
|
914
1070
|
do: async ({ id }, fx) => {
|
|
915
1071
|
const [b] = await fx.store(db).select().from(bookings).where(eq(bookings.id, id)).limit(1);
|
|
916
1072
|
return b;
|
|
@@ -920,7 +1076,8 @@ export const getBooking = flow({
|
|
|
920
1076
|
// The agent's second tool — refunding is a distinct, gated capability, never the same
|
|
921
1077
|
// permission as reading a booking, since the agent's tool list is exactly its authority.
|
|
922
1078
|
export const refundBooking = flow({
|
|
923
|
-
in: BookingId,
|
|
1079
|
+
in: BookingId,
|
|
1080
|
+
out: BookingRow,
|
|
924
1081
|
do: async ({ id }, fx) => {
|
|
925
1082
|
await fx.store(db).update(bookings).set({ status: "refunded" }).where(eq(bookings.id, id));
|
|
926
1083
|
const [b] = await fx.store(db).select().from(bookings).where(eq(bookings.id, id)).limit(1);
|
|
@@ -937,15 +1094,15 @@ import { z } from "zod";
|
|
|
937
1094
|
import { getBooking, refundBooking } from "./flows/bookings";
|
|
938
1095
|
|
|
939
1096
|
export const smart = ai.model("smart", { provider: "anthropic", tier: "opus" });
|
|
940
|
-
export const fast
|
|
1097
|
+
export const fast = ai.model("fast", { provider: "anthropic", tier: "haiku" });
|
|
941
1098
|
|
|
942
1099
|
// A prompt is a VERSIONED ARTIFACT with a validated output shape — not a string in a handler
|
|
943
1100
|
export const triage = smart.prompt("ticket-triage", {
|
|
944
|
-
in:
|
|
1101
|
+
in: z.object({ subject: z.string(), body: z.string() }),
|
|
945
1102
|
out: z.object({ urgency: z.enum(["low", "high"]), team: z.string(), summary: z.string() }),
|
|
946
1103
|
version: 3,
|
|
947
|
-
evals: "./evals/triage.jsonl",
|
|
948
|
-
budget: { maxCostPerCall: 0.02 },
|
|
1104
|
+
evals: "./evals/triage.jsonl", // regression-gated in CI via `oke eval`
|
|
1105
|
+
budget: { maxCostPerCall: 0.02 }, // cost is a first-class dimension
|
|
949
1106
|
});
|
|
950
1107
|
|
|
951
1108
|
export const embed = ai.embed("docs", { model: fast, into: store.index("kb") });
|
|
@@ -970,36 +1127,48 @@ import { db } from "../../core";
|
|
|
970
1127
|
import { tickets } from "../../schema";
|
|
971
1128
|
|
|
972
1129
|
// ① A prompt call with a provider fallback chain and a validated result
|
|
973
|
-
export const createTicket = on(
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
1130
|
+
export const createTicket = on(
|
|
1131
|
+
http.post("/tickets").gate(member),
|
|
1132
|
+
flow({
|
|
1133
|
+
in: z.object({ subject: z.string(), body: z.string() }),
|
|
1134
|
+
out: z.object({ id: z.string(), urgency: z.string() }),
|
|
1135
|
+
do: async (input, fx) => {
|
|
1136
|
+
const t = await fx.ask(triage, input, { via: [smart, fast] });
|
|
1137
|
+
const id = fx.id();
|
|
1138
|
+
await fx
|
|
1139
|
+
.store(db)
|
|
1140
|
+
.insert(tickets)
|
|
1141
|
+
.values({ id, ...input, ...t });
|
|
1142
|
+
return { id, urgency: t.urgency };
|
|
1143
|
+
},
|
|
1144
|
+
}),
|
|
1145
|
+
);
|
|
983
1146
|
// effects → writes[sql:tickets] asks[ticket-triage v3] cost[~$0.01] nondeterministic
|
|
984
1147
|
|
|
985
1148
|
// ② RAG — retrieve, then answer with streaming tokens
|
|
986
|
-
export const askDocs = on(
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
1149
|
+
export const askDocs = on(
|
|
1150
|
+
http.post("/ask").gate(member).live(),
|
|
1151
|
+
flow({
|
|
1152
|
+
in: z.object({ question: z.string() }),
|
|
1153
|
+
do: async ({ question }, fx) => {
|
|
1154
|
+
const context = await fx.search(embed, question, { topK: 5 });
|
|
1155
|
+
return fx.stream(smart, { prompt: "answer-with-context", data: { question, context } });
|
|
1156
|
+
// streaming reaches the client through the Signal element — no separate socket layer
|
|
1157
|
+
},
|
|
1158
|
+
}),
|
|
1159
|
+
);
|
|
994
1160
|
|
|
995
1161
|
// ③ A durable, bounded agent
|
|
996
|
-
export const supportAgent = on(
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1162
|
+
export const supportAgent = on(
|
|
1163
|
+
http.post("/support").gate(member),
|
|
1164
|
+
flow({
|
|
1165
|
+
durable: true, // nondeterministic calls are ALWAYS journaled
|
|
1166
|
+
in: z.object({ message: z.string() }),
|
|
1167
|
+
do: ({ message }, fx) => fx.run(support, { message }),
|
|
1168
|
+
// the agent can only call getBooking and refundBooking, and only within THIS user's
|
|
1169
|
+
// gates and tenant scope — it cannot exceed what the code declares
|
|
1170
|
+
}),
|
|
1171
|
+
);
|
|
1003
1172
|
```
|
|
1004
1173
|
|
|
1005
1174
|
**What the compiler enforces here, for free:**
|
|
@@ -1007,7 +1176,7 @@ export const supportAgent = on(http.post("/support").gate(member), flow({
|
|
|
1007
1176
|
- 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.
|
|
1008
1177
|
- `nondeterministic` forces journaling: on replay, a model is never re-called; the recorded answer is reused.
|
|
1009
1178
|
- Automatic caching is disabled for AI flows unless a semantic cache is explicitly enabled.
|
|
1010
|
-
- Cost accumulates per flow, per tenant and per release — visible in the Console and in Manifest Diff
|
|
1179
|
+
- Cost accumulates per flow, per tenant and per release — visible in the Console and in Manifest Diff _before_ deploy.
|
|
1011
1180
|
|
|
1012
1181
|
### Declaring what "working" means
|
|
1013
1182
|
|
|
@@ -1026,7 +1195,7 @@ journey("book-a-flight", {
|
|
|
1026
1195
|
});
|
|
1027
1196
|
```
|
|
1028
1197
|
|
|
1029
|
-
Forty services at 99.9% in sequence yield 96.1% for the user. Because the causal chain is known, **the compiler rejects the impossible**:
|
|
1198
|
+
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.
|
|
1030
1199
|
|
|
1031
1200
|
### Multi-tenancy as a dimension of `fx`
|
|
1032
1201
|
|
|
@@ -1034,11 +1203,11 @@ Forty services at 99.9% in sequence yield 96.1% for the user. Because the causal
|
|
|
1034
1203
|
|
|
1035
1204
|
### The three scaling axes, never conflated
|
|
1036
1205
|
|
|
1037
|
-
| Axis
|
|
1038
|
-
|
|
1039
|
-
| **Split** (`topology`) | one deployable, or one per unit? | `monolith` = in-process calls · `services` = a container per unit, `fx.call` becomes network — code unchanged
|
|
1040
|
-
| **Clone** (horizontal) | how many copies of the app?
|
|
1041
|
-
| **Data replicas**
|
|
1206
|
+
| Axis | Question | Mechanism |
|
|
1207
|
+
| ---------------------- | -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1208
|
+
| **Split** (`topology`) | one deployable, or one per unit? | `monolith` = in-process calls · `services` = a container per unit, `fx.call` becomes network — code unchanged |
|
|
1209
|
+
| **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` |
|
|
1210
|
+
| **Data replicas** | how many copies of the data? | `replicas:` on the driver; read-only flows auto-route, derived from effects |
|
|
1042
1211
|
|
|
1043
1212
|
### `src/app.ts`
|
|
1044
1213
|
|
|
@@ -1073,7 +1242,7 @@ const u = await t.auth.loginAs({});
|
|
|
1073
1242
|
const { data } = await t.api.support.createTicket({ subject: "…", body: "…" }, { as: u });
|
|
1074
1243
|
|
|
1075
1244
|
expect(data!.urgency).toBe("high");
|
|
1076
|
-
expect(t.ai.cost()).toBeLessThan(0.02);
|
|
1245
|
+
expect(t.ai.cost()).toBeLessThan(0.02); // budgets are assertable
|
|
1077
1246
|
```
|
|
1078
1247
|
|
|
1079
1248
|
### What you have
|
|
@@ -1081,6 +1250,7 @@ expect(t.ai.cost()).toBeLessThan(0.02); // budgets are assertable
|
|
|
1081
1250
|
All eight elements, all ten exports, and one law.
|
|
1082
1251
|
|
|
1083
1252
|
---
|
|
1253
|
+
|
|
1084
1254
|
---
|
|
1085
1255
|
|
|
1086
1256
|
# REFERENCE
|
|
@@ -1095,19 +1265,24 @@ All eight elements, all ten exports, and one law.
|
|
|
1095
1265
|
"bookings.create": {
|
|
1096
1266
|
"trigger": { "http": { "method": "POST", "path": "/bookings" } },
|
|
1097
1267
|
"gates": ["member", "booking:create", "rate:sliding-window-counter:300/1m"],
|
|
1098
|
-
"in": "…",
|
|
1268
|
+
"in": "…",
|
|
1269
|
+
"out": "…",
|
|
1270
|
+
"errors": ["FlightFull"],
|
|
1099
1271
|
"effects": {
|
|
1100
|
-
"reads":
|
|
1272
|
+
"reads": ["sql:bookings"],
|
|
1101
1273
|
"writes": ["sql:bookings"],
|
|
1102
|
-
"emits":
|
|
1274
|
+
"emits": ["order-placed", "seat-feed"],
|
|
1103
1275
|
"secrets": []
|
|
1104
1276
|
},
|
|
1105
1277
|
"slo": { "availability": "99.9%", "latency": { "p99": "200ms" } },
|
|
1106
1278
|
"source": "src/flows/bookings/index.ts:18"
|
|
1107
1279
|
},
|
|
1108
1280
|
"bookings.mine": { "live": true, "cacheKeys": "computed:sql:bookings/userId" },
|
|
1109
|
-
"payments.chargeBooking": {
|
|
1110
|
-
|
|
1281
|
+
"payments.chargeBooking": {
|
|
1282
|
+
"durable": true,
|
|
1283
|
+
"steps": ["create-intent", "confirm"],
|
|
1284
|
+
"effects": { "secrets": ["STRIPE_KEY"] }
|
|
1285
|
+
},
|
|
1111
1286
|
"support.createTicket": {
|
|
1112
1287
|
"effects": { "writes": ["sql:tickets"], "asks": ["ticket-triage@3"] },
|
|
1113
1288
|
"nondeterministic": true,
|
|
@@ -1117,14 +1292,15 @@ All eight elements, all ten exports, and one law.
|
|
|
1117
1292
|
},
|
|
1118
1293
|
"signals": {
|
|
1119
1294
|
"order-placed": { "delivery": "once", "retries": 5, "deadLetter": true },
|
|
1120
|
-
"seat-feed":
|
|
1295
|
+
"seat-feed": { "delivery": "live" }
|
|
1121
1296
|
},
|
|
1122
1297
|
"channels": { "booking-confirmed": { "medium": "email", "locales": ["en", "ar"] } },
|
|
1123
1298
|
"ai": {
|
|
1124
|
-
"models":
|
|
1299
|
+
"models": { "smart": { "provider": "anthropic", "tier": "opus" } },
|
|
1125
1300
|
"prompts": { "ticket-triage": { "version": 3, "evals": "./evals/triage.jsonl" } },
|
|
1126
|
-
"agents":
|
|
1127
|
-
|
|
1301
|
+
"agents": {
|
|
1302
|
+
"support": { "tools": ["bookings.getBooking", "bookings.refundBooking"], "maxSteps": 6 }
|
|
1303
|
+
}
|
|
1128
1304
|
},
|
|
1129
1305
|
"journeys": { "book-a-flight": { "slo": { "availability": "99.5%" }, "composes": "99.6%" } },
|
|
1130
1306
|
"drivers": { "prod": ["postgres", "redis", "s3", "smtp", "sops", "anthropic", "pgvector"] },
|
|
@@ -1141,8 +1317,10 @@ bun add okengine # ONE package
|
|
|
1141
1317
|
|
|
1142
1318
|
oke dev # watch · hot reload · Console :6533 · app :6530 · MCP :6535
|
|
1143
1319
|
# → also auto-syncs client types on every save
|
|
1144
|
-
oke dev --
|
|
1145
|
-
oke dev -
|
|
1320
|
+
oke dev --local # -l session-only local (in-memory; never writes .oke/mode)
|
|
1321
|
+
oke dev --docker # -d infra compose under docker/ (no app container; host Bun)
|
|
1322
|
+
oke dev -d store.sql,signal # partial: only these roles get real backends
|
|
1323
|
+
oke mode local|docker # get/set default oke dev mode (saved in .oke/mode)
|
|
1146
1324
|
|
|
1147
1325
|
oke start # runs exactly what production runs (this is the Docker CMD)
|
|
1148
1326
|
oke doctor # verify secrets, ports, drivers, tenancy, schema drift
|
|
@@ -1173,23 +1351,23 @@ Runtime actions execute directly; structural changes arrive as reviewable diffs
|
|
|
1173
1351
|
|
|
1174
1352
|
**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.
|
|
1175
1353
|
|
|
1176
|
-
**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
|
|
1354
|
+
**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.
|
|
1177
1355
|
|
|
1178
1356
|
## Element checklist across the four applications
|
|
1179
1357
|
|
|
1180
|
-
| Element
|
|
1181
|
-
|
|
1182
|
-
| **Flow**
|
|
1183
|
-
| **Store**
|
|
1184
|
-
| **Signal**
|
|
1185
|
-
| **Clock**
|
|
1186
|
-
| **Gate**
|
|
1187
|
-
| **Vault**
|
|
1188
|
-
| **Channel**
|
|
1189
|
-
| **AI**
|
|
1190
|
-
| **`fx` door** | Basic
|
|
1191
|
-
| **Manifest**
|
|
1192
|
-
| **Plugin**
|
|
1358
|
+
| Element | First appears | The unification it proves |
|
|
1359
|
+
| ------------- | ------------- | -------------------------------------------------------------------------------------------- |
|
|
1360
|
+
| **Flow** | Basic | endpoint = consumer = cron = CDC = workflow — one species |
|
|
1361
|
+
| **Store** | Basic | sql · kv · files · index; cache and replica routing derived from effects |
|
|
1362
|
+
| **Signal** | Intermediate | queue = pub/sub = stream; delivery is a property, not three ecosystems |
|
|
1363
|
+
| **Clock** | Intermediate | cron = delay = durable time |
|
|
1364
|
+
| **Gate** | Intermediate | auth = ABAC = rate limit, composable at the trigger |
|
|
1365
|
+
| **Vault** | Advanced | typed contracts, boot-time validation, per-flow read capability |
|
|
1366
|
+
| **Channel** | Advanced | email = sms = whatsapp = push; consent, locale and fallback built in |
|
|
1367
|
+
| **AI** | Complex | prompts versioned · agents bounded by your own flows · cost and PII enforced by the compiler |
|
|
1368
|
+
| **`fx` door** | Basic | journaling · tests · least privilege · transactions · tenancy · i18n · cost |
|
|
1369
|
+
| **Manifest** | Basic | client · docs · console · security · bundle · infrastructure |
|
|
1370
|
+
| **Plugin** | Advanced | extends through the same law; built-ins have no private API |
|
|
1193
1371
|
|
|
1194
1372
|
**Ten exports — `on, flow, signal, store, clock, gate, vault, channel, ai, plugin` — one law.**
|
|
1195
1373
|
|