@ultimat3/core 8.0.0 → 10.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
@@ -12,6 +12,7 @@ is a change to every package.
12
12
  | A value a CALLER supplied | `describeValue()` — shape, never content. `renderCauseValue` is safe against throwing, not against leaking |
13
13
  | Reading a caught value | `renderThrowable()` / `isThrownError()` / `stringField()`; never `error.message`, `error instanceof Error` or `typeof error.code === 'string'` directly — the probe throws before the renderer runs |
14
14
  | New code | add to `CORE_CODE_TITLES` in `error-codes.ts`, else the title is auto-humanised |
15
+ | Where an error points | `ERROR_DOCS_URL` — one constant, never a per-code URL. `docs:` is omitted at every construction site and resolved from the registry |
15
16
  | Time | take a `Clock`; `Date.now()` / `new Date()` only inside `clock.ts` |
16
17
  | Context | never thread `ctx` as a parameter — `useContext()` |
17
18
  | A value ambient across an `await` | `asyncContext<T>(subject)` from `async-context.ts`, in **every** package — never `new AsyncLocalStorage` |
@@ -110,6 +111,7 @@ shape against a locally declared sample interface for exactly that reason.
110
111
  | which deploy this is | `environment.ts` (`ULTIMATE_ENV`) | the twin of `ROLE`; never declare a second env var for it |
111
112
  | what this process does | `roles.ts` (`ROLE`) | |
112
113
  | 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 |
