@ultimat3/core 9.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 +21 -1
- package/package.json +1 -1
- package/src/config.ts +11 -3
- package/src/context.ts +4 -1
- package/src/error-codes.ts +13 -10
- package/src/exports/error-contract.ts +1 -2
- package/src/index.ts +1 -3
- package/src/metrics.ts +26 -0
- package/src/telemetry.ts +7 -1
- package/src/type-pins.ts +21 -1
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` |
|
|
@@ -276,10 +277,17 @@ in a different trace, which is worse than no span because it looks authoritative
|
|
|
276
277
|
including the three that open no other socket — `queue_depth` belongs to one of them.
|
|
277
278
|
|
|
278
279
|
```bash
|
|
279
|
-
bun test
|
|
280
|
+
bun test packages/core/src # from the REPO ROOT, never from packages/core
|
|
280
281
|
bun run typecheck
|
|
281
282
|
```
|
|
282
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
|
+
|
|
283
291
|
`markReady()` means **bound**, and readiness means **usable** — two different facts since
|
|
284
292
|
`registerReadinessCheck(name, check)`. `/readyz` is ready only when the state is `ready` AND every
|
|
285
293
|
named check passes, and `HealthReport.checks` carries them by name because "alert on check
|
|
@@ -348,6 +356,18 @@ the customer's. The non-blank-reason assert is `@ultimat3/entity`'s `crossTenant
|
|
|
348
356
|
verbatim — two escapes from the framework's default posture should not look like two things. Do
|
|
349
357
|
not add a second impersonation path.
|
|
350
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
|
+
|
|
351
371
|
Every `UltimateError` carries `retry` (`terminal | retryable | retry-after`), **defaulting to
|
|
352
372
|
`terminal`** — fail closed, because a client retrying on `status >= 500` hammers `X_DB_DRIFT` and
|
|
353
373
|
`X_TENANCY_UNSCOPED`, which are permanent config faults. `registerErrorRetry()` is the one
|
package/package.json
CHANGED
package/src/config.ts
CHANGED
|
@@ -15,7 +15,6 @@ import type { OfflineStrategy } from './route-vocabulary';
|
|
|
15
15
|
import { isIanaZoneName } from './time-zone-name';
|
|
16
16
|
|
|
17
17
|
export type ThemeMode = 'light' | 'dark' | 'system';
|
|
18
|
-
export type RealtimeTier = 'channels' | 'live-queries' | 'local-first';
|
|
19
18
|
export type RealtimeTransport = 'memory' | 'nats' | 'redis';
|
|
20
19
|
|
|
21
20
|
export interface ThemeConfig {
|
|
@@ -119,10 +118,19 @@ export interface JobsConfig {
|
|
|
119
118
|
* server config, and the presence beat is DERIVED (`PresenceRegistry.heartbeatMs` is
|
|
120
119
|
* `max(1000, floor(ttlMs / 3))`). A second knob is a second number that can disagree with the one
|
|
121
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.
|
|
122
131
|
*/
|
|
123
132
|
export interface RealtimeConfig {
|
|
124
133
|
readonly enabled: boolean;
|
|
125
|
-
readonly tier: RealtimeTier;
|
|
126
134
|
readonly transport: RealtimeTransport;
|
|
127
135
|
readonly urlEnv: string | undefined;
|
|
128
136
|
}
|
|
@@ -244,7 +252,7 @@ function defaults(name: string): Omit<AppConfig, 'name'> {
|
|
|
244
252
|
backoff: 'exponential',
|
|
245
253
|
visibilityTimeoutMs: 30_000,
|
|
246
254
|
},
|
|
247
|
-
realtime: { enabled: false,
|
|
255
|
+
realtime: { enabled: false, transport: 'memory', urlEnv: undefined },
|
|
248
256
|
ai: { mcp: { expose: true, path: '/mcp' } },
|
|
249
257
|
};
|
|
250
258
|
}
|
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
|
-
|
|
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',
|
package/src/error-codes.ts
CHANGED
|
@@ -19,11 +19,14 @@ export interface ErrorCodeEntry extends ErrorCodeDescriptor {
|
|
|
19
19
|
readonly code: string;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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(
|
|
79
|
-
return Object.freeze({ title: declaration.title, docs: declaration.docs ??
|
|
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(
|
|
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(
|
|
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(
|
|
123
|
+
return descriptor({ title: humanize(code) });
|
|
121
124
|
}
|
|
122
125
|
|
|
123
126
|
export function hasErrorCode(code: string): boolean {
|
package/src/index.ts
CHANGED
|
@@ -48,7 +48,6 @@ export type {
|
|
|
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
|
-
|
|
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/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
|
-
|
|
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
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import type { Actor, ActorFactMap, FactKeysOf, FactMapOf } from './actor';
|
|
8
8
|
import type { CacheTierName } from './cache-vocabulary';
|
|
9
|
-
import type { AppConfigInput, CacheConfig, DatabaseConfig } from './config';
|
|
9
|
+
import type { AppConfigInput, CacheConfig, DatabaseConfig, RealtimeConfig } from './config';
|
|
10
10
|
import type { CtxPatch } from './context';
|
|
11
11
|
import type { HydrateStrategy, OfflineStrategy, RenderMode } from './route-vocabulary';
|
|
12
12
|
|
|
@@ -119,6 +119,26 @@ type _CacheInputCarriesNoDeadField = Assert<
|
|
|
119
119
|
Extract<keyof NonNullable<AppConfigInput['cache']>, DeadCacheField> extends never ? true : false
|
|
120
120
|
>;
|
|
121
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
|
+
|
|
122
142
|
/**
|
|
123
143
|
* Neither id a child context may patch. `withChildContext` forwards the parent's `buildId`
|
|
124
144
|
* verbatim, so `{ buildId }` on the patch was an option that read as honoured and was dropped
|