okengine 0.20.0 → 0.21.1

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 (104) hide show
  1. package/package.json +3 -7
  2. package/site/content/docs/client/calling.mdx +117 -29
  3. package/site/content/docs/client/index.mdx +3 -3
  4. package/site/content/docs/elements/flow/http.mdx +18 -15
  5. package/site/content/docs/elements/flow/index.mdx +26 -18
  6. package/site/content/docs/elements/gate/tenancy.mdx +1 -1
  7. package/site/content/docs/elements/store/files.mdx +11 -5
  8. package/site/content/docs/elements/store/index.mdx +2 -3
  9. package/site/content/docs/elements/store/kv.mdx +8 -2
  10. package/site/content/docs/elements/store/sql.mdx +9 -6
  11. package/site/content/docs/elements/vault/index.mdx +11 -8
  12. package/site/content/docs/elements/vault/secrets.mdx +6 -3
  13. package/site/content/docs/index.mdx +1 -1
  14. package/site/content/docs/recipes/rustfs.mdx +1 -1
  15. package/site/content/docs/reference/configuration.mdx +1 -1
  16. package/site/content/docs/reference/errors.mdx +199 -25
  17. package/site/content/docs/reference/fx.mdx +6 -1
  18. package/site/content/docs/understand/the-architecture.mdx +2 -2
  19. package/site/content/docs/understand/try-it.mdx +758 -25
  20. package/src/cli/dev-app-runner.ts +2 -1
  21. package/src/cli/dev.test.ts +105 -2
  22. package/src/cli/dev.ts +37 -2
  23. package/src/cli/start.ts +2 -1
  24. package/src/client/create.ts +14 -27
  25. package/src/client/explain.test.ts +252 -0
  26. package/src/client/explain.ts +272 -0
  27. package/src/client/live.test.ts +44 -0
  28. package/src/client/live.ts +44 -101
  29. package/src/client/notes-contract.test.ts +10 -0
  30. package/src/client/sse.ts +26 -68
  31. package/src/client/stream.ts +25 -67
  32. package/src/client/transport.test.ts +67 -0
  33. package/src/client/transport.ts +51 -112
  34. package/src/client/types.ts +17 -6
  35. package/src/client/wire.ts +119 -0
  36. package/src/client-react/live-resource.ts +6 -2
  37. package/src/compiler/aot.ts +3 -32
  38. package/src/compiler/dynamic.ts +13 -11
  39. package/src/compiler/interpret.ts +45 -0
  40. package/src/compiler/response.ts +17 -27
  41. package/src/console/server/invoke-user-flow.test.ts +8 -2
  42. package/src/console/server/invoke-user-flow.ts +12 -18
  43. package/src/console/server/security.gate.test.ts +1 -1
  44. package/src/console/ui-next/dist/assets/{access-page-DFLu0wTA.js → access-page-Bgt9bq2r.js} +1 -1
  45. package/src/console/ui-next/dist/assets/{agent-disclosure-DGscxaF5.js → agent-disclosure-B43CXZZR.js} +1 -1
  46. package/src/console/ui-next/dist/assets/{cache-glyph-BGmRZk7d.js → cache-glyph-92uM5MO7.js} +1 -1
  47. package/src/console/ui-next/dist/assets/{call-pii-button--feUYxvG.js → call-pii-button-DtGVPtcs.js} +1 -1
  48. package/src/console/ui-next/dist/assets/{collapsible-JWvpaiGY.js → collapsible-DN7l6zmC.js} +1 -1
  49. package/src/console/ui-next/dist/assets/{duration-tone-D9yCJG4n.js → duration-tone-BmIR9FV8.js} +1 -1
  50. package/src/console/ui-next/dist/assets/{flows-page-Bs6MD9GB.js → flows-page-BMHs-IzK.js} +1 -1
  51. package/src/console/ui-next/dist/assets/{highlighted-json-xH8MrEnv.js → highlighted-json-BlAEVgNW.js} +1 -1
  52. package/src/console/ui-next/dist/assets/{http-method-C4vB6ZIw.js → http-method-BDf7OAHv.js} +1 -1
  53. package/src/console/ui-next/dist/assets/{index-yTCY4AcS.js → index-BKpaes3n.js} +3 -3
  54. package/src/console/ui-next/dist/assets/{observability-page-BxJ3R6dU.js → observability-page-WnVLI-0j.js} +1 -1
  55. package/src/console/ui-next/dist/assets/{replica-lag-QRKB_IE8.js → replica-lag-B3GLNVfF.js} +1 -1
  56. package/src/console/ui-next/dist/assets/{request-meta-DqZ-fMu5.js → request-meta-DMbnAe3f.js} +1 -1
  57. package/src/console/ui-next/dist/assets/{store-page-Dixb6L7a.js → store-page-KvFDingJ.js} +1 -1
  58. package/src/console/ui-next/dist/assets/{trace-detail-sheet-CazhjtiU.js → trace-detail-sheet-Htk8Cm9t.js} +1 -1
  59. package/src/console/ui-next/dist/assets/{tree-expand-toggle-DlnqYKfr.js → tree-expand-toggle-CFMPWX4f.js} +1 -1
  60. package/src/console/ui-next/dist/assets/{units-page-BXTLjU2-.js → units-page-C4NdNuxP.js} +1 -1
  61. package/src/console/ui-next/dist/assets/{vault-page-39KR__bc.js → vault-page-CBYl0LW_.js} +1 -1
  62. package/src/console/ui-next/dist/index.html +1 -1
  63. package/src/docker/docker.test.ts +3 -3
  64. package/src/docker/images-config.test.ts +4 -4
  65. package/src/docker/stack-id.test.ts +1 -1
  66. package/src/elements/store/files-errors.test.ts +149 -0
  67. package/src/elements/store/files-errors.ts +189 -0
  68. package/src/elements/store/kv-errors.test.ts +98 -0
  69. package/src/elements/store/kv-errors.ts +139 -0
  70. package/src/elements/store/resource.ts +11 -7
  71. package/src/elements/store/runtime.ts +18 -13
  72. package/src/elements/store/sql-errors.test.ts +197 -0
  73. package/src/elements/store/sql-errors.ts +294 -0
  74. package/src/elements/store/sql-session.test.ts +52 -0
  75. package/src/elements/store/sql-session.ts +26 -4
  76. package/src/elements/store/store-errors.ts +47 -0
  77. package/src/http.ts +9 -1
  78. package/src/i18n/catalogs/ar.ts +18 -0
  79. package/src/i18n/catalogs/en.ts +18 -0
  80. package/src/index.ts +9 -1
  81. package/src/kernel/app.ts +23 -4
  82. package/src/kernel/builtin-errors.test.ts +117 -0
  83. package/src/kernel/builtin-errors.ts +129 -0
  84. package/src/kernel/call.test.ts +182 -0
  85. package/src/kernel/client-descriptor.test.ts +78 -0
  86. package/src/kernel/client-descriptor.ts +23 -0
  87. package/src/kernel/errors-text.ts +99 -0
  88. package/src/kernel/errors-vault.ts +16 -0
  89. package/src/kernel/errors.registry.test.ts +7 -0
  90. package/src/kernel/errors.ts +184 -159
  91. package/src/kernel/fail-helpers.ts +34 -0
  92. package/src/kernel/fx-sql-handle.ts +305 -0
  93. package/src/kernel/fx.test.ts +8 -0
  94. package/src/kernel/fx.ts +49 -335
  95. package/src/kernel/index.ts +12 -1
  96. package/src/kernel/json-result.ts +59 -0
  97. package/src/kernel/project-out.ts +6 -1
  98. package/src/okid-extended.ts +175 -0
  99. package/src/okid-shared.ts +103 -0
  100. package/src/okid.ts +30 -213
  101. package/src/release/build-lib.ts +9 -0
  102. package/src/runtime/dev-request-log.ts +29 -11
  103. package/src/term.test.ts +76 -0
  104. package/src/term.ts +166 -3
