@ultimat3/core
Advanced tools
| // Single responsibility: a byte count as the short, machine-ish string an error message carries. | ||
| // | ||
| // Tier 0 because two tier-4 packages need exactly this and neither may import the other: | ||
| // `@ultimat3/render`'s `X_BUDGET_EXCEEDED` cause and `@ultimat3/pwa`'s precache warning. Each kept | ||
| // its own copy and they had diverged — render's stopped at `kb`, so one 5 MiB route read `5120kb` | ||
| // in the budget error and `5mb` in the warning about the same bytes. | ||
| /** 1024-based, because every producer here counts bundle bytes, which tooling reports in KiB. */ | ||
| const STEP = 1024; | ||
| /** | ||
| * Ascending, so the index into it IS the power of `STEP`. `gb` is the last rung on purpose: a | ||
| * precache or a route bundle past a terabyte is a bug in the caller, not a unit this should grow. | ||
| */ | ||
| const UNITS = ['b', 'kb', 'mb', 'gb'] as const; | ||
| const round1 = (value: number): number => Math.round(value * 10) / 10; | ||
| /** | ||
| * A size a message can state — `1023b`, `4.5kb`, `5mb`, `1.2gb`. | ||
| * | ||
| * Not `@ultimat3/ui`'s `formatBytes(bytes, locale)`, which is `Intl`-formatted, DECIMAL (kB = 1000 | ||
| * B, because that is what `Intl`'s unit means) and for a human reading a file picker. This one is | ||
| * for an error's `cause:`, where the number has to line up with a bundler's own KiB figures and | ||
| * must not change with the reader's locale. | ||
| * | ||
| * A negative or non-finite input answers `0b` rather than `-5b` or `NaNb`: axiom 4 says an error is | ||
| * an instruction, and `NaNb` instructs nobody. A size is never negative, so the input was already | ||
| * wrong by the time it arrived. | ||
| */ | ||
| export const formatBytes = (bytes: number): string => { | ||
| if (!Number.isFinite(bytes) || bytes <= 0) return `0${UNITS[0]}`; | ||
| let value = bytes; | ||
| let index = 0; | ||
| while (index < UNITS.length - 1 && value >= STEP) { | ||
| value /= STEP; | ||
| index += 1; | ||
| } | ||
| // One more rung when ROUNDING crosses the boundary the raw value did not: 1048575 is under a | ||
| // mebibyte, but one decimal place renders it `1024kb` — a number that disagrees with its own | ||
| // unit, the same class of bug as render's missing `mb` branch. | ||
| if (index < UNITS.length - 1 && round1(value) >= STEP) { | ||
| value /= STEP; | ||
| index += 1; | ||
| } | ||
| return `${round1(value)}${UNITS[index] ?? 'b'}`; | ||
| }; |
| // Single responsibility: the declared name a mistyped one most likely meant, so any error can lead | ||
| // with the real one. It lives in core because three packages need the same answer — `@ultimat3/cli` | ||
| // for an unknown command, flag or positional, `@ultimat3/policy` for an unknown permission — and | ||
| // two copies of one cutoff are two suggestions for one typo. | ||
| /** | ||
| * Levenshtein distance. A grid rather than two rolling rows because `noUncheckedIndexedAccess` | ||
| * makes every read an `?? 0`, and one `at()` reads better than four of them. | ||
| */ | ||
| const distance = (a: string, b: string): number => { | ||
| const rows = a.length + 1; | ||
| const cols = b.length + 1; | ||
| const grid: number[] = new Array<number>(rows * cols).fill(0); | ||
| const at = (r: number, c: number): number => grid[r * cols + c] ?? 0; | ||
| for (let r = 0; r < rows; r += 1) grid[r * cols] = r; | ||
| for (let c = 0; c < cols; c += 1) grid[c] = c; | ||
| for (let r = 1; r < rows; r += 1) { | ||
| for (let c = 1; c < cols; c += 1) { | ||
| const cost = a[r - 1] === b[c - 1] ? 0 : 1; | ||
| grid[r * cols + c] = Math.min(at(r - 1, c) + 1, at(r, c - 1) + 1, at(r - 1, c - 1) + cost); | ||
| } | ||
| } | ||
| return at(rows - 1, cols - 1); | ||
| }; | ||
| /** Past this many edits the "suggestion" is a different word, and a wrong lead is worse than none. */ | ||
| const MAX_EDITS = 3; | ||
| /** | ||
| * The nearest candidate within `MAX_EDITS`, or `undefined` when nothing is close enough. Ties keep | ||
| * the FIRST candidate, which is the order the caller declared them in — `definePermissions([...])` | ||
| * and a `CommandSpec` list are both authored orders, and a stable answer is what lets a test pin one. | ||
| */ | ||
| export const nearestName = (input: string, candidates: readonly string[]): string | undefined => { | ||
| let best: string | undefined; | ||
| let bestScore = MAX_EDITS + 1; | ||
| for (const candidate of candidates) { | ||
| const score = distance(input, candidate); | ||
| if (score < bestScore) { | ||
| best = candidate; | ||
| bestScore = score; | ||
| } | ||
| } | ||
| return best; | ||
| }; |
+58
-4
@@ -82,8 +82,13 @@ # @ultimat3/core — agent notes | ||
| `describeValue` in `error-render.ts` is a character-for-character duplicate of `describeValue` in | ||
| `describeValue` in `error-render.ts` is a deliberate duplicate of `describeValue` in | ||
| `packages/schema/src/describe-value.ts`, for the same tier-0 reason `SCHEMA_ERROR_CODE_TITLES` is | ||
| one: schema and core are both tier 0 and `core → schema` is **not** a declared edge in | ||
| `scripts/lib/tiers.ts`, so neither may import the other. Keep the two identical; a pin test in | ||
| `scripts/lib/tiers.ts`, so neither may import the other. Keep the two ANSWERING identically — that | ||
| is the contract, and the source is no longer character-for-character: schema counts characters | ||
| through `char-count.ts`, which core copies privately. A pin test in | ||
| `@ultimat3/cli` (which may legally import both) is the mechanical half, the same shape as | ||
| `schema-error-codes-pin.test.ts`. The rule it enforces: a `cause` reaches the log index AND the | ||
| `schema-error-codes-pin.test.ts`. **A string's length is CODE POINTS in both, `As of 2026-08-22`** | ||
| — `validators.ts` rejects in that unit and `json-schema.ts` publishes `minLength` in it, so | ||
| `.length` made `t.string.min(3).safeParse('👍a')` say "at least 3 chars, received a string of 3 | ||
| characters". The rule it enforces: a `cause` reaches the log index AND the | ||
| HTTP problem document, redaction is by log FIELD key, and a value baked into a message string has | ||
@@ -179,2 +184,13 @@ no key left to redact — so `parseId`/`uuidTimestamp` describe a rejected id and never echo it. | ||
| `format-bytes.ts` is the same rule at its smallest, `As of 2026-08-22`: one `formatBytes(bytes)`, | ||
| 1024-base, `b|kb|mb|gb`, for the byte count an error message carries. `@ultimat3/render` (t4) and | ||
| `@ultimat3/pwa` (t4) each had one and they had diverged — render's stopped at `kb`, so a 5 MiB route | ||
| read `5120kb` in `X_BUDGET_EXCEEDED` and `5mb` in the precache warning about the same bytes, and | ||
| `@ultimat3/cli`'s budget error imported render's. Deliberately NOT | ||
| `@ultimat3/ui`'s `formatBytes(bytes, locale)`, which is a different function and stays: that one is | ||
| `Intl`-formatted and DECIMAL (kB = 1000 B, which is what `Intl`'s unit means), for a human reading a | ||
| file picker, where this one must line up with a bundler's own KiB figures and must not move with the | ||
| reader's locale. **Not mechanised** — no gate refuses a third copy, unlike `render-modes.ts` for the | ||
| route vocabulary; a `formatBytes` reappearing in `packages/*/src` is caught by review only. | ||
| `mcp-exposure.ts` is the same shape for a declaration rather than an algorithm: `isMcpExposed` is | ||
@@ -212,2 +228,12 @@ the ONE answer to "did this primitive opt into being an MCP tool?", asked by `action`, `query` | ||
| **An empty `spanId` means "no inbound decision", and every reader must honour it, `As of | ||
| 2026-08-22`.** `currentSpanContext()` synthesises `{ traceId, spanId: '', traceFlags: 1 }` from the | ||
| request context — a trace id this process minted, plus the header it would send onward. Handing | ||
| that to `Sampler.shouldSample` as a parent made `parentBasedRatioSampler` inherit a bit nobody sent, | ||
| so at ratio 0 a root span outside a request exported 0 and one inside exported 1 — and | ||
| `@ultimat3/http`'s `pipeline.ts` is `runWithContext` then `withSpan`, so **every HTTP root span was | ||
| exported at every ratio**. `startSpan` now narrows through `inboundParent()`: the trace id is | ||
| carried, the decision is not. It is the same discriminator `end()` already used to drop a synthetic | ||
| `parentSpanId`. | ||
| The OTLP exporters are built, not wrapped, and the case is in | ||
@@ -222,2 +248,11 @@ [`docs/idea/18-build-vs-wrap.md`](../../docs/idea/18-build-vs-wrap.md): OTLP/HTTP JSON is `fetch` | ||
| **Three OTLP variables, three codes, `As of 2026-08-22`** — `X_OTLP_ENDPOINT_INVALID`, | ||
| `X_OTLP_HEADERS_INVALID`, `X_OTLP_PROTOCOL_UNSUPPORTED`, one per variable an operator sets. The | ||
| headers one is not a duplicate of the endpoint one: `otlpHeaders` percent-decodes, so `%zz` in | ||
| `OTEL_EXPORTER_OTLP_HEADERS` used to take the process down with a bare `URIError` at exporter | ||
| construction, and raising the ENDPOINT code instead would send the first reader of | ||
| `x errors explain` to inspect a variable that is fine. A title is what an agent reads first, and an | ||
| accurate `cause:` does not rescue one that misdirects. The header **key** is in the cause, the fix | ||
| and `meta`; the **value** is in none of them — it is the collector's credential. | ||
| `error-reporter.ts` is the same shape a third time: `ErrorReporter`, a no-op default, a memory | ||
@@ -254,3 +289,13 @@ reporter for tests, and a transport on the wire (`error-reporter-sentry.ts`, an optional separate | ||
| named check passes, and `HealthReport.checks` carries them by name because "alert on check | ||
| failures by check name" is not writable against a boolean. Checks are **synchronous** on purpose: | ||
| failures by check name" is not writable against a boolean. `HealthReport.registered` carries the | ||
| COUNT beside it, `As of 2026-08-22`: `checks: {}` reads identically for "every check passed" and | ||
| "nobody registered one", and only the second is a `/readyz` meaning no more than "the socket is | ||
| bound". Reported, never enforced — **an empty registry is still `ready`, and `/readyz` still | ||
| answers 200**: `Object.values({}).every(…)` is vacuously true, and that is deliberate so a role | ||
| with no dependency does not have to invent a check to boot. `registered` is the field a caller | ||
| reads to tell "all checks passed" from "there were none". | ||
| `readinessChecks()` builds its record through `Object.fromEntries`, never by assigning | ||
| `results[name]` — assignment to the one name `__proto__` sets the PROTOTYPE rather than adding a | ||
| key, so a check by that name disappeared from the report and a `failing` one answered 200. Checks are **synchronous** on purpose: | ||
| a probe that awaits a network call turns a slow dependency into a wedged endpoint and a restart | ||
@@ -262,2 +307,11 @@ loop, so the owner of the dependency keeps a boolean fresh and this reads it. Liveness ignores | ||
| **`drain()`'s memo is published BEFORE the first hook runs, `As of 2026-08-22`, and that ordering | ||
| is the whole of the function.** A hook may call back into `drain()` and one does — `handle.stop()` | ||
| in `@ultimat3/http` is `drain('manual')`, and an `accept` hook is exactly where a server stops | ||
| listening — while `settleWithin` invokes a hook SYNCHRONOUSLY. `drainPromise = (async () => …)()` | ||
| had therefore not assigned when the first hook ran: the re-entrant call read `undefined`, started a | ||
| second whole drain and recursed **~4,700 deep** until the stack ran out, every level swallowed by | ||
| `settleWithin` as `shutdown hook failed`. The guard and the registration are now one synchronous | ||
| step (`jobs`' `worker.ts` states the same rule), with the phases in `runDrain`. | ||
| **The drain deadline is enforced, not merely computed, and there is no unbounded state.** | ||
@@ -264,0 +318,0 @@ `ShutdownReason.deadlineAt` was always handed to every hook and **no hook has ever read it** — |
+1
-1
| { | ||
| "name": "@ultimat3/core", | ||
| "version": "7.0.0", | ||
| "version": "8.0.0", | ||
| "description": "Ultimate's foundation: errors, context, env, config, clock, ids, logging, telemetry, lifecycle", | ||
@@ -5,0 +5,0 @@ "license": "MIT", |
+9
-0
@@ -325,2 +325,11 @@ # 🧱 @ultimat3/core | ||
| `shutdownHookCount()` is the test-only probe that makes the leak assertable. | ||
| - `registerReadinessCheck(name, check)` is what makes `/readyz` mean **usable** rather than | ||
| **bound**. `ReadinessCheck` is `() => boolean` and must stay synchronous — a probe that awaits its | ||
| dependency turns a slow dependency into a wedged endpoint and then a restart loop; keep a boolean | ||
| fresh and let the check read it. It returns an unregister. `HealthReport.checks` is a map of name | ||
| → `'ok' | 'failing'`, so "alert on check failures by check name" is writable. | ||
| - **`HealthReport.registered` is the third state.** `checks: {}` reads identically for "every check | ||
| passed" and "nobody registered one", and an **empty registry is still ready** — reported, never | ||
| enforced, so a role with no dependency does not have to invent a check to boot. Read `registered` | ||
| before trusting an empty `checks`. | ||
| - Anything that opens a socket calls `markListening(server.url.origin)` and releases it on close. | ||
@@ -327,0 +336,0 @@ That is what tells the sealed test network a loopback request is this process, not egress. |
+25
-43
@@ -25,3 +25,3 @@ // Single responsibility: `app.config.ts` — the one config file. Deeply optional with real | ||
| /** | ||
| * Where a browser that failed `auth: 'required'` is sent, and where it lands afterwards. | ||
| * Where a browser that failed `auth: 'required'` is sent. | ||
| * | ||
@@ -32,28 +32,20 @@ * `signInPath: null` is the default and the redirect stays off until an app names its page: the | ||
| * right answer for an agent, and what a browser got in production until this existed. | ||
| * | ||
| * `afterSignInPath` was removed 2026-08 for the reason `urlEnv`, `poolSize` and `schema` were | ||
| * (below): accepted, defaulted and merged here, and read by NO file — `dummy/social-media-clone` | ||
| * set `/dashboard` and got whatever its sign-in route did on its own. The landing path belongs to | ||
| * the app's sign-in route, which is the only code that can honour it. | ||
| */ | ||
| export interface AuthConfig { | ||
| readonly signInPath: string | null; | ||
| /** | ||
| * Where sign-in lands when there is nowhere to return to, or `?next=` is not same-origin. | ||
| * | ||
| * **Consulted by nothing, `As of 2026-08.`** Accepted, defaulted and merged here and read by no | ||
| * file in the repo — `dummy/social-media-clone/app.config.ts` sets `/dashboard` and gets | ||
| * whatever the sign-in route does on its own. Same shape `urlEnv`, `poolSize` and `schema` were | ||
| * deleted for below; this one is not deleted yet only because its writer is a tracked app's | ||
| * config, so removing the key and the line that sets it is one commit across two file sets. | ||
| */ | ||
| readonly afterSignInPath: string; | ||
| } | ||
| /** | ||
| * `installPrompt` was removed 2026-08, same rule: `@ultimat3/pwa`'s `createInstallController` is | ||
| * real and complete, nothing ever threaded the flag into it, and both tracked apps plus every | ||
| * scaffolded app set a switch with no wire. Call the controller from your own affordance instead. | ||
| */ | ||
| export interface PwaConfig { | ||
| readonly enabled: boolean; | ||
| readonly offline: OfflineStrategy; | ||
| /** | ||
| * **Consulted by nothing, `As of 2026-08.`** `wiki/Configuration.md` describes it as "render | ||
| * your own install affordance from the deferred event", both tracked apps set it, and | ||
| * `x new`'s scaffold writes it into every generated app — and no file reads it. | ||
| * `@ultimat3/pwa`'s `install.ts` is real and complete; nothing threads this flag into it. | ||
| * Delete the key or thread it; leaving it is a switch with no wire. | ||
| */ | ||
| readonly installPrompt: boolean; | ||
| readonly backgroundSync: boolean; | ||
@@ -128,14 +120,12 @@ readonly push: boolean; | ||
| /** | ||
| * No `modelEnv`. It named the env KEY holding the model id, "so no model string is baked into the | ||
| * image" — and its only reader was this file's own merge, copying input to output. Nothing | ||
| * consumed the merged value, so `modelEnv: 'ANTHROPIC_MODEL'` selected no model: `@ultimat3/ai` | ||
| * reads env for API KEYS only, and the model is `request.model ?? DEFAULT_MODEL`, a compile-time | ||
| * constant in `models.ts`. The exact thing the key existed to prevent is what it delivered. | ||
| * Deleted 2026-08 — pass `model` on the request, or read your own env key and pass it. | ||
| */ | ||
| export interface AiConfig { | ||
| readonly mcp: McpConfig; | ||
| /** | ||
| * Env key for the model id, so no model string is baked into the image — **an intention, not a | ||
| * behaviour, `As of 2026-08`.** The only read of it in the repo is the merge two hundred lines | ||
| * below, which copies it from input to output; nothing consumes the merged value, so | ||
| * `examples/dummy`'s `modelEnv: 'ANTHROPIC_MODEL'` selects no model. `@ultimat3/ai` reads env | ||
| * for API KEYS only (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`); the model is | ||
| * `request.model ?? DEFAULT_MODEL`, a compile-time constant in `models.ts`. So the exact thing | ||
| * this key exists to prevent — a model string baked into the image — is what actually happens. | ||
| */ | ||
| readonly modelEnv: string | undefined; | ||
| } | ||
@@ -162,3 +152,4 @@ | ||
| export interface AiConfigInput extends Input<Omit<AiConfig, 'mcp'>> { | ||
| /** `mcp` is the only member, and it is NESTED — `Input<AiConfig>` would make it all-or-nothing. */ | ||
| export interface AiConfigInput { | ||
| readonly mcp?: Input<McpConfig> | undefined; | ||
@@ -231,10 +222,4 @@ } | ||
| theme: { defaultMode: 'system', tokens: {} }, | ||
| auth: { signInPath: null, afterSignInPath: '/' }, | ||
| pwa: { | ||
| enabled: false, | ||
| offline: 'network-only', | ||
| installPrompt: false, | ||
| backgroundSync: false, | ||
| push: false, | ||
| }, | ||
| auth: { signInPath: null }, | ||
| pwa: { enabled: false, offline: 'network-only', backgroundSync: false, push: false }, | ||
| roles: [...ROLES], | ||
@@ -251,3 +236,3 @@ database: { driver: 'postgres', ssl: false }, | ||
| realtime: { enabled: false, tier: 'channels', transport: 'memory', urlEnv: undefined }, | ||
| ai: { mcp: { expose: true, path: '/mcp' }, modelEnv: undefined }, | ||
| ai: { mcp: { expose: true, path: '/mcp' } }, | ||
| }; | ||
@@ -346,6 +331,3 @@ } | ||
| realtime: section(base.realtime, merged.realtime), | ||
| ai: { | ||
| mcp: section(base.ai.mcp, merged.ai?.mcp), | ||
| modelEnv: merged.ai?.modelEnv ?? base.ai.modelEnv, | ||
| }, | ||
| ai: { mcp: section(base.ai.mcp, merged.ai?.mcp) }, | ||
| }; | ||
@@ -352,0 +334,0 @@ |
+7
-1
@@ -73,3 +73,9 @@ // Single responsibility: the ambient request context. Authz, tracing, locale, tz and the | ||
| export type CtxPatch = Omit<CtxInit, 'requestId'>; | ||
| /** | ||
| * Neither id a child may change. `requestId` because one request is one request however many | ||
| * scopes it opens; `buildId` because a child context is the same DEPLOY — `withChildContext` | ||
| * has always forwarded the parent's, so accepting the key was an option that read as honoured and | ||
| * was dropped in silence. Pinned in `type-pins.ts`. | ||
| */ | ||
| export type CtxPatch = Omit<CtxInit, 'requestId' | 'buildId'>; | ||
@@ -76,0 +82,0 @@ /** |
@@ -51,3 +51,2 @@ /** | ||
| function compare(left: Decimal, right: Decimal): number { | ||
| if (left.negative !== right.negative) return left.negative ? -1 : 1; | ||
| const width = Math.max(left.fraction.length, right.fraction.length); | ||
@@ -58,2 +57,7 @@ const scaled = (value: Decimal): bigint => | ||
| const second = scaled(right); | ||
| // Magnitude BEFORE sign, because a `numeric` has exactly one zero: `select '-0'::numeric = | ||
| // '0'::numeric` is true, and so is `'-0.00' = '0'`. Comparing the sign first answered `-1` for | ||
| // that pair and cut a keyset page boundary between two rows the database calls equal. | ||
| if (first === 0n && second === 0n) return 0; | ||
| if (left.negative !== right.negative) return left.negative ? -1 : 1; | ||
| // Never a subtraction: the difference between two `bigint`s is exact and the return type is a | ||
@@ -60,0 +64,0 @@ // `number`, which cannot hold it. |
@@ -56,2 +56,5 @@ // Single responsibility: the framework-wide error-code registry (code -> title + docs). | ||
| X_OTLP_ENDPOINT_INVALID: 'the OTLP collector endpoint is missing or malformed', | ||
| // Its own code rather than the endpoint's, because a title is what an agent reads first: | ||
| // `x errors explain X_OTLP_ENDPOINT_INVALID` would send it to inspect a variable that is fine. | ||
| X_OTLP_HEADERS_INVALID: 'OTEL_EXPORTER_OTLP_HEADERS is malformed', | ||
| X_OTLP_PROTOCOL_UNSUPPORTED: 'the OTLP protocol requested is not OTLP/HTTP JSON', | ||
@@ -58,0 +61,0 @@ X_READINESS_CHECK_DUPLICATE: 'a readiness check name is registered twice', |
+17
-5
@@ -255,6 +255,6 @@ // Single responsibility: turn a value the framework does not control into text for an error's | ||
| * | ||
| * A deliberate, character-for-character duplicate of `describeValue` in | ||
| * `packages/schema/src/describe-value.ts`, for the reason `SCHEMA_ERROR_CODE_TITLES` is one: | ||
| * `@ultimat3/schema` is tier 0 alongside this package, so neither may import the other. Keep the | ||
| * two identical; changing one alone is the bug. | ||
| * A deliberate duplicate of `describeValue` in `packages/schema/src/describe-value.ts`, for the | ||
| * reason `SCHEMA_ERROR_CODE_TITLES` is one: `@ultimat3/schema` is tier 0 alongside this package, | ||
| * so neither may import the other. Keep the two answering IDENTICALLY — that is what | ||
| * `packages/cli/src/describe-value-pin.test.ts` holds — and changing one alone is the bug. | ||
| */ | ||
@@ -266,3 +266,3 @@ export function describeValue(value: unknown): string { | ||
| case 'string': | ||
| return countOf(value.length, 'string', 'character'); | ||
| return countOf(charCount(value), 'string', 'character'); | ||
| case 'number': | ||
@@ -299,1 +299,13 @@ return describeNumber(value); | ||
| } | ||
| /** | ||
| * The twin of `@ultimat3/schema`'s `char-count.ts`, duplicated for the same reason `describeValue` | ||
| * is: both packages are tier 0 and neither may import the other. Code points, because the rules | ||
| * that reject a string count in them and the message must quote the same unit — `'👍'.length` is 2. | ||
| * Only a surrogate makes the two counts differ, so every ASCII value keeps the O(1) read. | ||
| */ | ||
| const HAS_SURROGATE = /[\uD800-\uDBFF]/; | ||
| function charCount(value: string): number { | ||
| return HAS_SURROGATE.test(value) ? [...value].length : value.length; | ||
| } |
@@ -45,2 +45,3 @@ // The observability slice of `@ultimat3/core`'s public surface, in one place: logging, metrics, | ||
| setLoggerContextFields, | ||
| setLogStream, | ||
| } from '../logger'; | ||
@@ -92,2 +93,3 @@ export type { | ||
| OtlpEndpointInvalidError, | ||
| OtlpHeadersInvalidError, | ||
| OtlpProtocolUnsupportedError, | ||
@@ -94,0 +96,0 @@ otlpAttributes, |
+12
-7
@@ -271,2 +271,3 @@ // Single responsibility: the public API of @ultimat3/core. Explicit named exports only — | ||
| OtlpEndpointInvalidError, | ||
| OtlpHeadersInvalidError, | ||
| OtlpProtocolUnsupportedError, | ||
@@ -307,2 +308,3 @@ OVERFLOW_ATTRIBUTE, | ||
| setLoggerContextFields, | ||
| setLogStream, | ||
| startMetricExport, | ||
@@ -374,2 +376,3 @@ startSpan, | ||
| } from './exports/secrets'; | ||
| export { formatBytes } from './format-bytes'; | ||
| export type { Brand, Id } from './ids'; | ||
@@ -465,13 +468,15 @@ export { | ||
| } from './lifecycle'; | ||
| export { | ||
| isSelfOrigin, | ||
| listeningOrigins, | ||
| markListening, | ||
| resetListeners, | ||
| } from './listeners'; | ||
| export { isSelfOrigin, listeningOrigins, markListening, resetListeners } from './listeners'; | ||
| export { isMcpExposed, type McpExposureDeclaration } from './mcp-exposure'; | ||
| export { nearestName } from './nearest-name'; | ||
| export { type CappedBody, readWithinLimit } from './read-capped'; | ||
| export type { ModuleRegistrar, PrimitiveKind, RegisteredPrimitive } from './registrar'; | ||
| export type { | ||
| ModuleRegistrar, | ||
| PrimitiveFactory, | ||
| PrimitiveKind, | ||
| RegisteredPrimitive, | ||
| } from './registrar'; | ||
| export { | ||
| hasPrimitiveRegistrar, | ||
| PRIMITIVE_FACTORIES, | ||
| PRIMITIVE_KINDS, | ||
@@ -478,0 +483,0 @@ primitiveRegistrar, |
+73
-38
@@ -85,2 +85,10 @@ // Single responsibility: process lifecycle and graceful drain. Every role runs the same three | ||
| readonly checks: Readonly<Record<string, ReadinessStatus>>; | ||
| /** | ||
| * How many checks are registered. `checks: {}` reads identically for "every check passed" and | ||
| * "nobody registered one", and only the second is a `/readyz` that means no more than "the | ||
| * socket is bound" — which is what the chart's and compose's healthchecks route traffic on. | ||
| * Reported rather than enforced: an empty registry is still ready, so a role that genuinely has | ||
| * no dependency does not have to invent a check to boot. | ||
| */ | ||
| readonly registered: number; | ||
| } | ||
@@ -196,14 +204,21 @@ | ||
| /** Every check, run now, by name. A check that throws is `failing` — never an unhandled error. */ | ||
| /** | ||
| * Every check, run now, by name. A check that throws is `failing` — never an unhandled error. | ||
| * | ||
| * Built through `Object.fromEntries`, never by assigning `results[name]`: assignment to the one | ||
| * name `__proto__` sets the PROTOTYPE instead of adding a key, so that check vanished from the | ||
| * report, `ready` was computed over an empty object — vacuously true — and a failing check | ||
| * answered 200. `fromEntries` defines own properties and has no such name. | ||
| */ | ||
| export function readinessChecks(): Readonly<Record<string, ReadinessStatus>> { | ||
| const results: Record<string, ReadinessStatus> = {}; | ||
| const results: [string, ReadinessStatus][] = []; | ||
| for (const [name, check] of readiness) { | ||
| try { | ||
| results[name] = check() ? 'ok' : 'failing'; | ||
| results.push([name, check() ? 'ok' : 'failing']); | ||
| } catch (thrown) { | ||
| results[name] = 'failing'; | ||
| results.push([name, 'failing']); | ||
| report('warn', 'readiness check threw', { check: name, error: thrown }); | ||
| } | ||
| } | ||
| return results; | ||
| return Object.fromEntries(results); | ||
| } | ||
@@ -333,3 +348,46 @@ | ||
| /** Idempotent: concurrent signals join the same drain. */ | ||
| /** The three phases, in order, under one budget. Never rejects — `drain()` depends on that. */ | ||
| async function runDrain(signal: string, reason: ShutdownReason): Promise<void> { | ||
| try { | ||
| report('info', 'draining', { signal, deadlineMs, inflight }); | ||
| await runPhase('accept', reason); | ||
| // Real monotonic, like `deadlineAt` itself: `waitForIdle` sleeps on a real `setTimeout`, and | ||
| // a budget read off an injected clock is a number that timer will never honour. | ||
| const remaining = Math.max(0, reason.deadlineAt - systemClock.monotonic()); | ||
| const idle = await waitForIdle(remaining); | ||
| if (!idle) { | ||
| report('warn', 'X_SHUTDOWN_TIMEOUT', { | ||
| code: 'X_SHUTDOWN_TIMEOUT', | ||
| cause: `${inflight} in-flight operations still running after ${deadlineMs}ms`, | ||
| 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', | ||
| }); | ||
| } | ||
| await runPhase('inflight', reason); | ||
| await runPhase('close', reason); | ||
| } catch (thrown) { | ||
| // Nothing above should reach here — every hook is caught by `settleWithin` and every line | ||
| // goes through `report`. If something does, the drain still ENDS: a rejected `drainPromise` | ||
| // is a memo that re-rejects for every later caller and an unhandled rejection that kills the | ||
| // process mid-drain, which is strictly worse than a drain that finished badly and said so. | ||
| report('error', 'drain failed', { signal, error: thrown }); | ||
| } finally { | ||
| state = 'stopped'; | ||
| } | ||
| report('info', 'stopped', { signal }); | ||
| } | ||
| /** | ||
| * Idempotent: concurrent signals join the same drain, and so does a RE-ENTRANT one. | ||
| * | ||
| * The memo is published before `runDrain` is called, and that ordering is the whole of this | ||
| * function. A hook may call back in here — `handle.stop()` in `@ultimat3/http` is `drain('manual')` | ||
| * and an `accept` hook is exactly where a server stops listening — and `settleWithin` invokes a | ||
| * hook SYNCHRONOUSLY, so the old `drainPromise = (async () => …)()` had not assigned yet when the | ||
| * first hook ran: the re-entrant call saw `undefined`, started a second whole drain, and recursed | ||
| * ~4,700 deep until the stack ran out, every level swallowed by `settleWithin` as | ||
| * `shutdown hook failed`. Same rule as `packages/jobs/src/worker.ts` — guard and registration in | ||
| * one synchronous step. | ||
| */ | ||
| export function drain(signal = 'manual'): Promise<void> { | ||
@@ -339,34 +397,10 @@ if (drainPromise !== undefined) return drainPromise; | ||
| const reason: ShutdownReason = { signal, deadlineAt: systemClock.monotonic() + deadlineMs }; | ||
| drainPromise = (async () => { | ||
| try { | ||
| report('info', 'draining', { signal, deadlineMs, inflight }); | ||
| await runPhase('accept', reason); | ||
| // Real monotonic, like `deadlineAt` itself: `waitForIdle` sleeps on a real `setTimeout`, and | ||
| // a budget read off an injected clock is a number that timer will never honour. | ||
| const remaining = Math.max(0, reason.deadlineAt - systemClock.monotonic()); | ||
| const idle = await waitForIdle(remaining); | ||
| if (!idle) { | ||
| report('warn', 'X_SHUTDOWN_TIMEOUT', { | ||
| code: 'X_SHUTDOWN_TIMEOUT', | ||
| cause: `${inflight} in-flight operations still running after ${deadlineMs}ms`, | ||
| 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', | ||
| }); | ||
| } | ||
| await runPhase('inflight', reason); | ||
| await runPhase('close', reason); | ||
| } catch (thrown) { | ||
| // Nothing above should reach here — every hook is caught by `settleWithin` and every line | ||
| // goes through `report`. If something does, the drain still ENDS: a rejected `drainPromise` | ||
| // is a memo that re-rejects for every later caller and an unhandled rejection that kills the | ||
| // process mid-drain, which is strictly worse than a drain that finished badly and said so. | ||
| report('error', 'drain failed', { signal, error: thrown }); | ||
| } finally { | ||
| state = 'stopped'; | ||
| } | ||
| report('info', 'stopped', { signal }); | ||
| })(); | ||
| let published!: () => void; | ||
| drainPromise = new Promise<void>((resolve) => { | ||
| published = resolve; | ||
| }); | ||
| // Both settle paths, for the reason `installSignalHandlers` gives below: `runDrain` cannot | ||
| // reject today — that is its `try/finally`, not luck — and a rejected memo would re-reject for | ||
| // every later caller and end the process the drain was trying to end cleanly. | ||
| void runDrain(signal, reason).then(published, published); | ||
| return drainPromise; | ||
@@ -417,2 +451,3 @@ } | ||
| checks, | ||
| registered: readiness.size, | ||
| }; | ||
@@ -419,0 +454,0 @@ } |
+21
-1
@@ -104,4 +104,24 @@ // Single responsibility: structured JSON logging. One line per event, machine-readable by | ||
| /** | ||
| * Where a line with no explicit writer lands, for everything below `error`. A fact about the | ||
| * PROCESS and not about the line: a container's stdout IS its log stream (12-factor), while a | ||
| * CLI's stdout is the answer it was asked for. `x db migrate --json` printed the boot logger's | ||
| * `ultimate migrate applied` and then the command's own JSON to fd 1, so a caller doing what | ||
| * `--json` exists for — parsing the output — raised on the second object. | ||
| */ | ||
| let logStream: 'stdout' | 'stderr' = 'stdout'; | ||
| /** | ||
| * Send everything below `error` to stderr, or back to stdout. The process's own call, made once at | ||
| * entry: a per-line choice would be the second logging path axiom 1 refuses, and a per-logger one | ||
| * already exists as `LoggerOptions.writer` — what had no seam is the module-scope `logger`, which | ||
| * is the one `serve.ts` and every boot path write through. | ||
| */ | ||
| export function setLogStream(stream: 'stdout' | 'stderr'): void { | ||
| logStream = stream; | ||
| } | ||
| function defaultWriter(line: string, level: LogLevel): void { | ||
| const stream = LEVEL_WEIGHT[level] >= LEVEL_WEIGHT.error ? process.stderr : process.stdout; | ||
| const toStderr = logStream === 'stderr' || LEVEL_WEIGHT[level] >= LEVEL_WEIGHT.error; | ||
| const stream = toStderr ? process.stderr : process.stdout; | ||
| stream.write(`${line}\n`); | ||
@@ -108,0 +128,0 @@ } |
+34
-1
@@ -18,2 +18,16 @@ // Single responsibility: the pieces both OTLP exporters share — endpoint resolution from the env | ||
| /** | ||
| * Separate from `OtlpEndpointInvalidError` on purpose. The endpoint code's title says the ENDPOINT | ||
| * is missing or malformed, so raising it for a bad header escape sends the first reader of | ||
| * `x errors explain` to inspect `OTEL_EXPORTER_OTLP_ENDPOINT` — a variable that is fine. An | ||
| * accurate `cause:` does not rescue a title that misdirects. | ||
| */ | ||
| export class OtlpHeadersInvalidError extends UltimateError { | ||
| static readonly code = 'X_OTLP_HEADERS_INVALID'; | ||
| override readonly name = 'OtlpHeadersInvalidError'; | ||
| constructor(init: CodedErrorInit) { | ||
| super({ ...init, code: OtlpHeadersInvalidError.code }); | ||
| } | ||
| } | ||
| export class OtlpProtocolUnsupportedError extends UltimateError { | ||
@@ -116,2 +130,21 @@ static readonly code = 'X_OTLP_PROTOCOL_UNSUPPORTED'; | ||
| /** | ||
| * One header value, percent-decoded. `decodeURIComponent` throws a bare `URIError` on a malformed | ||
| * escape (`%zz`, a lone `%`), and the whole of `otlpHeaders` runs at exporter construction — so a | ||
| * typo in an operator-set variable took the process down with an error carrying no code, no cause | ||
| * and no fix. Refused instead, naming the variable and the header KEY: the value is the | ||
| * collector's credential and a `cause:` is folded into a log line. | ||
| */ | ||
| function decodeHeaderValue(key: string, raw: string): string { | ||
| try { | ||
| return decodeURIComponent(raw); | ||
| } catch { | ||
| throw new OtlpHeadersInvalidError({ | ||
| cause: `${OTLP_HEADERS_KEY} carries a malformed percent-escape in the "${key}" value, so the header cannot be decoded`, | ||
| 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`, | ||
| meta: { header: key }, | ||
| }); | ||
| } | ||
| } | ||
| /** `key=value,key2=value2`, percent-decoded — the spec's format for collector auth headers. */ | ||
@@ -130,3 +163,3 @@ export function otlpHeaders( | ||
| if (key === '') continue; | ||
| headers[key] = decodeURIComponent(pair.slice(index + 1).trim()); | ||
| headers[key] = decodeHeaderValue(key, pair.slice(index + 1).trim()); | ||
| } | ||
@@ -133,0 +166,0 @@ } |
+37
-0
@@ -30,3 +30,40 @@ // Hands a module of primitives to the package that owns them, without a sideways import: | ||
| /** One factory over one primitive: the export's name, the package that ships it, what it returns. */ | ||
| export interface PrimitiveFactory { | ||
| readonly factory: string; | ||
| /** The package specifier the factory is imported from, so a `fix:` can be pasted. */ | ||
| readonly pkg: string; | ||
| readonly kind: PrimitiveKind; | ||
| } | ||
| /** | ||
| * The other half of "never invent a ninth": the factories that already exist, in one table. | ||
| * | ||
| * Prose counted them — "the fourth instance of the framework's factory rule" — in three files that | ||
| * cannot see each other, so every ordinal was wrong the moment a fifth landed and none of them | ||
| * could be checked. A list here can be: `@ultimat3/cli` is tier 5, may import `ai`, `jobs` and | ||
| * `scraping`, and pins that every exported function returning an `Action`/`JobHandle` from outside | ||
| * their owning packages has a row. Adding a factory means adding a row, not editing a sentence. | ||
| * | ||
| * Sorted by package then name so the diff of a new row is one line. | ||
| * | ||
| * Every ROW is frozen, not just the list. `readonly` fields are a compile-time claim and this is a | ||
| * public export: freezing the array alone left `PRIMITIVE_FACTORIES[0].kind = 'entity'` a silent | ||
| * write from any untyped caller, which is the same defect `@ultimat3/money`'s currency rows | ||
| * carried — a table the framework hands out is a constant at RUNTIME or it is not a constant. | ||
| */ | ||
| export const PRIMITIVE_FACTORIES = Object.freeze<readonly PrimitiveFactory[]>( | ||
| ( | ||
| [ | ||
| { factory: 'agent', pkg: '@ultimat3/ai', kind: 'action' }, | ||
| { factory: 'agentJob', pkg: '@ultimat3/ai', kind: 'job' }, | ||
| { factory: 'hive', pkg: '@ultimat3/ai', kind: 'action' }, | ||
| { factory: 'llm', pkg: '@ultimat3/ai', kind: 'action' }, | ||
| { factory: 'backfill', pkg: '@ultimat3/jobs', kind: 'job' }, | ||
| { factory: 'scrape', pkg: '@ultimat3/scraping', kind: 'job' }, | ||
| ] satisfies readonly PrimitiveFactory[] | ||
| ).map((entry) => Object.freeze(entry)), | ||
| ); | ||
| /** | ||
| * What a registrar hands back: the primitives it actually took, each carrying the name | ||
@@ -33,0 +70,0 @@ * registration stamped on it. Returning the registered set — rather than nothing — is what lets |
+21
-3
@@ -170,3 +170,9 @@ // Single responsibility: OpenTelemetry-shaped tracing that is always on. The default exporter | ||
| /** The trace the caller is inside: active span, else the request context, else a fresh trace. */ | ||
| /** | ||
| * The trace the caller is inside: active span, else the request context, else a fresh trace. | ||
| * | ||
| * The context branch carries an EMPTY `spanId` on purpose, and that emptiness is the discriminator | ||
| * every reader must honour: it is a trace id this process minted, not a span an upstream reported. | ||
| * `traceFlags: 1` here is the header this process would send onward, never a decision it received. | ||
| */ | ||
| export function currentSpanContext(): SpanContext | undefined { | ||
@@ -180,4 +186,16 @@ const span = activeSpan.get(); | ||
| /** | ||
| * A parent an upstream actually reported, as opposed to the synthetic one `currentSpanContext()` | ||
| * builds from a request context. Only the first carries a sampling decision: reading the synthetic | ||
| * one as inbound made `parentBasedRatioSampler` inherit a bit nobody sent, so every HTTP root span | ||
| * was exported at every ratio — `pipeline.ts` is `runWithContext` then `withSpan`, which is that | ||
| * exact pair — and the one lever between "tracing is on" and "the collector melts" did nothing. | ||
| */ | ||
| function inboundParent(parent: SpanContext | undefined): SpanContext | undefined { | ||
| return parent === undefined || parent.spanId === '' ? undefined : parent; | ||
| } | ||
| export function startSpan(name: string, options?: StartSpanOptions): Span { | ||
| const parent = options?.parent ?? currentSpanContext(); | ||
| const inbound = inboundParent(parent); | ||
| const attributes: Record<string, AttributeValue> = { ...(options?.attributes ?? {}) }; | ||
@@ -191,3 +209,3 @@ // The bit is decided ONCE, here, and every child of this span inherits it through `parent` — | ||
| spanId: newSpanId(), | ||
| traceFlags: currentSampler().shouldSample(name, parent, attributes) ? 1 : 0, | ||
| traceFlags: currentSampler().shouldSample(name, inbound, attributes) ? 1 : 0, | ||
| }; | ||
@@ -251,3 +269,3 @@ const events: SpanEvent[] = []; | ||
| const endedAt = clock.now().getTime(); | ||
| const parentSpanId = parent === undefined || parent.spanId === '' ? undefined : parent.spanId; | ||
| const parentSpanId = inbound?.spanId; | ||
| exporter.export({ | ||
@@ -254,0 +272,0 @@ name, |
+20
-1
@@ -1,2 +0,3 @@ | ||
| // Compile-time pins for the actor-facts seam, the config surface and the route vocabulary. | ||
| // Compile-time pins for the actor-facts seam, the config surface, the request-context patch and | ||
| // the route vocabulary. | ||
| // Source, not a `.test.ts`, on purpose: `tsconfig.json` excludes `src/**/*.test.ts`, so `tsc -b` | ||
@@ -8,2 +9,3 @@ // never reads a test file and a type-level assertion written there can never fail. This module | ||
| import type { AppConfigInput, DatabaseConfig } from './config'; | ||
| import type { CtxPatch } from './context'; | ||
| import type { HydrateStrategy, OfflineStrategy, RenderMode } from './route-vocabulary'; | ||
@@ -104,2 +106,19 @@ | ||
| /** | ||
| * Neither id a child context may patch. `withChildContext` forwards the parent's `buildId` | ||
| * verbatim, so `{ buildId }` on the patch was an option that read as honoured and was dropped | ||
| * without a word — the same silent-no-op class as the three `database` fields above, one tier | ||
| * lower. `requestId` is here beside it because the two are refused for the same reason. | ||
| */ | ||
| type UnpatchableCtxKey = 'requestId' | 'buildId'; | ||
| type _CtxPatchRefusesTheIds = Assert< | ||
| Extract<keyof CtxPatch, UnpatchableCtxKey> extends never ? true : false | ||
| >; | ||
| /** And the keys a child MAY change are still there — a pin that empties the type is not a pin. */ | ||
| type _CtxPatchStillPatchesTheRest = Assert< | ||
| 'actor' | 'locale' | 'tz' extends keyof CtxPatch ? true : false | ||
| >; | ||
| /** | ||
| * Mutual assignability, not one-way. The tuples are load-bearing: a bare `A extends B` distributes | ||
@@ -106,0 +125,0 @@ * over a union and answers `true` for every member separately, so it cannot see a widening — which |
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
473097
4.24%73
2.82%9019
2.85%532
1.72%