@ultimat3/core 6.0.0 → 8.0.0

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/CLAUDE.md CHANGED
@@ -14,6 +14,7 @@ is a change to every package.
14
14
  | New code | add to `CORE_CODE_TITLES` in `error-codes.ts`, else the title is auto-humanised |
15
15
  | Time | take a `Clock`; `Date.now()` / `new Date()` only inside `clock.ts` |
16
16
  | Context | never thread `ctx` as a parameter — `useContext()` |
17
+ | A value ambient across an `await` | `asyncContext<T>(subject)` from `async-context.ts`, in **every** package — never `new AsyncLocalStorage` |
17
18
  | Exports | add to `src/index.ts` explicitly; no `export *`. Three subjects that each span a dozen modules arrive through `src/exports/` — every name is still written out in `index.ts`, so the public surface is one file to read |
18
19
  | Files | < 200 LOC, 500 hard ceiling, one responsibility, `kebab-case.ts`, test beside source |
19
20
  | Type claims | `type-pins.ts`, never a `.test.ts` — `tsconfig.json` excludes tests, so `tsc` never reads one |
@@ -22,6 +23,24 @@ Deliberate cycles (safe — nothing is referenced at module-evaluation time):
22
23
  `errors.ts ⇄ error-codes.ts`. Keep it that way: no top-level `UltimateError` use in
23
24
  `error-codes.ts`.
24
25
 
26
+ **`async-context.ts` is the framework's ONE `AsyncLocalStorage`, and that is a framework rule
27
+ rather than a core one, `As of 2026-08-20`.** `asyncContext` is exported from `src/index.ts` and
28
+ six modules outside this package opened their own before they adopted it — `@ultimat3/db`'s
29
+ transaction, statement attribution and expected-loop scopes, `@ultimat3/entity`'s `crossTenant`,
30
+ `@ultimat3/ai`'s budget ledger and LLM stream sink. Each was a module-scope `new` a browser bundler
31
+ turns into `TypeError: undefined is not a constructor` at module EVALUATION, so importing any of
32
+ those packages from a client bundle failed before a line of app code ran. Reads degrade to
33
+ `undefined`, writes throw `X_ASYNC_CONTEXT_UNAVAILABLE`; deferring the construction changes nothing
34
+ a server can observe — the storage is built on the first `get()` or `run()` rather than at module
35
+ evaluation, and `getStore()` outside a scope answers `undefined` either way.
36
+
37
+ The mechanical half is `scripts/async-context-guard.ts`, collected by `x verify`'s `unit` step
38
+ through `scripts/async-context-guard.test.ts` — it refuses a `new AsyncLocalStorage` **and** the
39
+ import that binds the class, aliased or namespaced, anywhere but this one file. The browser-barrel
40
+ test in `async-context.test.ts` covers the same defect for core alone and cannot see another
41
+ package; the guard cannot see a runtime `await import('node:async_hooks')`. Neither is the other's
42
+ duplicate.
43
+
25
44
  `error-render.ts` imports nothing, including from this package — an error factory that dies
26
45
  formatting its own message is the failure it exists to prevent, so it cannot depend on anything
27
46
  that could itself throw. The same defect shipped three times (`entity`, `flags`, `cli`) before
@@ -60,12 +79,17 @@ escape, so a cause could repaint the screen or hide the line above it. `@ultimat
60
79
  deliberate duplicate for the tier-0 reason below, pinned behaviourally by
61
80
  `single-line-pin.test.ts` in `@ultimat3/cli`.
62
81
 
63
- `describeValue` in `error-render.ts` is a character-for-character duplicate of `describeValue` in
82
+ `describeValue` in `error-render.ts` is a deliberate duplicate of `describeValue` in
64
83
  `packages/schema/src/describe-value.ts`, for the same tier-0 reason `SCHEMA_ERROR_CODE_TITLES` is
65
84
  one: schema and core are both tier 0 and `core → schema` is **not** a declared edge in
66
- `scripts/lib/tiers.ts`, so neither may import the other. Keep the two identical; a pin test in
85
+ `scripts/lib/tiers.ts`, so neither may import the other. Keep the two ANSWERING identically that
86
+ is the contract, and the source is no longer character-for-character: schema counts characters
87
+ through `char-count.ts`, which core copies privately. A pin test in
67
88
  `@ultimat3/cli` (which may legally import both) is the mechanical half, the same shape as
68
- `schema-error-codes-pin.test.ts`. The rule it enforces: a `cause` reaches the log index AND the
89
+ `schema-error-codes-pin.test.ts`. **A string's length is CODE POINTS in both, `As of 2026-08-22`**
90
+ — `validators.ts` rejects in that unit and `json-schema.ts` publishes `minLength` in it, so
91
+ `.length` made `t.string.min(3).safeParse('👍a')` say "at least 3 chars, received a string of 3
92
+ characters". The rule it enforces: a `cause` reaches the log index AND the
69
93
  HTTP problem document, redaction is by log FIELD key, and a value baked into a message string has
70
94
  no key left to redact — so `parseId`/`uuidTimestamp` describe a rejected id and never echo it.
71
95
 
@@ -85,6 +109,7 @@ shape against a locally declared sample interface for exactly that reason.
85
109
  |---|---|---|
86
110
  | which deploy this is | `environment.ts` (`ULTIMATE_ENV`) | the twin of `ROLE`; never declare a second env var for it |
87
111
  | what this process does | `roles.ts` (`ROLE`) | |
112
+ | how a route renders, caches offline and hydrates | `route-vocabulary.ts` (`RENDER_MODES`, `OFFLINE_STRATEGIES`, `HYDRATE_STRATEGIES`) | tier 0 because SIX packages name them and imports only go down — `render`, `http`, `seo`, `manifest` and `pwa` each kept a hand-copy until 2026-08, and `'spa'` was deleted from one while five went on admitting it under a green typecheck. Every union is `(typeof ARRAY)[number]`, pinned in `type-pins.ts`; `scripts/render-modes.test.ts` refuses a second declaration anywhere in `packages/*/src`. Re-export it, never restate it |
88
113
  | which build of the APP this is | `app-version.ts` (`APP_VERSION`) | one reader, `dev` by default: `db` writes it into `x_migrations` and `jobs` into `x_backfills`, and `jobs` cannot reach `db` for the answer |
89
114
  | the values | `env.ts` | `checkEnv().values` holds REAL secrets — anything that prints goes through `maskedEnvValues()` |
90
115
  | `.env.example` | `env-example.ts` | a projection of the schema, never hand-maintained |
@@ -155,6 +180,17 @@ NOT — `@ultimat3/query`, whose `OrderKey` is a name and a direction — must n
155
180
  orders a `text` column of digits lexically and a comparator guessing would trade one disagreement
156
181
  with the SQL it printed for another.
157
182
 
183
+ `format-bytes.ts` is the same rule at its smallest, `As of 2026-08-22`: one `formatBytes(bytes)`,
184
+ 1024-base, `b|kb|mb|gb`, for the byte count an error message carries. `@ultimat3/render` (t4) and
185
+ `@ultimat3/pwa` (t4) each had one and they had diverged — render's stopped at `kb`, so a 5 MiB route
186
+ read `5120kb` in `X_BUDGET_EXCEEDED` and `5mb` in the precache warning about the same bytes, and
187
+ `@ultimat3/cli`'s budget error imported render's. Deliberately NOT
188
+ `@ultimat3/ui`'s `formatBytes(bytes, locale)`, which is a different function and stays: that one is
189
+ `Intl`-formatted and DECIMAL (kB = 1000 B, which is what `Intl`'s unit means), for a human reading a
190
+ file picker, where this one must line up with a bundler's own KiB figures and must not move with the
191
+ reader's locale. **Not mechanised** — no gate refuses a third copy, unlike `render-modes.ts` for the
192
+ route vocabulary; a `formatBytes` reappearing in `packages/*/src` is caught by review only.
193
+
158
194
  `mcp-exposure.ts` is the same shape for a declaration rather than an algorithm: `isMcpExposed` is
159
195
  the ONE answer to "did this primitive opt into being an MCP tool?", asked by `action`, `query`
160
196
  (t3), `mcp`, `ai`, `manifest` (t4) — five packages that cannot import each other, so core is the
@@ -187,6 +223,16 @@ which is what stops an exporter from turning 40k rps into 40k rps of spans. `con
187
223
  takes a `Sampler`; the default reads `OTEL_TRACES_SAMPLER*` **at the first span, never at module
