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.
- package/package.json +1 -1
- package/site/content/docs/ai/meta.json +1 -1
- package/site/content/docs/ai/skills.mdx +1 -1
- package/site/content/docs/client/index.mdx +1 -2
- package/site/content/docs/elements/ai/agents.mdx +5 -2
- package/site/content/docs/elements/ai/index.mdx +4 -3
- package/site/content/docs/elements/ai/prompts.mdx +3 -2
- package/site/content/docs/elements/channel/email.mdx +5 -2
- package/site/content/docs/elements/channel/index.mdx +1 -1
- package/site/content/docs/elements/clock/index.mdx +23 -12
- package/site/content/docs/elements/clock/schedules.mdx +6 -3
- package/site/content/docs/elements/clock/sleep.mdx +10 -7
- package/site/content/docs/elements/flow/consumers.mdx +96 -58
- package/site/content/docs/elements/flow/index.mdx +15 -15
- package/site/content/docs/elements/flow/routing.mdx +11 -2
- package/site/content/docs/elements/gate/tenancy.mdx +5 -2
- package/site/content/docs/elements/signal/broadcast.mdx +6 -4
- package/site/content/docs/elements/signal/index.mdx +14 -1
- package/site/content/docs/elements/signal/live.mdx +3 -2
- package/site/content/docs/elements/signal/once.mdx +3 -2
- package/site/content/docs/elements/store/files.mdx +24 -16
- package/site/content/docs/elements/store/index.mdx +11 -8
- package/site/content/docs/elements/store/kv.mdx +24 -16
- package/site/content/docs/elements/store/search.mdx +18 -14
- package/site/content/docs/elements/store/sql.mdx +16 -12
- package/site/content/docs/elements/vault/config.mdx +5 -2
- package/site/content/docs/elements/vault/rotation.mdx +5 -2
- package/site/content/docs/elements/vault/secrets.mdx +5 -2
- package/site/content/docs/index.mdx +4 -19
- package/site/content/docs/plugins/anonymous.mdx +1 -1
- package/site/content/docs/plugins/cors.mdx +1 -1
- package/site/content/docs/plugins/csrf.mdx +1 -1
- package/site/content/docs/plugins/headers.mdx +1 -1
- package/site/content/docs/plugins/ip-allowlist.mdx +1 -1
- package/site/content/docs/plugins/maintenance-mode.mdx +1 -1
- package/site/content/docs/reference/cli.mdx +1 -1
- package/site/content/docs/reference/errors.mdx +1 -0
- package/site/content/docs/reference/fx.mdx +9 -8
- package/site/content/docs/reference/okid.mdx +1 -1
- package/site/content/docs/reference/plugins.mdx +1 -1
- package/site/content/docs/reference/security.mdx +2 -2
- package/site/content/docs/understand/meta.json +1 -1
- package/site/content/docs/understand/the-architecture.mdx +264 -0
- package/src/bench/REPORT.md +48 -17
- package/src/bench/g17-hybrid-search.bench.ts +13 -13
- package/src/compiler/extract.test.ts +76 -1
- package/src/compiler/extract.ts +106 -7
- package/src/compiler/search-writer-isolation.test.ts +0 -1
- package/src/console/ui-next/dist/assets/{access-page-DFeymU07.js → access-page-DceEWH9u.js} +1 -1
- package/src/console/ui-next/dist/assets/{agent-disclosure-U1rdfblp.js → agent-disclosure-tC9s2VFd.js} +1 -1
- package/src/console/ui-next/dist/assets/{cache-glyph-BeFJeqBG.js → cache-glyph-C-naNQSR.js} +1 -1
- package/src/console/ui-next/dist/assets/{call-pii-button-CVAONPii.js → call-pii-button-DnZ_MlDn.js} +1 -1
- package/src/console/ui-next/dist/assets/{collapsible-D2A6NJ-3.js → collapsible-RekgR6Qz.js} +1 -1
- package/src/console/ui-next/dist/assets/{duration-tone-Cgk_h5ja.js → duration-tone-DugtWBS0.js} +1 -1
- package/src/console/ui-next/dist/assets/flows-page-DVmn1ZuQ.js +1 -0
- package/src/console/ui-next/dist/assets/{highlighted-json-MYZQtRnw.js → highlighted-json-CvBGaiSD.js} +1 -1
- package/src/console/ui-next/dist/assets/{http-method-Jrh39p7A.js → http-method-D7_OXbdC.js} +1 -1
- package/src/console/ui-next/dist/assets/{index-DXP2dBIF.js → index-DH0K2f6N.js} +3 -3
- package/src/console/ui-next/dist/assets/{observability-page-HvolXxTI.js → observability-page-qJzF2nSN.js} +1 -1
- package/src/console/ui-next/dist/assets/{replica-lag-CSh2dzrb.js → replica-lag-Vqk0pUBA.js} +1 -1
- package/src/console/ui-next/dist/assets/request-meta-BtShi4sG.js +1 -0
- package/src/console/ui-next/dist/assets/{store-page-eiKiHnNe.js → store-page-CMsYH_vH.js} +1 -1
- package/src/console/ui-next/dist/assets/{trace-detail-sheet-CZkMeKS-.js → trace-detail-sheet-ycFB2uua.js} +1 -1
- package/src/console/ui-next/dist/assets/{tree-expand-toggle-CW8y5A2h.js → tree-expand-toggle-iG1jcWgU.js} +1 -1
- package/src/console/ui-next/dist/assets/{units-page-B_RWJrEO.js → units-page-Bavchke4.js} +1 -1
- package/src/console/ui-next/dist/assets/{vault-page-CWrg-A68.js → vault-page-DxUFhiZI.js} +1 -1
- package/src/console/ui-next/dist/index.html +1 -1
- package/src/console/ui-next/src/features/flows/graph/element-map.test.ts +1 -1
- package/src/console/ui-next/src/features/flows/graph/element-map.ts +3 -3
- package/src/console/ui-next/src/features/flows/traces/trace-detail.test.ts +4 -5
- package/src/console/ui-next/src/features/flows/traces/trace-gates.ts +3 -6
- package/src/elements/gate/declare.ts +1 -1
- package/src/elements/gate.ts +1 -1
- package/src/elements/store/live-default.test.ts +8 -0
- package/src/elements/store/search-embed-flow.ts +2 -2
- package/src/elements/store/search-lsh.ts +35 -0
- package/src/elements/store/search-runtime.pglite.test.ts +186 -0
- package/src/elements/store/search-runtime.ts +30 -16
- package/src/elements/store/search.test.ts +83 -1
- package/src/elements/store.ts +2 -0
- package/src/full.ts +3 -0
- package/src/http.ts +3 -0
- package/src/index.ts +3 -0
- package/src/kernel/app.ts +30 -10
- package/src/kernel/boot.ts +1 -1
- package/src/kernel/cdc-payload.test.ts +224 -0
- package/src/kernel/cdc-payload.ts +146 -0
- package/src/kernel/errors-flow-name.ts +17 -0
- package/src/kernel/errors.registry.test.ts +6 -4
- package/src/kernel/flow-name.test.ts +104 -0
- package/src/kernel/flow.ts +3 -2
- package/src/kernel/fx-emit-types.test.ts +31 -0
- package/src/kernel/fx.test.ts +2 -1
- package/src/kernel/fx.ts +13 -3
- package/src/kernel/index.ts +2 -0
- package/src/kernel/on.ts +5 -0
- package/src/kernel/stamp-http.test.ts +13 -0
- package/src/kernel/stamp-http.ts +21 -3
- package/src/kernel/unit.ts +4 -2
- package/src/kernel-entry.ts +3 -0
- package/src/mcp/docs-index.ts +3 -3
- package/src/mcp/docs-mcp.test.ts +2 -2
- package/src/mcp/docs-tools.ts +2 -1
- package/site/content/docs/understand/the-anatomy.mdx +0 -132
- package/site/content/docs/understand/the-model.mdx +0 -32
- package/site/content/docs/understand/the-problem.mdx +0 -74
- package/site/content/docs/understand/the-vocabulary.mdx +0 -26
- package/src/console/ui-next/dist/assets/flows-page-Dss7941e.js +0 -1
- package/src/console/ui-next/dist/assets/request-meta-BatF8KrK.js +0 -1
- /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("
|
|
168
|
-
|
|
169
|
-
expect(
|
|
170
|
-
expect(
|
|
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
|
+
});
|
package/src/kernel/flow.ts
CHANGED
|
@@ -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()`.
|
|
244
|
-
*
|
|
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
|
+
});
|
package/src/kernel/fx.test.ts
CHANGED
|
@@ -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:
|
|
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 (
|
|
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) {
|
package/src/kernel/index.ts
CHANGED
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", () => {
|
package/src/kernel/stamp-http.ts
CHANGED
|
@@ -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
|
|
49
|
-
if (
|
|
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;
|
package/src/kernel/unit.ts
CHANGED
|
@@ -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 (
|
|
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;
|
package/src/kernel-entry.ts
CHANGED
package/src/mcp/docs-index.ts
CHANGED
|
@@ -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-
|
|
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-
|
|
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-
|
|
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, "");
|
package/src/mcp/docs-mcp.test.ts
CHANGED
|
@@ -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-
|
|
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-
|
|
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
|
|
package/src/mcp/docs-tools.ts
CHANGED
|
@@ -52,7 +52,8 @@ export const DOCS_MCP_TOOLS: readonly DocsToolDescriptor[] = [
|
|
|
52
52
|
properties: {
|
|
53
53
|
slug: {
|
|
54
54
|
type: "string",
|
|
55
|
-
description:
|
|
55
|
+
description:
|
|
56
|
+
"Page slug (e.g. understand/the-architecture) or path (…/the-architecture.mdx)",
|
|
56
57
|
},
|
|
57
58
|
},
|
|
58
59
|
required: ["slug"],
|