@@ -0,0 +1,99 @@
1
+ /**
2
+ * English cause/fix templates for {@link OKE_ERRORS}.
3
+ *
4
+ * Loaded on first read so the kernel edge profile does not carry the
5
+ * strings. Localization still prefers the i18n catalogs when a locale
6
+ * overrides `oke.{code}.cause` / `oke.{code}.fix`.
7
+ */
8
+
9
+ /** One registry template pair. */
10
+ export interface OkeErrorText {
11
+ /** One-line cause template. */
12
+ readonly cause: string;
13
+ /** Suggested fix template. */
14
+ readonly fix: string;
15
+ }
16
+
17
+ /** Keyed by {@link OKE_ERRORS} property name. */
18
+ export const OKE_ERROR_TEXT: Readonly<Record<string, OkeErrorText>> = {
19
+ UNDECLARED_READ: {
20
+ cause: 'Flow "{flow}" reads "{resource}" without declaring it.',
21
+ fix: 'Add "{resource}" to this flow\'s effects.reads.',
22
+ },
23
+ UNDECLARED_WRITE: {
24
+ cause: 'Flow "{flow}" writes "{resource}" without declaring it.',
25
+ fix: 'Add "{resource}" to this flow\'s effects.writes.',
26
+ },
27
+ UNDECLARED_EMIT: {
28
+ cause: 'Flow "{flow}" emits "{resource}" without declaring it.',
29
+ fix: 'Add "{resource}" to this flow\'s effects.emits.',
30
+ },
31
+ UNDECLARED_SEND: {
32
+ cause: 'Flow "{flow}" sends "{resource}" without declaring it.',
33
+ fix: 'Add "{resource}" to this flow\'s effects.sends.',
34
+ },
35
+ UNDECLARED_ASK: {
36
+ cause: 'Flow "{flow}" asks "{resource}" without declaring it.',
37
+ fix: 'Add "{resource}" to this flow\'s effects.asks.',
38
+ },
39
+ UNDECLARED_SECRET: {
40
+ cause: 'Flow "{flow}" reads secret "{resource}" without declaring it.',
41
+ fix: 'Add "{resource}" to this flow\'s effects.secrets.',
42
+ },
43
+ UNDECLARED_CALL: {
44
+ cause: 'Flow "{flow}" calls "{resource}" without declaring it.',
45
+ fix: 'Add "{resource}" to this flow\'s effects.calls.',
46
+ },
47
+ UNDECLARED_FETCH: {
48
+ cause: 'Flow "{flow}" fetches "{resource}" without declaring it.',
49
+ fix: 'Add "{resource}" to this flow\'s effects.fetches.',
50
+ },
51
+ UNDECLARED_EMBED: {
52
+ cause: 'Flow "{flow}" embeds with "{resource}" without declaring it.',
53
+ fix: 'Add "{resource}" to this flow\'s effects.embeds.',
54
+ },
55
+ NO_EFFECTS_DECLARED: {
56
+ cause: 'Flow "{flow}" has no declared effects and no Manifest to derive them from.{extract}',
57
+ fix:
58
+ "Add explicit `effects` to this flow, or boot with a Manifest (`oke build`) / " +
59
+ "`rootDir` so effects can be derived. If extract failed, ensure `oxc-parser` is " +
60
+ "installed (okengine dependency). dev+compose/prod refuse an open capability token.",
61
+ },
62
+ ADOPT_BARREL_STALE: {
63
+ cause: "src/flows/{unit} exists on disk but adopted no flows — the .adopt() barrel is stale.",
64
+ fix: "Run `oke dev` or `oke build` to regenerate `src/flows/index.ts`.",
65
+ },
66
+ HTTP_PATH_UNRESOLVED: {
67
+ cause: 'Flow "{flow}" bound {method} with no path — the file-tree stamp never ran.',
68
+ fix: 'Put the file under `src/flows/<unit>/` and import `@/flows`, or pass an explicit path to `http.{method}("/…")`.',
69
+ },
70
+ HTTP_ROUTE_DUPLICATE: {
71
+ cause: '{method} {path} is bound twice (flow "{flow}").',
72
+ fix: "Give each HTTP flow a unique method + path.",
73
+ },
74
+ HTTP_FLOW_UNNAMED: {
75
+ cause: "An HTTP flow on {method} {path} has no name.",
76
+ fix: 'Use `flow("unit.export", {…})` or export it from a `src/flows/<unit>/` file so the tree can stamp `unit.export`.',
77
+ },
78
+ LIVE_EXPOSURE_DUPLICATE: {
79
+ cause:
80
+ 'Live signal "{signal}" is exposed twice with the same gates ({gates}) and match ({match}).',
81
+ fix: "Use a different gate or path-param filter, or drop the extra route.",
82
+ },
83
+ MCP_TOOL_DUPLICATE: {
84
+ cause: 'MCP tool "{tool}" is bound twice (flow "{flow}").',
85
+ fix: "Give each MCP tool exposure a unique tool name.",
86
+ },
87
+ ORPHAN_EMIT: {
88
+ cause: 'Flow "{flow}" emits signal "{resource}" with no subscriber.',
89
+ fix: "Add `on({resource}, …)` or mark the signal `{ optional: true }`.",
90
+ },
91
+ SIGNAL_SCHEMA: {
92
+ cause: '"{resource}": {detail}',
93
+ fix: "Fix schema payload.",
94
+ },
95
+ DOMAIN_SCHEMA_MISSING: {
96
+ cause: "domain table not found — migrations have not been applied.",
97
+ fix: "run `oke db migrate` against this environment.",
98
+ },
99
+ };
@@ -0,0 +1,16 @@
1
+ /**
2
+ * OKE1510 — kept off the kernel edge graph.
3
+ *
4
+ * Vault boot gaps. Loaded via computed `import.meta.require` from
5
+ * {@link lookupOkeError}.
6
+ */
7
+
8
+ import type { OkeErrorDefinition } from "./errors.ts";
9
+
10
+ /** Vault contract has no value in any resolution layer (boot). */
11
+ export const VAULT_SECRET_MISSING: OkeErrorDefinition = {
12
+ code: 1510,
13
+ domain: "vault",
14
+ cause: "{count} secrets have no value in any resolution layer.",
15
+ fix: "Set each name (`oke vault set <name>`, or `.env.local`).",
16
+ };
@@ -20,6 +20,7 @@ import { CHANNEL_SCHEMA } from "./errors-channel.ts";
20
20
  import { FLOW_NAME_DUPLICATE, FLOW_UNNAMED } from "./errors-flow-name.ts";