188
224
  scope** (same call-time rule as `cursor.ts`'s secret). `resetTelemetry()` drops both.
189
225
 
226
+ **An empty `spanId` means "no inbound decision", and every reader must honour it, `As of
227
+ 2026-08-22`.** `currentSpanContext()` synthesises `{ traceId, spanId: '', traceFlags: 1 }` from the
228
+ request context — a trace id this process minted, plus the header it would send onward. Handing
229
+ that to `Sampler.shouldSample` as a parent made `parentBasedRatioSampler` inherit a bit nobody sent,
230
+ so at ratio 0 a root span outside a request exported 0 and one inside exported 1 — and
231
+ `@ultimat3/http`'s `pipeline.ts` is `runWithContext` then `withSpan`, so **every HTTP root span was
232
+ exported at every ratio**. `startSpan` now narrows through `inboundParent()`: the trace id is
233
+ carried, the decision is not. It is the same discriminator `end()` already used to drop a synthetic
234
+ `parentSpanId`.
235
+
190
236
  The OTLP exporters are built, not wrapped, and the case is in
191
237
  [`docs/idea/18-build-vs-wrap.md`](../../docs/idea/18-build-vs-wrap.md): OTLP/HTTP JSON is `fetch`
192
238
  plus `JSON.stringify`, while `@opentelemetry/api` would put a SECOND `Span` type in the framework
@@ -196,6 +242,15 @@ plus `JSON.stringify`, while `@opentelemetry/api` would put a SECOND `Span` type
196
242
  non-`http/json` `OTEL_EXPORTER_OTLP_PROTOCOL` throw `X_OTLP_PROTOCOL_UNSUPPORTED` naming `:4318`.
197
243
  A boot that must not throw asks `tryOtlpEndpoint(signal)` first.
198
244
 
245
+ **Three OTLP variables, three codes, `As of 2026-08-22`** — `X_OTLP_ENDPOINT_INVALID`,
246
+ `X_OTLP_HEADERS_INVALID`, `X_OTLP_PROTOCOL_UNSUPPORTED`, one per variable an operator sets. The
247
+ headers one is not a duplicate of the endpoint one: `otlpHeaders` percent-decodes, so `%zz` in
248
+ `OTEL_EXPORTER_OTLP_HEADERS` used to take the process down with a bare `URIError` at exporter
249
+ construction, and raising the ENDPOINT code instead would send the first reader of
250
+ `x errors explain` to inspect a variable that is fine. A title is what an agent reads first, and an
251
+ accurate `cause:` does not rescue one that misdirects. The header **key** is in the cause, the fix
252
+ and `meta`; the **value** is in none of them — it is the collector's credential.
253
+
199
254
  `error-reporter.ts` is the same shape a third time: `ErrorReporter`, a no-op default, a memory
200
255
  reporter for tests, and a transport on the wire (`error-reporter-sentry.ts`, an optional separate
201
256
  export — the DSN is the app's typed env, never a constant here). `reportError` never throws and
@@ -227,13 +282,32 @@ bun run typecheck
227
282
  `markReady()` means **bound**, and readiness means **usable** — two different facts since
228
283
  `registerReadinessCheck(name, check)`. `/readyz` is ready only when the state is `ready` AND every
229
284
  named check passes, and `HealthReport.checks` carries them by name because "alert on check
230
- failures by check name" is not writable against a boolean. Checks are **synchronous** on purpose:
285
+ failures by check name" is not writable against a boolean. `HealthReport.registered` carries the
286
+ COUNT beside it, `As of 2026-08-22`: `checks: {}` reads identically for "every check passed" and
287
+ "nobody registered one", and only the second is a `/readyz` meaning no more than "the socket is
288
+ bound". Reported, never enforced — **an empty registry is still `ready`, and `/readyz` still
289
+ answers 200**: `Object.values({}).every(…)` is vacuously true, and that is deliberate so a role
290
+ with no dependency does not have to invent a check to boot. `registered` is the field a caller
291
+ reads to tell "all checks passed" from "there were none".
292
+
293
+ `readinessChecks()` builds its record through `Object.fromEntries`, never by assigning
294
+ `results[name]` — assignment to the one name `__proto__` sets the PROTOTYPE rather than adding a
295
+ key, so a check by that name disappeared from the report and a `failing` one answered 200. Checks are **synchronous** on purpose:
231
296
  a probe that awaits a network call turns a slow dependency into a wedged endpoint and a restart
232
297
  loop, so the owner of the dependency keeps a boolean fresh and this reads it. Liveness ignores
233
298
  them — a database outage that failed `/healthz` would restart the whole fleet into the same
234
299
  outage. The registration returns its unregister, same shape and same ownership rule as
235
300
  `onShutdown`; `readinessCheckCount()` is the leak probe.
236
301
 
302
+ **`drain()`'s memo is published BEFORE the first hook runs, `As of 2026-08-22`, and that ordering
303
+ is the whole of the function.** A hook may call back into `drain()` and one does — `handle.stop()`
304
+ in `@ultimat3/http` is `drain('manual')`, and an `accept` hook is exactly where a server stops
305
+ listening — while `settleWithin` invokes a hook SYNCHRONOUSLY. `drainPromise = (async () => …)()`
306
+ had therefore not assigned when the first hook ran: the re-entrant call read `undefined`, started a
307
+ second whole drain and recursed **~4,700 deep** until the stack ran out, every level swallowed by
308
+ `settleWithin` as `shutdown hook failed`. The guard and the registration are now one synchronous
309
+ step (`jobs`' `worker.ts` states the same rule), with the phases in `runDrain`.
310
+
237
311
  **The drain deadline is enforced, not merely computed, and there is no unbounded state.**
238
312
  `ShutdownReason.deadlineAt` was always handed to every hook and **no hook has ever read it** —
239
313
  `jobs`' worker awaits every in-flight job and `driver.close()`, `jobs`' scheduler awaits its round,
package/README.md CHANGED
@@ -9,7 +9,8 @@ Zero dependencies, zero `@ultimat3/*` imports.
9
9
  | rendering an app's value into a `cause` / `fix` without throwing | `error-render.ts` |
10
10
  | code → `{ title, docs }` registry, `registerErrorCodes()` | `error-codes.ts` |
11
11
  | `Result<T, E>` for boundaries where throwing is wrong | `result.ts` |
12
- | request context on `AsyncLocalStorage` | `context.ts` |
12
+ | the one lazy `AsyncLocalStorage`, every ambient scope in the framework | `async-context.ts` |
13
+ | request context on that seam | `context.ts` |
13
14
  | `Actor` (`user \| service \| agent \| anonymous`) | `actor.ts` |
14
15
  | acting as another actor, with an origin and a reason | `impersonate.ts` |
15
16
  | is an error worth retrying? one classification per code | `error-retry.ts` |
@@ -20,6 +21,7 @@ Zero dependencies, zero `@ultimat3/*` imports.
20
21
  | the committed encrypted secrets envelope, AES-256-GCM | `secrets.ts` |
21
22
  | the two secrets files, and decrypted values → `defineEnv` | `secrets-store.ts` |
22
23
  | `defineConfig()` for `app.config.ts` | `config.ts` |
24
+ | the closed route vocabulary every renderer names | `route-vocabulary.ts` |
23
25
  | runtime roles + `ROLE` resolution | `roles.ts` |
24
26
  | `Clock` — the only source of "now" | `clock.ts` |
25
27
  | UUIDv7, nanoid, branded ids | `ids.ts` |
@@ -320,6 +322,15 @@ nothing and still reports healthy.
320
322
  once has to keep it: a discarded one is a hook per `start()`, each retaining the resource it
321
323
  was going to drain, and the next drain runs every one of them against a torn-down copy.
322
324
  `shutdownHookCount()` is the test-only probe that makes the leak assertable.
325
+ - `registerReadinessCheck(name, check)` is what makes `/readyz` mean **usable** rather than
326
+ **bound**. `ReadinessCheck` is `() => boolean` and must stay synchronous — a probe that awaits its
327
+ dependency turns a slow dependency into a wedged endpoint and then a restart loop; keep a boolean
328
+ fresh and let the check read it. It returns an unregister. `HealthReport.checks` is a map of name
329
+ → `'ok' | 'failing'`, so "alert on check failures by check name" is writable.
330
+ - **`HealthReport.registered` is the third state.** `checks: {}` reads identically for "every check
331
+ passed" and "nobody registered one", and an **empty registry is still ready** — reported, never
332
+ enforced, so a role with no dependency does not have to invent a check to boot. Read `registered`
333
+ before trusting an empty `checks`.
323
334
  - Anything that opens a socket calls `markListening(server.url.origin)` and releases it on close.
324
335
  That is what tells the sealed test network a loopback request is this process, not egress.
325
336
 
package/package.json CHANGED
@@ -1,9 +1,15 @@
1
1
  {
2
2
  "name": "@ultimat3/core",
3
- "version": "6.0.0",
3
+ "version": "8.0.0",
4
4
  "description": "Ultimate's foundation: errors, context, env, config, clock, ids, logging, telemetry, lifecycle",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
+ "sideEffects": [
8
+ "./src/context.ts",
9
+ "./src/lifecycle-errors.ts",
10
+ "./src/schema-error-codes.ts",
11
+ "./src/secrets-errors.ts"
12
+ ],
7
13
  "repository": {
8
14
  "type": "git",
9
15
  "url": "git+https://github.com/developerz-ai/ultimate.git",
@@ -16,16 +16,18 @@ export interface AsyncContext<T> {
16
16
  }
17
17
 
18
18
  /**
19
- * The storage is constructed on first `run()`, never at module scope. That is the whole point of
20
- * this file: a browser bundler stubs `node:async_hooks` to `{}` — Bun's `target: 'browser'` emits
21
- * `var { AsyncLocalStorage } = (() => ({}))` — so a module-scope `new` threw
19
+ * The storage is constructed on the first `get()` or `run()`, never at module scope. That is the
20
+ * whole point of this file: a browser bundler stubs `node:async_hooks` to `{}` — Bun's
21
+ * `target: 'browser'` emits `var { AsyncLocalStorage } = (() => ({}))` — so a module-scope `new` threw
22
22
  * `TypeError: undefined is not a constructor` at module EVALUATION, and every package that
23
23
  * transitively imports core was dead on arrival in a client bundle. `@ultimat3/ui` calls itself a
24
24
  * SolidJS design system and could not be put on a client by the only client bundler the framework
25
25
  * has, for this reason and no other.
26
26
  *
27
- * The server pays nothing: `getStore()` before any `run()` answers `undefined` whether the storage
28
- * was ever constructed or not, so deferring the construction changes no observable behaviour.
27
+ * The laziness buys the browser bundle, not a server allocation. `open()` runs on a READ as well as
28
+ * a write, so a server whose first call is `get()` constructs the storage there it is deferred,
29
+ * never skipped. What deferring changes is nothing observable: `getStore()` outside a scope answers
30
+ * `undefined` whether the storage was ever constructed or not, which is what makes it safe.
29
31
  *
30
32
  * **Reads degrade, writes throw**, and that split is the doctrine rather than a convenience.
31
33
  * `get()` answers `undefined` in a browser because that is TRUE — nothing is in flight there, so
package/src/config.ts CHANGED
@@ -4,10 +4,13 @@
4
4
 
5
5
  import { ConfigInvalidError } from './errors';
6
6
  import { ROLES, type Role } from './roles';
7
+ // `app.config.ts` CONSUMES the route vocabulary; it does not own it. Declaring `OfflineStrategy`
8
+ // here is what made it copyable — `render`, `manifest` and `pwa` each wrote their own rather than
9
+ // import a name that reads like a config key.
10
+ import type { OfflineStrategy } from './route-vocabulary';
7
11
  import { isIanaZoneName } from './time-zone-name';
8
12
 
9
13
  export type ThemeMode = 'light' | 'dark' | 'system';
10
- export type OfflineStrategy = 'precache' | 'runtime' | 'network-only';
11
14
  export type CacheTier = 'memo' | 'lru' | 'shared' | 'isr' | 'cdn';
12
15
  export type RealtimeTier = 'channels' | 'live-queries' | 'local-first';
13
16
  export type RealtimeTransport = 'memory' | 'nats' | 'redis';
@@ -19,38 +22,30 @@ export interface ThemeConfig {
19
22
  }
20
23
 
21
24
  /**
22
- * Where a browser that failed `auth: 'required'` is sent, and where it lands afterwards.
25
+ * Where a browser that failed `auth: 'required'` is sent.
23
26
  *
24
27
  * `signInPath: null` is the default and the redirect stays off until an app names its page: the
25
28
  * framework may not invent one of its app's routes, and an app that spells it `/login` would send
26
29
  * every unauthenticated visitor to a 404. Null means the visitor gets the problem document — the
27
30
  * right answer for an agent, and what a browser got in production until this existed.
31
+ *
32
+ * `afterSignInPath` was removed 2026-08 for the reason `urlEnv`, `poolSize` and `schema` were
33
+ * (below): accepted, defaulted and merged here, and read by NO file — `dummy/social-media-clone`
34
+ * set `/dashboard` and got whatever its sign-in route did on its own. The landing path belongs to
35
+ * the app's sign-in route, which is the only code that can honour it.
28
36
  */
29
37
  export interface AuthConfig {
30
38
  readonly signInPath: string | null;
31
- /**
32
- * Where sign-in lands when there is nowhere to return to, or `?next=` is not same-origin.
33
- *
34
- * **Consulted by nothing, `As of 2026-08.`** Accepted, defaulted and merged here and read by no
35
- * file in the repo — `dummy/social-media-clone/app.config.ts` sets `/dashboard` and gets
36
- * whatever the sign-in route does on its own. Same shape `urlEnv`, `poolSize` and `schema` were
37
- * deleted for below; this one is not deleted yet only because its writer is a tracked app's
38
- * config, so removing the key and the line that sets it is one commit across two file sets.
39
- */
40
- readonly afterSignInPath: string;
41
39
  }
42
40
 
41
+ /**
42
+ * `installPrompt` was removed 2026-08, same rule: `@ultimat3/pwa`'s `createInstallController` is
43
+ * real and complete, nothing ever threaded the flag into it, and both tracked apps plus every
44
+ * scaffolded app set a switch with no wire. Call the controller from your own affordance instead.
45
+ */
43
46
  export interface PwaConfig {
44
47
  readonly enabled: boolean;
45
48
  readonly offline: OfflineStrategy;
46
- /**
47
- * **Consulted by nothing, `As of 2026-08.`** `wiki/Configuration.md` describes it as "render
48
- * your own install affordance from the deferred event", both tracked apps set it, and
49
- * `x new`'s scaffold writes it into every generated app — and no file reads it.
50
- * `@ultimat3/pwa`'s `install.ts` is real and complete; nothing threads this flag into it.
51
- * Delete the key or thread it; leaving it is a switch with no wire.
52
- */
53
- readonly installPrompt: boolean;
54
49
  readonly backgroundSync: boolean;
55
50
  readonly push: boolean;
56
51
  }
@@ -120,18 +115,16 @@ export interface McpConfig {
120
115
  readonly path: string;
121
116
  }
122
117
 
118
+ /**
119
+ * No `modelEnv`. It named the env KEY holding the model id, "so no model string is baked into the
120
+ * image" — and its only reader was this file's own merge, copying input to output. Nothing
121
+ * consumed the merged value, so `modelEnv: 'ANTHROPIC_MODEL'` selected no model: `@ultimat3/ai`
122
+ * reads env for API KEYS only, and the model is `request.model ?? DEFAULT_MODEL`, a compile-time
123
+ * constant in `models.ts`. The exact thing the key existed to prevent is what it delivered.
124
+ * Deleted 2026-08 — pass `model` on the request, or read your own env key and pass it.
125
+ */
123
126
  export interface AiConfig {
124
127
  readonly mcp: McpConfig;
125
- /**
126
- * Env key for the model id, so no model string is baked into the image — **an intention, not a
127
- * behaviour, `As of 2026-08`.** The only read of it in the repo is the merge two hundred lines
128
- * below, which copies it from input to output; nothing consumes the merged value, so
129
- * `examples/dummy`'s `modelEnv: 'ANTHROPIC_MODEL'` selects no model. `@ultimat3/ai` reads env
130
- * for API KEYS only (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`); the model is
131
- * `request.model ?? DEFAULT_MODEL`, a compile-time constant in `models.ts`. So the exact thing
132
- * this key exists to prevent — a model string baked into the image — is what actually happens.
133
- */
134
- readonly modelEnv: string | undefined;
135
128
  }
136
129
 
137
130
  export interface AppConfig {
@@ -153,7 +146,8 @@ export interface AppConfig {
153
146
 
154
147
  type Input<T> = { readonly [K in keyof T]?: T[K] | undefined };
155
148
 
156
- export interface AiConfigInput extends Input<Omit<AiConfig, 'mcp'>> {
149
+ /** `mcp` is the only member, and it is NESTED — `Input<AiConfig>` would make it all-or-nothing. */
150
+ export interface AiConfigInput {
157
151
  readonly mcp?: Input<McpConfig> | undefined;
158
152
  }
159
153
 
@@ -221,14 +215,8 @@ function defaults(name: string): Omit<AppConfig, 'name'> {
221
215
  defaultTimeZone: 'UTC',
222
216
  defaultCurrency: 'USD',
223
217
  theme: { defaultMode: 'system', tokens: {} },
224
- auth: { signInPath: null, afterSignInPath: '/' },
225
- pwa: {
226
- enabled: false,
227
- offline: 'network-only',
228
- installPrompt: false,
229
- backgroundSync: false,
230
- push: false,
231
- },
218
+ auth: { signInPath: null },
219
+ pwa: { enabled: false, offline: 'network-only', backgroundSync: false, push: false },
232
220
  roles: [...ROLES],
233
221
  database: { driver: 'postgres', ssl: false },
234
222
  cache: { driver: 'memory', urlEnv: undefined, defaultTtlMs: 60_000, tiers: ['memo', 'lru'] },
@@ -240,7 +228,7 @@ function defaults(name: string): Omit<AppConfig, 'name'> {
240
228
  visibilityTimeoutMs: 30_000,
241
229
  },
242
230
  realtime: { enabled: false, tier: 'channels', transport: 'memory', urlEnv: undefined },
243
- ai: { mcp: { expose: true, path: '/mcp' }, modelEnv: undefined },
231
+ ai: { mcp: { expose: true, path: '/mcp' } },
244
232
  };
245
233
  }
246
234
 
@@ -334,10 +322,7 @@ export function defineConfig(
334
322
  cache: section(base.cache, merged.cache),
335
323
  jobs: section(base.jobs, merged.jobs),
336
324
  realtime: section(base.realtime, merged.realtime),
337
- ai: {
338
- mcp: section(base.ai.mcp, merged.ai?.mcp),
339
- modelEnv: merged.ai?.modelEnv ?? base.ai.modelEnv,
340
- },
325
+ ai: { mcp: section(base.ai.mcp, merged.ai?.mcp) },
341
326
  };
342
327
 
343
328
  validate(config);
package/src/context.ts CHANGED
@@ -70,7 +70,13 @@ export interface CtxInit {
70
70
  readonly services?: ServiceBag | undefined;
71
71
  }
72
72
 
73
- export type CtxPatch = Omit<CtxInit, 'requestId'>;
73
+ /**
74
+ * Neither id a child may change. `requestId` because one request is one request however many
75
+ * scopes it opens; `buildId` because a child context is the same DEPLOY — `withChildContext`
76
+ * has always forwarded the parent's, so accepting the key was an option that read as honoured and
77
+ * was dropped in silence. Pinned in `type-pins.ts`.
78
+ */
79
+ export type CtxPatch = Omit<CtxInit, 'requestId' | 'buildId'>;
74
80
 
75
81
  /**
76
82
  * `async-context.ts` owns why this is a lazily-opened seam rather than a module-scope
@@ -48,12 +48,16 @@ function decimalOf(value: unknown): Decimal | undefined {
48
48
  * a 38-digit `numeric` orders by its digits rather than by whatever a `Number` rounded it to.
49
49
  */
50
50
  function compare(left: Decimal, right: Decimal): number {
51
- if (left.negative !== right.negative) return left.negative ? -1 : 1;
52
51
  const width = Math.max(left.fraction.length, right.fraction.length);
53
52
  const scaled = (value: Decimal): bigint =>
54
53
  BigInt(`${value.whole}${value.fraction.padEnd(width, '0')}`);
55
54
  const first = scaled(left);
56
55
  const second = scaled(right);
56
+ // Magnitude BEFORE sign, because a `numeric` has exactly one zero: `select '-0'::numeric =
57
+ // '0'::numeric` is true, and so is `'-0.00' = '0'`. Comparing the sign first answered `-1` for
58
+ // that pair and cut a keyset page boundary between two rows the database calls equal.
59
+ if (first === 0n && second === 0n) return 0;
60
+ if (left.negative !== right.negative) return left.negative ? -1 : 1;
57
61
  // Never a subtraction: the difference between two `bigint`s is exact and the return type is a
58
62
  // `number`, which cannot hold it.
59
63
  const order = first < second ? -1 : first > second ? 1 : 0;
@@ -53,6 +53,9 @@ const CORE_CODE_TITLES = {
53
53
  X_NO_CONTEXT: 'no request context is active',
54
54
  X_NOT_IMPLEMENTED: 'this driver does not implement the requested feature',
55
55
  X_OTLP_ENDPOINT_INVALID: 'the OTLP collector endpoint is missing or malformed',
56
+ // Its own code rather than the endpoint's, because a title is what an agent reads first:
57
+ // `x errors explain X_OTLP_ENDPOINT_INVALID` would send it to inspect a variable that is fine.
58
+ X_OTLP_HEADERS_INVALID: 'OTEL_EXPORTER_OTLP_HEADERS is malformed',
56
59
  X_OTLP_PROTOCOL_UNSUPPORTED: 'the OTLP protocol requested is not OTLP/HTTP JSON',
57
60
  X_READINESS_CHECK_DUPLICATE: 'a readiness check name is registered twice',
58
61
  X_REGISTRAR_CONFLICT: 'two different registrars are loaded for one primitive kind',
@@ -252,17 +252,17 @@ export function renderMetaRecord(
252
252
  * There is no dev-only escape hatch on purpose — a flag is one misconfigured environment away
253
253
  * from being the same breach.
254
254
  *
255
- * A deliberate, character-for-character duplicate of `describeValue` in
256
- * `packages/schema/src/describe-value.ts`, for the reason `SCHEMA_ERROR_CODE_TITLES` is one:
257
- * `@ultimat3/schema` is tier 0 alongside this package, so neither may import the other. Keep the
258
- * two identical; changing one alone is the bug.
255
+ * A deliberate duplicate of `describeValue` in `packages/schema/src/describe-value.ts`, for the
256
+ * reason `SCHEMA_ERROR_CODE_TITLES` is one: `@ultimat3/schema` is tier 0 alongside this package,
257
+ * so neither may import the other. Keep the two answering IDENTICALLY — that is what
258
+ * `packages/cli/src/describe-value-pin.test.ts` holds — and changing one alone is the bug.
259
259
  */
260
260
  export function describeValue(value: unknown): string {
261
261
  if (value === undefined) return 'undefined';
262
262
  if (value === null) return 'null';
263
263
  switch (typeof value) {
264
264
  case 'string':
265
- return countOf(value.length, 'string', 'character');
265
+ return countOf(charCount(value), 'string', 'character');
266
266
  case 'number':
267
267
  return describeNumber(value);
268
268
  case 'boolean':
@@ -294,3 +294,15 @@ function countOf(size: number, noun: string, unit: string): string {
294
294
  const article = noun === 'array' ? 'an' : 'a';
295
295
  return `${article} ${noun} of ${size} ${unit}${size === 1 ? '' : 's'}`;
296
296
  }
297
+
298
+ /**
299
+ * The twin of `@ultimat3/schema`'s `char-count.ts`, duplicated for the same reason `describeValue`
300
+ * is: both packages are tier 0 and neither may import the other. Code points, because the rules
301
+ * that reject a string count in them and the message must quote the same unit — `'👍'.length` is 2.
302
+ * Only a surrogate makes the two counts differ, so every ASCII value keeps the O(1) read.
303
+ */
304
+ const HAS_SURROGATE = /[\uD800-\uDBFF]/;
305
+
306
+ function charCount(value: string): number {
307
+ return HAS_SURROGATE.test(value) ? [...value].length : value.length;
308
+ }
@@ -64,7 +64,7 @@ export function parseSentryDsn(dsn: string): SentryDsn {
64
64
  }
65
65
 
66
66
  /** The protocol's own level names. `warning`/`error`/`fatal` happen to be the same three words. */
67
- const LEVELS: Readonly<Record<ErrorSeverity, string>> = Object.freeze({
67
+ const LEVELS = Object.freeze<Record<ErrorSeverity, string>>({
68
68
  warning: 'warning',
69
69
  error: 'error',
70
70
  fatal: 'fatal',
@@ -42,6 +42,7 @@ export {
42
42
  REDACTED,
43
43
  redactKeys,
44
44
  setLoggerContextFields,
45
+ setLogStream,
45
46
  } from '../logger';
46
47
  export type {
47
48
  Counter,
@@ -88,6 +89,7 @@ export {
88
89
  OTLP_PROTOCOL_KEY,
89
90
  OTLP_SCOPE,
90
91
  OtlpEndpointInvalidError,
92
+ OtlpHeadersInvalidError,
91
93
  OtlpProtocolUnsupportedError,
92
94
  otlpAttributes,
93
95
  otlpEndpoint,
@@ -0,0 +1,47 @@
1
+ // Single responsibility: a byte count as the short, machine-ish string an error message carries.
2
+ //
3
+ // Tier 0 because two tier-4 packages need exactly this and neither may import the other:
4
+ // `@ultimat3/render`'s `X_BUDGET_EXCEEDED` cause and `@ultimat3/pwa`'s precache warning. Each kept
5
+ // its own copy and they had diverged — render's stopped at `kb`, so one 5 MiB route read `5120kb`
6
+ // in the budget error and `5mb` in the warning about the same bytes.
7
+
8
+ /** 1024-based, because every producer here counts bundle bytes, which tooling reports in KiB. */
9
+ const STEP = 1024;
10
+
11
+ /**
12
+ * Ascending, so the index into it IS the power of `STEP`. `gb` is the last rung on purpose: a
13
+ * precache or a route bundle past a terabyte is a bug in the caller, not a unit this should grow.
14
+ */
15
+ const UNITS = ['b', 'kb', 'mb', 'gb'] as const;
16
+
17
+ const round1 = (value: number): number => Math.round(value * 10) / 10;
18
+
19
+ /**
20
+ * A size a message can state — `1023b`, `4.5kb`, `5mb`, `1.2gb`.
21
+ *
22
+ * Not `@ultimat3/ui`'s `formatBytes(bytes, locale)`, which is `Intl`-formatted, DECIMAL (kB = 1000
23
+ * B, because that is what `Intl`'s unit means) and for a human reading a file picker. This one is
24
+ * for an error's `cause:`, where the number has to line up with a bundler's own KiB figures and
25
+ * must not change with the reader's locale.
26
+ *
27
+ * A negative or non-finite input answers `0b` rather than `-5b` or `NaNb`: axiom 4 says an error is
28
+ * an instruction, and `NaNb` instructs nobody. A size is never negative, so the input was already
29
+ * wrong by the time it arrived.
30
+ */
31
+ export const formatBytes = (bytes: number): string => {
32
+ if (!Number.isFinite(bytes) || bytes <= 0) return `0${UNITS[0]}`;
33
+ let value = bytes;
34
+ let index = 0;
35
+ while (index < UNITS.length - 1 && value >= STEP) {
36
+ value /= STEP;
37
+ index += 1;
38
+ }
39
+ // One more rung when ROUNDING crosses the boundary the raw value did not: 1048575 is under a
40
+ // mebibyte, but one decimal place renders it `1024kb` — a number that disagrees with its own
41
+ // unit, the same class of bug as render's missing `mb` branch.
42
+ if (index < UNITS.length - 1 && round1(value) >= STEP) {
43
+ value /= STEP;
44
+ index += 1;
45
+ }
46
+ return `${round1(value)}${UNITS[index] ?? 'b'}`;
47
+ };
package/src/index.ts CHANGED
@@ -31,6 +31,7 @@ export {
31
31
  } from './actor';
32
32
  export { APP_VERSION_KEY, appVersion, DEFAULT_APP_VERSION } from './app-version';
33
33
  export { assert, assertNever, type InvariantOptions, invariant } from './assert';
34
+ export { type AsyncContext, asyncContext } from './async-context';
34
35
  export { canonicalJson, fingerprint } from './canonical-json';
35
36
  export { type Clock, type FrozenClock, frozenClock, systemClock } from './clock';
36
37
  export type {
@@ -45,7 +46,6 @@ export type {
45
46
  DatabaseConfig,
46
47
  JobsConfig,
47
48
  McpConfig,
48
- OfflineStrategy,
49
49
  PwaConfig,
50
50
  RealtimeConfig,
51
51
  RealtimeTier,
@@ -268,6 +268,7 @@ export {
268
268
  OTLP_PROTOCOL_KEY,
269
269
  OTLP_SCOPE,
270
270
  OtlpEndpointInvalidError,
271
+ OtlpHeadersInvalidError,
271
272
  OtlpProtocolUnsupportedError,
272
273
  OVERFLOW_ATTRIBUTE,
273
274
  otlpAttributes,
@@ -303,6 +304,7 @@ export {
303
304
  sentryErrorReporter,
304
305
  serviceResource,
305
306
  setLoggerContextFields,
307
+ setLogStream,
306
308
  startMetricExport,
307
309
  startSpan,
308
310
  traceparent,
@@ -369,6 +371,7 @@ export {
369
371
  writeMasterKeyFile,
370
372
  writeSecretsFile,
371
373
  } from './exports/secrets';
374
+ export { formatBytes } from './format-bytes';
372
375
  export type { Brand, Id } from './ids';
373
376
  export {
374
377
  isSpanId,
@@ -459,17 +462,19 @@ export {
459
462
  SHUTDOWN_PHASES,
460
463
  shutdownHookCount,
461
464
  } from './lifecycle';
462
- export {
463
- isSelfOrigin,
464
- listeningOrigins,
465
- markListening,
466
- resetListeners,
467
- } from './listeners';
465
+ export { isSelfOrigin, listeningOrigins, markListening, resetListeners } from './listeners';
468
466
  export { isMcpExposed, type McpExposureDeclaration } from './mcp-exposure';
467
+ export { nearestName } from './nearest-name';
469
468
  export { type CappedBody, readWithinLimit } from './read-capped';
470
- export type { ModuleRegistrar, PrimitiveKind, RegisteredPrimitive } from './registrar';
469
+ export type {
470
+ ModuleRegistrar,
471
+ PrimitiveFactory,
472
+ PrimitiveKind,
473
+ RegisteredPrimitive,
474
+ } from './registrar';
471
475
  export {
472
476
  hasPrimitiveRegistrar,
477
+ PRIMITIVE_FACTORIES,
473
478
  PRIMITIVE_KINDS,
474
479
  primitiveRegistrar,
475
480
  registerPrimitiveRegistrar,
@@ -479,6 +484,8 @@ export type { Err, Ok, Result } from './result';
479
484
  export { err, isErr, isOk, map, mapErr, ok, tryCatch, unwrap, unwrapOr } from './result';
480
485
  export type { ResolveRoleOptions, Role, RoleInfo, ScalingSignal } from './roles';
481
486
  export { DEFAULT_ROLE, isRole, ROLE_INFO, ROLES, resolveRole } from './roles';
487
+ export type { HydrateStrategy, OfflineStrategy, RenderMode } from './route-vocabulary';
488
+ export { HYDRATE_STRATEGIES, OFFLINE_STRATEGIES, RENDER_MODES } from './route-vocabulary';
482
489
  export { safeUrl, URL_ATTRIBUTES } from './safe-url';
483
490
  export { defineService, resetServices, type ServiceFactory } from './service';
484
491
  export { timingSafeEqual } from './timing-safe-equal';
package/src/lifecycle.ts CHANGED
@@ -82,6 +82,14 @@ export interface HealthReport {
82
82
  readonly buildId: string;
83
83
  /** Named, because "alert on check failures BY CHECK NAME" is not writable against a boolean. */
84
84
  readonly checks: Readonly<Record<string, ReadinessStatus>>;
85
+ /**
86
+ * How many checks are registered. `checks: {}` reads identically for "every check passed" and
87
+ * "nobody registered one", and only the second is a `/readyz` that means no more than "the
88
+ * socket is bound" — which is what the chart's and compose's healthchecks route traffic on.
89
+ * Reported rather than enforced: an empty registry is still ready, so a role that genuinely has
90
+ * no dependency does not have to invent a check to boot.
91
+ */
92
+ readonly registered: number;
85
93
  }
86
94
 
87
95
  export interface HealthPayload {
@@ -192,18 +200,25 @@ function report(level: 'info' | 'warn' | 'error', message: string, fields: LogFi
192
200
  }
193
201
  }
194
202
 
195
- /** Every check, run now, by name. A check that throws is `failing` — never an unhandled error. */
203
+ /**
204
+ * Every check, run now, by name. A check that throws is `failing` — never an unhandled error.
205
+ *
206
+ * Built through `Object.fromEntries`, never by assigning `results[name]`: assignment to the one
207
+ * name `__proto__` sets the PROTOTYPE instead of adding a key, so that check vanished from the
208
+ * report, `ready` was computed over an empty object — vacuously true — and a failing check
209
+ * answered 200. `fromEntries` defines own properties and has no such name.
210
+ */
196
211
  export function readinessChecks(): Readonly<Record<string, ReadinessStatus>> {
197
- const results: Record<string, ReadinessStatus> = {};
212
+ const results: [string, ReadinessStatus][] = [];
198
213
  for (const [name, check] of readiness) {
199
214
  try {
200
- results[name] = check() ? 'ok' : 'failing';
215
+ results.push([name, check() ? 'ok' : 'failing']);
201
216
  } catch (thrown) {
202
- results[name] = 'failing';
217
+ results.push([name, 'failing']);
203
218
  report('warn', 'readiness check threw', { check: name, error: thrown });
204
219
  }
205
220
  }
206
- return results;
221
+ return Object.fromEntries(results);
207
222
  }
208
223
 
209
224
  export function inflightCount(): number {
@@ -328,43 +343,62 @@ async function runPhase(phase: ShutdownPhase, reason: ShutdownReason): Promise<v
328
343
  }
329
344
  }
330
345
 
331
- /** Idempotent: concurrent signals join the same drain. */
346
+ /** The three phases, in order, under one budget. Never rejects — `drain()` depends on that. */
347
+ async function runDrain(signal: string, reason: ShutdownReason): Promise<void> {
348
+ try {
349
+ report('info', 'draining', { signal, deadlineMs, inflight });
350
+ await runPhase('accept', reason);
351
+
352
+ // Real monotonic, like `deadlineAt` itself: `waitForIdle` sleeps on a real `setTimeout`, and
353
+ // a budget read off an injected clock is a number that timer will never honour.
354
+ const remaining = Math.max(0, reason.deadlineAt - systemClock.monotonic());
355
+ const idle = await waitForIdle(remaining);
356
+ if (!idle) {
357
+ report('warn', 'X_SHUTDOWN_TIMEOUT', {
358
+ code: 'X_SHUTDOWN_TIMEOUT',
359
+ cause: `${inflight} in-flight operations still running after ${deadlineMs}ms`,
360
+ fix: 'raise the budget past the slowest handler — configureLifecycle({ deadlineMs: 600_000 }) for a 10-minute one — and set terminationGracePeriodSeconds to at least as many seconds, or shorten the handler',
361
+ });
362
+ }
363
+
364
+ await runPhase('inflight', reason);
365
+ await runPhase('close', reason);
366
+ } catch (thrown) {
367
+ // Nothing above should reach here — every hook is caught by `settleWithin` and every line
368
+ // goes through `report`. If something does, the drain still ENDS: a rejected `drainPromise`
369
+ // is a memo that re-rejects for every later caller and an unhandled rejection that kills the
370
+ // process mid-drain, which is strictly worse than a drain that finished badly and said so.
371
+ report('error', 'drain failed', { signal, error: thrown });
372
+ } finally {
373
+ state = 'stopped';
374
+ }
375
+ report('info', 'stopped', { signal });
376
+ }
377
+
378
+ /**
379
+ * Idempotent: concurrent signals join the same drain, and so does a RE-ENTRANT one.
380
+ *
381
+ * The memo is published before `runDrain` is called, and that ordering is the whole of this
382
+ * function. A hook may call back in here — `handle.stop()` in `@ultimat3/http` is `drain('manual')`
383
+ * and an `accept` hook is exactly where a server stops listening — and `settleWithin` invokes a
384
+ * hook SYNCHRONOUSLY, so the old `drainPromise = (async () => …)()` had not assigned yet when the
385
+ * first hook ran: the re-entrant call saw `undefined`, started a second whole drain, and recursed
386
+ * ~4,700 deep until the stack ran out, every level swallowed by `settleWithin` as
387
+ * `shutdown hook failed`. Same rule as `packages/jobs/src/worker.ts` — guard and registration in
388
+ * one synchronous step.
389
+ */
332
390
  export function drain(signal = 'manual'): Promise<void> {
333
391
  if (drainPromise !== undefined) return drainPromise;
334
392
  state = 'draining';
335
393
  const reason: ShutdownReason = { signal, deadlineAt: systemClock.monotonic() + deadlineMs };
336
-
337
- drainPromise = (async () => {
338
- try {
339
- report('info', 'draining', { signal, deadlineMs, inflight });
340
- await runPhase('accept', reason);
341
-
342
- // Real monotonic, like `deadlineAt` itself: `waitForIdle` sleeps on a real `setTimeout`, and
343
- // a budget read off an injected clock is a number that timer will never honour.
344
- const remaining = Math.max(0, reason.deadlineAt - systemClock.monotonic());
345
- const idle = await waitForIdle(remaining);
346
- if (!idle) {
347
- report('warn', 'X_SHUTDOWN_TIMEOUT', {
348
- code: 'X_SHUTDOWN_TIMEOUT',
349
- cause: `${inflight} in-flight operations still running after ${deadlineMs}ms`,
350
- fix: 'raise the budget past the slowest handler — configureLifecycle({ deadlineMs: 600_000 }) for a 10-minute one — and set terminationGracePeriodSeconds to at least as many seconds, or shorten the handler',
351
- });
352
- }
353
-
354
- await runPhase('inflight', reason);
355
- await runPhase('close', reason);
356
- } catch (thrown) {
357
- // Nothing above should reach here — every hook is caught by `settleWithin` and every line
358
- // goes through `report`. If something does, the drain still ENDS: a rejected `drainPromise`
359
- // is a memo that re-rejects for every later caller and an unhandled rejection that kills the
360
- // process mid-drain, which is strictly worse than a drain that finished badly and said so.
361
- report('error', 'drain failed', { signal, error: thrown });
362
- } finally {
363
- state = 'stopped';
364
- }
365
- report('info', 'stopped', { signal });
366
- })();
367
-
394
+ let published!: () => void;
395
+ drainPromise = new Promise<void>((resolve) => {
396
+ published = resolve;
397
+ });
398
+ // Both settle paths, for the reason `installSignalHandlers` gives below: `runDrain` cannot
399
+ // reject today — that is its `try/finally`, not luck — and a rejected memo would re-reject for
400
+ // every later caller and end the process the drain was trying to end cleanly.
401
+ void runDrain(signal, reason).then(published, published);
368
402
  return drainPromise;
369
403
  }
370
404
 
@@ -410,6 +444,7 @@ export function healthReport(): HealthReport {
410
444
  inflight,
411
445
  buildId: process.env['BUILD_ID'] ?? 'dev',
412
446
  checks,
447
+ registered: readiness.size,
413
448
  };
414
449
  }
415
450
 
package/src/logger.ts CHANGED
@@ -17,7 +17,7 @@ export const LOG_LEVELS = ['trace', 'debug', 'info', 'warn', 'error', 'fatal', '
17
17
 
18
18
  export type LogLevel = (typeof LOG_LEVELS)[number];
19
19
 
20
- const LEVEL_WEIGHT: Readonly<Record<LogLevel, number>> = Object.freeze({
20
+ const LEVEL_WEIGHT = Object.freeze<Record<LogLevel, number>>({
21
21
  trace: 10,
22
22
  debug: 20,
23
23
  info: 30,
@@ -101,8 +101,28 @@ export function setLoggerContextFields(provider: () => LogFields | undefined): v
101
101
  contextFields = provider;
102
102
  }
103
103
 
104
+ /**
105
+ * Where a line with no explicit writer lands, for everything below `error`. A fact about the
106
+ * PROCESS and not about the line: a container's stdout IS its log stream (12-factor), while a
107
+ * CLI's stdout is the answer it was asked for. `x db migrate --json` printed the boot logger's
108
+ * `ultimate migrate applied` and then the command's own JSON to fd 1, so a caller doing what
109
+ * `--json` exists for — parsing the output — raised on the second object.
110
+ */
111
+ let logStream: 'stdout' | 'stderr' = 'stdout';
112
+
113
+ /**
114
+ * Send everything below `error` to stderr, or back to stdout. The process's own call, made once at
115
+ * entry: a per-line choice would be the second logging path axiom 1 refuses, and a per-logger one
116
+ * already exists as `LoggerOptions.writer` — what had no seam is the module-scope `logger`, which
117
+ * is the one `serve.ts` and every boot path write through.
118
+ */
119
+ export function setLogStream(stream: 'stdout' | 'stderr'): void {
120
+ logStream = stream;
121
+ }
122
+
104
123
  function defaultWriter(line: string, level: LogLevel): void {
105
- const stream = LEVEL_WEIGHT[level] >= LEVEL_WEIGHT.error ? process.stderr : process.stdout;
124
+ const toStderr = logStream === 'stderr' || LEVEL_WEIGHT[level] >= LEVEL_WEIGHT.error;
125
+ const stream = toStderr ? process.stderr : process.stdout;
106
126
  stream.write(`${line}\n`);
107
127
  }
108
128
 
@@ -0,0 +1,45 @@
1
+ // Single responsibility: the declared name a mistyped one most likely meant, so any error can lead
2
+ // with the real one. It lives in core because three packages need the same answer — `@ultimat3/cli`
3
+ // for an unknown command, flag or positional, `@ultimat3/policy` for an unknown permission — and
4
+ // two copies of one cutoff are two suggestions for one typo.
5
+
6
+ /**
7
+ * Levenshtein distance. A grid rather than two rolling rows because `noUncheckedIndexedAccess`
8
+ * makes every read an `?? 0`, and one `at()` reads better than four of them.
9
+ */
10
+ const distance = (a: string, b: string): number => {
11
+ const rows = a.length + 1;
12
+ const cols = b.length + 1;
13
+ const grid: number[] = new Array<number>(rows * cols).fill(0);
14
+ const at = (r: number, c: number): number => grid[r * cols + c] ?? 0;
15
+ for (let r = 0; r < rows; r += 1) grid[r * cols] = r;
16
+ for (let c = 0; c < cols; c += 1) grid[c] = c;
17
+ for (let r = 1; r < rows; r += 1) {
18
+ for (let c = 1; c < cols; c += 1) {
19
+ const cost = a[r - 1] === b[c - 1] ? 0 : 1;
20
+ grid[r * cols + c] = Math.min(at(r - 1, c) + 1, at(r, c - 1) + 1, at(r - 1, c - 1) + cost);
21
+ }
22
+ }
23
+ return at(rows - 1, cols - 1);
24
+ };
25
+
26
+ /** Past this many edits the "suggestion" is a different word, and a wrong lead is worse than none. */
27
+ const MAX_EDITS = 3;
28
+
29
+ /**
30
+ * The nearest candidate within `MAX_EDITS`, or `undefined` when nothing is close enough. Ties keep
31
+ * the FIRST candidate, which is the order the caller declared them in — `definePermissions([...])`
32
+ * and a `CommandSpec` list are both authored orders, and a stable answer is what lets a test pin one.
33
+ */
34
+ export const nearestName = (input: string, candidates: readonly string[]): string | undefined => {
35
+ let best: string | undefined;
36
+ let bestScore = MAX_EDITS + 1;
37
+ for (const candidate of candidates) {
38
+ const score = distance(input, candidate);
39
+ if (score < bestScore) {
40
+ best = candidate;
41
+ bestScore = score;
42
+ }
43
+ }
44
+ return best;
45
+ };
@@ -23,7 +23,7 @@ import type {
23
23
  } from './telemetry';
24
24
 
25
25
  /** OTLP's `SpanKind` enum; `UNSPECIFIED` is 0 and Ultimate never emits it. */
26
- const SPAN_KIND: Readonly<Record<SpanKind, number>> = Object.freeze({
26
+ const SPAN_KIND = Object.freeze<Record<SpanKind, number>>({
27
27
  internal: 1,
28
28
  server: 2,
29
29
  client: 3,
@@ -31,7 +31,7 @@ const SPAN_KIND: Readonly<Record<SpanKind, number>> = Object.freeze({
31
31
  consumer: 5,
32
32
  });
33
33
 
34
- const STATUS_CODE: Readonly<Record<SpanStatusCode, number>> = Object.freeze({
34
+ const STATUS_CODE = Object.freeze<Record<SpanStatusCode, number>>({
35
35
  unset: 0,
36
36
  ok: 1,
37
37
  error: 2,
package/src/otlp.ts CHANGED
@@ -15,6 +15,20 @@ export class OtlpEndpointInvalidError extends UltimateError {
15
15
  }
16
16
  }
17
17
 
18
+ /**
19
+ * Separate from `OtlpEndpointInvalidError` on purpose. The endpoint code's title says the ENDPOINT
20
+ * is missing or malformed, so raising it for a bad header escape sends the first reader of
21
+ * `x errors explain` to inspect `OTEL_EXPORTER_OTLP_ENDPOINT` — a variable that is fine. An
22
+ * accurate `cause:` does not rescue a title that misdirects.
23
+ */
24
+ export class OtlpHeadersInvalidError extends UltimateError {
25
+ static readonly code = 'X_OTLP_HEADERS_INVALID';
26
+ override readonly name = 'OtlpHeadersInvalidError';
27
+ constructor(init: CodedErrorInit) {
28
+ super({ ...init, code: OtlpHeadersInvalidError.code });
29
+ }
30
+ }
31
+
18
32
  export class OtlpProtocolUnsupportedError extends UltimateError {
19
33
  static readonly code = 'X_OTLP_PROTOCOL_UNSUPPORTED';
20
34
  override readonly name = 'OtlpProtocolUnsupportedError';
@@ -112,6 +126,25 @@ export function otlpEndpoint(
112
126
  });
113
127
  }
114
128
 
129
+ /**
130
+ * One header value, percent-decoded. `decodeURIComponent` throws a bare `URIError` on a malformed
131
+ * escape (`%zz`, a lone `%`), and the whole of `otlpHeaders` runs at exporter construction — so a
132
+ * typo in an operator-set variable took the process down with an error carrying no code, no cause
133
+ * and no fix. Refused instead, naming the variable and the header KEY: the value is the
134
+ * collector's credential and a `cause:` is folded into a log line.
135
+ */
136
+ function decodeHeaderValue(key: string, raw: string): string {
137
+ try {
138
+ return decodeURIComponent(raw);
139
+ } catch {
140
+ throw new OtlpHeadersInvalidError({
141
+ cause: `${OTLP_HEADERS_KEY} carries a malformed percent-escape in the "${key}" value, so the header cannot be decoded`,
142
+ fix: `set ${OTLP_HEADERS_KEY}=${key}=<encoded>, where <encoded> is what bun -e 'console.log(encodeURIComponent(process.argv[1]))' <value> prints — or drop the stray % from the "${key}" value if it was meant literally`,
143
+ meta: { header: key },
144
+ });
145
+ }
146
+ }
147
+
115
148
  /** `key=value,key2=value2`, percent-decoded — the spec's format for collector auth headers. */
116
149
  export function otlpHeaders(
117
150
  explicit?: Readonly<Record<string, string>> | undefined,
@@ -125,7 +158,7 @@ export function otlpHeaders(
125
158
  if (index <= 0) continue;
126
159
  const key = pair.slice(0, index).trim().toLowerCase();
127
160
  if (key === '') continue;
128
- headers[key] = decodeURIComponent(pair.slice(index + 1).trim());
161
+ headers[key] = decodeHeaderValue(key, pair.slice(index + 1).trim());
129
162
  }
130
163
  }
131
164
  for (const [key, value] of Object.entries(explicit ?? {})) headers[key.toLowerCase()] = value;
package/src/registrar.ts CHANGED
@@ -27,6 +27,43 @@ export const PRIMITIVE_KINDS = [
27
27
 
28
28
  export type PrimitiveKind = (typeof PRIMITIVE_KINDS)[number];
29
29
 
30
+ /** One factory over one primitive: the export's name, the package that ships it, what it returns. */
31
+ export interface PrimitiveFactory {
32
+ readonly factory: string;
33
+ /** The package specifier the factory is imported from, so a `fix:` can be pasted. */
34
+ readonly pkg: string;
35
+ readonly kind: PrimitiveKind;
36
+ }
37
+
38
+ /**
39
+ * The other half of "never invent a ninth": the factories that already exist, in one table.
40
+ *
41
+ * Prose counted them — "the fourth instance of the framework's factory rule" — in three files that
42
+ * cannot see each other, so every ordinal was wrong the moment a fifth landed and none of them
43
+ * could be checked. A list here can be: `@ultimat3/cli` is tier 5, may import `ai`, `jobs` and
44
+ * `scraping`, and pins that every exported function returning an `Action`/`JobHandle` from outside
45
+ * their owning packages has a row. Adding a factory means adding a row, not editing a sentence.
46
+ *
47
+ * Sorted by package then name so the diff of a new row is one line.
48
+ *
49
+ * Every ROW is frozen, not just the list. `readonly` fields are a compile-time claim and this is a
50
+ * public export: freezing the array alone left `PRIMITIVE_FACTORIES[0].kind = 'entity'` a silent
51
+ * write from any untyped caller, which is the same defect `@ultimat3/money`'s currency rows
52
+ * carried — a table the framework hands out is a constant at RUNTIME or it is not a constant.
53
+ */
54
+ export const PRIMITIVE_FACTORIES = Object.freeze<readonly PrimitiveFactory[]>(
55
+ (
56
+ [
57
+ { factory: 'agent', pkg: '@ultimat3/ai', kind: 'action' },
58
+ { factory: 'agentJob', pkg: '@ultimat3/ai', kind: 'job' },
59
+ { factory: 'hive', pkg: '@ultimat3/ai', kind: 'action' },
60
+ { factory: 'llm', pkg: '@ultimat3/ai', kind: 'action' },
61
+ { factory: 'backfill', pkg: '@ultimat3/jobs', kind: 'job' },
62
+ { factory: 'scrape', pkg: '@ultimat3/scraping', kind: 'job' },
63
+ ] satisfies readonly PrimitiveFactory[]
64
+ ).map((entry) => Object.freeze(entry)),
65
+ );
66
+
30
67
  /**
31
68
  * What a registrar hands back: the primitives it actually took, each carrying the name
32
69
  * registration stamped on it. Returning the registered set — rather than nothing — is what lets
package/src/roles.ts CHANGED
@@ -26,7 +26,7 @@ export interface RoleInfo {
26
26
  readonly stateful: boolean;
27
27
  }
28
28
 
29
- export const ROLE_INFO: Readonly<Record<Role, RoleInfo>> = Object.freeze({
29
+ export const ROLE_INFO = Object.freeze<Record<Role, RoleInfo>>({
30
30
  web: { role: 'web', scalesOn: 'rps', maxReplicas: null, stateful: false },
31
31
  sync: { role: 'sync', scalesOn: 'ws-connections', maxReplicas: null, stateful: false },
32
32
  worker: { role: 'worker', scalesOn: 'queue-depth', maxReplicas: null, stateful: false },
@@ -0,0 +1,23 @@
1
+ // Single responsibility: the three closed vocabularies a route is declared in — how it renders,
2
+ // how it survives offline, when it hydrates. Tier 0 so every package that names one imports it.
3
+ // Deliberately not `config.ts`: `app.config.ts` CONSUMES `OfflineStrategy`, it does not own it.
4
+
5
+ /**
6
+ * Each union is DERIVED from its array rather than written twice, so the pair cannot disagree:
7
+ * the array is the one place a member is added or removed and the type follows.
8
+ *
9
+ * This module exists because the alternative was measured. Twelve declarations of these three sets
10
+ * lived across six packages — `render` alone spelled `RenderMode` and `RENDER_MODES` separately —
11
+ * and `'spa'` was deleted from one of them while five others went on admitting it under a green
12
+ * project-wide typecheck. `@ultimat3/pwa`'s copy mapped `spa` to `cache-first`, the one strategy
13
+ * that gives an `app/` route a SHARED cache entry: one member's authed HTML served to the next.
14
+ * A copy is not a style question. `scripts/render-modes.test.ts` refuses a second declaration.
15
+ */
16
+ export const RENDER_MODES = ['static', 'isr', 'ssr', 'stream'] as const;
17
+ export type RenderMode = (typeof RENDER_MODES)[number];
18
+
19
+ export const OFFLINE_STRATEGIES = ['precache', 'runtime', 'network-only'] as const;
20
+ export type OfflineStrategy = (typeof OFFLINE_STRATEGIES)[number];
21
+
22
+ export const HYDRATE_STRATEGIES = ['idle', 'visible', 'interaction', 'never'] as const;
23
+ export type HydrateStrategy = (typeof HYDRATE_STRATEGIES)[number];
@@ -14,7 +14,7 @@ import type { ScalingSignal } from './roles';
14
14
  * own comment ("via the ingress metric adapter") already assumes. The other two are instantaneous
15
15
  * values a scrape can read directly, so their series names are the chart's words verbatim.
16
16
  */
17
- export const SCALING_METRICS: Readonly<Record<ScalingSignal, string | null>> = Object.freeze({
17
+ export const SCALING_METRICS = Object.freeze<Record<ScalingSignal, string | null>>({
18
18
  rps: 'http_requests_total',
19
19
  'ws-connections': 'connections',
20
20
  'queue-depth': 'queue_depth',
package/src/telemetry.ts CHANGED
@@ -167,7 +167,13 @@ export function currentSpan(): Span | undefined {
167
167
  return activeSpan.get();
168
168
  }
169
169
 
170
- /** The trace the caller is inside: active span, else the request context, else a fresh trace. */
170
+ /**
171
+ * The trace the caller is inside: active span, else the request context, else a fresh trace.
172
+ *
173
+ * The context branch carries an EMPTY `spanId` on purpose, and that emptiness is the discriminator
174
+ * every reader must honour: it is a trace id this process minted, not a span an upstream reported.
175
+ * `traceFlags: 1` here is the header this process would send onward, never a decision it received.
176
+ */
171
177
  export function currentSpanContext(): SpanContext | undefined {
172
178
  const span = activeSpan.get();
173
179
  if (span !== undefined) return span.context;
@@ -176,8 +182,20 @@ export function currentSpanContext(): SpanContext | undefined {
176
182
  return { traceId: ctx.traceId, spanId: '', traceFlags: 1 };
177
183
  }
178
184
 
185
+ /**
186
+ * A parent an upstream actually reported, as opposed to the synthetic one `currentSpanContext()`
187
+ * builds from a request context. Only the first carries a sampling decision: reading the synthetic
188
+ * one as inbound made `parentBasedRatioSampler` inherit a bit nobody sent, so every HTTP root span
189
+ * was exported at every ratio — `pipeline.ts` is `runWithContext` then `withSpan`, which is that
190
+ * exact pair — and the one lever between "tracing is on" and "the collector melts" did nothing.
191
+ */
192
+ function inboundParent(parent: SpanContext | undefined): SpanContext | undefined {
193
+ return parent === undefined || parent.spanId === '' ? undefined : parent;
194
+ }
195
+
179
196
  export function startSpan(name: string, options?: StartSpanOptions): Span {
180
197
  const parent = options?.parent ?? currentSpanContext();
198
+ const inbound = inboundParent(parent);
181
199
  const attributes: Record<string, AttributeValue> = { ...(options?.attributes ?? {}) };
182
200
  // The bit is decided ONCE, here, and every child of this span inherits it through `parent` —
183
201
  // so one trace is sampled or not sampled as a whole. Before this, `traceFlags` was hardcoded to
@@ -186,7 +204,7 @@ export function startSpan(name: string, options?: StartSpanOptions): Span {
186
204
  const context: SpanContext = {
187
205
  traceId: parent?.traceId ?? newTraceId(),
188
206
  spanId: newSpanId(),
189
- traceFlags: currentSampler().shouldSample(name, parent, attributes) ? 1 : 0,
207
+ traceFlags: currentSampler().shouldSample(name, inbound, attributes) ? 1 : 0,
190
208
  };
191
209
  const events: SpanEvent[] = [];
192
210
  const startedAt = clock.now().getTime();
@@ -245,7 +263,7 @@ export function startSpan(name: string, options?: StartSpanOptions): Span {
245
263
  // `traceparent`; it is simply not exported.
246
264
  if ((context.traceFlags & 1) === 0) return;
247
265
  const endedAt = clock.now().getTime();
248
- const parentSpanId = parent === undefined || parent.spanId === '' ? undefined : parent.spanId;
266
+ const parentSpanId = inbound?.spanId;
249
267
  exporter.export({
250
268
  name,
251
269
  kind: options?.kind ?? 'internal',
package/src/type-pins.ts CHANGED
@@ -1,11 +1,13 @@
1
- // Compile-time pins for the actor-facts seam and the config surface. Source, not a `.test.ts`,
2
- // on purpose:
3
- // `tsconfig.json` excludes `src/**/*.test.ts`, so `tsc -b` never reads a test file and a
4
- // type-level assertion written there can never fail. This module emits nothing and exports
5
- // nothing anybody imports — a regression is a build error, the only enforcement that counts.
1
+ // Compile-time pins for the actor-facts seam, the config surface, the request-context patch and
2
+ // the route vocabulary.
3
+ // Source, not a `.test.ts`, on purpose: `tsconfig.json` excludes `src/**/*.test.ts`, so `tsc -b`
4
+ // never reads a test file and a type-level assertion written there can never fail. This module
5
+ // emits nothing and exports nothing anybody imports — a regression is a build error.
6
6
 
7
7
  import type { Actor, ActorFactMap, FactKeysOf, FactMapOf } from './actor';
8
8
  import type { AppConfigInput, DatabaseConfig } from './config';
9
+ import type { CtxPatch } from './context';
10
+ import type { HydrateStrategy, OfflineStrategy, RenderMode } from './route-vocabulary';
9
11
 
10
12
  /** Fails to compile when `T` is anything but `true`. The whole mechanism. */
11
13
  type Assert<T extends true> = T;
@@ -98,3 +100,45 @@ type _DatabaseInputCarriesNoDeadField = Assert<
98
100
  ? true
99
101
  : false
100
102
  >;
103
+
104
+ /**
105
+ * Neither id a child context may patch. `withChildContext` forwards the parent's `buildId`
106
+ * verbatim, so `{ buildId }` on the patch was an option that read as honoured and was dropped
107
+ * without a word — the same silent-no-op class as the three `database` fields above, one tier
108
+ * lower. `requestId` is here beside it because the two are refused for the same reason.
109
+ */
110
+ type UnpatchableCtxKey = 'requestId' | 'buildId';
111
+
112
+ type _CtxPatchRefusesTheIds = Assert<
113
+ Extract<keyof CtxPatch, UnpatchableCtxKey> extends never ? true : false
114
+ >;
115
+
116
+ /** And the keys a child MAY change are still there — a pin that empties the type is not a pin. */
117
+ type _CtxPatchStillPatchesTheRest = Assert<
118
+ 'actor' | 'locale' | 'tz' extends keyof CtxPatch ? true : false
119
+ >;
120
+
121
+ /**
122
+ * Mutual assignability, not one-way. The tuples are load-bearing: a bare `A extends B` distributes
123
+ * over a union and answers `true` for every member separately, so it cannot see a widening — which
124
+ * is the only thing these three pins are looking for.
125
+ */
126
+ type Exact<A, B> = [A] extends [B] ? ([B] extends [A] ? true : false) : false;
127
+
128
+ /**
129
+ * Each route vocabulary's union must stay DERIVED from its array. `(typeof ARRAY)[number]` is what
130
+ * makes the pair unable to disagree, and it is one careless edit from being a hand-written union
131
+ * again — which is the shape six packages shipped until `route-vocabulary.ts` existed. Restating
132
+ * the members here is a pin, not a copy: nothing imports these, and a member added to the array
133
+ * without a word in the changelog is a build error rather than a silent widening five packages
134
+ * inherit through a re-export.
135
+ */
136
+ type _RenderModeIsItsArray = Assert<Exact<RenderMode, 'static' | 'isr' | 'ssr' | 'stream'>>;
137
+
138
+ type _OfflineStrategyIsItsArray = Assert<
139
+ Exact<OfflineStrategy, 'precache' | 'runtime' | 'network-only'>
140
+ >;
141
+
142
+ type _HydrateStrategyIsItsArray = Assert<
143
+ Exact<HydrateStrategy, 'idle' | 'visible' | 'interaction' | 'never'>
144
+ >;