okengine 0.19.0 → 0.19.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 (110) hide show
  1. package/package.json +1 -1
  2. package/site/content/docs/ai/meta.json +1 -1
  3. package/site/content/docs/ai/skills.mdx +1 -1
  4. package/site/content/docs/client/index.mdx +1 -2
  5. package/site/content/docs/elements/ai/agents.mdx +5 -2
  6. package/site/content/docs/elements/ai/index.mdx +4 -3
  7. package/site/content/docs/elements/ai/prompts.mdx +3 -2
  8. package/site/content/docs/elements/channel/email.mdx +5 -2
  9. package/site/content/docs/elements/channel/index.mdx +1 -1
  10. package/site/content/docs/elements/clock/index.mdx +23 -12
  11. package/site/content/docs/elements/clock/schedules.mdx +6 -3
  12. package/site/content/docs/elements/clock/sleep.mdx +10 -7
  13. package/site/content/docs/elements/flow/consumers.mdx +96 -58
  14. package/site/content/docs/elements/flow/index.mdx +15 -15
  15. package/site/content/docs/elements/flow/routing.mdx +11 -2
  16. package/site/content/docs/elements/gate/tenancy.mdx +5 -2
  17. package/site/content/docs/elements/signal/broadcast.mdx +6 -4
  18. package/site/content/docs/elements/signal/index.mdx +14 -1
  19. package/site/content/docs/elements/signal/live.mdx +3 -2
  20. package/site/content/docs/elements/signal/once.mdx +3 -2
  21. package/site/content/docs/elements/store/files.mdx +24 -16
  22. package/site/content/docs/elements/store/index.mdx +11 -8
  23. package/site/content/docs/elements/store/kv.mdx +24 -16
  24. package/site/content/docs/elements/store/search.mdx +18 -14
  25. package/site/content/docs/elements/store/sql.mdx +16 -12
  26. package/site/content/docs/elements/vault/config.mdx +5 -2
  27. package/site/content/docs/elements/vault/rotation.mdx +5 -2
  28. package/site/content/docs/elements/vault/secrets.mdx +5 -2
  29. package/site/content/docs/index.mdx +4 -19
  30. package/site/content/docs/plugins/anonymous.mdx +1 -1
  31. package/site/content/docs/plugins/cors.mdx +1 -1
  32. package/site/content/docs/plugins/csrf.mdx +1 -1
  33. package/site/content/docs/plugins/headers.mdx +1 -1
  34. package/site/content/docs/plugins/ip-allowlist.mdx +1 -1
  35. package/site/content/docs/plugins/maintenance-mode.mdx +1 -1
  36. package/site/content/docs/reference/cli.mdx +1 -1
  37. package/site/content/docs/reference/errors.mdx +1 -0
  38. package/site/content/docs/reference/fx.mdx +9 -8
  39. package/site/content/docs/reference/okid.mdx +1 -1
  40. package/site/content/docs/reference/plugins.mdx +1 -1
  41. package/site/content/docs/reference/security.mdx +2 -2
  42. package/site/content/docs/understand/meta.json +1 -1
  43. package/site/content/docs/understand/the-architecture.mdx +264 -0
  44. package/src/bench/REPORT.md +48 -17
  45. package/src/bench/g17-hybrid-search.bench.ts +13 -13
  46. package/src/compiler/extract.test.ts +76 -1
  47. package/src/compiler/extract.ts +106 -7
  48. package/src/compiler/search-writer-isolation.test.ts +0 -1
  49. package/src/console/ui-next/dist/assets/{access-page-DFeymU07.js → access-page-DceEWH9u.js} +1 -1
  50. package/src/console/ui-next/dist/assets/{agent-disclosure-U1rdfblp.js → agent-disclosure-tC9s2VFd.js} +1 -1
  51. package/src/console/ui-next/dist/assets/{cache-glyph-BeFJeqBG.js → cache-glyph-C-naNQSR.js} +1 -1
  52. package/src/console/ui-next/dist/assets/{call-pii-button-CVAONPii.js → call-pii-button-DnZ_MlDn.js} +1 -1
  53. package/src/console/ui-next/dist/assets/{collapsible-D2A6NJ-3.js → collapsible-RekgR6Qz.js} +1 -1
  54. package/src/console/ui-next/dist/assets/{duration-tone-Cgk_h5ja.js → duration-tone-DugtWBS0.js} +1 -1
  55. package/src/console/ui-next/dist/assets/flows-page-DVmn1ZuQ.js +1 -0
  56. package/src/console/ui-next/dist/assets/{highlighted-json-MYZQtRnw.js → highlighted-json-CvBGaiSD.js} +1 -1
  57. package/src/console/ui-next/dist/assets/{http-method-Jrh39p7A.js → http-method-D7_OXbdC.js} +1 -1
  58. package/src/console/ui-next/dist/assets/{index-DXP2dBIF.js → index-DH0K2f6N.js} +3 -3
  59. package/src/console/ui-next/dist/assets/{observability-page-HvolXxTI.js → observability-page-qJzF2nSN.js} +1 -1
  60. package/src/console/ui-next/dist/assets/{replica-lag-CSh2dzrb.js → replica-lag-Vqk0pUBA.js} +1 -1
  61. package/src/console/ui-next/dist/assets/request-meta-BtShi4sG.js +1 -0
  62. package/src/console/ui-next/dist/assets/{store-page-eiKiHnNe.js → store-page-CMsYH_vH.js} +1 -1
  63. package/src/console/ui-next/dist/assets/{trace-detail-sheet-CZkMeKS-.js → trace-detail-sheet-ycFB2uua.js} +1 -1
  64. package/src/console/ui-next/dist/assets/{tree-expand-toggle-CW8y5A2h.js → tree-expand-toggle-iG1jcWgU.js} +1 -1
  65. package/src/console/ui-next/dist/assets/{units-page-B_RWJrEO.js → units-page-Bavchke4.js} +1 -1
  66. package/src/console/ui-next/dist/assets/{vault-page-CWrg-A68.js → vault-page-DxUFhiZI.js} +1 -1
  67. package/src/console/ui-next/dist/index.html +1 -1
  68. package/src/console/ui-next/src/features/flows/graph/element-map.test.ts +1 -1
  69. package/src/console/ui-next/src/features/flows/graph/element-map.ts +3 -3
  70. package/src/console/ui-next/src/features/flows/traces/trace-detail.test.ts +4 -5
  71. package/src/console/ui-next/src/features/flows/traces/trace-gates.ts +3 -6
  72. package/src/elements/gate/declare.ts +1 -1
  73. package/src/elements/gate.ts +1 -1
  74. package/src/elements/store/live-default.test.ts +8 -0
  75. package/src/elements/store/search-embed-flow.ts +2 -2
  76. package/src/elements/store/search-lsh.ts +35 -0
  77. package/src/elements/store/search-runtime.pglite.test.ts +186 -0
  78. package/src/elements/store/search-runtime.ts +30 -16
  79. package/src/elements/store/search.test.ts +83 -1
  80. package/src/elements/store.ts +2 -0
  81. package/src/full.ts +3 -0
  82. package/src/http.ts +3 -0
  83. package/src/index.ts +3 -0
  84. package/src/kernel/app.ts +30 -10
  85. package/src/kernel/boot.ts +1 -1
  86. package/src/kernel/cdc-payload.test.ts +224 -0
  87. package/src/kernel/cdc-payload.ts +146 -0
  88. package/src/kernel/errors-flow-name.ts +17 -0
  89. package/src/kernel/errors.registry.test.ts +6 -4
  90. package/src/kernel/flow-name.test.ts +104 -0
  91. package/src/kernel/flow.ts +3 -2
  92. package/src/kernel/fx-emit-types.test.ts +31 -0
  93. package/src/kernel/fx.test.ts +2 -1
  94. package/src/kernel/fx.ts +13 -3
  95. package/src/kernel/index.ts +2 -0
  96. package/src/kernel/on.ts +5 -0
  97. package/src/kernel/stamp-http.test.ts +13 -0
  98. package/src/kernel/stamp-http.ts +21 -3
  99. package/src/kernel/unit.ts +4 -2
  100. package/src/kernel-entry.ts +3 -0
  101. package/src/mcp/docs-index.ts +3 -3
  102. package/src/mcp/docs-mcp.test.ts +2 -2
  103. package/src/mcp/docs-tools.ts +2 -1
  104. package/site/content/docs/understand/the-anatomy.mdx +0 -132
  105. package/site/content/docs/understand/the-model.mdx +0 -32
  106. package/site/content/docs/understand/the-problem.mdx +0 -74
  107. package/site/content/docs/understand/the-vocabulary.mdx +0 -26
  108. package/src/console/ui-next/dist/assets/flows-page-Dss7941e.js +0 -1
  109. package/src/console/ui-next/dist/assets/request-meta-BatF8KrK.js +0 -1
  110. /package/site/content/docs/{ai → understand}/try-it.mdx +0 -0
