okengine 0.10.1 → 0.10.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/README.md +1 -1
  2. package/package.json +1 -1
  3. package/site/content/docs/elements/clock.mdx +2 -2
  4. package/site/content/docs/elements/flow.mdx +10 -10
  5. package/site/content/docs/elements/gate.mdx +4 -4
  6. package/site/content/docs/elements/signal.mdx +4 -7
  7. package/site/content/docs/elements/store.mdx +1 -1
  8. package/site/content/docs/elements/vault.mdx +1 -1
  9. package/site/content/docs/get-started/basic-usage.mdx +1 -3
  10. package/site/content/docs/get-started/introduction.mdx +1 -1
  11. package/site/content/docs/reference/cli.md +12 -12
  12. package/site/content/docs/reference/fx.mdx +3 -4
  13. package/src/auth/auth.test.ts +1 -2
  14. package/src/auth/bindings.ts +5 -15
  15. package/src/auth/gate-auth.test.ts +2 -2
  16. package/src/cli/ai-setup/apply.ts +13 -0
  17. package/src/cli/build.test.ts +67 -0
  18. package/src/cli/build.ts +34 -0
  19. package/src/cli/db-seed.ts +1 -1
  20. package/src/cli/dev-controls.test.ts +43 -48
  21. package/src/cli/dev-controls.ts +23 -172
  22. package/src/cli/dev.test.ts +2 -4
  23. package/src/cli/dev.ts +37 -16
  24. package/src/client/notes-contract.test.ts +3 -4
  25. package/src/compiler/aot.test.ts +1 -2
  26. package/src/compiler/extract.test.ts +76 -21
  27. package/src/compiler/extract.ts +3 -3
  28. package/src/compiler/fixtures/raw/raw-unannotated.ts +2 -4
  29. package/src/compiler/fixtures/skyport/src/flows/bookings/index.ts +4 -8
  30. package/src/compiler/fixtures/skyport/src/flows/payments/index.ts +1 -2
  31. package/src/compiler/fixtures/skyport/src/flows/support/index.ts +1 -2
  32. package/src/compiler/fixtures/skyport.expected.json +3 -3
  33. package/src/compiler/fixtures/triggers/five-triggers.ts +5 -10
  34. package/src/compiler/generate-adopt.test.ts +85 -0
  35. package/src/compiler/generate-adopt.ts +85 -0
  36. package/src/console/server/dry-run.audit.test.ts +2 -2
  37. package/src/console/server/flows.ts +50 -150
  38. package/src/docker/compose.ts +13 -3
  39. package/src/docker/recipes/llama-cpp.ts +10 -1
  40. package/src/docker/stack-id.ts +1 -0
  41. package/src/elements/channel/declare.ts +27 -1
  42. package/src/elements/clock/chaos-child.ts +3 -6
  43. package/src/elements/clock/chaos.test.ts +1 -2
  44. package/src/elements/clock.test.ts +2 -4
  45. package/src/elements/signal/declare.ts +25 -1
  46. package/src/elements/store/declare.ts +27 -1
  47. package/src/elements/store/resource.ts +6 -11
  48. package/src/elements/store/upsert-app.test.ts +1 -2
  49. package/src/elements/vault/declare.ts +29 -1
  50. package/src/kernel/adopt-barrel-fresh.test.ts +103 -0
  51. package/src/kernel/app.ts +66 -9
  52. package/src/kernel/auto-registry.test.ts +198 -0
  53. package/src/kernel/boot-bind/honor-config.test.ts +5 -5
  54. package/src/kernel/boot.test.ts +19 -15
  55. package/src/kernel/boot.ts +81 -1
  56. package/src/kernel/call.test.ts +5 -10
  57. package/src/kernel/compensate.test.ts +3 -6
  58. package/src/kernel/concurrency.test.ts +3 -5
  59. package/src/kernel/correlation.test.ts +4 -8
  60. package/src/kernel/edge.test.ts +1 -1
  61. package/src/kernel/effects-stamping.test.ts +4 -7
  62. package/src/kernel/element-registries.ts +27 -0
  63. package/src/kernel/errors.ts +11 -0
  64. package/src/kernel/flow.test.ts +8 -15
  65. package/src/kernel/flow.ts +12 -14
  66. package/src/kernel/hook-timing.test.ts +2 -2
  67. package/src/kernel/hooks.test.ts +5 -10
  68. package/src/kernel/horizontal-child.ts +5 -10
  69. package/src/kernel/journal-boot.test.ts +2 -4
  70. package/src/kernel/pipeline.test.ts +6 -12
  71. package/src/kernel/plugin/capabilities.test.ts +1 -1
  72. package/src/kernel/plugin/decorate.test.ts +3 -8
  73. package/src/kernel/plugin/scoping.test.ts +6 -16
  74. package/src/kernel/plugin-elements.test.ts +1 -1
  75. package/src/kernel/plugin-needs.test.ts +1 -1
  76. package/src/kernel/ready.test.ts +1 -2
  77. package/src/kernel/registry-isolation.test.ts +5 -5
  78. package/src/kernel/triggers.ts +1 -1
  79. package/src/plugins/anonymous.ts +1 -3
  80. package/src/plugins/compression.test.ts +6 -8
  81. package/src/plugins/config-source.test.ts +1 -1
  82. package/src/plugins/config-source.ts +1 -2
  83. package/src/plugins/cors.test.ts +10 -10
  84. package/src/plugins/csrf.test.ts +2 -2
  85. package/src/plugins/headers.test.ts +14 -15
  86. package/src/plugins/ip-allowlist.test.ts +9 -9
  87. package/src/plugins/magic-link.ts +2 -6
  88. package/src/plugins/maintenance-mode.test.ts +6 -6
  89. package/src/plugins/otp.ts +5 -15
  90. package/src/plugins/passkey.ts +4 -12
  91. package/src/plugins/two-factor.ts +3 -9
  92. package/src/plugins/username.ts +2 -6
  93. package/src/release/measure.ts +1 -1
  94. package/src/runs/runs.test.ts +3 -5
  95. package/src/runtime/serve.test.ts +2 -4
  96. package/src/test/create-test-app.test.ts +3 -7
  97. package/src/test/provisions.integration.test.ts +4 -9
  98. package/src/test/reset-element-registries.ts +31 -0