114
+ | which rungs a cache ladder has | `cache-vocabulary.ts` (`CACHE_TIERS`) | tier 0 for the same reason, one tier lower down the stack: `app.config.ts` picks tiers by name and `@ultimat3/cache` builds them by name, and until 2026-08-22 those were two different vocabularies — config accepted `memo \| lru \| shared \| isr \| cdn`, the ladder ordered `request-memo \| lru \| redis \| cdn`, nothing mapped one onto the other, and `cache: { tiers: ['isr'] }` typechecked and selected nothing (issue #293). `TIER_ORDER` IS this array, so read order and the config vocabulary cannot disagree. `isr` is a `RenderMode`, never a tier. Pinned in `type-pins.ts` and by `scripts/render-modes.ts` |
113
115
  | 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 |
114
116
  | the values | `env.ts` | `checkEnv().values` holds REAL secrets — anything that prints goes through `maskedEnvValues()` |
115
117
  | `.env.example` | `env-example.ts` | a projection of the schema, never hand-maintained |
@@ -275,10 +277,17 @@ in a different trace, which is worse than no span because it looks authoritative
275
277
  including the three that open no other socket — `queue_depth` belongs to one of them.
276
278
 
277
279
  ```bash
278
- bun test # from packages/core
280
+ bun test packages/core/src # from the REPO ROOT, never from packages/core
279
281
  bun run typecheck
280
282
  ```
281
283
 
284
+ **The root is not a preference.** `bunfig.toml`'s `preload = ["./scripts/test-setup.ts"]` is what
285
+ installs `@ultimat3/testing`'s matchers, and Bun reads `bunfig.toml` from the cwd — so `bun test`
286
+ run inside `packages/core` loads no preload and 17 tests in `secrets.test.ts` die on
287
+ `expect(...).rejects.toBeUltimateError is not a function`, which reads as this package's failure
288
+ and is the shell's. `.github/workflows/ci.yml`'s `package` job spawns `bun test packages/<pkg>`
289
+ with `cwd` at the root for the same reason (`scripts/coverage-gate.ts`).
290
+
282
291
  `markReady()` means **bound**, and readiness means **usable** — two different facts since
283
292
  `registerReadinessCheck(name, check)`. `/readyz` is ready only when the state is `ready` AND every
284
293
  named check passes, and `HealthReport.checks` carries them by name because "alert on check
@@ -347,6 +356,18 @@ the customer's. The non-blank-reason assert is `@ultimat3/entity`'s `crossTenant
347
356
  verbatim — two escapes from the framework's default posture should not look like two things. Do
348
357
  not add a second impersonation path.
349
358
 
359
+ **`ERROR_DOCS_URL` replaced `ERROR_DOCS_BASE` + `errorDocsUrl(code)` `As of 2026-08-23`, and it is
360
+ a breaking change** — it lands in the next major, not in the released line. `https://ultimate.dev/errors/<code>` answered **404**, host included, on every error the
361
+ framework has ever thrown — including the first line a new agent reads (`x --json` →
362
+ `"docs":"https://ultimate.dev/errors/X_CLI_UNKNOWN_COMMAND"`). A dead link in every error is a
363
+ defect under axiom 4, and it is not "not built yet": `wiki/` is the only public documentation
364
+ surface there is. There is no per-code URL because there is no per-code ANCHOR — codes live in
365
+ `wiki/Error-Codes.md` as TABLE ROWS, and a `#X_DB_DRIFT` fragment would be a second dead
366
+ declaration rather than a fix for the first. So the function is gone rather than kept with an
367
+ ignored parameter, and `descriptor()` lost its `code` parameter with it. A package constructing an
368
+ `UltimateError` now OMITS `docs:` entirely and lets the constructor resolve the registered
369
+ descriptor — one URL, one place, instead of the fifteen packages that each spelled the base out.
370
+
350
371
  Every `UltimateError` carries `retry` (`terminal | retryable | retry-after`), **defaulting to
351
372
  `terminal`** — fail closed, because a client retrying on `status >= 500` hammers `X_DB_DRIFT` and
352
373
  `X_TENANCY_UNSCOPED`, which are permanent config faults. `registerErrorRetry()` is the one
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ultimat3/core",
3
- "version": "8.0.0",
3
+ "version": "10.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",
@@ -0,0 +1,25 @@
1
+ // Single responsibility: the cache tiers' NAMES, in read order — the ladder itself is
2
+ // `@ultimat3/cache`'s. Tier 0 because that is the one place a tier-0 config declaration and a
3
+ // tier-1 implementation can both see; not `config.ts`, because `app.config.ts` consumes these
4
+ // names rather than owning them.
5
+
6
+ /**
7
+ * The rungs, near to far. **Order is load-bearing**: `sortTiers` in `@ultimat3/cache` orders a
8
+ * stack by index in this array, so a name's position here IS its distance from the request, and a
9
+ * name missing from it sorts to `-1` — ahead of the request memo.
10
+ *
11
+ * Spelled once because it was spelled twice and the two disagreed (issue #293): `app.config.ts`
12
+ * accepted `memo | lru | shared | isr | cdn` while the stack ordered `request-memo | lru | redis |
13
+ * cdn`, so `cache: { tiers: ['isr'] }` typechecked and selected nothing. `memo`/`request-memo` and
14
+ * `shared`/`redis` were one rung spelled twice; the ladder's spelling wins, because it is the one
15
+ * a `TierInvalidation`, a `TierFailure` and the `/_x` panel already report.
16
+ *
17
+ * **`isr` is not here and is not a cache tier.** It is a `RenderMode` (`route-vocabulary.ts`) — a
18
+ * per-route rendering decision, revalidated by a tag bust — and `@ultimat3/cache` has no ISR store
19
+ * to build: `'isr'` appears nowhere in `packages/cache/src` except one invalidation label. Serving
20
+ * ISR is `render: 'isr'` on the route, never a rung of this ladder.
21
+ */
22
+ export const CACHE_TIERS = ['request-memo', 'lru', 'redis', 'cdn'] as const;
23
+
24
+ /** Derived from the array rather than written twice, so the pair cannot disagree. */
25
+ export type CacheTierName = (typeof CACHE_TIERS)[number];
package/src/config.ts CHANGED
@@ -2,6 +2,10 @@
2
2
  // defaults, validated eagerly, and composable so a big app can split it across `config/*.ts`
3
3
  // without inventing a second config mechanism.
4
4
 
5
+ // Same rule for the same reason: `app.config.ts` CONSUMES the cache tier names, it does not own
6
+ // them. Declaring them here is what let `cache.tiers` and the ladder `@ultimat3/cache` orders by
7
+ // drift into two vocabularies with no map between them (issue #293).
8
+ import { CACHE_TIERS, type CacheTierName } from './cache-vocabulary';
5
9
  import { ConfigInvalidError } from './errors';
6
10
  import { ROLES, type Role } from './roles';
7
11
  // `app.config.ts` CONSUMES the route vocabulary; it does not own it. Declaring `OfflineStrategy`
@@ -11,8 +15,6 @@ import type { OfflineStrategy } from './route-vocabulary';
11
15
  import { isIanaZoneName } from './time-zone-name';
12
16
 
13
17
  export type ThemeMode = 'light' | 'dark' | 'system';
14
- export type CacheTier = 'memo' | 'lru' | 'shared' | 'isr' | 'cdn';
15
- export type RealtimeTier = 'channels' | 'live-queries' | 'local-first';
16
18
  export type RealtimeTransport = 'memory' | 'nats' | 'redis';
17
19
 
18
20
  export interface ThemeConfig {
@@ -70,11 +72,25 @@ export interface DatabaseConfig {
70
72
  readonly ssl: boolean;
71
73
  }
72
74
 
75
+ /**
76
+ * No `CacheTier`. It was a SECOND spelling of the ladder — `memo | lru | shared | isr | cdn`
77
+ * against `@ultimat3/cache`'s `request-memo | lru | redis | cdn` — with nothing mapping one onto
78
+ * the other, so `cache: { tiers: ['isr'] }` typechecked and selected nothing. Deleted 2026-08-22
79
+ * in favour of `CacheTierName`, which is the ladder's own names and the only ones `sortTiers` can
80
+ * place. It was also the second exported type called `CacheTier` in the tree; the other is
81
+ * `@ultimat3/cache`'s tier INTERFACE, which is the one every implementation names.
82
+ *
83
+ * No `driver` and no `urlEnv` either, deleted 2026-08-22 and for the same reason one rung further
84
+ * up: `tiers` is what BUILDS the ladder (`packages/cli/src/dev-cache.ts`), so `driver: 'redis'`
85
+ * beside `tiers: ['request-memo', 'lru']` was a second selector that selected nothing — the shape
86
+ * `examples/dummy/app.config.ts` shipped. `urlEnv` was `database.urlEnv` verbatim: the Redis tier
87
+ * reads the literal `REDIS_URL`, so `urlEnv: 'MY_REDIS'` made nothing read `MY_REDIS`. Which rungs
88
+ * exist is `cache.tiers` and only that; a rung the environment cannot supply refuses the boot.
89
+ */
73
90
  export interface CacheConfig {
74
- readonly driver: 'memory' | 'redis';
75
- readonly urlEnv: string | undefined;
76
91
  readonly defaultTtlMs: number;
77
- readonly tiers: readonly CacheTier[];
92
+ /** Order is fixed by `TIER_ORDER`; listing order here selects rungs, it does not rank them. */
93
+ readonly tiers: readonly CacheTierName[];
78
94
  }
79
95
 
80
96
  export interface JobsConfig {
@@ -102,10 +118,19 @@ export interface JobsConfig {
102
118
  * server config, and the presence beat is DERIVED (`PresenceRegistry.heartbeatMs` is
103
119
  * `max(1000, floor(ttlMs / 3))`). A second knob is a second number that can disagree with the one
104
120
  * it is a fraction of, and a knob nothing reads is a knob nothing enforces — axioms 1 and 3.
121
+ *
122
+ * No `tier` either, and no `RealtimeTier` — deleted 2026-08-23, the thirteenth instance of the
123
+ * same defect and the dangerous shape of it. It accepted
124
+ * `'channels' | 'live-queries' | 'local-first'`, defaulted to `'channels'`, was documented with
125
+ * per-value semantics, was set by both tracked apps — and no file anywhere compared it, branched
126
+ * on it or dereferenced it. `transport` and `urlEnv` are the only two fields of this section any
127
+ * code reads. So `tier: 'local-first'` bought the durable client store that does not exist
128
+ * (`createOpfsLocalStore` still throws `X_NOT_IMPLEMENTED`), exactly as `jobs: { driver: 'redis' }`
129
+ * bought Postgres. Which realtime tier an app is on is decided by what it DECLARES — a `channel()`
130
+ * topic, a `live: true` query, a local store — never by a config key.
105
131
  */
106
132
  export interface RealtimeConfig {
107
133
  readonly enabled: boolean;
108
- readonly tier: RealtimeTier;
109
134
  readonly transport: RealtimeTransport;
110
135
  readonly urlEnv: string | undefined;
111
136
  }
@@ -219,7 +244,7 @@ function defaults(name: string): Omit<AppConfig, 'name'> {
219
244
  pwa: { enabled: false, offline: 'network-only', backgroundSync: false, push: false },
220
245
  roles: [...ROLES],
221
246
  database: { driver: 'postgres', ssl: false },
222
- cache: { driver: 'memory', urlEnv: undefined, defaultTtlMs: 60_000, tiers: ['memo', 'lru'] },
247
+ cache: { defaultTtlMs: 60_000, tiers: ['request-memo', 'lru'] },
223
248
  jobs: {
224
249
  queues: [`${name}-default`],
225
250
  concurrency: 8,
@@ -227,7 +252,7 @@ function defaults(name: string): Omit<AppConfig, 'name'> {
227
252
  backoff: 'exponential',
228
253
  visibilityTimeoutMs: 30_000,
229
254
  },
230
- realtime: { enabled: false, tier: 'channels', transport: 'memory', urlEnv: undefined },
255
+ realtime: { enabled: false, transport: 'memory', urlEnv: undefined },
231
256
  ai: { mcp: { expose: true, path: '/mcp' } },
232
257
  };
233
258
  }
@@ -244,10 +269,21 @@ const BASE_FIX = 'edit app.config.ts to fix the fields named in cause, then run:
244
269
  const TIMEZONE_FIX =
245
270
  "set defaultTimeZone to an Area/Location name, or UTC — list every accepted one with bun -e \"console.log(Intl.supportedValuesOf('timeZone').join('\\n'))\" — where a legacy single-label name swaps mechanically (Japan → Asia/Tokyo, GB → Europe/London, Universal → UTC), while an abbreviation or numeric offset (CET, EST5EDT, +01:00) carries no DST rule and has no replacement, so name the city whose clock you mean (Europe/Paris, America/New_York)";
246
271
 
272
+ /**
273
+ * Appended only when a tier name is what failed, and it names the rename rather than the rule: the
274
+ * three refused spellings are the ones 8.0.0 accepted, and two of them have a mechanical
275
+ * replacement while `isr` has none — it is a `RenderMode`, and no cache tier ever served it.
276
+ */
277
+ const CACHE_TIER_FIX =
278
+ "in app.config.ts, rewrite cache.tiers with the rung names the ladder serves — request-memo, lru, redis, cdn — where memo becomes request-memo and shared becomes redis, and isr is dropped: it is a render mode, so move it to render: 'isr' on the routes that want it";
279
+
247
280
  function validate(config: AppConfig): void {
248
281
  const issues: string[] = [];
249
282
  // Zero or one entry: the zone's own remedy, carried only when the zone is what failed.
250
283
  const zoneFix: string[] = [];
284
+ // Same shape, and it exists for the upgrade: an 8.0.0 app carrying `['memo', 'shared']` in an
285
+ // untyped config file reaches here rather than the compiler, and needs the new spelling.
286
+ const tierFix: string[] = [];
251
287
 
252
288
  if (!NAME_RE.test(config.name)) {
253
289
  issues.push(`name "${config.name}" must match ${String(NAME_RE)}`);
@@ -277,8 +313,13 @@ function validate(config: AppConfig): void {
277
313
  if (config.realtime.transport !== 'memory' && config.realtime.urlEnv === undefined) {
278
314
  issues.push(`realtime.transport "${config.realtime.transport}" requires realtime.urlEnv`);
279
315
  }
280
- if (config.cache.driver === 'redis' && config.cache.urlEnv === undefined) {
281
- issues.push('cache.driver "redis" requires cache.urlEnv');
316
+ // A rung the ladder cannot build is the defect this key had: `sortTiers` places a name by its
317
+ // index in `CACHE_TIERS`, and a name missing from it sorts to `-1` — AHEAD of the request memo.
318
+ // So an unknown tier is refused at boot rather than silently ignored or silently placed first.
319
+ for (const tier of config.cache.tiers) {
320
+ if (CACHE_TIERS.includes(tier)) continue;
321
+ issues.push(`cache.tiers contains "${tier}", which is not one of ${CACHE_TIERS.join(', ')}`);
322
+ if (tierFix.length === 0) tierFix.push(CACHE_TIER_FIX);
282
323
  }
283
324
 
284
325
  if (issues.length > 0) {
@@ -286,7 +327,7 @@ function validate(config: AppConfig): void {
286
327
  cause: issues.join('; '),
287
328
  // The generic instruction goes LAST so the fix line still ends in a command that can be
288
329
  // pasted — a trailing `.` after `x verify` is a command nobody can run.
289
- fix: [...zoneFix, BASE_FIX].join('. '),
330
+ fix: [...zoneFix, ...tierFix, BASE_FIX].join('. '),
290
331
  meta: { issues },
291
332
  });
292
333
  }
package/src/context.ts CHANGED
@@ -200,7 +200,10 @@ export function withChildContext<T>(patch: CtxPatch, fn: () => T): T {
200
200
  /** Resolve a late-bound service. Throws `X_SERVICE_MISSING` rather than returning undefined. */
201
201
  export function useService<T>(name: string): T {
202
202
  const ctx = useContext();
203
- const service = ctx.services[name];
203
+ // Own keys only, and the SAME read the cause below lists. A raw index walks the prototype, so
204
+ // `useService('constructor')` answered with the `Object` function and the caller's first method
205
+ // call was a bare `TypeError` frames away — which is the failure this function exists to name.
206
+ const service = Object.hasOwn(ctx.services, name) ? ctx.services[name] : undefined;
204
207
  if (service === undefined) {
205
208
  throw new UltimateError({
206
209
  code: 'X_SERVICE_MISSING',
@@ -19,11 +19,14 @@ export interface ErrorCodeEntry extends ErrorCodeDescriptor {
19
19
  readonly code: string;
20
20
  }
21
21
 
22
- export const ERROR_DOCS_BASE = 'https://ultimate.dev/errors/';
23
-
24
- export function errorDocsUrl(code: string): string {
25
- return `${ERROR_DOCS_BASE}${code}`;
26
- }
22
+ /**
23
+ * Where an error sends its reader. One URL for every code, and deliberately not one per code:
24
+ * `wiki/` is the framework's only public documentation surface, codes live there in TABLE ROWS,
25
+ * and a table row has no anchor — so a `#X_DB_DRIFT` fragment would land on the page top while
26
+ * declaring a target that does not exist. The `https://ultimate.dev/errors/<code>` links this
27
+ * shipped until 9.x answered 404, host included, on every error the framework has ever thrown.
28
+ */
29
+ export const ERROR_DOCS_URL = 'https://github.com/developerz-ai/ultimate/wiki/Error-Codes';
27
30
 
28
31
  /** Codes owned by `@ultimat3/core`. Every other package calls `registerErrorCodes()`. */
29
32
  const CORE_CODE_TITLES = {
@@ -75,13 +78,13 @@ const CORE_CODE_TITLES = {
75
78
 
76
79
  export type CoreErrorCode = keyof typeof CORE_CODE_TITLES;
77
80
 
78
- function descriptor(code: string, declaration: ErrorCodeDeclaration): ErrorCodeDescriptor {
79
- return Object.freeze({ title: declaration.title, docs: declaration.docs ?? errorDocsUrl(code) });
81
+ function descriptor(declaration: ErrorCodeDeclaration): ErrorCodeDescriptor {
82
+ return Object.freeze({ title: declaration.title, docs: declaration.docs ?? ERROR_DOCS_URL });
80
83
  }
81
84
 
82
85
  export const CORE_ERROR_CODES: Readonly<Record<CoreErrorCode, ErrorCodeDescriptor>> = Object.freeze(
83
86
  Object.fromEntries(
84
- Object.entries(CORE_CODE_TITLES).map(([code, title]) => [code, descriptor(code, { title })]),
87
+ Object.entries(CORE_CODE_TITLES).map(([code, title]) => [code, descriptor({ title })]),
85
88
  ) as Record<CoreErrorCode, ErrorCodeDescriptor>,
86
89
  );
87
90
 
@@ -105,7 +108,7 @@ export function registerErrorCodes(codes: Readonly<Record<string, ErrorCodeDecla
105
108
  });
106
109
  }
107
110
  for (const [code, declaration] of Object.entries(codes)) {
108
- registry.set(code, descriptor(code, declaration));
111
+ registry.set(code, descriptor(declaration));
109
112
  }
110
113
  }
111
114
 
@@ -117,7 +120,7 @@ function humanize(code: string): string {
117
120
  export function describeErrorCode(code: string): ErrorCodeDescriptor {
118
121
  const known = registry.get(code);
119
122
  if (known !== undefined) return known;
120
- return descriptor(code, { title: humanize(code) });
123
+ return descriptor({ title: humanize(code) });
121
124
  }
122
125
 
123
126
  export function hasErrorCode(code: string): boolean {
@@ -12,9 +12,8 @@ export type {
12
12
  export {
13
13
  CORE_ERROR_CODES,
14
14
  describeErrorCode,
15
- ERROR_DOCS_BASE,
15
+ ERROR_DOCS_URL,
16
16
  errorCodeSnapshot,
17
- errorDocsUrl,
18
17
  hasErrorCode,
19
18
  listErrorCodes,
20
19
  registerErrorCodes,
package/src/index.ts CHANGED
@@ -32,6 +32,7 @@ export {
32
32
  export { APP_VERSION_KEY, appVersion, DEFAULT_APP_VERSION } from './app-version';
33
33
  export { assert, assertNever, type InvariantOptions, invariant } from './assert';
34
34
  export { type AsyncContext, asyncContext } from './async-context';
35
+ export { CACHE_TIERS, type CacheTierName } from './cache-vocabulary';
35
36
  export { canonicalJson, fingerprint } from './canonical-json';
36
37
  export { type Clock, type FrozenClock, frozenClock, systemClock } from './clock';
37
38
  export type {
@@ -42,13 +43,11 @@ export type {
42
43
  AppConfigOverlay,
43
44
  AuthConfig,
44
45
  CacheConfig,
45
- CacheTier,
46
46
  DatabaseConfig,
47
47
  JobsConfig,
48
48
  McpConfig,
49
49
  PwaConfig,
50
50
  RealtimeConfig,
51
- RealtimeTier,
52
51
  RealtimeTransport,
53
52
  ThemeConfig,
54
53
  ThemeMode,
@@ -133,10 +132,9 @@ export {
133
132
  describeErrorCode,
134
133
  describeValue,
135
134
  EnvMissingError,
136
- ERROR_DOCS_BASE,
135
+ ERROR_DOCS_URL,
137
136
  ERROR_RETRY_KINDS,
138
137
  errorCodeSnapshot,
139
- errorDocsUrl,
140
138
  errorRetry,
141
139
  formatError,
142
140
  hasErrorCode,
package/src/metrics.ts CHANGED
@@ -195,6 +195,31 @@ function finite(name: string, value: number): number {
195
195
  return value;
196
196
  }
197
197
 
198
+ /**
199
+ * Bounds are strictly ascending finite numbers, refused at DECLARATION like `maxSeries` beside it.
200
+ * `record` takes the first bound an observation fits, and the exposition format emits one
201
+ * cumulative `le` series per bound in array order — so `[1, 0.5, 5]` both counted observations
202
+ * into a bucket that was not theirs and rendered a non-monotonic `le` series that Prometheus and
203
+ * OpenMetrics each reject. Two wrong numbers, neither visible from the other, and nothing at the
204
+ * call site to notice: the observations themselves were all valid.
205
+ */
206
+ function assertBounds(name: string, bounds: readonly number[] | undefined): void {
207
+ if (bounds === undefined) return;
208
+ const bad = bounds.findIndex((bound, index) => {
209
+ const previous = index === 0 ? Number.NEGATIVE_INFINITY : (bounds[index - 1] as number);
210
+ return !Number.isFinite(bound) || bound <= previous;
211
+ });
212
+ if (bad === -1) return;
213
+ const repaired = [...new Set(bounds.filter((bound) => Number.isFinite(bound)))].sort(
214
+ (left, right) => left - right,
215
+ );
216
+ throw new MetricNameInvalidError({
217
+ cause: `${name} declared bounds [${bounds.map((bound) => String(bound)).join(', ')}], which are not strictly ascending finite numbers — [${String(bad)}] is ${String(bounds[bad])}`,
218
+ fix: `sort the bounds and drop the duplicates: histogram('${name}', { bounds: [${repaired.join(', ')}] })`,
219
+ meta: { metric: name, bounds: bounds.map((bound) => String(bound)), at: bad },
220
+ });
221
+ }
222
+
198
223
  function declare(name: string, kind: MetricKind, options: GaugeOptions & HistogramOptions) {
199
224
  if (!METRIC_NAME_RE.test(name)) {
200
225
  throw new MetricNameInvalidError({
@@ -203,6 +228,7 @@ function declare(name: string, kind: MetricKind, options: GaugeOptions & Histogr
203
228
  meta: { name },
204
229
  });
205
230
  }
231
+ assertBounds(name, options.bounds);
206
232
  const existing = instruments.get(name);
207
233
  if (existing !== undefined) {
208
234
  if (existing.descriptor.kind !== kind) {
package/src/registrar.ts CHANGED
@@ -59,6 +59,7 @@ export const PRIMITIVE_FACTORIES = Object.freeze<readonly PrimitiveFactory[]>(
59
59
  { factory: 'hive', pkg: '@ultimat3/ai', kind: 'action' },
60
60
  { factory: 'llm', pkg: '@ultimat3/ai', kind: 'action' },
61
61
  { factory: 'backfill', pkg: '@ultimat3/jobs', kind: 'job' },
62
+ { factory: 'purge', pkg: '@ultimat3/jobs', kind: 'job' },
62
63
  { factory: 'scrape', pkg: '@ultimat3/scraping', kind: 'job' },
63
64
  ] satisfies readonly PrimitiveFactory[]
64
65
  ).map((entry) => Object.freeze(entry)),
package/src/telemetry.ts CHANGED
@@ -326,7 +326,13 @@ const TRACEPARENT_RE = /^00-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$/;
326
326
  */
327
327
  export function traceparent(context: SpanContext): string {
328
328
  const flags = (context.traceFlags & 0xff).toString(16).padStart(2, '0');
329
- return `00-${context.traceId}-${context.spanId}-${flags}`;
329
+ // The empty `spanId` `currentSpanContext()` synthesises is the one value this function cannot
330
+ // interpolate bare: `00-<trace>--01` is 39 characters and `TRACEPARENT_RE` — like every
331
+ // collector — rejects it, so the trace the header exists to continue is lost either way. A
332
+ // freshly minted id is what a propagator with no reported parent sends, and it keeps the trace
333
+ // id joinable. Deliberately not all-zero: `parseTraceparent` refuses that, as the spec requires.
334
+ const parentId = context.spanId === '' ? newSpanId() : context.spanId;
335
+ return `00-${context.traceId}-${parentId}-${flags}`;
330
336
  }
331
337
 
332
338
  export function parseTraceparent(header: string | null | undefined): SpanContext | undefined {
package/src/type-pins.ts CHANGED
@@ -1,11 +1,12 @@
1
1
  // Compile-time pins for the actor-facts seam, the config surface, the request-context patch and
2
- // the route vocabulary.
2
+ // the route and cache-tier vocabularies.
3
3
  // Source, not a `.test.ts`, on purpose: `tsconfig.json` excludes `src/**/*.test.ts`, so `tsc -b`
4
4
  // never reads a test file and a type-level assertion written there can never fail. This module
5
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
- import type { AppConfigInput, DatabaseConfig } from './config';
8
+ import type { CacheTierName } from './cache-vocabulary';
9
+ import type { AppConfigInput, CacheConfig, DatabaseConfig, RealtimeConfig } from './config';
9
10
  import type { CtxPatch } from './context';
10
11
  import type { HydrateStrategy, OfflineStrategy, RenderMode } from './route-vocabulary';
11
12
 
@@ -101,6 +102,43 @@ type _DatabaseInputCarriesNoDeadField = Assert<
101
102
  : false
102
103
  >;
103
104
 
105
+ /**
106
+ * The two `config.cache` fields deleted 2026-08-22, held down for the reason the `database` three
107
+ * are: `cache.tiers` is what BUILDS the ladder, so `driver: 'redis'` was a second selector that
108
+ * selected nothing and `urlEnv` named an env key the Redis tier never reads — it reads the literal
109
+ * `REDIS_URL`. Re-adding either restores a knob an SRE sets, redeploys, and sees no effect from.
110
+ */
111
+ type DeadCacheField = 'driver' | 'urlEnv';
112
+
113
+ type _CacheConfigCarriesNoDeadField = Assert<
114
+ Extract<keyof CacheConfig, DeadCacheField> extends never ? true : false
115
+ >;
116
+
117
+ /** And the input side with it — `Input<CacheConfig>` is what an `app.config.ts` writes. */
118
+ type _CacheInputCarriesNoDeadField = Assert<
119
+ Extract<keyof NonNullable<AppConfigInput['cache']>, DeadCacheField> extends never ? true : false
120
+ >;
121
+
122
+ /**
123
+ * The two `config.realtime` fields deleted for the same rule — `heartbeatMs` (2026-08-19) and
124
+ * `tier` (2026-08-23). `tier` is the worse of the two and the reason this pin exists: it accepted
125
+ * three values with three documented meanings, and `transport`/`urlEnv` are the only fields of
126
+ * this section any code reads, so all three meanings were one behaviour. Re-adding it restores a
127
+ * knob whose `'local-first'` setting promises a durable local store the framework does not build.
128
+ */
129
+ type DeadRealtimeField = 'tier' | 'heartbeatMs';
130
+
131
+ type _RealtimeConfigCarriesNoDeadField = Assert<
132
+ Extract<keyof RealtimeConfig, DeadRealtimeField> extends never ? true : false
133
+ >;
134
+
135
+ /** And the input side with it — `Input<RealtimeConfig>` is what an `app.config.ts` writes. */
136
+ type _RealtimeInputCarriesNoDeadField = Assert<
137
+ Extract<keyof NonNullable<AppConfigInput['realtime']>, DeadRealtimeField> extends never
138
+ ? true
139
+ : false
140
+ >;
141
+
104
142
  /**
105
143
  * Neither id a child context may patch. `withChildContext` forwards the parent's `buildId`
106
144
  * verbatim, so `{ buildId }` on the patch was an option that read as honoured and was dropped
@@ -142,3 +180,33 @@ type _OfflineStrategyIsItsArray = Assert<
142
180
  type _HydrateStrategyIsItsArray = Assert<
143
181
  Exact<HydrateStrategy, 'idle' | 'visible' | 'interaction' | 'never'>
144
182
  >;
183
+
184
+ /**
185
+ * The cache ladder's rungs, same rule as the three above and for a defect that shipped: 8.0.0's
186
+ * `CacheTier` was a hand-written union in `config.ts` — `memo | lru | shared | isr | cdn` — while
187
+ * `@ultimat3/cache` ordered `request-memo | lru | redis | cdn`, so `cache: { tiers: ['isr'] }`
188
+ * typechecked and selected nothing (issue #293). Derived from `CACHE_TIERS` now, and pinned here
189
+ * because a `@ts-expect-error` in an excluded test file asserts nothing.
190
+ */
191
+ type _CacheTierNameIsItsArray = Assert<
192
+ Exact<CacheTierName, 'request-memo' | 'lru' | 'redis' | 'cdn'>
193
+ >;
194
+
195
+ /**
196
+ * The three spellings deleted in 9.0.0 must stay deleted. `memo` and `shared` were the ladder's
197
+ * near and shared rungs under a second name; `isr` was never a tier at all — it is a `RenderMode`,
198
+ * and re-admitting it would put a rung `sortTiers` places at `-1` (AHEAD of the request memo)
199
+ * back within reach of `app.config.ts`.
200
+ */
201
+ type DeadCacheTier = 'memo' | 'shared' | 'isr';
202
+
203
+ type _CacheTiersRefuseTheOldSpellings = Assert<
204
+ Extract<CacheTierName, DeadCacheTier> extends never ? true : false
205
+ >;
206
+
207
+ /** And `cache.tiers` is that vocabulary, not a second one — the whole of the fix. */
208
+ type _CacheConfigNamesTheLadder = Assert<Exact<CacheConfig['tiers'], readonly CacheTierName[]>>;
209
+
210
+ type _CacheInputNamesTheLadder = Assert<
211
+ Exact<NonNullable<AppConfigInput['cache']>['tiers'], readonly CacheTierName[] | undefined>
212
+ >;