@@ -0,0 +1,146 @@
1
+ /**
2
+ * CDC flow payload — `{ before, after }` plus additive `table` / `action` / `id`.
3
+ *
4
+ * PK lookup reuses the Manifest `DeclaredColumn.primaryKey` walk already proven
5
+ * by search-embed (not `resolvePkColumn`'s `"id"`/`"code"` heuristics).
6
+ */
7
+
8
+ import type { DeclaredColumn, Manifest, Table } from "../manifest/types.ts";
9
+
10
+ /** Insert / update / delete derived from before/after presence. */
11
+ export type CdcAction = "created" | "updated" | "deleted";
12
+
13
+ /** Payload for a CDC invocation. */
14
+ export interface CdcPayload {
15
+ readonly before: Record<string, unknown> | null;
16
+ readonly after: Record<string, unknown> | null;
17
+ readonly table: string;
18
+ readonly action: CdcAction;
19
+ readonly id: string | number;
20
+ }
21
+
22
+ /**
23
+ * Images-only (or partially enriched) input accepted by {@link dispatchCdc}.
24
+ * Missing `table` / `action` / `id` are filled in before the Flow runs.
25
+ */
26
+ export type CdcPayloadInput = Pick<CdcPayload, "before" | "after"> &
27
+ Partial<Pick<CdcPayload, "table" | "action" | "id">>;
28
+
29
+ /** JS key + SQL name for a declared primary-key column. */
30
+ export interface DeclaredPk {
31
+ readonly key: string;
32
+ readonly sqlName: string;
33
+ }
34
+
35
+ const DEFAULT_PK: DeclaredPk = { key: "id", sqlName: "id" };
36
+
37
+ /**
38
+ * Declared PK from a Manifest table column map.
39
+ * `primaryKey: true` wins; otherwise `"id"` (CDC does not require a PK).
40
+ *
41
+ * @param columns - Manifest `tables.*.columns`
42
+ */
43
+ export function declaredPk(columns: Table["columns"] | undefined): DeclaredPk {
44
+ for (const [key, col] of Object.entries(columns ?? {})) {
45
+ if (!col || typeof col !== "object") continue;
46
+ const c = col as DeclaredColumn;
47
+ if (c.primaryKey === true) return { key, sqlName: c.sqlName ?? key };
48
+ }
49
+ return DEFAULT_PK;
50
+ }
51
+
52
+ /**
53
+ * SQL name of the declared PK (search-embed SQL `WHERE` uses this).
54
+ *
55
+ * @param columns - Manifest `tables.*.columns`
56
+ */
57
+ export function declaredPkColumn(columns: Table["columns"] | undefined): string {
58
+ return declaredPk(columns).sqlName;
59
+ }
60
+
61
+ /**
62
+ * Table name → declared PK for every SQL table in the Manifest.
63
+ *
64
+ * @param manifest - Compiled Manifest (optional)
65
+ */
66
+ export function pkColumnByTableFromManifest(
67
+ manifest: Pick<Manifest, "stores"> | undefined,
68
+ ): ReadonlyMap<string, DeclaredPk> {
69
+ const out = new Map<string, DeclaredPk>();
70
+ for (const store of Object.values(manifest?.stores ?? {})) {
71
+ if (store.facet !== "sql" || !store.tables) continue;
72
+ for (const [tableName, table] of Object.entries(store.tables)) {
73
+ out.set(tableName, declaredPk(table.columns));
74
+ }
75
+ }
76
+ return out;
77
+ }
78
+
79
+ /**
80
+ * Derive action from row images. `before === null` → created; `after === null`
81
+ * → deleted; otherwise updated. Not `event.op`.
82
+ *
83
+ * @param before - Prior row image
84
+ * @param after - New row image
85
+ */
86
+ export function cdcActionFromImages(
87
+ before: Record<string, unknown> | null,
88
+ after: Record<string, unknown> | null,
89
+ ): CdcAction {
90
+ if (before === null) return "created";
91
+ if (after === null) return "deleted";
92
+ return "updated";
93
+ }
94
+
95
+ function pkNames(pkColumn: string | DeclaredPk): DeclaredPk {
96
+ return typeof pkColumn === "string" ? { key: pkColumn, sqlName: pkColumn } : pkColumn;
97
+ }
98
+
99
+ function asCdcId(raw: unknown): string | number {
100
+ if (typeof raw === "number" && Number.isFinite(raw)) return raw;
101
+ if (typeof raw === "string") return raw;
102
+ return String(raw ?? "");
103
+ }
104
+
105
+ /**
106
+ * Extract the PK value from the surviving image (`after` for created/updated,
107
+ * `before` for deleted). Tries the JS key then the SQL name.
108
+ *
109
+ * @param before - Prior row image
110
+ * @param after - New row image
111
+ * @param pkColumn - Declared PK key and/or SQL name
112
+ */
113
+ export function cdcIdFromImages(
114
+ before: Record<string, unknown> | null,
115
+ after: Record<string, unknown> | null,
116
+ pkColumn: string | DeclaredPk,
117
+ ): string | number {
118
+ const { key, sqlName } = pkNames(pkColumn);
119
+ const row = after ?? before;
120
+ if (!row) return "";
121
+ const raw = key in row ? row[key] : row[sqlName];
122
+ return asCdcId(raw);
123
+ }
124
+
125
+ /**
126
+ * Build the full CDC payload from table name, images, and the declared PK.
127
+ *
128
+ * @param tableName - Physical table name (`resolveTableName` / `dispatchCdc` arg)
129
+ * @param before - Prior row image
130
+ * @param after - New row image
131
+ * @param pkColumn - Declared PK (Manifest) or a column name string
132
+ */
133
+ export function enrichCdcPayload(
134
+ tableName: string,
135
+ before: Record<string, unknown> | null,
136
+ after: Record<string, unknown> | null,
137
+ pkColumn: string | DeclaredPk = DEFAULT_PK,
138
+ ): CdcPayload {
139
+ return {
140
+ before,
141
+ after,
142
+ table: tableName,
143
+ action: cdcActionFromImages(before, after),
144
+ id: cdcIdFromImages(before, after, pkColumn),
145
+ };
146
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * OKE1070 — kept off the kernel edge profile.
3
+ *
4
+ * Duplicate Flow names throw this at `oke()` construction. `oke()` imports
5
+ * it directly (that graph is not in `budget-entry`). Registry discovery
6
+ * includes this file; the edge `lookupOkeError` path stays off this def.
7
+ */
8
+
9
+ import type { OkeErrorDefinition } from "./errors.ts";
10
+
11
+ /** Two Flow definitions share the same Manifest / `fx.call` name. */
12
+ export const FLOW_NAME_DUPLICATE: OkeErrorDefinition = {
13
+ code: 1070,
14
+ domain: "kernel",
15
+ cause: 'Flow "{flow}" is defined twice.',
16
+ fix: 'Give at least one Flow an explicit flow("…") or a distinct tree export.',
17
+ };
@@ -17,6 +17,7 @@ import {
17
17
  } from "./errors.ts";
18
18
  import { LIVE_RESUME_GAP } from "./errors-live-resume.ts";
19
19
  import { CHANNEL_SCHEMA } from "./errors-channel.ts";
20
+ import { FLOW_NAME_DUPLICATE } from "./errors-flow-name.ts";
20
21
  import { TENANT_NOT_MEMBER, TENANT_REQUIRED, TENANT_UNKNOWN_SCOPE } from "./errors-tenant.ts";
21
22
  import {
22
23
  assertCodesInDomainRanges,
@@ -164,10 +165,11 @@ describe("OKE error-code registry", () => {
164
165
  expect(lookupOkeError(1830)).toEqual(TENANT_UNKNOWN_SCOPE);
165
166
  });
166
167
 
167
- test("lookupOkeError finds UNDECLARED_EMBED at 1009 (not tenant)", () => {
168
- expect(lookupOkeError(1009)).toEqual(OKE_ERRORS.UNDECLARED_EMBED);
169
- expect(OKE_ERRORS.UNDECLARED_EMBED.code).toBe(1009);
170
- expect(TENANT_REQUIRED.code).toBe(1810);
168
+ test("lazy discovery finds FLOW_NAME_DUPLICATE at 1070", async () => {
169
+ const { files, defs } = await discoverLazyErrorDefs(KERNEL_DIR);
170
+ expect(files).toContain("errors-flow-name.ts");
171
+ expect(defs.some((d) => d.code === 1070)).toBe(true);
172
+ expect(FLOW_NAME_DUPLICATE.code).toBe(1070);
171
173
  });
172
174
 
173
175
  test("NO_EFFECTS_DECLARED owns 1020 after renumber (was UNDECLARED_EMBED)", () => {
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Nameless Flow inherits Signal/Clock trigger names; duplicates fail OKE1070.
3
+ */
4
+
5
+ import { beforeEach, describe, expect, test } from "bun:test";
6
+ import { clock, resetClocks } from "../elements/clock/declare.ts";
7
+ import { resetSignals, signal } from "../elements/signal/declare.ts";
8
+ import { oke } from "./app.ts";
9
+ import { OkeError } from "./errors.ts";
10
+ import { FLOW_NAME_DUPLICATE } from "./errors-flow-name.ts";
11
+ import { flow, resetFlowSeq } from "./flow.ts";
12
+ import { on, resetBindings } from "./on.ts";
13
+ import { stampFlowName } from "./stamp-http.ts";
14
+ import { http } from "./triggers.ts";
15
+
16
+ beforeEach(() => {
17
+ resetBindings();
18
+ resetFlowSeq();
19
+ resetSignals();
20
+ resetClocks();
21
+ });
22
+
23
+ describe("on() — Signal/Clock name inheritance", () => {
24
+ test("nameless flow inherits inline signal.once name", () => {
25
+ const bound = on(
26
+ signal.once("link-clicked"),
27
+ flow({
28
+ do: () => ({ ok: true }),
29
+ }),
30
+ );
31
+ expect(bound.name).toBe("link-clicked");
32
+ });
33
+
34
+ test("nameless flow inherits clock.every name", () => {
35
+ const bound = on(
36
+ clock.every("cleanup", "10m"),
37
+ flow({
38
+ do: () => ({ ok: true }),
39
+ }),
40
+ );
41
+ expect(bound.name).toBe("cleanup");
42
+ });
43
+
44
+ test("explicit flow name wins over the trigger name", () => {
45
+ const bound = on(
46
+ signal.once("order-placed"),
47
+ flow("orders.fulfill", { do: () => ({ ok: true }) }),
48
+ );
49
+ expect(bound.name).toBe("orders.fulfill");
50
+ });
51
+
52
+ test("HTTP does not inherit a name from the trigger", () => {
53
+ const bound = on(
54
+ http.post("/notes"),
55
+ flow({
56
+ do: () => ({ ok: true }),
57
+ }),
58
+ );
59
+ expect(bound.name).toBe("");
60
+ });
61
+
62
+ test("file-tree stamp overwrites a trigger-inherited name", () => {
63
+ const bound = on(
64
+ signal.once("note-created"),
65
+ flow({
66
+ do: () => ({ ok: true }),
67
+ }),
68
+ );
69
+ expect(bound.name).toBe("note-created");
70
+ stampFlowName(bound, "notes.onCreated");
71
+ expect(bound.name).toBe("notes.onCreated");
72
+ });
73
+ });
74
+
75
+ describe("oke — FLOW_NAME_DUPLICATE OKE1070", () => {
76
+ test("two nameless inheritances of the same trigger name fail at construction", () => {
77
+ const ping = signal.once("health.ping");
78
+ on(ping, flow({ do: () => ({ a: true }) }));
79
+ on(ping, flow({ do: () => ({ b: true }) }));
80
+ try {
81
+ oke({ name: "t", autoBoot: false });
82
+ expect.unreachable("oke() should throw OKE1070");
83
+ } catch (err) {
84
+ expect(err).toBeInstanceOf(OkeError);
85
+ const okeErr = err as OkeError;
86
+ expect(okeErr.code).toBe(1070);
87
+ expect(okeErr.code).toBe(FLOW_NAME_DUPLICATE.code);
88
+ expect(okeErr.message).toMatch(/OKE1070/);
89
+ }
90
+ });
91
+
92
+ test("two explicit flows with the same name fail OKE1070", () => {
93
+ on(signal.once("a"), flow("shared", { do: () => 1 }));
94
+ on(clock.every("b", "1h"), flow("shared", { do: () => 2 }));
95
+ expect(() => oke({ name: "t", autoBoot: false })).toThrow(/OKE1070/);
96
+ });
97
+
98
+ test("the same flow object bound twice is not a duplicate", () => {
99
+ const shared = flow("shared.work", { do: (input: { n: number }) => input.n });
100
+ on(http.post("/work"), shared);
101
+ on(signal.once("order-placed"), shared);
102
+ expect(() => oke({ name: "t", autoBoot: false })).not.toThrow();
103
+ });
104
+ });
@@ -240,8 +240,9 @@ export const flowBrand: unique symbol = Symbol("oke.flow");
240
240
  * {@link call} / `mcp.tool`, not here.
241
241
  *
242
242
  * A nameless `flow({ do })` is stamped `unit.export` by the file-tree
243
- * generator, {@link unit}, or `.adopt()`. Explicit `flow("notes.get", {…})`
244
- * still wins.
243
+ * generator, {@link unit}, or `.adopt()`. Bound to a named Signal or Clock
244
+ * with no explicit / tree / export name, it inherits the trigger's name.
245
+ * Explicit `flow("notes.get", {…})` still wins.
245
246
  *
246
247
  * @param options - Runtime options and handler (name stamped later)
247
248
  */
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Typed `fx.emit(SignalDecl<T>, payload)` vs untyped string-name emits.
3
+ */
4
+
5
+ import { describe, expect, test } from "bun:test";
6
+ import { signal } from "../elements/signal/declare.ts";
7
+ import { createFxContext, type Fx } from "./fx.ts";
8
+
9
+ describe("fx.emit — SignalDecl payload inference", () => {
10
+ test("exported handle type-checks a matching payload; string names stay unknown", () => {
11
+ const orderPlaced = signal.once<{ orderId: string }>("order-placed");
12
+ const { fx } = createFxContext({
13
+ flow: "orders.create",
14
+ effects: { emits: ["order-placed"] },
15
+ });
16
+
17
+ const typed: (s: typeof orderPlaced, p: { orderId: string }) => Promise<void> = (s, p) =>
18
+ fx.emit(s, p);
19
+ const untyped: (name: string, p: { extra: boolean }) => Promise<void> = (name, p) =>
20
+ fx.emit(name, p);
21
+
22
+ expect(typeof typed).toBe("function");
23
+ expect(typeof untyped).toBe("function");
24
+
25
+ function _wrongPayload(fxArg: Fx): void {
26
+ // @ts-expect-error payload must match SignalDecl
27
+ void fxArg.emit(orderPlaced, { orderId: 1 });
28
+ }
29
+ expect(typeof _wrongPayload).toBe("function");
30
+ });
31
+ });
@@ -8,6 +8,7 @@ import {
8
8
  jsonResultBrand,
9
9
  type Fx,
10
10
  type FxStubStoreHandle,
11
+ type NamedRef,
11
12
  } from "./fx.ts";
12
13
  import { Redacted } from "./redacted.ts";
13
14
 
@@ -426,7 +427,7 @@ describe("fx — wholesale swap", () => {
426
427
  throw new Error("runs must not be used");
427
428
  },
428
429
  },
429
- async emit(signal) {
430
+ async emit(signal: NamedRef) {
430
431
  calls.push(`emit:${typeof signal === "string" ? signal : signal.name}`);
431
432
  },
432
433
  async deadLetters() {
package/src/kernel/fx.ts CHANGED
@@ -686,11 +686,21 @@ export interface Fx {
686
686
  /**
687
687
  * Emit a signal (records `emit`).
688
688
  *
689
+ * Pass a {@link SignalDecl} handle for compile-time payload checking.
690
+ * A string name accepts `unknown` (runtime `schema` still applies).
691
+ *
689
692
  * @param signal - Signal name or handle
690
693
  * @param payload - Payload
691
694
  * @param options - Optional emit options (`key` for per-key once ordering)
692
695
  */
693
- emit(signal: NamedRef, payload?: unknown, options?: SignalEmitOptions): Promise<void>;
696
+ emit<T>(signal: SignalDecl<T>, payload?: T, options?: SignalEmitOptions): Promise<void>;
697
+ emit(
698
+ signal:
699
+ | string
700
+ | { readonly name: string; readonly version?: number; readonly delivery?: never },
701
+ payload?: unknown,
702
+ options?: SignalEmitOptions,
703
+ ): Promise<void>;
694
704
  /**
695
705
  * Query dead-lettered messages for one signal (records `read` on `signal:<name>`).
696
706
  *
@@ -847,7 +857,7 @@ export interface Fx {
847
857
  t(key: AppMessageKey, values?: MessageValues): string;
848
858
  /** Active locale for {@link Fx.t} and default channel sends. */
849
859
  readonly locale: string;
850
- /** Generate a unique id (UUID). */
860
+ /** Generate a unique id (OKID). */
851
861
  id(): string;
852
862
  /** User-plane auth principal. */
853
863
  readonly auth: FxAuth;
@@ -1928,7 +1938,7 @@ export function createFxContext(options: CreateFxOptions): FxContext {
1928
1938
  const fx: Fx = {
1929
1939
  store: storeHandle,
1930
1940
  runs: runsSurface,
1931
- emit(signal, payload, emitOptions) {
1941
+ emit(signal: NamedRef, payload?: unknown, emitOptions?: SignalEmitOptions) {
1932
1942
  const name = resolveName(signal);
1933
1943
  return gated("emit", name, async () => {
1934
1944
  if (options.signalRuntime) {
@@ -4,7 +4,9 @@
4
4
 
5
5
  export {
6
6
  oke,
7
+ type CdcAction,
7
8
  type CdcPayload,
9
+ type CdcPayloadInput,
8
10
  type ExecuteResult,
9
11
  type OkeApp,
10
12
  type OkeOptions,
package/src/kernel/on.ts CHANGED
@@ -203,6 +203,11 @@ export function on(
203
203
  list.push(normalized);
204
204
  // Stamp runtime carrier for the first bound trigger (type follows BoundTriggerOf).
205
205
  (flowDef as { $trigger: Trigger }).$trigger = normalized;
206
+ // `$n` = name inherited from this Signal/Clock (file-tree / unit() may overwrite).
207
+ if ((normalized.kind === "signal" || normalized.kind === "clock") && !flowDef.name) {
208
+ (flowDef as { name: string; $n?: boolean }).name = normalized.name;
209
+ (flowDef as { $n?: boolean }).$n = true;
210
+ }
206
211
  stampExposureContract(flowDef as AnyFlowDef, normalized, triggerOrMount);
207
212
  bindings.push({ trigger: normalized, flow: flowDef as AnyFlowDef });
208
213
  return flowDef;
@@ -3,6 +3,7 @@
3
3
  */
4
4
 
5
5
  import { describe, expect, test, beforeEach } from "bun:test";
6
+ import { resetSignals, signal } from "../elements/signal/declare.ts";
6
7
  import type { RuntimeRouteMap } from "./adopt-routes.ts";
7
8
  import { oke } from "./app.ts";
8
9
  import { flow, resetFlowSeq } from "./flow.ts";
@@ -16,6 +17,7 @@ beforeEach(() => {
16
17
  resetBindings();
17
18
  resetFlowSeq();
18
19
  resetRegisteredFlowUnits();
20
+ resetSignals();
19
21
  });
20
22
 
21
23
  describe("stampHttpPath", () => {
@@ -49,6 +51,17 @@ describe("stampFlowName", () => {
49
51
  stampFlowName(f, "notes.list");
50
52
  expect(f.name).toBe("notes.get");
51
53
  });
54
+
55
+ test("file-tree stamp overwrites a trigger-inherited name", () => {
56
+ const bound = on(
57
+ signal.once("note-created"),
58
+ flow({
59
+ do: () => ({ ok: true }),
60
+ }),
61
+ );
62
+ stampFlowName(bound, "notes.onCreated");
63
+ expect(bound.name).toBe("notes.onCreated");
64
+ });
52
65
  });
53
66
 
54
67
  describe("oke — unresolved sentinel and duplicate routes", () => {
@@ -35,8 +35,25 @@ export function stampHttpPath<F extends AnyFlowDef, P extends string>(
35
35
  return flow as StampHttpPath<F, P>;
36
36
  }
37
37
 
38
+ /** Mutable name fields used by {@link stampFlowName} / {@link on}. */
39
+ type FlowNameCarrier = {
40
+ name: string;
41
+ unit: string | undefined;
42
+ $n?: boolean;
43
+ };
44
+
45
+ /**
46
+ * True when a Flow name is still a placeholder: empty, synthetic `flow_*`,
47
+ * or inherited from a Signal/Clock trigger (`$n`). File-tree / `unit()` may overwrite.
48
+ *
49
+ * @param flow - Flow name carrier
50
+ */
51
+ export function isUnsetFlowName(flow: { readonly name: string; readonly $n?: boolean }): boolean {
52
+ return !flow.name || flow.name.startsWith("flow_") || flow.$n === true;
53
+ }
54
+
38
55
  /**
39
- * Stamp `unit.export` onto a nameless (or `flow_*`) Flow.
56
+ * Stamp `unit.export` onto a nameless (or `flow_*` / trigger-inherited) Flow.
40
57
  *
41
58
  * Explicit `flow("notes.get")` wins. Fills `unit` when missing.
42
59
  *
@@ -45,9 +62,10 @@ export function stampHttpPath<F extends AnyFlowDef, P extends string>(
45
62
  */
46
63
  export function stampFlowName<F extends AnyFlowDef>(flow: F, name: string): F {
47
64
  if (!isFlow(flow)) return flow;
48
- const f = flow as { name: string; unit: string | undefined };
49
- if (!f.name || f.name.startsWith("flow_")) {
65
+ const f = flow as FlowNameCarrier;
66
+ if (isUnsetFlowName(f)) {
50
67
  f.name = name;
68
+ f.$n = false;
51
69
  }
52
70
  if (!f.unit) {
53
71
  const resolved = f.name || name;
@@ -5,6 +5,7 @@
5
5
  * unit. Plugins queue until an app flushes them at boot / adopt time.
6
6
  */
7
7
 
8
+ import { isUnsetFlowName } from "./stamp-http.ts";
8
9
  import type { AnyFlowDef, FlowDef } from "./flow.ts";
9
10
  import type { PluginDef } from "./plugin.ts";
10
11
 
@@ -40,8 +41,9 @@ export function unit<const T extends Record<string, AnyFlowDef>>(
40
41
  const pending: PluginDef[] = [];
41
42
  for (const [exportName, flowDef] of Object.entries(flows)) {
42
43
  const f = flowDef as FlowDef;
43
- if (!f.name || f.name.startsWith("flow_")) {
44
- (f as { name: string }).name = `${name}.${exportName}`;
44
+ if (isUnsetFlowName(f)) {
45
+ (f as { name: string; $n?: boolean }).name = `${name}.${exportName}`;
46
+ (f as { $n?: boolean }).$n = false;
45
47
  }
46
48
  if (!f.unit) {
47
49
  (f as { unit: string }).unit = name;
@@ -27,6 +27,9 @@ export {
27
27
  type OkeApp,
28
28
  type OkeOptions,
29
29
  type ReadyState,
30
+ type CdcAction,
31
+ type CdcPayload,
32
+ type CdcPayloadInput,
30
33
  type FlowDef,
31
34
  type FlowFailure,
32
35
  type FlowErrorValue,
@@ -12,7 +12,7 @@ import { join } from "node:path";
12
12
  export interface DocsPage {
13
13
  /** URL slug under `/docs` (`""` for the index page). */
14
14
  readonly slug: string;
15
- /** Content-relative path (e.g. `understand/the-problem.mdx`). */
15
+ /** Content-relative path (e.g. `understand/the-architecture.mdx`). */
16
16
  readonly path: string;
17
17
  /** Frontmatter title. */
18
18
  readonly title: string;
@@ -39,7 +39,7 @@ export interface DocsIndex {
39
39
  /**
40
40
  * Look up a page by slug or content path.
41
41
  *
42
- * @param id - Slug (`understand/the-problem`) or path (`…/the-problem.mdx`)
42
+ * @param id - Slug (`understand/the-architecture`) or path (`…/the-architecture.mdx`)
43
43
  */
44
44
  readonly get: (id: string) => DocsPage | null;
45
45
  /**
@@ -203,7 +203,7 @@ export async function loadDocsIndex(
203
203
  }
204
204
 
205
205
  /**
206
- * @param relativePath - e.g. `understand/the-problem.mdx`
206
+ * @param relativePath - e.g. `understand/the-architecture.mdx`
207
207
  */
208
208
  function pathToSlug(relativePath: string): string {
209
209
  const noExt = relativePath.replace(/\.mdx?$/i, "");
@@ -112,7 +112,7 @@ describe("docs MCP tools", () => {
112
112
 
113
113
  test("oke.docs.get returns body byte-identical to on-disk source", async () => {
114
114
  const mcp = await createDocsMcpServer({ contentDir: CONTENT });
115
- const slug = "understand/the-problem";
115
+ const slug = "understand/the-architecture";
116
116
  const res = await mcpPost(mcp.fetch, {
117
117
  jsonrpc: "2.0",
118
118
  id: 2,
@@ -133,7 +133,7 @@ describe("docs MCP tools", () => {
133
133
  const content = envelope.content as { body: string; slug: string };
134
134
  expect(content.slug).toBe(slug);
135
135
 
136
- const raw = await Bun.file(join(CONTENT, "understand", "the-problem.mdx")).text();
136
+ const raw = await Bun.file(join(CONTENT, "understand", "the-architecture.mdx")).text();
137
137
  expect(content.body).toBe(stripYamlFrontmatter(raw));
138
138
  });
139
139
 
@@ -52,7 +52,8 @@ export const DOCS_MCP_TOOLS: readonly DocsToolDescriptor[] = [
52
52
  properties: {
53
53
  slug: {
54
54
  type: "string",
55
- description: "Page slug (e.g. understand/the-problem) or path (…/the-problem.mdx)",
55
+ description:
56
+ "Page slug (e.g. understand/the-architecture) or path (…/the-architecture.mdx)",
56
57
  },
57
58
  },
58
59
  required: ["slug"],