okengine 0.10.1 → 0.10.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/package.json +1 -1
- package/site/content/docs/ai/mcp.mdx +0 -1
- package/site/content/docs/elements/ai.mdx +0 -6
- package/site/content/docs/elements/channel.mdx +0 -6
- package/site/content/docs/elements/clock.mdx +2 -8
- package/site/content/docs/elements/flow.mdx +10 -17
- package/site/content/docs/elements/gate.mdx +4 -10
- package/site/content/docs/elements/signal.mdx +4 -13
- package/site/content/docs/elements/store.mdx +9 -16
- package/site/content/docs/elements/vault.mdx +1 -7
- package/site/content/docs/get-started/basic-usage.mdx +1 -9
- package/site/content/docs/get-started/introduction.mdx +1 -1
- package/site/content/docs/get-started/why.mdx +1 -2
- package/site/content/docs/index.mdx +0 -1
- package/site/content/docs/meta.json +0 -1
- package/site/content/docs/reference/cli.md +12 -12
- package/site/content/docs/reference/fx.mdx +6 -7
- package/src/auth/auth.test.ts +1 -2
- package/src/auth/bindings.ts +5 -15
- package/src/auth/gate-auth.test.ts +2 -2
- package/src/cli/ai-setup/ai-setup.test.ts +3 -1
- package/src/cli/ai-setup/apply.ts +13 -0
- package/src/cli/build.test.ts +67 -0
- package/src/cli/build.ts +33 -0
- package/src/cli/db-seed.ts +1 -1
- package/src/cli/dev-controls.test.ts +43 -48
- package/src/cli/dev-controls.ts +23 -172
- package/src/cli/dev.test.ts +90 -5
- package/src/cli/dev.ts +39 -16
- package/src/cli/doctor.ts +98 -5
- package/src/cli/load-config.images.test.ts +31 -1
- package/src/cli/load-config.ts +2 -2
- package/src/cli/safe-defaults.test.ts +1 -1
- package/src/client/notes-contract.test.ts +3 -4
- package/src/compiler/aot.test.ts +1 -2
- package/src/compiler/extract.test.ts +76 -21
- package/src/compiler/extract.ts +15 -3
- package/src/compiler/fixtures/raw/raw-unannotated.ts +2 -4
- package/src/compiler/fixtures/skyport/oke.config.ts +4 -2
- package/src/compiler/fixtures/skyport/src/flows/bookings/index.ts +4 -8
- package/src/compiler/fixtures/skyport/src/flows/payments/index.ts +1 -2
- package/src/compiler/fixtures/skyport/src/flows/support/index.ts +1 -2
- package/src/compiler/fixtures/skyport.expected.json +3 -3
- package/src/compiler/fixtures/triggers/five-triggers.ts +5 -10
- package/src/compiler/generate-adopt.test.ts +85 -0
- package/src/compiler/generate-adopt.ts +111 -0
- package/src/config/driver-defaults.test.ts +122 -0
- package/src/config/driver-defaults.ts +186 -0
- package/src/config/index.ts +97 -7
- package/src/console/server/dry-run.audit.test.ts +2 -2
- package/src/console/server/flows.ts +50 -150
- package/src/docker/compose.ts +13 -3
- package/src/docker/images-config.test.ts +168 -0
- package/src/docker/recipes/llama-cpp.ts +10 -1
- package/src/docker/stack-id.ts +1 -0
- package/src/elements/channel/declare.ts +27 -1
- package/src/elements/clock/chaos-child.ts +3 -6
- package/src/elements/clock/chaos.test.ts +1 -2
- package/src/elements/clock.test.ts +2 -4
- package/src/elements/signal/declare.ts +25 -1
- package/src/elements/store/declare.ts +27 -1
- package/src/elements/store/resource.ts +6 -11
- package/src/elements/store/upsert-app.test.ts +1 -2
- package/src/elements/vault/declare.ts +29 -1
- package/src/kernel/adopt-barrel-fresh.test.ts +103 -0
- package/src/kernel/app.ts +66 -9
- package/src/kernel/auto-registry.test.ts +198 -0
- package/src/kernel/boot-bind/channel.ts +12 -4
- package/src/kernel/boot-bind/clock.ts +3 -3
- package/src/kernel/boot-bind/gate.ts +7 -4
- package/src/kernel/boot-bind/honor-config.test.ts +5 -5
- package/src/kernel/boot-bind/journal.ts +3 -3
- package/src/kernel/boot-bind/signal.ts +3 -1
- package/src/kernel/boot-bind/store.test.ts +19 -0
- package/src/kernel/boot-bind/store.ts +24 -9
- package/src/kernel/boot-bind/vault.ts +3 -3
- package/src/kernel/boot.test.ts +19 -15
- package/src/kernel/boot.ts +81 -1
- package/src/kernel/call.test.ts +5 -10
- package/src/kernel/compensate.test.ts +3 -6
- package/src/kernel/concurrency.test.ts +3 -5
- package/src/kernel/correlation.test.ts +4 -8
- package/src/kernel/edge.test.ts +1 -1
- package/src/kernel/effects-stamping.test.ts +4 -7
- package/src/kernel/element-registries.ts +27 -0
- package/src/kernel/errors.ts +11 -0
- package/src/kernel/flow.test.ts +8 -15
- package/src/kernel/flow.ts +12 -14
- package/src/kernel/hook-timing.test.ts +2 -2
- package/src/kernel/hooks.test.ts +5 -10
- package/src/kernel/horizontal-child.ts +5 -10
- package/src/kernel/journal-boot.test.ts +2 -4
- package/src/kernel/pipeline.test.ts +6 -12
- package/src/kernel/plugin/capabilities.test.ts +1 -1
- package/src/kernel/plugin/decorate.test.ts +3 -8
- package/src/kernel/plugin/scoping.test.ts +6 -16
- package/src/kernel/plugin-elements.test.ts +1 -1
- package/src/kernel/plugin-needs.test.ts +1 -1
- package/src/kernel/ready.test.ts +1 -2
- package/src/kernel/registry-isolation.test.ts +5 -5
- package/src/kernel/triggers.ts +1 -1
- package/src/plugins/anonymous.ts +1 -3
- package/src/plugins/compression.test.ts +6 -8
- package/src/plugins/config-source.test.ts +1 -1
- package/src/plugins/config-source.ts +1 -2
- package/src/plugins/cors.test.ts +10 -10
- package/src/plugins/csrf.test.ts +2 -2
- package/src/plugins/headers.test.ts +14 -15
- package/src/plugins/ip-allowlist.test.ts +9 -9
- package/src/plugins/magic-link.ts +2 -6
- package/src/plugins/maintenance-mode.test.ts +6 -6
- package/src/plugins/otp.ts +5 -15
- package/src/plugins/passkey.ts +4 -12
- package/src/plugins/two-factor.ts +3 -9
- package/src/plugins/username.ts +2 -6
- package/src/release/measure.ts +1 -1
- package/src/runs/runs.test.ts +3 -5
- package/src/runtime/serve.test.ts +2 -4
- package/src/test/create-test-app.test.ts +3 -7
- package/src/test/provisions.integration.test.ts +4 -9
- package/src/test/reset-element-registries.ts +31 -0
- package/site/content/docs/console/access.mdx +0 -29
- package/site/content/docs/console/ai.mdx +0 -35
- package/site/content/docs/console/architecture.mdx +0 -35
- package/site/content/docs/console/channels.mdx +0 -37
- package/site/content/docs/console/clock.mdx +0 -51
- package/site/content/docs/console/flows.mdx +0 -31
- package/site/content/docs/console/gates.mdx +0 -73
- package/site/content/docs/console/index.mdx +0 -54
- package/site/content/docs/console/manifest-diff.mdx +0 -34
- package/site/content/docs/console/meta.json +0 -24
- package/site/content/docs/console/overview.mdx +0 -40
- package/site/content/docs/console/plugins.mdx +0 -41
- package/site/content/docs/console/privacy.mdx +0 -32
- package/site/content/docs/console/runs.mdx +0 -50
- package/site/content/docs/console/signals.mdx +0 -40
- package/site/content/docs/console/store.mdx +0 -32
- package/site/content/docs/console/tenancy.mdx +0 -32
- package/site/content/docs/console/traces.mdx +0 -38
- package/site/content/docs/console/vault.mdx +0 -41
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ImagesConfig` (config surface, nested) → internal flat role map.
|
|
3
|
+
*
|
|
4
|
+
* Two guarantees:
|
|
5
|
+
* 1. Every built-in image recipe is reachable through the nested shape,
|
|
6
|
+
* correctly placed under its owning element (or flat, for roles with no
|
|
7
|
+
* driver counterpart) — the type-safety gap this closes.
|
|
8
|
+
* 2. A nested config produces byte-identical `deriveInfrastructure()` output
|
|
9
|
+
* (Dockerfile / compose / stack env) to the flat-shape equivalent it
|
|
10
|
+
* replaces — nesting is a config-surface change only, never an internal
|
|
11
|
+
* representation change.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { describe, expect, test } from "bun:test";
|
|
15
|
+
import { flattenImagesConfig, type ImagesConfig } from "../config/index.ts";
|
|
16
|
+
import { deriveInfrastructure } from "./derive.ts";
|
|
17
|
+
import { builtinRecipes, recipeFor } from "./recipes/index.ts";
|
|
18
|
+
import type { ServiceCredentials } from "./types.ts";
|
|
19
|
+
|
|
20
|
+
describe("ImagesConfig nesting — recipe coverage", () => {
|
|
21
|
+
test("every built-in recipe id is reachable, correctly nested/flat by role", () => {
|
|
22
|
+
const cases: ReadonlyArray<{ images: ImagesConfig; role: string; image: string }> = [
|
|
23
|
+
// store.sql — postgres-family
|
|
24
|
+
{
|
|
25
|
+
images: { store: { sql: "postgres:18-alpine" } },
|
|
26
|
+
role: "store.sql",
|
|
27
|
+
image: "postgres:18-alpine",
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
images: { store: { sql: "cockroachdb/cockroach:v24.1.0" } },
|
|
31
|
+
role: "store.sql",
|
|
32
|
+
image: "cockroachdb/cockroach:v24.1.0",
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
images: { store: { sql: "yugabytedb/yugabyte:2024.1.0.0-b1" } },
|
|
36
|
+
role: "store.sql",
|
|
37
|
+
image: "yugabytedb/yugabyte:2024.1.0.0-b1",
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
images: { store: { sql: "timescale/timescaledb:2.15.0-pg16" } },
|
|
41
|
+
role: "store.sql",
|
|
42
|
+
image: "timescale/timescaledb:2.15.0-pg16",
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
images: { store: { sql: "supabase/postgres:15.1.0.117" } },
|
|
46
|
+
role: "store.sql",
|
|
47
|
+
image: "supabase/postgres:15.1.0.117",
|
|
48
|
+
},
|
|
49
|
+
// store.kv — redis-family
|
|
50
|
+
{ images: { store: { kv: "redis:8-alpine" } }, role: "store.kv", image: "redis:8-alpine" },
|
|
51
|
+
{
|
|
52
|
+
images: { store: { kv: "valkey/valkey:8-alpine" } },
|
|
53
|
+
role: "store.kv",
|
|
54
|
+
image: "valkey/valkey:8-alpine",
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
images: { store: { kv: "docker.dragonflydb.io/dragonflydb/dragonfly:v1.24.0" } },
|
|
58
|
+
role: "store.kv",
|
|
59
|
+
image: "docker.dragonflydb.io/dragonflydb/dragonfly:v1.24.0",
|
|
60
|
+
},
|
|
61
|
+
// store.files / store.index
|
|
62
|
+
{
|
|
63
|
+
images: { store: { files: "rustfs/rustfs:1.0.0-beta.11" } },
|
|
64
|
+
role: "store.files",
|
|
65
|
+
image: "rustfs/rustfs:1.0.0-beta.11",
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
images: { store: { index: "getmeili/meilisearch:v1.37" } },
|
|
69
|
+
role: "store.index",
|
|
70
|
+
image: "getmeili/meilisearch:v1.37",
|
|
71
|
+
},
|
|
72
|
+
// channel.email
|
|
73
|
+
{
|
|
74
|
+
images: { channel: { email: "axllent/mailpit:v1.22.3" } },
|
|
75
|
+
role: "channel.email",
|
|
76
|
+
image: "axllent/mailpit:v1.22.3",
|
|
77
|
+
},
|
|
78
|
+
// flat roles — no driver counterpart in DriversConfig
|
|
79
|
+
{
|
|
80
|
+
images: { vault: "openbao/openbao:2.6.1" },
|
|
81
|
+
role: "vault",
|
|
82
|
+
image: "openbao/openbao:2.6.1",
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
images: { ai: "ghcr.io/ggml-org/llama.cpp:server-b10290" },
|
|
86
|
+
role: "ai",
|
|
87
|
+
image: "ghcr.io/ggml-org/llama.cpp:server-b10290",
|
|
88
|
+
},
|
|
89
|
+
{ images: { ai: "vllm/vllm-openai:v0.26.0" }, role: "ai", image: "vllm/vllm-openai:v0.26.0" },
|
|
90
|
+
{
|
|
91
|
+
images: { ai: "lmsysorg/sglang:v0.5.16-runtime" },
|
|
92
|
+
role: "ai",
|
|
93
|
+
image: "lmsysorg/sglang:v0.5.16-runtime",
|
|
94
|
+
},
|
|
95
|
+
{ images: { ai: "ollama/ollama:0.32.6" }, role: "ai", image: "ollama/ollama:0.32.6" },
|
|
96
|
+
{
|
|
97
|
+
images: { pgdog: "ghcr.io/pgdogdev/pgdog:v0.1.51" },
|
|
98
|
+
role: "pgdog",
|
|
99
|
+
image: "ghcr.io/pgdogdev/pgdog:v0.1.51",
|
|
100
|
+
},
|
|
101
|
+
{ images: { proxy: "caddy:2-alpine" }, role: "proxy", image: "caddy:2-alpine" },
|
|
102
|
+
{ images: { proxy: "traefik:v3.1" }, role: "proxy", image: "traefik:v3.1" },
|
|
103
|
+
];
|
|
104
|
+
|
|
105
|
+
const matchedIds = new Set<string>();
|
|
106
|
+
for (const { images, role, image } of cases) {
|
|
107
|
+
// The nested literal flattens to exactly the dotted/flat role key the
|
|
108
|
+
// rest of the pipeline (buildSpecs, credentials, env prefixes) expects.
|
|
109
|
+
expect(flattenImagesConfig(images)).toEqual({ [role]: image });
|
|
110
|
+
matchedIds.add(recipeFor(image).id);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
expect(matchedIds).toEqual(new Set(builtinRecipes.map((r) => r.id)));
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe("ImagesConfig nesting — compose output parity", () => {
|
|
118
|
+
test("nested config derives byte-identical output to its flat-shape equivalent", () => {
|
|
119
|
+
const flatImages: Readonly<Record<string, string>> = {
|
|
120
|
+
"store.sql": "postgres:18-alpine",
|
|
121
|
+
pgdog: "ghcr.io/pgdogdev/pgdog:v0.1.51",
|
|
122
|
+
"store.kv": "redis:8-alpine",
|
|
123
|
+
"store.files": "rustfs/rustfs:1.0.0-beta.11",
|
|
124
|
+
"channel.email": "axllent/mailpit:v1.22.3",
|
|
125
|
+
vault: "openbao/openbao:2.6.1",
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const nestedImages: ImagesConfig = {
|
|
129
|
+
store: {
|
|
130
|
+
sql: "postgres:18-alpine",
|
|
131
|
+
kv: "redis:8-alpine",
|
|
132
|
+
files: "rustfs/rustfs:1.0.0-beta.11",
|
|
133
|
+
},
|
|
134
|
+
channel: { email: "axllent/mailpit:v1.22.3" },
|
|
135
|
+
vault: "openbao/openbao:2.6.1",
|
|
136
|
+
pgdog: "ghcr.io/pgdogdev/pgdog:v0.1.51",
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
// Same nested→flat set, written in a different field order — proves the
|
|
140
|
+
// parity holds independent of source object key order.
|
|
141
|
+
expect(flattenImagesConfig(nestedImages)).toEqual(flatImages);
|
|
142
|
+
|
|
143
|
+
const credentials: Readonly<Record<string, ServiceCredentials>> = {
|
|
144
|
+
"store.sql": { user: "oke", password: "fixed-pw-sql", database: "oke" },
|
|
145
|
+
"store.kv": { user: "oke", password: "fixed-pw-kv", database: "oke" },
|
|
146
|
+
"store.files": { user: "oke", password: "fixed-pw-files", database: "oke" },
|
|
147
|
+
"channel.email": { user: "oke", password: "fixed-pw-email", database: "oke" },
|
|
148
|
+
vault: { user: "oke", password: "fixed-pw-vault", database: "oke" },
|
|
149
|
+
pgdog: { user: "oke", password: "fixed-pw-pgdog", database: "oke" },
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const fromFlat = deriveInfrastructure({
|
|
153
|
+
images: flatImages,
|
|
154
|
+
app: "images-nesting-parity",
|
|
155
|
+
credentials,
|
|
156
|
+
});
|
|
157
|
+
const fromNested = deriveInfrastructure({
|
|
158
|
+
images: flattenImagesConfig(nestedImages),
|
|
159
|
+
app: "images-nesting-parity",
|
|
160
|
+
credentials,
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
expect(fromNested.specs).toEqual(fromFlat.specs);
|
|
164
|
+
expect(fromNested.files).toEqual(fromFlat.files);
|
|
165
|
+
expect(fromNested.stackEnv).toEqual(fromFlat.stackEnv);
|
|
166
|
+
expect(fromNested.composeFiles).toEqual(fromFlat.composeFiles);
|
|
167
|
+
});
|
|
168
|
+
});
|
|
@@ -221,7 +221,14 @@ def ensure_model(model: str) -> str:
|
|
|
221
221
|
def main() -> None:
|
|
222
222
|
model = (os.environ.get("OKE_AI_MODEL") or "smollm2").strip() or "smollm2"
|
|
223
223
|
gguf = ensure_model(model)
|
|
224
|
-
|
|
224
|
+
# Without an explicit --ctx-size, llama-server defaults to the model's
|
|
225
|
+
# full native training context (32K-256K+ for many current models). The
|
|
226
|
+
# KV cache for that is allocated up front and can be many times the
|
|
227
|
+
# model's own file size, OOM-killing the container even with generous
|
|
228
|
+
# Docker memory — independent of how small the model itself is.
|
|
229
|
+
# OKE_AI_CTX_SIZE (docker/.env.docker) overrides this default.
|
|
230
|
+
ctx_size = (os.environ.get("OKE_AI_CTX_SIZE") or "4096").strip() or "4096"
|
|
231
|
+
print(f"oke ai: serving {model} from {gguf} (ctx={ctx_size})", flush=True)
|
|
225
232
|
os.execv(
|
|
226
233
|
"/app/llama-server",
|
|
227
234
|
[
|
|
@@ -234,6 +241,8 @@ def main() -> None:
|
|
|
234
241
|
gguf,
|
|
235
242
|
"--alias",
|
|
236
243
|
model,
|
|
244
|
+
"--ctx-size",
|
|
245
|
+
ctx_size,
|
|
237
246
|
],
|
|
238
247
|
)
|
|
239
248
|
|
package/src/docker/stack-id.ts
CHANGED
|
@@ -128,6 +128,7 @@ export const STACK_CONTROL_KEYS = [
|
|
|
128
128
|
"MP_SMTP_AUTH_ACCEPT_ANY",
|
|
129
129
|
"MP_SMTP_AUTH_ALLOW_INSECURE",
|
|
130
130
|
"OKE_AI_MODEL",
|
|
131
|
+
"OKE_AI_CTX_SIZE",
|
|
131
132
|
"OKE_PROXY_HOST",
|
|
132
133
|
"OKE_PROXY_ACME_EMAIL",
|
|
133
134
|
// OpenBao app token — minted by bootstrap, must survive `writeDerivedFiles`.
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type { ChannelMedium } from "../../manifest/types.ts";
|
|
8
|
+
import { channelTemplateRegistry } from "../../kernel/element-registries.ts";
|
|
8
9
|
|
|
9
10
|
/** Options for a medium binder (`channel.email`, …). */
|
|
10
11
|
export interface ChannelMediumOptions {
|
|
@@ -49,6 +50,29 @@ export interface ChannelMediumBinder {
|
|
|
49
50
|
): ChannelTemplateDecl;
|
|
50
51
|
}
|
|
51
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Medium binders' `.template()` (email / sms / whatsapp / push) push into
|
|
55
|
+
* the shared {@link channelTemplateRegistry} (`src/kernel/element-registries.ts`)
|
|
56
|
+
* so {@link oke} can auto-populate `channel.templates` with zero explicit
|
|
57
|
+
* array — mirrors the {@link on} trigger-drain registry
|
|
58
|
+
* (`src/kernel/on.ts`). The medium-agnostic `channel.template()` is
|
|
59
|
+
* intentionally not auto-registered (out of scope).
|
|
60
|
+
*
|
|
61
|
+
* Snapshot of every medium-binder template declared since the last reset.
|
|
62
|
+
*/
|
|
63
|
+
export function listChannelTemplates(): readonly ChannelTemplateDecl[] {
|
|
64
|
+
return channelTemplateRegistry.slice();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Clear the channel-template registry (tests / fresh app adopt).
|
|
69
|
+
*
|
|
70
|
+
* @internal
|
|
71
|
+
*/
|
|
72
|
+
export function resetChannelTemplates(): void {
|
|
73
|
+
channelTemplateRegistry.length = 0;
|
|
74
|
+
}
|
|
75
|
+
|
|
52
76
|
/**
|
|
53
77
|
* Create a medium binder.
|
|
54
78
|
*
|
|
@@ -64,7 +88,7 @@ function mediumBinder(
|
|
|
64
88
|
medium,
|
|
65
89
|
...(from !== undefined ? { from } : {}),
|
|
66
90
|
template(name, opts = {}) {
|
|
67
|
-
|
|
91
|
+
const decl: ChannelTemplateDecl = {
|
|
68
92
|
kind: "template",
|
|
69
93
|
name,
|
|
70
94
|
medium,
|
|
@@ -73,6 +97,8 @@ function mediumBinder(
|
|
|
73
97
|
...(opts.schema !== undefined ? { schema: opts.schema } : {}),
|
|
74
98
|
...(from !== undefined ? { from } : {}),
|
|
75
99
|
};
|
|
100
|
+
channelTemplateRegistry.push(decl);
|
|
101
|
+
return decl;
|
|
76
102
|
},
|
|
77
103
|
};
|
|
78
104
|
}
|
|
@@ -169,8 +169,7 @@ if (mode === "tick-loop") {
|
|
|
169
169
|
}
|
|
170
170
|
|
|
171
171
|
const journalStore = createFileJournalStore(join(journalPath));
|
|
172
|
-
const charge = flow({
|
|
173
|
-
name: "chaos.charge",
|
|
172
|
+
const charge = flow("chaos.charge", {
|
|
174
173
|
durable: true,
|
|
175
174
|
do: async (_input, fx) => {
|
|
176
175
|
const intent = await fx.step("create-intent", async () => {
|
|
@@ -329,8 +328,7 @@ async function logHasStep(path: string, step: string, instanceId: string): Promi
|
|
|
329
328
|
|
|
330
329
|
/** Durable charge flow: step 1 → block → step 2 (both logged). */
|
|
331
330
|
function chargeFlow(instanceId: string, stepLogPath: string, blockMs: number): AnyFlowDef {
|
|
332
|
-
return flow({
|
|
333
|
-
name: "chaos.journal.charge",
|
|
331
|
+
return flow("chaos.journal.charge", {
|
|
334
332
|
durable: true,
|
|
335
333
|
do: async (_input, fx) => {
|
|
336
334
|
const intent = await fx.step("create-intent", async () => {
|
|
@@ -349,8 +347,7 @@ function chargeFlow(instanceId: string, stepLogPath: string, blockMs: number): A
|
|
|
349
347
|
|
|
350
348
|
/** Durable sleeper flow: step 1 → sleep → confirm (steps logged). */
|
|
351
349
|
function sleeperFlow(instanceId: string, stepLogPath: string, wakeMs: number): AnyFlowDef {
|
|
352
|
-
return flow({
|
|
353
|
-
name: "chaos.journal.sleeper",
|
|
350
|
+
return flow("chaos.journal.sleeper", {
|
|
354
351
|
durable: true,
|
|
355
352
|
do: async (_input, fx) => {
|
|
356
353
|
await fx.step("create-intent", async () => {
|
|
@@ -173,8 +173,7 @@ describe("chaos — SIGKILL durable mid-step", () => {
|
|
|
173
173
|
|
|
174
174
|
const calls: string[] = [];
|
|
175
175
|
const journalStore = createFileJournalStore(journalPath);
|
|
176
|
-
const charge = flow({
|
|
177
|
-
name: "chaos.charge",
|
|
176
|
+
const charge = flow("chaos.charge", {
|
|
178
177
|
durable: true,
|
|
179
178
|
do: async (_input, fx) => {
|
|
180
179
|
const intent = await fx.step("create-intent", () => {
|
|
@@ -67,8 +67,7 @@ describe("durable journal", () => {
|
|
|
67
67
|
const tt = createTimeTravel(0);
|
|
68
68
|
let crashAfterFirstStep = true;
|
|
69
69
|
|
|
70
|
-
const charge = flow({
|
|
71
|
-
name: "payments.charge",
|
|
70
|
+
const charge = flow("payments.charge", {
|
|
72
71
|
durable: true,
|
|
73
72
|
do: async (_input, fx) => {
|
|
74
73
|
const intent = await fx.step("create-intent", () => {
|
|
@@ -118,8 +117,7 @@ describe("durable journal", () => {
|
|
|
118
117
|
const tt = createTimeTravel(0);
|
|
119
118
|
|
|
120
119
|
try {
|
|
121
|
-
const sleepy = flow({
|
|
122
|
-
name: "trial.wait",
|
|
120
|
+
const sleepy = flow("trial.wait", {
|
|
123
121
|
durable: true,
|
|
124
122
|
do: async (_input, fx) => {
|
|
125
123
|
await fx.step("start", () => "begun");
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { SignalDelivery } from "../../manifest/types.ts";
|
|
9
|
+
import { signalRegistry } from "../../kernel/element-registries.ts";
|
|
9
10
|
|
|
10
11
|
/** Options for {@link signal}. */
|
|
11
12
|
export interface SignalOptions {
|
|
@@ -54,6 +55,27 @@ export interface SignalDecl<T = unknown> {
|
|
|
54
55
|
readonly _payload?: T;
|
|
55
56
|
}
|
|
56
57
|
|
|
58
|
+
/**
|
|
59
|
+
* `signal()` pushes into the shared {@link signalRegistry}
|
|
60
|
+
* (`src/kernel/element-registries.ts`) so {@link oke} can auto-populate
|
|
61
|
+
* `signals` with zero explicit array — mirrors the {@link on} trigger-drain
|
|
62
|
+
* registry (`src/kernel/on.ts`).
|
|
63
|
+
*
|
|
64
|
+
* Snapshot of every signal declared since the last reset.
|
|
65
|
+
*/
|
|
66
|
+
export function listSignals(): readonly SignalDecl[] {
|
|
67
|
+
return signalRegistry.slice();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Clear the signal registry (tests / fresh app adopt).
|
|
72
|
+
*
|
|
73
|
+
* @internal
|
|
74
|
+
*/
|
|
75
|
+
export function resetSignals(): void {
|
|
76
|
+
signalRegistry.length = 0;
|
|
77
|
+
}
|
|
78
|
+
|
|
57
79
|
/**
|
|
58
80
|
* Declare a signal. `delivery` is mandatory — omitting it is a type error.
|
|
59
81
|
*
|
|
@@ -68,7 +90,7 @@ export function signal<T = unknown>(name: string, options: SignalOptions): Signa
|
|
|
68
90
|
) {
|
|
69
91
|
throw new TypeError(`signal("${name}"): delivery is mandatory (once | broadcast | live)`);
|
|
70
92
|
}
|
|
71
|
-
|
|
93
|
+
const decl: SignalDecl<T> = {
|
|
72
94
|
name,
|
|
73
95
|
delivery: options.delivery,
|
|
74
96
|
...(options.description !== undefined ? { description: options.description } : {}),
|
|
@@ -77,4 +99,6 @@ export function signal<T = unknown>(name: string, options: SignalOptions): Signa
|
|
|
77
99
|
schema: options.schema,
|
|
78
100
|
optional: options.optional ?? false,
|
|
79
101
|
};
|
|
102
|
+
signalRegistry.push(decl as SignalDecl);
|
|
103
|
+
return decl;
|
|
80
104
|
}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type { ColumnClassification, ResourceRef, StoreFacet } from "../../manifest/types.ts";
|
|
8
|
+
import { storeRegistry } from "../../kernel/element-registries.ts";
|
|
8
9
|
import { resource } from "./resource.ts";
|
|
9
10
|
import { schema } from "./schema-decl.ts";
|
|
10
11
|
import { resolveTableName, type TableHandle } from "./table.ts";
|
|
@@ -114,6 +115,28 @@ export interface IndexStoreDecl extends StoreDeclBase {
|
|
|
114
115
|
/** Any store declaration. */
|
|
115
116
|
export type StoreDecl = SqlStoreDecl | KvStoreDecl | FilesStoreDecl | IndexStoreDecl;
|
|
116
117
|
|
|
118
|
+
/**
|
|
119
|
+
* `store.sql` / `store.files` push into the shared {@link storeRegistry}
|
|
120
|
+
* (`src/kernel/element-registries.ts`) so {@link oke} can auto-populate
|
|
121
|
+
* `stores` with zero explicit array — mirrors the {@link on} trigger-drain
|
|
122
|
+
* registry (`src/kernel/on.ts`). `store.kv` / `store.index` are
|
|
123
|
+
* intentionally not auto-registered (out of scope).
|
|
124
|
+
*
|
|
125
|
+
* Snapshot of every `store.sql` / `store.files` declared since the last reset.
|
|
126
|
+
*/
|
|
127
|
+
export function listStores(): readonly StoreDecl[] {
|
|
128
|
+
return storeRegistry.slice();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Clear the store registry (tests / fresh app adopt).
|
|
133
|
+
*
|
|
134
|
+
* @internal
|
|
135
|
+
*/
|
|
136
|
+
export function resetStores(): void {
|
|
137
|
+
storeRegistry.length = 0;
|
|
138
|
+
}
|
|
139
|
+
|
|
117
140
|
/**
|
|
118
141
|
* Declare a SQL store.
|
|
119
142
|
*
|
|
@@ -140,6 +163,7 @@ export function sql(name: string, options: SqlStoreOptions = {}): SqlStoreDecl {
|
|
|
140
163
|
};
|
|
141
164
|
},
|
|
142
165
|
};
|
|
166
|
+
storeRegistry.push(decl);
|
|
143
167
|
return decl;
|
|
144
168
|
}
|
|
145
169
|
|
|
@@ -165,12 +189,14 @@ export function kv(name: string, options: KvStoreOptions = {}): KvStoreDecl {
|
|
|
165
189
|
* @param options - Options
|
|
166
190
|
*/
|
|
167
191
|
export function files(name: string, options: FilesStoreOptions = {}): FilesStoreDecl {
|
|
168
|
-
|
|
192
|
+
const decl: FilesStoreDecl = {
|
|
169
193
|
facet: "files",
|
|
170
194
|
name,
|
|
171
195
|
ref: `files:${name}`,
|
|
172
196
|
...(options.description !== undefined ? { description: options.description } : {}),
|
|
173
197
|
};
|
|
198
|
+
storeRegistry.push(decl);
|
|
199
|
+
return decl;
|
|
174
200
|
}
|
|
175
201
|
|
|
176
202
|
/**
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `store.resource(db, table, opts)` — a CRUD + list resource factory.
|
|
3
3
|
*
|
|
4
|
-
* Declarative sugar, never new physics: every op is an ordinary `flow({…})`
|
|
4
|
+
* Declarative sugar, never new physics: every op is an ordinary `flow(name, {…})`
|
|
5
5
|
* whose body composes `fx.store(db)` (select/insert/update/findById/delete +
|
|
6
6
|
* `page`/`count`) through the existing Drizzle-condition compiler. Wire
|
|
7
7
|
* binding stays in `on(http.resource(path, resource.all()))` — the factory
|
|
@@ -649,8 +649,7 @@ export function resource(db: SqlStoreDecl, table: unknown, options: ResourceOpti
|
|
|
649
649
|
return { data, meta };
|
|
650
650
|
}
|
|
651
651
|
|
|
652
|
-
const listFlow = flow({
|
|
653
|
-
name: "list",
|
|
652
|
+
const listFlow = flow("list", {
|
|
654
653
|
unit,
|
|
655
654
|
...(breaking ? { breaking: true as const } : {}),
|
|
656
655
|
// Loose record so the HTTP AoT infers `query` and lets every list URL
|
|
@@ -664,8 +663,7 @@ export function resource(db: SqlStoreDecl, table: unknown, options: ResourceOpti
|
|
|
664
663
|
},
|
|
665
664
|
});
|
|
666
665
|
|
|
667
|
-
const createFlow = flow({
|
|
668
|
-
name: "create",
|
|
666
|
+
const createFlow = flow("create", {
|
|
669
667
|
unit,
|
|
670
668
|
...(breaking ? { breaking: true as const } : {}),
|
|
671
669
|
in: options.in as never,
|
|
@@ -682,8 +680,7 @@ export function resource(db: SqlStoreDecl, table: unknown, options: ResourceOpti
|
|
|
682
680
|
},
|
|
683
681
|
});
|
|
684
682
|
|
|
685
|
-
const getFlow = flow({
|
|
686
|
-
name: "get",
|
|
683
|
+
const getFlow = flow("get", {
|
|
687
684
|
unit,
|
|
688
685
|
...(breaking ? { breaking: true as const } : {}),
|
|
689
686
|
errors,
|
|
@@ -709,8 +706,7 @@ export function resource(db: SqlStoreDecl, table: unknown, options: ResourceOpti
|
|
|
709
706
|
: patchSchema instanceof z.ZodObject
|
|
710
707
|
? patchSchema.extend({ [idKey]: z.string() })
|
|
711
708
|
: patchSchema;
|
|
712
|
-
const updateFlow = flow({
|
|
713
|
-
name: "update",
|
|
709
|
+
const updateFlow = flow("update", {
|
|
714
710
|
unit,
|
|
715
711
|
...(breaking ? { breaking: true as const } : {}),
|
|
716
712
|
in: updateIn as never,
|
|
@@ -736,8 +732,7 @@ export function resource(db: SqlStoreDecl, table: unknown, options: ResourceOpti
|
|
|
736
732
|
},
|
|
737
733
|
});
|
|
738
734
|
|
|
739
|
-
const removeFlow = flow({
|
|
740
|
-
name: "remove",
|
|
735
|
+
const removeFlow = flow("remove", {
|
|
741
736
|
unit,
|
|
742
737
|
...(breaking ? { breaking: true as const } : {}),
|
|
743
738
|
errors,
|
|
@@ -46,8 +46,7 @@ describe("fx.store(db).upsert via app.fetch (real boot)", () => {
|
|
|
46
46
|
|
|
47
47
|
const seed = on(
|
|
48
48
|
http.post("/seed").gate(gate.public),
|
|
49
|
-
flow({
|
|
50
|
-
name: "notes.seed",
|
|
49
|
+
flow("notes.seed", {
|
|
51
50
|
in: UpsertIn,
|
|
52
51
|
out: UpsertOut,
|
|
53
52
|
effects: { writes: ["sql:app"], reads: ["sql:app"] },
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* Physics: secrets · config · environment.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import { secretRegistry } from "../../kernel/element-registries.ts";
|
|
8
|
+
|
|
7
9
|
/** Options for {@link vault} / {@link vault.secret} / {@link vault.config}. */
|
|
8
10
|
export interface VaultSecretOptions {
|
|
9
11
|
/** Human description shown in boot-gap listings. */
|
|
@@ -35,6 +37,30 @@ export interface VaultSecretDecl {
|
|
|
35
37
|
readonly sensitive: boolean;
|
|
36
38
|
}
|
|
37
39
|
|
|
40
|
+
/**
|
|
41
|
+
* `vault.secret` (only) pushes into the shared {@link secretRegistry}
|
|
42
|
+
* (`src/kernel/element-registries.ts`) so {@link oke} can auto-populate
|
|
43
|
+
* `secrets` with zero explicit array — mirrors the {@link on} trigger-drain
|
|
44
|
+
* registry (`src/kernel/on.ts`). `vault.config` is intentionally not
|
|
45
|
+
* auto-registered (out of scope).
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Snapshot of every `vault.secret` declared since the last reset.
|
|
50
|
+
*/
|
|
51
|
+
export function listSecrets(): readonly VaultSecretDecl[] {
|
|
52
|
+
return secretRegistry.slice();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Clear the vault-secret registry (tests / fresh app adopt).
|
|
57
|
+
*
|
|
58
|
+
* @internal
|
|
59
|
+
*/
|
|
60
|
+
export function resetSecrets(): void {
|
|
61
|
+
secretRegistry.length = 0;
|
|
62
|
+
}
|
|
63
|
+
|
|
38
64
|
/**
|
|
39
65
|
* Declare a vault secret contract (fingerprinted — never revealed).
|
|
40
66
|
*
|
|
@@ -45,7 +71,7 @@ function declareSecret(name: string, options: VaultSecretOptions = {}): VaultSec
|
|
|
45
71
|
if (!name) {
|
|
46
72
|
throw new TypeError("vault.secret: name is required");
|
|
47
73
|
}
|
|
48
|
-
|
|
74
|
+
const decl: VaultSecretDecl = {
|
|
49
75
|
kind: "secret",
|
|
50
76
|
name,
|
|
51
77
|
sensitive: options.sensitive ?? true,
|
|
@@ -54,6 +80,8 @@ function declareSecret(name: string, options: VaultSecretOptions = {}): VaultSec
|
|
|
54
80
|
...(options.schema !== undefined ? { schema: options.schema } : {}),
|
|
55
81
|
...(options.dev !== undefined ? { dev: options.dev } : {}),
|
|
56
82
|
};
|
|
83
|
+
secretRegistry.push(decl);
|
|
84
|
+
return decl;
|
|
57
85
|
}
|
|
58
86
|
|
|
59
87
|
/**
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Boot-level proof: a `src/flows/<unit>` folder on disk with no adopted
|
|
3
|
+
* flow under that unit — the stale/missing `.adopt()` barrel case.
|
|
4
|
+
*
|
|
5
|
+
* Mirrors `effects-stamping.test.ts`'s OKE1008 shape exactly: opt-in only
|
|
6
|
+
* via `rootDir`, `local`/`test` warn once (dev-loop stays unbroken),
|
|
7
|
+
* `docker`/`prod` hard-fail (`OKE1009`) — never a silently-incomplete route
|
|
8
|
+
* table in a deploy-shaped environment.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
12
|
+
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
13
|
+
import { tmpdir } from "node:os";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import { gate } from "../elements/gate.ts";
|
|
16
|
+
import { assertAdoptBarrelFresh, resetStaleAdoptBarrelWarnForTests } from "./boot.ts";
|
|
17
|
+
import { flow, resetFlowSeq } from "./flow.ts";
|
|
18
|
+
import { on, resetBindings } from "./on.ts";
|
|
19
|
+
import { http } from "./triggers.ts";
|
|
20
|
+
import { oke } from "./app.ts";
|
|
21
|
+
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
resetBindings();
|
|
24
|
+
resetFlowSeq();
|
|
25
|
+
resetStaleAdoptBarrelWarnForTests();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
async function makeUnitDir(root: string, unit: string): Promise<void> {
|
|
29
|
+
const dir = join(root, "src/flows", unit);
|
|
30
|
+
await mkdir(dir, { recursive: true });
|
|
31
|
+
await writeFile(join(dir, "index.ts"), "export const nothing = 1;\n");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
describe("assertAdoptBarrelFresh — direct", () => {
|
|
35
|
+
test("no rootDir: no-op, never reads the filesystem", async () => {
|
|
36
|
+
await expect(assertAdoptBarrelFresh([], "prod", undefined)).resolves.toBeUndefined();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("fresh: every disk unit has an adopted flow — no warn, no throw, any env", async () => {
|
|
40
|
+
const root = await mkdtemp(join(tmpdir(), "oke-adopt-fresh-"));
|
|
41
|
+
try {
|
|
42
|
+
await makeUnitDir(root, "notes");
|
|
43
|
+
const notesFlow = flow("notes.create", { do: () => ({ ok: true as const }) });
|
|
44
|
+
await expect(assertAdoptBarrelFresh([notesFlow], "docker", root)).resolves.toBeUndefined();
|
|
45
|
+
} finally {
|
|
46
|
+
await rm(root, { recursive: true, force: true });
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("stale: unit on disk, zero adopted flows for it — docker hard-fails OKE1009", async () => {
|
|
51
|
+
const root = await mkdtemp(join(tmpdir(), "oke-adopt-fresh-"));
|
|
52
|
+
try {
|
|
53
|
+
await makeUnitDir(root, "notes");
|
|
54
|
+
await expect(assertAdoptBarrelFresh([], "docker", root)).rejects.toThrow(/OKE1009/);
|
|
55
|
+
} finally {
|
|
56
|
+
await rm(root, { recursive: true, force: true });
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("stale: prod — same posture as docker", async () => {
|
|
61
|
+
const root = await mkdtemp(join(tmpdir(), "oke-adopt-fresh-"));
|
|
62
|
+
try {
|
|
63
|
+
await makeUnitDir(root, "notes");
|
|
64
|
+
await expect(assertAdoptBarrelFresh([], "prod", root)).rejects.toThrow(/OKE1009/);
|
|
65
|
+
} finally {
|
|
66
|
+
await rm(root, { recursive: true, force: true });
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("stale: local — warns, never throws (dev-loop stays unbroken)", async () => {
|
|
71
|
+
const root = await mkdtemp(join(tmpdir(), "oke-adopt-fresh-"));
|
|
72
|
+
try {
|
|
73
|
+
await makeUnitDir(root, "notes");
|
|
74
|
+
await expect(assertAdoptBarrelFresh([], "local", root)).resolves.toBeUndefined();
|
|
75
|
+
} finally {
|
|
76
|
+
await rm(root, { recursive: true, force: true });
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe("boot-level: stale barrel through a real oke() boot", () => {
|
|
82
|
+
test("docker: real app.boot() hard-fails OKE1009 when a unit folder was never adopted", async () => {
|
|
83
|
+
const root = await mkdtemp(join(tmpdir(), "oke-adopt-fresh-"));
|
|
84
|
+
try {
|
|
85
|
+
// "notes" exists on disk but this app only ever adopted "main" —
|
|
86
|
+
// exactly what a stale `src/flows/generated.ts` after adding a unit
|
|
87
|
+
// (and forgetting to rerun `oke dev` / `oke build`) looks like.
|
|
88
|
+
await makeUnitDir(root, "notes");
|
|
89
|
+
const mainFlow = flow("main.health", {
|
|
90
|
+
effects: {},
|
|
91
|
+
do: () => ({ ok: true as const }),
|
|
92
|
+
});
|
|
93
|
+
on(http.get("/health").gate(gate.public), mainFlow);
|
|
94
|
+
const app = oke({ name: "stale-barrel", gate: { unguardedHttp: "allow" } });
|
|
95
|
+
|
|
96
|
+
await expect(
|
|
97
|
+
app.boot({ env: "docker", rootDir: root, unguardedHttp: "allow", startScheduler: false }),
|
|
98
|
+
).rejects.toThrow(/OKE1009/);
|
|
99
|
+
} finally {
|
|
100
|
+
await rm(root, { recursive: true, force: true });
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
});
|