21
21
  import { ONCE_SIGNAL_MULTI_FLOW } from "./errors-once-signal.ts";
22
22
  import { TENANT_NOT_MEMBER, TENANT_REQUIRED, TENANT_UNKNOWN_SCOPE } from "./errors-tenant.ts";
23
+ import { VAULT_SECRET_MISSING } from "./errors-vault.ts";
23
24
  import {
24
25
  assertCodesInDomainRanges,
25
26
  assertUniqueCodes,
@@ -65,8 +66,10 @@ describe("OKE error-code registry", () => {
65
66
  expect(files).toContain("errors-live-resume.ts");
66
67
  expect(files).toContain("errors-channel.ts");
67
68
  expect(files).toContain("errors-tenant.ts");
69
+ expect(files).toContain("errors-vault.ts");
68
70
  const codes = defs.map((d) => d.code);
69
71
  expect(codes).toContain(1210);
72
+ expect(codes).toContain(1510);
70
73
  expect(codes).toContain(1605);
71
74
  expect(codes).toContain(1810);
72
75
  expect(codes).toContain(1820);
@@ -160,6 +163,10 @@ describe("OKE error-code registry", () => {
160
163
  expect(lookupOkeError(1605)).toEqual(CHANNEL_SCHEMA);
161
164
  });
162
165
 
166
+ test("lookupOkeError finds the lazy VAULT_SECRET_MISSING entry", () => {
167
+ expect(lookupOkeError(1510)).toEqual(VAULT_SECRET_MISSING);
168
+ });
169
+
163
170
  test("lookupOkeError finds lazy tenant entries", () => {
164
171
  expect(lookupOkeError(1810)).toEqual(TENANT_REQUIRED);
165
172
  expect(lookupOkeError(1820)).toEqual(TENANT_NOT_MEMBER);
@@ -9,6 +9,7 @@
9
9
  import { docsUrl as absoluteDocsUrl } from "../docs-origin.ts";
10
10
  import { getActiveDefaultLocale, getActiveLocale } from "../i18n/locale-context.ts";
11
11
  import { lazyRequire } from "./lazy-require.ts";
12
+ import type { BuiltinErrorMap } from "./builtin-errors.ts";
12
13
 
13
14
  /** Catalogs — kept off the edge `fail` / `OKE_ERRORS` static graph. */
14
15
  function loadMessages(): typeof import("../i18n/messages.ts") {
@@ -144,10 +145,100 @@ export interface FailOptions {
144
145
  readonly message?: string;
145
146
  }
146
147
 
148
+ /**
149
+ * Callable `fail` plus built-in helpers (`fail.notFound`, `fail.forbidden`, …).
150
+ */
151
+ export interface FailFn {
152
+ /**
153
+ * Flow-boundary failure value (does not throw).
154
+ *
155
+ * @param code - Declared or built-in error code
156
+ * @param data - Error payload
157
+ * @param opts - Optional message override
158
+ */
159
+ <E>(code: string, data: E, opts?: FailOptions): FlowFailure<E>;
160
+ /** Missing row / object — HTTP 404. */
161
+ notFound(
162
+ data?: BuiltinErrorMap["NotFound"],
163
+ opts?: FailOptions,
164
+ ): FlowFailure<BuiltinErrorMap["NotFound"]>;
165
+ /** Not authenticated — HTTP 401. */
166
+ unauthorized(
167
+ data?: BuiltinErrorMap["Unauthorized"],
168
+ opts?: FailOptions,
169
+ ): FlowFailure<BuiltinErrorMap["Unauthorized"]>;
170
+ /** Authenticated but denied — HTTP 403. */
171
+ forbidden(
172
+ data?: BuiltinErrorMap["Forbidden"],
173
+ opts?: FailOptions,
174
+ ): FlowFailure<BuiltinErrorMap["Forbidden"]>;
175
+ /** Unique / exclusion conflict — HTTP 409. */
176
+ conflict(
177
+ data?: BuiltinErrorMap["Conflict"],
178
+ opts?: FailOptions,
179
+ ): FlowFailure<BuiltinErrorMap["Conflict"]>;
180
+ /** Foreign-key violation — HTTP 409. */
181
+ foreignKey(
182
+ data?: BuiltinErrorMap["ForeignKey"],
183
+ opts?: FailOptions,
184
+ ): FlowFailure<BuiltinErrorMap["ForeignKey"]>;
185
+ /** Rate budget exhausted — HTTP 429. */
186
+ rateLimited(
187
+ data?: BuiltinErrorMap["RateLimited"],
188
+ opts?: FailOptions,
189
+ ): FlowFailure<BuiltinErrorMap["RateLimited"]>;
190
+ /** Maintenance / connection — HTTP 503. */
191
+ serviceUnavailable(
192
+ data?: BuiltinErrorMap["ServiceUnavailable"],
193
+ opts?: FailOptions,
194
+ ): FlowFailure<BuiltinErrorMap["ServiceUnavailable"]>;
195
+ /** SQL / store constraint that is not unique or FK. */
196
+ database(
197
+ data: BuiltinErrorMap["DatabaseError"],
198
+ opts?: FailOptions,
199
+ ): FlowFailure<BuiltinErrorMap["DatabaseError"]>;
200
+ /** Unhandled throw — HTTP 500. Catalog message only. */
201
+ internal(
202
+ data?: BuiltinErrorMap["InternalError"],
203
+ opts?: FailOptions,
204
+ ): FlowFailure<BuiltinErrorMap["InternalError"]>;
205
+ }
206
+
207
+ /**
208
+ * Create a flow-boundary failure value (does not throw).
209
+ *
210
+ * When `opts.message` is omitted, attaches a localized message from the
211
+ * built-in / app catalogs (`errors.{code}.{reason}` → `errors.{code}`) using
212
+ * the active request locale. Custom codes with no catalog entry stay
213
+ * message-less.
214
+ *
215
+ * @param code - Declared error code from the flow's `errors` map
216
+ * @param data - Error payload
217
+ * @param opts - Optional message override
218
+ */
219
+ function failImpl<E>(code: string, data: E, opts?: FailOptions): FlowFailure<E> {
220
+ const message =
221
+ opts?.message !== undefined
222
+ ? opts.message
223
+ : loadFailureMessage().resolveFailureMessage(code, data);
224
+ const error: FlowErrorValue<E> = message !== undefined ? { code, data, message } : { code, data };
225
+ return { data: null, error };
226
+ }
227
+
228
+ /**
229
+ * Flow-boundary failure (callable). Built-in helpers live on the lazy
230
+ * `fail-helpers` chunk so they stay off the kernel edge graph — {@link createFx}
231
+ * and the public `okengine` export load that chunk.
232
+ */
233
+ export const fail: FailFn = failImpl as FailFn;
234
+
147
235
  /**
148
236
  * Permanent error registry. Codes are stable within their domain range after
149
237
  * the domain-range renumber; each entry declares `domain` for range guards.
150
238
  *
239
+ * Cause/fix templates load from {@link OKE_ERROR_TEXT} on first read so the
240
+ * edge profile does not carry the English strings.
241
+ *
151
242
  * Ranges:
152
243
  * - `1000–1099` — Kernel (Flow / Trigger / Gate-posture)
153
244
  * - `1100–1199` — Store
@@ -160,168 +251,117 @@ export interface FailOptions {
160
251
  * - `1800–1899` — MCP + Tenancy
161
252
  * - `1900–1999` — Compiler / Manifest
162
253
  */
163
- export const OKE_ERRORS = {
254
+ const OKE_ERROR_ROWS = {
164
255
  /** Flow reads a store resource not listed in `effects.reads`. */
165
- UNDECLARED_READ: {
166
- code: 1001,
167
- domain: "kernel",
168
- cause: 'Flow "{flow}" reads "{resource}" without declaring it.',
169
- fix: 'Add "{resource}" to this flow\'s effects.reads.',
170
- },
256
+ UNDECLARED_READ: { code: 1001, domain: "kernel" },
171
257
  /** Flow writes a store resource not listed in `effects.writes`. */
172
- UNDECLARED_WRITE: {
173
- code: 1002,
174
- domain: "kernel",
175
- cause: 'Flow "{flow}" writes "{resource}" without declaring it.',
176
- fix: 'Add "{resource}" to this flow\'s effects.writes.',
177
- },
258
+ UNDECLARED_WRITE: { code: 1002, domain: "kernel" },
178
259
  /** Flow emits a signal not listed in `effects.emits`. */
179
- UNDECLARED_EMIT: {
180
- code: 1003,
181
- domain: "kernel",
182
- cause: 'Flow "{flow}" emits "{resource}" without declaring it.',
183
- fix: 'Add "{resource}" to this flow\'s effects.emits.',
184
- },
260
+ UNDECLARED_EMIT: { code: 1003, domain: "kernel" },
185
261
  /** Flow sends a channel template not listed in `effects.sends`. */
186
- UNDECLARED_SEND: {
187
- code: 1004,
188
- domain: "kernel",
189
- cause: 'Flow "{flow}" sends "{resource}" without declaring it.',
190
- fix: 'Add "{resource}" to this flow\'s effects.sends.',
191
- },
262
+ UNDECLARED_SEND: { code: 1004, domain: "kernel" },
192
263
  /** Flow asks a prompt not listed in `effects.asks`. */
193
- UNDECLARED_ASK: {
194
- code: 1005,
195
- domain: "kernel",
196
- cause: 'Flow "{flow}" asks "{resource}" without declaring it.',
197
- fix: 'Add "{resource}" to this flow\'s effects.asks.',
198
- },
264
+ UNDECLARED_ASK: { code: 1005, domain: "kernel" },
199
265
  /** Flow reads a secret not listed in `effects.secrets`. */
200
- UNDECLARED_SECRET: {
201
- code: 1006,
202
- domain: "kernel",
203
- cause: 'Flow "{flow}" reads secret "{resource}" without declaring it.',
204
- fix: 'Add "{resource}" to this flow\'s effects.secrets.',
205
- },
266
+ UNDECLARED_SECRET: { code: 1006, domain: "kernel" },
206
267
  /** Flow calls another flow not listed in `effects.calls`. */
207
- UNDECLARED_CALL: {
208
- code: 1007,
209
- domain: "kernel",
210
- cause: 'Flow "{flow}" calls "{resource}" without declaring it.',
211
- fix: 'Add "{resource}" to this flow\'s effects.calls.',
212
- },
268
+ UNDECLARED_CALL: { code: 1007, domain: "kernel" },
213
269
  /** Flow fetches a host not listed in `effects.fetches`. */
214
- UNDECLARED_FETCH: {
215
- code: 1008,
216
- domain: "kernel",
217
- cause: 'Flow "{flow}" fetches "{resource}" without declaring it.',
218
- fix: 'Add "{resource}" to this flow\'s effects.fetches.',
219
- },
270
+ UNDECLARED_FETCH: { code: 1008, domain: "kernel" },
220
271
  /** Flow embeds via a model not listed in `effects.embeds`. */
221
- UNDECLARED_EMBED: {
222
- code: 1009,
223
- domain: "kernel",
224
- cause: 'Flow "{flow}" embeds with "{resource}" without declaring it.',
225
- fix: 'Add "{resource}" to this flow\'s effects.embeds.',
226
- },
272
+ UNDECLARED_EMBED: { code: 1009, domain: "kernel" },
227
273
  /**
228
274
  * Flow has no declared `effects` and no Manifest-derived effects were
229
275
  * available to stamp at boot (dev+compose / prod — never a silent open token).
230
276
  */
231
- NO_EFFECTS_DECLARED: {
232
- code: 1020,
233
- domain: "kernel",
234
- cause: 'Flow "{flow}" has no declared effects and no Manifest to derive them from.{extract}',
235
- fix:
236
- "Add explicit `effects` to this flow, or boot with a Manifest (`oke build`) / " +
237
- "`rootDir` so effects can be derived. If extract failed, ensure `oxc-parser` is " +
238
- "installed (okengine dependency). dev+compose/prod refuse an open capability token.",
239
- },
277
+ NO_EFFECTS_DECLARED: { code: 1020, domain: "kernel" },
240
278
  /**
241
279
  * A `src/flows/<unit>` folder exists on disk but no adopted flow carries
242
280
  * that unit — the generated `.adopt()` barrel (`src/flows/index.ts`)
243
281
  * is stale or was hand-edited. dev+compose / prod — never a silently-incomplete
244
282
  * route table in a deploy-shaped environment.
245
283
  */
246
- ADOPT_BARREL_STALE: {
247
- code: 1030,
248
- domain: "kernel",
249
- cause: "src/flows/{unit} exists on disk but adopted no flows — the .adopt() barrel is stale.",
250
- fix: "Run `oke dev` or `oke build` to regenerate `src/flows/index.ts`.",
251
- },
252
- /**
253
- * `http.get()` was never stamped from the file tree — refuse a silent `/`.
254
- */
255
- HTTP_PATH_UNRESOLVED: {
256
- code: 1040,
257
- domain: "kernel",
258
- cause: 'Flow "{flow}" bound {method} with no path — the file-tree stamp never ran.',
259
- fix: 'Put the file under `src/flows/<unit>/` and import `@/flows`, or pass an explicit path to `http.{method}("/…")`.',
260
- },
261
- /**
262
- * Two HTTP bindings share method + path — last-add-wins is the opposite of this DX.
263
- */
264
- HTTP_ROUTE_DUPLICATE: {
265
- code: 1041,
266
- domain: "kernel",
267
- cause: '{method} {path} is bound twice (flow "{flow}").',
268
- fix: "Give each HTTP flow a unique method + path.",
269
- },
270
- /**
271
- * Adopted HTTP flow still has no name (`flow({ do })` outside a unit).
272
- */
273
- HTTP_FLOW_UNNAMED: {
274
- code: 1045,
275
- domain: "kernel",
276
- cause: "An HTTP flow on {method} {path} has no name.",
277
- fix: 'Use `flow("unit.export", {…})` or export it from a `src/flows/<unit>/` file so the tree can stamp `unit.export`.',
278
- },
279
- /**
280
- * Two live HTTP exposures of the same signal share gates + match shape.
281
- */
282
- LIVE_EXPOSURE_DUPLICATE: {
283
- code: 1050,
284
- domain: "kernel",
285
- cause:
286
- 'Live signal "{signal}" is exposed twice with the same gates ({gates}) and match ({match}).',
287
- fix: "Use a different gate or path-param filter, or drop the extra route.",
288
- },
289
- /**
290
- * Two MCP tool bindings share the same exposed tool name.
291
- */
292
- MCP_TOOL_DUPLICATE: {
293
- code: 1060,
294
- domain: "kernel",
295
- cause: 'MCP tool "{tool}" is bound twice (flow "{flow}").',
296
- fix: "Give each MCP tool exposure a unique tool name.",
297
- },
284
+ ADOPT_BARREL_STALE: { code: 1030, domain: "kernel" },
285
+ /** `http.get()` was never stamped from the file tree — refuse a silent `/`. */
286
+ HTTP_PATH_UNRESOLVED: { code: 1040, domain: "kernel" },
287
+ /** Two HTTP bindings share method + path. */
288
+ HTTP_ROUTE_DUPLICATE: { code: 1041, domain: "kernel" },
289
+ /** Adopted HTTP flow still has no name (`flow({ do })` outside a unit). */
290
+ HTTP_FLOW_UNNAMED: { code: 1045, domain: "kernel" },
291
+ /** Two live HTTP exposures of the same signal share gates + match shape. */
292
+ LIVE_EXPOSURE_DUPLICATE: { code: 1050, domain: "kernel" },
293
+ /** Two MCP tool bindings share the same exposed tool name. */
294
+ MCP_TOOL_DUPLICATE: { code: 1060, domain: "kernel" },
298
295
  /**
299
296
  * Emit target has no subscriber (unified-theory §21).
300
297
  * Thrown at emit when `optional` is false and nobody is subscribed.
301
298
  */
302
- ORPHAN_EMIT: {
303
- code: 1240,
304
- domain: "signal",
305
- cause: 'Flow "{flow}" emits signal "{resource}" with no subscriber.',
306
- fix: "Add `on({resource}, …)` or mark the signal `{ optional: true }`.",
307
- },
299
+ ORPHAN_EMIT: { code: 1240, domain: "signal" },
308
300
  /** Emit payload failed the signal's declared Standard Schema. */
309
- SIGNAL_SCHEMA: {
310
- code: 1250,
311
- domain: "signal",
312
- cause: '"{resource}": {detail}',
313
- fix: "Fix schema payload.",
314
- },
315
- /**
316
- * Domain table/column missing under docker/prod (migrations not applied).
317
- */
318
- DOMAIN_SCHEMA_MISSING: {
319
- code: 1110,
320
- domain: "store",
321
- cause: "domain table not found — migrations have not been applied.",
322
- fix: "run `oke db migrate` against this environment.",
323
- },
324
- } as const satisfies Record<string, OkeErrorDefinition>;
301
+ SIGNAL_SCHEMA: { code: 1250, domain: "signal" },
302
+ /** Domain table/column missing under docker/prod (migrations not applied). */
303
+ DOMAIN_SCHEMA_MISSING: { code: 1110, domain: "store" },
304
+ } as const satisfies Record<string, { readonly code: number; readonly domain: OkeErrorDomain }>;
305
+
306
+ type OkeErrorRowTable = typeof OKE_ERROR_ROWS;
307
+
308
+ /** Registry entries with literal codes and lazy cause/fix. */
309
+ type OkeErrorTable = {
310
+ readonly [K in keyof OkeErrorRowTable]: OkeErrorDefinition & {
311
+ readonly code: OkeErrorRowTable[K]["code"];
312
+ readonly domain: OkeErrorRowTable[K]["domain"];
313
+ };
314
+ };
315
+
316
+ /**
317
+ * Load English cause/fix templates. Computed stem so Bun.build cannot inline them.
318
+ */
319
+ function loadErrorText(): typeof import("./errors-text.ts") {
320
+ return lazyRequire(import.meta.dir, ["errors", "text"].join("-"));
321
+ }
322
+
323
+ /**
324
+ * Read one cause or fix template.
325
+ *
326
+ * @param key - {@link OKE_ERRORS} property name
327
+ * @param part - `cause` or `fix`
328
+ */
329
+ function errorPart(key: string, part: "cause" | "fix"): string {
330
+ const row = loadErrorText().OKE_ERROR_TEXT[key];
331
+ if (!row) throw new Error(`Missing OKE error text for ${key}.${part}`);
332
+ return row[part];
333
+ }
334
+
335
+ /**
336
+ * Attach lazy cause/fix getters. Strings stay in the text chunk until read.
337
+ *
338
+ * @param rows - Code and domain table
339
+ */
340
+ function attachErrorText(rows: OkeErrorRowTable): OkeErrorTable {
341
+ const out: Record<string, OkeErrorDefinition> = {};
342
+ for (const key of Object.keys(rows) as (keyof OkeErrorRowTable)[]) {
343
+ const row = rows[key];
344
+ out[key] = {
345
+ code: row.code,
346
+ domain: row.domain,
347
+ get cause() {
348
+ return errorPart(key, "cause");
349
+ },
350
+ get fix() {
351
+ return errorPart(key, "fix");
352
+ },
353
+ };
354
+ }
355
+ return out as OkeErrorTable;
356
+ }
357
+
358
+ /**
359
+ * Permanent error registry. Cause/fix load from the lazy text chunk.
360
+ *
361
+ * `@__PURE__` lets bundles that never read the registry drop the getter
362
+ * table. A plain call looks like a side effect and stays in every importer.
363
+ */
364
+ export const OKE_ERRORS: OkeErrorTable = /* @__PURE__ */ attachErrorText(OKE_ERROR_ROWS);
325
365
 
326
366
  /** Tenant error keys — definitions live in the lazy `errors-tenant` chunk. */
327
367
  export type TenantOkeErrorKey = "TENANT_REQUIRED" | "TENANT_NOT_MEMBER" | "TENANT_UNKNOWN_SCOPE";
@@ -368,6 +408,12 @@ export function lookupOkeError(code: OkeErrorCode): OkeErrorDefinition | undefin
368
408
  ["errors", "live", "resume"].join("-"),
369
409
  ).LIVE_RESUME_GAP;
370
410
  }
411
+ if (code === 1510) {
412
+ return lazyRequire<typeof import("./errors-vault.ts")>(
413
+ import.meta.dir,
414
+ ["errors", "vault"].join("-"),
415
+ ).VAULT_SECRET_MISSING;
416
+ }
371
417
  if (code === 1605) {
372
418
  return lazyRequire<typeof import("./errors-channel.ts")>(
373
419
  import.meta.dir,
@@ -383,27 +429,6 @@ export function lookupOkeError(code: OkeErrorCode): OkeErrorDefinition | undefin
383
429
  return undefined;
384
430
  }
385
431
 
386
- /**
387
- * Create a flow-boundary failure value (does not throw).
388
- *
389
- * When `opts.message` is omitted, attaches a localized message from the
390
- * built-in / app catalogs (`errors.{code}.{reason}` → `errors.{code}`) using
391
- * the active request locale. Custom codes with no catalog entry stay
392
- * message-less.
393
- *
394
- * @param code - Declared error code from the flow's `errors` map
395
- * @param data - Error payload
396
- * @param opts - Optional message override
397
- */
398
- export function fail<E>(code: string, data: E, opts?: FailOptions): FlowFailure<E> {
399
- const message =
400
- opts?.message !== undefined
401
- ? opts.message
402
- : loadFailureMessage().resolveFailureMessage(code, data);
403
- const error: FlowErrorValue<E> = message !== undefined ? { code, data, message } : { code, data };
404
- return { data: null, error };
405
- }
406
-
407
432
  /**
408
433
  * Format the canonical multi-line OKE error message (§21).
409
434
  *
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Built-in `fail.notFound` / `fail.forbidden` / … helpers.
3
+ *
4
+ * Kept off the kernel edge graph — loaded via computed `import.meta.require`.
5
+ * Mutates the shared {@link fail} function so `fx.fail` and `import { fail }`
6
+ * share one identity.
7
+ */
8
+
9
+ import { fail as failImpl, type FailFn, type FailOptions } from "./errors.ts";
10
+ import type { BuiltinErrorMap } from "./builtin-errors.ts";
11
+
12
+ const empty = {};
13
+ const helper =
14
+ (code: string) =>
15
+ (data = empty, opts?: FailOptions) =>
16
+ failImpl(code, data, opts);
17
+
18
+ /**
19
+ * Callable `fail` plus helpers. Same function object as kernel `fail`.
20
+ */
21
+ export const fail: FailFn = Object.assign(failImpl, {
22
+ notFound: helper("NotFound"),
23
+ unauthorized: helper("Unauthorized"),
24
+ forbidden: helper("Forbidden"),
25
+ conflict: helper("Conflict"),
26
+ foreignKey: helper("ForeignKey"),
27
+ rateLimited: helper("RateLimited"),
28
+ serviceUnavailable: helper("ServiceUnavailable"),
29
+ database: (data: BuiltinErrorMap["DatabaseError"], opts?: FailOptions) =>
30
+ failImpl("DatabaseError", data, opts),
31
+ internal: helper("InternalError"),
32
+ });
33
+
34
+ export type { FailFn, FailOptions } from "./errors.ts";