@@ -0,0 +1,198 @@
1
+ /**
2
+ * `oke({...})` auto-populates `stores` / `secrets` / `signals` /
3
+ * `channel.templates` from the same kind of module-evaluation registry
4
+ * already proven for `on()`'s trigger drain (`registry-isolation.test.ts`).
5
+ *
6
+ * `store.sql` / `store.files`, `vault.secret`, `signal()`, and medium-binder
7
+ * `.template()` push into a shared registry (`src/kernel/element-registries.ts`)
8
+ * at call time — zero change to how developers call them. `oke()` drains it
9
+ * at construction under the same `registry: "consume" | "keep" | "ignore"`
10
+ * switch that already governs bindings.
11
+ */
12
+
13
+ import { describe, expect, test } from "bun:test";
14
+ import { channel } from "../elements/channel/declare.ts";
15
+ import { signal } from "../elements/signal/declare.ts";
16
+ import { store } from "../elements/store/declare.ts";
17
+ import { vault } from "../elements/vault/declare.ts";
18
+ import { oke } from "./app.ts";
19
+ import { flow } from "./flow.ts";
20
+ import { on, resetBindings } from "./on.ts";
21
+ import { http } from "./triggers.ts";
22
+
23
+ describe("oke() auto-registry — stores/secrets/signals/channel.templates", () => {
24
+ test("before/after: zero explicit arrays boots identically to the explicit-array form", async () => {
25
+ resetBindings();
26
+
27
+ // "before" — today's explicit-array form (registry: "ignore" so this
28
+ // app cannot see anything auto-drained; proves the old path is unchanged).
29
+ const dbBefore = store.sql("before-app", { schema: {} });
30
+ const filesBefore = store.files("before-uploads");
31
+ const secretBefore = vault.secret("BEFORE_WEBHOOK_SECRET", { dev: "dev-secret-before" });
32
+ const signalBefore = signal("before-note-created", { delivery: "once", optional: true });
33
+ const mailBefore = channel.email({ from: "Before <before@localhost>" });
34
+ const templateBefore = mailBefore.template("before-note-created");
35
+
36
+ const beforeFlow = flow("before.onCreated", {
37
+ effects: {
38
+ secrets: ["BEFORE_WEBHOOK_SECRET"],
39
+ emits: ["before-note-created"],
40
+ sends: ["before-note-created"],
41
+ },
42
+ do: async (_input, fx) => {
43
+ const secret = fx.vault(secretBefore);
44
+ await fx.emit(signalBefore, {});
45
+ await fx.send(templateBefore, { to: "you@localhost", data: {} });
46
+ return { secret };
47
+ },
48
+ });
49
+ on(http.post("/before"), beforeFlow);
50
+
51
+ const appBefore = oke({
52
+ name: "before-app",
53
+ registry: "ignore",
54
+ bindings: [{ trigger: http.post("/before"), flow: beforeFlow }],
55
+ gate: { unguardedHttp: "allow" },
56
+ vault: { allowDevFallbacks: true },
57
+ stores: [dbBefore, filesBefore],
58
+ secrets: [secretBefore],
59
+ signals: [signalBefore],
60
+ channel: { templates: [templateBefore] },
61
+ startScheduler: false,
62
+ });
63
+
64
+ // "after" — same shape, zero explicit arrays; registry auto-populates.
65
+ // Declared (and registered) for their side effect only — never passed
66
+ // to `oke()` directly, proving the registry alone carries them.
67
+ store.sql("after-app", { schema: {} });
68
+ store.files("after-uploads");
69
+ const secretAfter = vault.secret("AFTER_WEBHOOK_SECRET", { dev: "dev-secret-after" });
70
+ const signalAfter = signal("after-note-created", { delivery: "once", optional: true });
71
+ const mailAfter = channel.email({ from: "After <after@localhost>" });
72
+ const templateAfter = mailAfter.template("after-note-created");
73
+
74
+ const afterFlow = flow("after.onCreated", {
75
+ effects: {
76
+ secrets: ["AFTER_WEBHOOK_SECRET"],
77
+ emits: ["after-note-created"],
78
+ sends: ["after-note-created"],
79
+ },
80
+ do: async (_input, fx) => {
81
+ const secret = fx.vault(secretAfter);
82
+ await fx.emit(signalAfter, {});
83
+ await fx.send(templateAfter, { to: "you@localhost", data: {} });
84
+ return { secret };
85
+ },
86
+ });
87
+ resetBindings();
88
+ on(http.post("/after"), afterFlow);
89
+
90
+ const appAfter = oke({
91
+ name: "after-app",
92
+ gate: { unguardedHttp: "allow" },
93
+ vault: { allowDevFallbacks: true },
94
+ startScheduler: false,
95
+ // No stores / secrets / signals / channel — registry supplies them.
96
+ });
97
+
98
+ await appBefore.boot({ env: "test", unguardedHttp: "allow" });
99
+ await appAfter.boot({ env: "test", unguardedHttp: "allow" });
100
+
101
+ // Both boots resolved the same shape from stores → secrets → signals → channel.
102
+ expect(appBefore.bootResult?.store?.declarations.has("sql:before-app")).toBe(true);
103
+ expect(appBefore.bootResult?.store?.declarations.has("files:before-uploads")).toBe(true);
104
+ expect(appAfter.bootResult?.store?.declarations.has("sql:after-app")).toBe(true);
105
+ expect(appAfter.bootResult?.store?.declarations.has("files:after-uploads")).toBe(true);
106
+
107
+ expect(appBefore.bootResult?.vault?.contracts.has("BEFORE_WEBHOOK_SECRET")).toBe(true);
108
+ expect(appAfter.bootResult?.vault?.contracts.has("AFTER_WEBHOOK_SECRET")).toBe(true);
109
+ expect(appBefore.bootResult?.vault?.read("BEFORE_WEBHOOK_SECRET")).toBe("dev-secret-before");
110
+ expect(appAfter.bootResult?.vault?.read("AFTER_WEBHOOK_SECRET")).toBe("dev-secret-after");
111
+
112
+ expect(appBefore.bootResult?.signal?.declarations.has("before-note-created")).toBe(true);
113
+ expect(appAfter.bootResult?.signal?.declarations.has("after-note-created")).toBe(true);
114
+
115
+ expect(appBefore.bootResult?.channel?.templates.has("before-note-created")).toBe(true);
116
+ expect(appAfter.bootResult?.channel?.templates.has("after-note-created")).toBe(true);
117
+
118
+ // Same flow execution shape through the real HTTP path either way —
119
+ // both resolve, emit, and send successfully; the secret value itself
120
+ // comes back redacted (fx output redaction), same on both sides.
121
+ const resBefore = await appBefore.fetch(
122
+ new Request("http://localhost/before", { method: "POST" }),
123
+ );
124
+ const resAfter = await appAfter.fetch(
125
+ new Request("http://localhost/after", { method: "POST" }),
126
+ );
127
+ expect(resBefore.status).toBe(200);
128
+ expect(resAfter.status).toBe(200);
129
+ const bodyBefore = (await resBefore.json()) as { data: { secret: string } };
130
+ const bodyAfter = (await resAfter.json()) as { data: { secret: string } };
131
+ expect(bodyBefore.data.secret).toBe(bodyAfter.data.secret);
132
+ });
133
+
134
+ test("explicit passing is additive, never silently ignored — same object via both paths is deduped, not doubled", async () => {
135
+ const db = store.sql("dedup-app", { schema: {} });
136
+
137
+ // `db` is already sitting in the registry from the call above *and*
138
+ // passed explicitly — must not double-register / conflict.
139
+ const app = oke({
140
+ name: "dedup-app",
141
+ autoBoot: false,
142
+ stores: [db],
143
+ startScheduler: false,
144
+ });
145
+
146
+ await app.boot({ env: "test", unguardedHttp: "allow" });
147
+ expect(app.bootResult?.store?.declarations.has("sql:dedup-app")).toBe(true);
148
+ });
149
+
150
+ test('registry: "ignore" does not auto-populate', async () => {
151
+ vault.secret("IGNORED_SECRET", { description: "never passed, never auto-read" });
152
+
153
+ const app = oke({
154
+ name: "ignore-mode-app",
155
+ registry: "ignore",
156
+ startScheduler: false,
157
+ });
158
+ // Nothing requires the secret, so boot succeeds — but it must not have
159
+ // silently picked up IGNORED_SECRET from the registry either way.
160
+ await app.boot({ env: "test", unguardedHttp: "allow" });
161
+ expect(app.bootResult?.vault?.contracts.has("IGNORED_SECRET")).toBeFalsy();
162
+ });
163
+
164
+ test('registry: "consume" (default) drains stores/secrets/signals/channel.templates — a later app does not inherit them', async () => {
165
+ store.sql("leak-app", { schema: {} });
166
+ vault.secret("LEAK_SECRET", { dev: "dev-leak" });
167
+ signal("leak-signal", { delivery: "once" });
168
+ channel.email().template("leak-template");
169
+
170
+ const appA = oke({
171
+ name: "leak-app-a",
172
+ autoBoot: false,
173
+ vault: { allowDevFallbacks: true },
174
+ startScheduler: false,
175
+ });
176
+ await appA.boot({ env: "test", unguardedHttp: "allow" });
177
+ expect(appA.bootResult?.store?.declarations.has("sql:leak-app")).toBe(true);
178
+ expect(appA.bootResult?.vault?.contracts.has("LEAK_SECRET")).toBe(true);
179
+
180
+ // Second app, constructed after the first drained the registry.
181
+ const appB = oke({
182
+ name: "leak-app-b",
183
+ autoBoot: false,
184
+ startScheduler: false,
185
+ });
186
+ await appB.boot({ env: "test", unguardedHttp: "allow" });
187
+ // Nothing was passed and nothing was left in the registry (App A drained
188
+ // it) — App B needed store/vault/signal not at all, so no runtime was
189
+ // built for them. Channel boots unconditionally regardless of registry
190
+ // content (pre-existing, unrelated to this feature — `resolveElementNeeds`
191
+ // gates channel on object *presence*, not template count) — assert its
192
+ // template map, not runtime absence.
193
+ expect(appB.bootResult?.store).toBeUndefined();
194
+ expect(appB.bootResult?.vault).toBeUndefined();
195
+ expect(appB.bootResult?.signal).toBeUndefined();
196
+ expect(appB.bootResult?.channel?.templates.has("leak-template") ?? false).toBe(false);
197
+ });
198
+ });
@@ -157,7 +157,7 @@ describe("boot binders honour drivers.* config", () => {
157
157
  const result = await bootApplication({
158
158
  env: "local",
159
159
  startScheduler: false,
160
- flows: [flow({ name: "charge", durable: true, do: () => ({ ok: true }) })],
160
+ flows: [flow("charge", { durable: true, do: () => ({ ok: true }) })],
161
161
  });
162
162
  try {
163
163
  expect(result.journal?.driverId).toBe("memory");
@@ -172,7 +172,7 @@ describe("boot binders honour drivers.* config", () => {
172
172
  const result = await bootApplication({
173
173
  env: "local",
174
174
  startScheduler: false,
175
- flows: [flow({ name: "plain", do: () => ({ ok: true }) })],
175
+ flows: [flow("plain", { do: () => ({ ok: true }) })],
176
176
  });
177
177
  try {
178
178
  expect(result.journal).toBeUndefined();
@@ -187,7 +187,7 @@ describe("boot binders honour drivers.* config", () => {
187
187
  const result = await bootApplication({
188
188
  env: "local",
189
189
  startScheduler: false,
190
- flows: [flow({ name: "charge", durable: true, do: () => ({ ok: true }) })],
190
+ flows: [flow("charge", { durable: true, do: () => ({ ok: true }) })],
191
191
  config: {
192
192
  drivers: {
193
193
  journal: { local: "file", docker: "postgres", test: "memory", prod: "postgres" },
@@ -211,7 +211,7 @@ describe("boot binders honour drivers.* config", () => {
211
211
  bootApplication({
212
212
  env: "local",
213
213
  startScheduler: false,
214
- flows: [flow({ name: "charge", durable: true, do: () => ({ ok: true }) })],
214
+ flows: [flow("charge", { durable: true, do: () => ({ ok: true }) })],
215
215
  config: {
216
216
  drivers: {
217
217
  journal: { local: "postgres" },
@@ -232,7 +232,7 @@ describe("boot binders honour drivers.* config", () => {
232
232
  bootApplication({
233
233
  env: "local",
234
234
  startScheduler: false,
235
- flows: [flow({ name: "charge", durable: true, do: () => ({ ok: true }) })],
235
+ flows: [flow("charge", { durable: true, do: () => ({ ok: true }) })],
236
236
  config: {
237
237
  drivers: {
238
238
  journal: { local: "neon" },
@@ -79,7 +79,7 @@ describe("boot — lazy element needs", () => {
79
79
  expect(needs.signal).toBe(false);
80
80
  });
81
81
 
82
- test("oke() Store-only graph stays under the prior 54 kB baseline", async () => {
82
+ test("oke() Store-only graph stays under the prior 54.3 kB baseline", async () => {
83
83
  const dir = await mkdtemp(join(tmpdir(), "oke-store-only-"));
84
84
  const entry = join(dir, "entry.ts");
85
85
  const appPath = join(import.meta.dir, "app.ts");
@@ -120,8 +120,18 @@ describe("boot — lazy element needs", () => {
120
120
  // gzip), durable-journal lease surface (~51.2 kB), fx.deliverOtp +
121
121
  // unified otp() (~50.9 kB → 53 kB), then fx.runs / SLO window on the
122
122
  // shared surface (~53.3 kB → 54 kB cap). Clock/channel/journal drivers
123
- // stay lazy-bound; far below eager bind.
124
- expect(total).toBeLessThan(54_000);
123
+ // stay lazy-bound; far below eager bind. Rebased again for the
124
+ // store/vault/signal/channel auto-registry drain (`element-registries.ts`
125
+ // + the `oke()` merge/dedup logic) — every app now carries this
126
+ // regardless of which elements it uses, since the drain must run
127
+ // synchronously at construction (same isolation guarantee as the
128
+ // `on()` binding registry) and can't be deferred behind a lazy import
129
+ // (~54.0 kB → 54.3 kB cap). Rebased again for `assertAdoptBarrelFresh`
130
+ // + `tryListFlowsUnits` in `boot.ts` (`.adopt()` barrel staleness
131
+ // check, OKE1009) — part of the already-lazily-loaded boot graph, not
132
+ // the `oke()` construction path, but `Bun.build` still traces and
133
+ // sums the dynamic-import chunk (~54.3 kB → 54.7 kB cap).
134
+ expect(total).toBeLessThan(54_700);
125
135
  } finally {
126
136
  await rm(dir, { recursive: true, force: true });
127
137
  }
@@ -130,8 +140,7 @@ describe("boot — lazy element needs", () => {
130
140
 
131
141
  describe("boot — capabilities from Manifest effects", () => {
132
142
  test("tokens cover declared effects only", async () => {
133
- const charge = flow({
134
- name: "payments.charge",
143
+ const charge = flow("payments.charge", {
135
144
  effects: { secrets: ["STRIPE_KEY"], writes: ["sql:orders"] },
136
145
  do: () => ({ ok: true }),
137
146
  });
@@ -158,8 +167,7 @@ describe("boot — cron fires without manual dispatch", () => {
158
167
  let ran = 0;
159
168
  on(
160
169
  every("1h"),
161
- flow({
162
- name: "cleanup.expire",
170
+ flow("cleanup.expire", {
163
171
  do: () => {
164
172
  ran += 1;
165
173
  },
@@ -200,8 +208,7 @@ describe("boot — HTTP gate wiring on the default path", () => {
200
208
  resetFlowSeq();
201
209
  on(
202
210
  http.get("/ping"),
203
- flow({
204
- name: "ping",
211
+ flow("ping", {
205
212
  do: () => ({ ok: true }),
206
213
  }),
207
214
  );
@@ -217,8 +224,7 @@ describe("boot — HTTP gate wiring on the default path", () => {
217
224
  const { GateBootError } = await import("../elements/gate/boot.ts");
218
225
  on(
219
226
  http.get("/ping"),
220
- flow({
221
- name: "ping",
227
+ flow("ping", {
222
228
  do: () => ({ ok: true }),
223
229
  }),
224
230
  );
@@ -235,8 +241,7 @@ describe("boot — HTTP gate wiring on the default path", () => {
235
241
  const { gate } = await import("../elements/gate.ts");
236
242
  on(
237
243
  http.get("/ping").gate(gate.public),
238
- flow({
239
- name: "ping-public",
244
+ flow("ping-public", {
240
245
  do: () => ({ ok: true }),
241
246
  }),
242
247
  );
@@ -261,8 +266,7 @@ describe("boot — cron autostart via real timer loop", () => {
261
266
  let ran = 0;
262
267
  on(
263
268
  every("10m"),
264
- flow({
265
- name: "cleanup.autostart",
269
+ flow("cleanup.autostart", {
266
270
  do: () => {
267
271
  ran += 1;
268
272
  },
@@ -285,7 +285,7 @@ export async function bootApplication(input: BootOptions = {}): Promise<BootResu
285
285
  input.docker === true || (input.docker !== false && process.env.OKE_DOCKER === "1");
286
286
  // `-d` always selects the `docker` driver profile — not a mix of local/test +
287
287
  // prod store overrides when `$options.env` is unset.
288
- const env: ConfigEnv = docker ? "docker" : (input.env ?? "local");
288
+ const env: ConfigEnv = input.env ?? (docker ? "docker" : "local");
289
289
  let config = input.config;
290
290
  if (config === undefined) {
291
291
  try {
@@ -484,6 +484,11 @@ export async function bootApplication(input: BootOptions = {}): Promise<BootResu
484
484
  rootDir: options.rootDir,
485
485
  });
486
486
 
487
+ // 9. `.adopt()` barrel freshness — opt-in only (same `rootDir` gate as
488
+ // capability minting above; never a filesystem read the kernel performs
489
+ // on its own).
490
+ await assertAdoptBarrelFresh(options.flows ?? [], env, options.rootDir);
491
+
487
492
  return {
488
493
  vault,
489
494
  store,
@@ -603,6 +608,81 @@ export async function mintCapabilities(
603
608
  return map;
604
609
  }
605
610
 
611
+ /** Per-process guard so the stale-barrel boot warning fires once. */
612
+ let staleAdoptBarrelWarned = false;
613
+
614
+ /** Reset the once-per-process stale-barrel warn latch (tests only). */
615
+ export function resetStaleAdoptBarrelWarnForTests(): void {
616
+ staleAdoptBarrelWarned = false;
617
+ }
618
+
619
+ /**
620
+ * Best-effort disk scan for `<rootDir>/src/flows/*` unit folders — mirrors
621
+ * {@link tryAutoExtractManifest}'s lazy, defensive, never-throws posture and
622
+ * the same `new URL` trick so the generator never enters the bundler graph
623
+ * for apps that never opt into `rootDir`.
624
+ *
625
+ * @param rootDir - Project root to scan
626
+ */
627
+ async function tryListFlowsUnits(rootDir: string): Promise<readonly string[] | undefined> {
628
+ try {
629
+ const url = new URL("../compiler/generate-adopt.ts", import.meta.url);
630
+ const { generateAdoptBarrel } = (await import(
631
+ url.href
632
+ )) as typeof import("../compiler/generate-adopt.ts");
633
+ return (await generateAdoptBarrel({ rootDir })).units;
634
+ } catch {
635
+ return undefined;
636
+ }
637
+ }
638
+
639
+ /**
640
+ * Confirm every `src/flows/<unit>` folder on disk actually reached this
641
+ * boot's adopted flows — the disk-file counterpart of a stale/missing
642
+ * generated `.adopt()` barrel (`src/flows/generated.ts`). A folder present
643
+ * on disk with zero adopted flows under that unit means the barrel wasn't
644
+ * regenerated (or was hand-edited) after the folder was added.
645
+ *
646
+ * Opt-in only via {@link BootOptions.rootDir} / `OKE_ROOT_DIR` (same gate as
647
+ * {@link mintCapabilities}'s Manifest fallback) — never a filesystem read
648
+ * the kernel performs on its own. `local` / `test` warn once per process;
649
+ * `docker` / `prod` fail loud (`OKE1009`) — same class as `NO_EFFECTS_DECLARED`,
650
+ * never a silently-incomplete route table in a deploy-shaped environment.
651
+ *
652
+ * @param flows - Adopted flows
653
+ * @param env - Resolved {@link ConfigEnv}
654
+ * @param rootDir - Explicit project root (falls back to `OKE_ROOT_DIR`)
655
+ */
656
+ export async function assertAdoptBarrelFresh(
657
+ flows: readonly AnyFlowDef[],
658
+ env: ConfigEnv = "local",
659
+ rootDir?: string,
660
+ ): Promise<void> {
661
+ const dir = rootDir ?? process.env["OKE_ROOT_DIR"];
662
+ if (!dir) return;
663
+
664
+ const diskUnits = await tryListFlowsUnits(dir);
665
+ if (diskUnits === undefined || diskUnits.length === 0) return;
666
+
667
+ const adoptedUnits = new Set(
668
+ flows.map((f) => f.unit).filter((u): u is string => u !== undefined),
669
+ );
670
+ const missing = diskUnits.filter((u) => !adoptedUnits.has(u));
671
+ if (missing.length === 0) return;
672
+
673
+ if (env === "docker" || env === "prod") {
674
+ throwOke("ADOPT_BARREL_STALE", { unit: missing[0]! });
675
+ }
676
+ if (!staleAdoptBarrelWarned) {
677
+ staleAdoptBarrelWarned = true;
678
+ emitBootWarn(
679
+ `oke boot: src/flows/${missing[0]} exists on disk but adopted no flows — the ` +
680
+ '.adopt() barrel ("src/flows/generated.ts") is stale. Run `oke dev` or `oke build` ' +
681
+ "to regenerate it — docker/prod refuse to boot this way.",
682
+ );
683
+ }
684
+ }
685
+
606
686
  function isRunsRuntime(value: RunsRuntime | CreateRunsRuntimeOptions): value is RunsRuntime {
607
687
  return (
608
688
  typeof value === "object" &&
@@ -11,15 +11,13 @@ beforeEach(() => {
11
11
 
12
12
  describe("fx.call — untriggered flows", () => {
13
13
  test("untriggered flow is callable via app.call and fx.call", async () => {
14
- const stats = flow({
15
- name: "links.stats",
14
+ const stats = flow("links.stats", {
16
15
  do: ({ code }: { code: string }) => ({ code, clicks: 7 }),
17
16
  });
18
17
 
19
18
  const parent = on(
20
19
  http.post("/run"),
21
- flow({
22
- name: "links.run",
20
+ flow("links.run", {
23
21
  effects: { calls: ["links.stats"] },
24
22
  do: async ({ code }: { code: string }, fx) => {
25
23
  const result = await fx.call("links.stats", { code });
@@ -54,8 +52,7 @@ describe("fx.call — untriggered flows", () => {
54
52
  });
55
53
 
56
54
  test("fx.call propagates fx.principal without filling fx.auth", async () => {
57
- const audit = flow({
58
- name: "audit.log",
55
+ const audit = flow("audit.log", {
59
56
  do: (_input: { event: string }, fx) => ({
60
57
  authUserId: fx.auth.userId,
61
58
  principalUserId: fx.principal.userId,
@@ -65,8 +62,7 @@ describe("fx.call — untriggered flows", () => {
65
62
 
66
63
  const act = on(
67
64
  http.post("/act"),
68
- flow({
69
- name: "act",
65
+ flow("act", {
70
66
  effects: { calls: ["audit.log"] },
71
67
  do: async (_input: Record<string, never>, fx) => {
72
68
  return fx.call("audit.log", { event: "act" });
@@ -99,8 +95,7 @@ describe("fx.call — untriggered flows", () => {
99
95
 
100
96
  test("http and signal invocations of the same flow execute identically", async () => {
101
97
  let runs = 0;
102
- const work = flow({
103
- name: "work",
98
+ const work = flow("work", {
104
99
  do: (input: { v: number }) => {
105
100
  runs += 1;
106
101
  return { out: input.v + 1 };
@@ -24,8 +24,7 @@ describe("durable compensate", () => {
24
24
  const store = createMemoryJournalStore();
25
25
  const undos: string[] = [];
26
26
 
27
- const pay = flow({
28
- name: "pay.charge",
27
+ const pay = flow("pay.charge", {
29
28
  durable: true,
30
29
  do: async (_input: { amount: number }, fx) => {
31
30
  await fx.step("reserve", async () => "reserved");
@@ -72,8 +71,7 @@ describe("durable compensate", () => {
72
71
  test("compensate does not run on successful path", async () => {
73
72
  let compensated = 0;
74
73
  const store = createMemoryJournalStore();
75
- const ok = flow({
76
- name: "pay.ok",
74
+ const ok = flow("pay.ok", {
77
75
  durable: true,
78
76
  do: async (_input, fx) => {
79
77
  await fx.step("a", () => "done");
@@ -103,8 +101,7 @@ describe("durable compensate", () => {
103
101
  await runs.open();
104
102
  const store = createMemoryJournalStore();
105
103
 
106
- const boom = flow({
107
- name: "pay.boom",
104
+ const boom = flow("pay.boom", {
108
105
  durable: true,
109
106
  do: async () => {
110
107
  throw new Error("boom");
@@ -218,8 +218,7 @@ describe("retry + durable journal", () => {
218
218
  let attempts = 0;
219
219
  let crash = true;
220
220
 
221
- const f = flow({
222
- name: "pay.retry",
221
+ const f = flow("pay.retry", {
223
222
  durable: true,
224
223
  do: async (_input, fx) => {
225
224
  const charged = await fx.step("charge", () =>
@@ -250,13 +249,12 @@ describe("retry + durable journal", () => {
250
249
  expect(attempts).toBe(1);
251
250
  });
252
251
 
253
- test("flow({ retry }) reuses journal — completed steps do not re-run", async () => {
252
+ test("flow(name, { retry }) reuses journal — completed steps do not re-run", async () => {
254
253
  const journalStore = createMemoryJournalStore();
255
254
  const calls: string[] = [];
256
255
  let failOnce = true;
257
256
 
258
- const f = flow({
259
- name: "flow.retry",
257
+ const f = flow("flow.retry", {
260
258
  durable: true,
261
259
  retry: { retries: 2, delay: 0, jitter: false },
262
260
  do: async (_input, fx) => {
@@ -17,8 +17,7 @@ describe("correlation — parentId across Flow → Signal → Flow", () => {
17
17
  const runs = createRunsRuntime({ driver: "memory" });
18
18
  await runs.open();
19
19
 
20
- const producer = flow({
21
- name: "orders.create",
20
+ const producer = flow("orders.create", {
22
21
  effects: { emits: ["corr-order-placed"] },
23
22
  do: async (input: { id: string }, fx) => {
24
23
  await fx.emit(orderPlaced, { id: input.id });
@@ -26,8 +25,7 @@ describe("correlation — parentId across Flow → Signal → Flow", () => {
26
25
  },
27
26
  });
28
27
 
29
- const consumer = flow({
30
- name: "orders.notify",
28
+ const consumer = flow("orders.notify", {
31
29
  do: async (_input: { id: string }) => ({ notified: true as const }),
32
30
  });
33
31
 
@@ -72,13 +70,11 @@ describe("correlation — parentId across Flow → Signal → Flow", () => {
72
70
  const runs = createRunsRuntime({ driver: "memory" });
73
71
  await runs.open();
74
72
 
75
- const child = flow({
76
- name: "inner.work",
73
+ const child = flow("inner.work", {
77
74
  do: () => ({ done: true as const }),
78
75
  });
79
76
 
80
- const parent = flow({
81
- name: "outer.work",
77
+ const parent = flow("outer.work", {
82
78
  effects: { calls: ["inner.work"] },
83
79
  do: async (_input, fx) => {
84
80
  await fx.call(child, {});
@@ -19,7 +19,7 @@ beforeEach(() => {
19
19
 
20
20
  describe("plugin edge handlers", () => {
21
21
  test("an edge handler answers an unmatched request; the flow still owns matched ones", async () => {
22
- on(http.get("/x"), flow({ name: "x.get", do: () => ({ ok: true }) }));
22
+ on(http.get("/x"), flow("x.get", { do: () => ({ ok: true }) }));
23
23
  const edge = plugin("edge-test", { version: "0.0.1" }).edge((_request, info) => {
24
24
  if (info.method === "OPTIONS") {
25
25
  return new Response(null, { status: 204, headers: { "x-edge": "yes" } });
@@ -2,7 +2,7 @@
2
2
  * Boot-level proof: compiled/inferred effects reaching a REAL running app's
3
3
  * capability token — not an isolated compiler-only or kernel-only test.
4
4
  *
5
- * Before this file existed, `flow({...})` with no hand-declared `effects`
5
+ * Before this file existed, `flow(name, {...})` with no hand-declared `effects`
6
6
  * always minted an OPEN capability token (every access allowed, no gate at
7
7
  * all) in every environment, including `docker` / `prod` — `extractManifest`
8
8
  * inference only ever fed the static `manifest.oke.json` artifact (Console,
@@ -48,8 +48,7 @@ const CreateOut = z.object({ ok: z.boolean() });
48
48
  function buildUnannotatedCreateFlow(db: ReturnType<typeof store.sql>) {
49
49
  return on(
50
50
  http.post("/notes").gate(gate.public),
51
- flow({
52
- name: "notes.create",
51
+ flow("notes.create", {
53
52
  in: CreateIn,
54
53
  out: CreateOut,
55
54
  // Deliberately no `effects` — the "let the compiler infer it" case.
@@ -117,8 +116,7 @@ describe("boot-level: table-ref resolution stays backward compatible", () => {
117
116
  const db = store.sql("app", { schema: { notes } });
118
117
  const create = on(
119
118
  http.post("/notes").gate(gate.public),
120
- flow({
121
- name: "notes.create",
119
+ flow("notes.create", {
122
120
  in: CreateIn,
123
121
  out: CreateOut,
124
122
  // The convention every template/test predates Direction B with —
@@ -171,8 +169,7 @@ const db = {} as any;
171
169
  const notes = {} as any;
172
170
  export const create = on(
173
171
  http.post("/notes").gate(gate.public),
174
- flow({
175
- name: "notes.create",
172
+ flow("notes.create", {
176
173
  do: async (input, fx) => {
177
174
  await fx.store(db).insert(notes).values({ id: input.id, title: input.title, createdAt: 1 });
178
175
  return { ok: true };
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Module-evaluation registries for `store.sql` / `store.files`, `vault.secret`,
3
+ * `signal()`, and `channel.<medium>().template()` — the plain arrays behind
4
+ * each declare module's `listX()` / `resetX()` pair (mirrors `on.ts`'s
5
+ * trigger-drain `bindings` array).
6
+ *
7
+ * Lives here, not in the declare modules themselves, so {@link oke} can read
8
+ * every registry with one lightweight import instead of statically pulling
9
+ * in all four element `declare.ts` modules (and every unrelated export they
10
+ * carry) into every app's bundle — see the `oke()` Store-only bundle-size
11
+ * budget in `boot.test.ts`. Only type imports cross back to the declare
12
+ * modules, so this file has zero runtime dependencies of its own.
13
+ */
14
+
15
+ import type { StoreDecl } from "../elements/store/declare.ts";
16
+ import type { VaultSecretDecl } from "../elements/vault/declare.ts";
17
+ import type { SignalDecl } from "../elements/signal/declare.ts";
18
+ import type { ChannelTemplateDecl } from "../elements/channel/declare.ts";
19
+
20
+ /** `store.sql` / `store.files` declarations since the last reset. */
21
+ export const storeRegistry: StoreDecl[] = [];
22
+ /** `vault.secret` declarations since the last reset. */
23
+ export const secretRegistry: VaultSecretDecl[] = [];
24
+ /** `signal()` declarations since the last reset. */
25
+ export const signalRegistry: SignalDecl[] = [];
26
+ /** Medium-binder `.template()` declarations since the last reset. */
27
+ export const channelTemplateRegistry: ChannelTemplateDecl[] = [];
@@ -153,6 +153,17 @@ export const OKE_ERRORS = {
153
153
  "Add explicit `effects` to this flow, or boot with a Manifest (`oke build`) / " +
154
154
  "`rootDir` so effects can be derived. docker/prod refuse an open capability token.",
155
155
  },
156
+ /**
157
+ * A `src/flows/<unit>` folder exists on disk but no adopted flow carries
158
+ * that unit — the generated `.adopt()` barrel (`src/flows/generated.ts`)
159
+ * is stale or was hand-edited. docker/prod — never a silently-incomplete
160
+ * route table in a deploy-shaped environment.
161
+ */
162
+ ADOPT_BARREL_STALE: {
163
+ code: 1009,
164
+ cause: "src/flows/{unit} exists on disk but adopted no flows — the .adopt() barrel is stale.",
165
+ fix: "Run `oke dev` or `oke build` to regenerate `src/flows/generated.ts`.",
166
+ },
156
167
  /**
157
168
  * Emit target has no subscriber (unified-theory §21).
158
169
  * Thrown at emit when `optional` is false and nobody is subscribed.