@ultimat3/core
Advanced tools
| // Single responsibility: the `pwa` block of `app.config.ts` — what an app must say before a | ||
| // browser will offer to install it, and the boot-time refusal when it has not. | ||
| // | ||
| // SPLIT OUT OF `config.ts` because that file reached its 500-line ceiling, and this is the seam | ||
| // that costs nothing to cross: `enabled` turns four other requirements on, so the shape, the screen | ||
| // and the remedy are one subject. `config.ts` keeps the one call. | ||
| // | ||
| // WHY THE SCREEN IS HERE AND NOT AT EMIT. `@ultimat3/pwa`'s `generateWebManifest` refuses a blank | ||
| // title too, and that refusal arrives at `x build` — which is the wrong moment: an agent that ran | ||
| // `x dev`, saw a served app and shipped it would meet the missing title in CI. Two checks, two | ||
| // subjects (a config file, and a function argument a library caller supplies directly). | ||
| import { describeValue } from './error-render'; | ||
| // `app.config.ts` CONSUMES the route vocabulary; it does not own it — `config.ts`'s rule, and the | ||
| // reason `OfflineStrategy` is imported rather than restated. | ||
| import type { OfflineStrategy } from './route-vocabulary'; | ||
| /** | ||
| * `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; | ||
| readonly backgroundSync: boolean; | ||
| readonly push: boolean; | ||
| /** | ||
| * The install title, and the one manifest member nothing can derive. `AppConfig.name` is a slug | ||
| * (`^[a-z][a-z0-9-]{1,63}$`) and an install prompt shows a person a title, so `ledger-demo` is | ||
| * the wrong answer rather than a rough one. Required once `enabled` is true; `''` is what a | ||
| * disabled block resolves to. | ||
| */ | ||
| readonly name: string; | ||
| /** | ||
| * `theme_color` and `background_color`, per scheme. Required once `enabled` is true, and | ||
| * `undefined` otherwise — never a colour the framework picked. There is no defensible default: | ||
| * an install splash painted in a colour nobody chose is a wrong-looking app that boots, which is | ||
| * worse than a boot that names the four values it needs. | ||
| */ | ||
| readonly colors: PwaColors | undefined; | ||
| } | ||
| /** | ||
| * The install chrome's two colours for one scheme, as CSS colour strings. | ||
| * | ||
| * ONE OF THE TWO PLACES A RAW COLOUR IS LEGAL, alongside `ThemeConfig.tokens` one section up, and | ||
| * for a stronger reason than that one has: a browser paints the install splash and the address bar | ||
| * from these before a single stylesheet has loaded, so there is no token to resolve them against | ||
| * and no component anywhere in the loop. | ||
| */ | ||
| export interface PwaSchemeColors { | ||
| readonly themeColor: string; | ||
| readonly backgroundColor: string; | ||
| } | ||
| /** | ||
| * Both schemes, because a web manifest carries exactly one `theme_color` and `<head>` carries a | ||
| * media-scoped `<meta name="theme-color">` per scheme. One value would make the dark answer the | ||
| * light one's, on the surface a reader sees before the app has painted anything. | ||
| */ | ||
| export interface PwaColors { | ||
| readonly light: PwaSchemeColors; | ||
| readonly dark: PwaSchemeColors; | ||
| } | ||
| /** | ||
| * The two schemes and the two colours `validate` screens, for `INBOX_RETENTION_KEYS`' reason: a | ||
| * third member added to `PwaColors` or `PwaSchemeColors` without a row here is a value an app can | ||
| * leave blank, and `config.test.ts` asserts both lists against the types so the omission is a red | ||
| * test rather than a manifest with an empty `theme_color`. | ||
| */ | ||
| export const PWA_SCHEMES = ['light', 'dark'] as const; | ||
| export const PWA_COLOR_KEYS = ['themeColor', 'backgroundColor'] as const; | ||
| /** | ||
| * Appended only when the `pwa` block is what failed, and it carries the whole block rather than | ||
| * the missing key: `pwa.enabled` is what turns four other requirements on, so a reader who set one | ||
| * boolean needs to see the complete shape and the opt-out in the same line. | ||
| */ | ||
| export const PWA_FIX = | ||
| "in app.config.ts, complete the pwa block: pwa: { enabled: true, offline: 'runtime', name: 'My App', colors: { light: { themeColor: '#1b1f3b', backgroundColor: '#ffffff' }, dark: { themeColor: '#1b1f3b', backgroundColor: '#0b0d1a' } } } — a browser paints the install splash and the address bar from those four values before any stylesheet loads, so there is nothing for the framework to derive them from; or set pwa.enabled: false"; | ||
| /** | ||
| * Every rule `validate` applies to the block, appended to the caller's own issue list. Answers | ||
| * whether IT found anything, so the pwa remedy rides only on a pwa finding: `issues` may already | ||
| * hold a bad locale, and a fix line naming the install block for that is axiom 4 broken. | ||
| * | ||
| * `describeValue` for the reason the retention windows use it — this is where an untyped config | ||
| * object crosses into the framework, so every value here is `unknown` however the interface types it. | ||
| */ | ||
| export function pwaIssues(pwa: PwaConfig, issues: string[]): boolean { | ||
| if (!pwa.enabled) return false; | ||
| const before = issues.length; | ||
| const { name, colors } = pwa; | ||
| if (typeof name !== 'string' || name.trim() === '') { | ||
| issues.push(`pwa.name is required when pwa.enabled is true, and is ${describeValue(name)}`); | ||
| } | ||
| // `typeof !== 'object' || null`, never `=== undefined`: an untyped `app.config.ts` writing | ||
| // `pwa.colors: null` reached `null[scheme]` one line down and took the boot out with a native | ||
| // `TypeError`, from the validator whose whole job is producing an instruction instead of one. | ||
| if (colors === null || typeof colors !== 'object') { | ||
| issues.push( | ||
| `pwa.colors is required when pwa.enabled is true, for both light and dark, and is ${describeValue(colors)}`, | ||
| ); | ||
| } else { | ||
| for (const scheme of PWA_SCHEMES) { | ||
| for (const key of PWA_COLOR_KEYS) { | ||
| const value: unknown = colors[scheme]?.[key]; | ||
| if (typeof value === 'string' && value.trim() !== '') continue; | ||
| issues.push( | ||
| `pwa.colors.${scheme}.${key} must be a CSS colour, not ${describeValue(value)}`, | ||
| ); | ||
| } | ||
| } | ||
| } | ||
| return issues.length > before; | ||
| } |
+30
-17
@@ -80,13 +80,24 @@ # @ultimat3/core — agent notes | ||
| escape, so a cause could repaint the screen or hide the line above it. `@ultimat3/schema` carries a | ||
| deliberate duplicate for the tier-0 reason below, pinned behaviourally by | ||
| `single-line-pin.test.ts` in `@ultimat3/cli`. | ||
| deliberate duplicate — `schema -> core` stays forbidden, so that direction cannot be collapsed — | ||
| pinned behaviourally by `single-line-pin.test.ts`, which lives HERE `As of 2026-08-27`, at the tier | ||
| the invariant belongs to, and pins `ERROR_DOCS_URL` and the brand key beside it. | ||
| `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 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`. **A string's length is CODE POINTS in both, `As of 2026-08-22`** | ||
| **`core -> schema` is a declared edge `As of 2026-08-27`, and FIVE copies went with it.** | ||
| `describeValue`, `charCount`, `CURRENCY_CODE_PATTERN`, `SCHEMA_ERROR_CODES` and `isIanaZoneName` | ||
| were all restated here because both packages are tier 0 and neither could import the other; they | ||
| were held equal by 394 lines of pin test in `@ultimat3/cli`, a TIER-5 package pinning a tier-0 | ||
| invariant that no rule required to exist. `describeValue` is the one that made it urgent — it | ||
| prints INSTEAD of a rejected password, so the safety property of the framework's most | ||
| security-sensitive renderer rested on a 63-line behavioural pin at tier 5. `error-render.ts` | ||
| re-exports schema's now, and the four pin files are deleted. | ||
| The edge cost was MEASURED before it was declared, because axiom 6 makes it a measurement and not | ||
| an argument — `docs/architecture/01-package-map.md` carries the table. Short version: the edge | ||
| alone TRIPLED a core-only browser chunk (6,362 → 19,018 B), because importing schema's barrel with | ||
| no `sideEffects` field forces a bundler to keep every module it reaches; `@ultimat3/schema` now | ||
| declares `sideEffects: false`, which `bun run side-effects` had already measured as true of it, and | ||
| the cost drops to ~1 kB — while `moneyText` from `@ultimat3/ui`, which always carried schema, comes | ||
| out 7.8 kB SMALLER. | ||
| **A string's length is CODE POINTS, `As of 2026-08-22`** | ||
| — `validators.ts` rejects in that unit and `json-schema.ts` publishes `minLength` in it, so | ||
@@ -129,3 +140,3 @@ `.length` made `t.string.min(3).safeParse('👍a')` say "at least 3 chars, received a string of 3 | ||
| | a value that must not be printed | `secret.ts` | redacted by VALUE; `revealSecret()` is the one way out, on purpose greppable | | ||
| | an `Intl` formatter cache, and the screen in front of it | `intl-cache.ts` (`cachedFormatter`, `canonicalLocale`, `assertLocale`, `MAX_CACHED_FORMATTERS`) | a locale and a zone arrive from a request header, so the tag must be REFUSED when it is not a tag (`X_LOCALE_INVALID`), the key must be canonical AND the cache bounded — never a second copy of any of the three | | ||
| | an `Intl` formatter cache, and the screen in front of it | `intl-cache.ts` (`cachedFormatter`, `canonicalLocale`, `assertLocale`, `MAX_CACHED_FORMATTERS`, `MAX_LOCALE_EXCERPT`) | a locale and a zone arrive from a request header, so the tag must be REFUSED when it is not a tag (`X_LOCALE_INVALID`), the key must be canonical AND the cache bounded — never a second copy of any of the three. The refusal is bounded too: the `cause` quotes back at most `MAX_LOCALE_EXCERPT` (35, RFC 5646 §4.4.1) code points and says so when it cut, and the whole tag rides in `meta.locale` — a `cause` reaches the 400 body and the log line, where a value with no key has nothing a redactor can address | | ||
| | the committed encrypted values | `secrets.ts` (envelope) + `secrets-store.ts` (files, `installSecrets`) | plaintext is a flat map of ENV NAMES; there is no `secrets.get()` | | ||
@@ -167,8 +178,10 @@ | ||
| `schema-error-codes.ts` is the same shape a second time, for codes this package does not even own. | ||
| `@ultimat3/schema` is tier 0 like `core` and so can neither call `registerErrorCodes()` itself nor | ||
| import core to reach it — the four codes' titles are a deliberate, tested duplicate of | ||
| `SCHEMA_ERROR_CODES` in `packages/schema/src/errors.ts`, registered unconditionally at import time | ||
| so any process that imports core (not just `@ultimat3/cli`, which used to be the only registrant) | ||
| renders schema's real titles. Neither tier-0 package can check the duplicate against its source, so | ||
| the pin (`schema-error-codes-pin.test.ts`) lives in `@ultimat3/cli`, which may legally import both. | ||
| `@ultimat3/schema` is tier 0 like `core` and cannot call `registerErrorCodes()` itself — that would | ||
| be `schema -> core`, which stays forbidden. So core READS `SCHEMA_ERROR_CODES` over the declared | ||
| edge and registers it unconditionally at import time, and any process that imports core (not just | ||
| `@ultimat3/cli`, which used to be the only registrant) renders schema's real titles. The retry | ||
| classification is derived from the same set rather than typed out beside it: a fifth code would | ||
| have been silently absent from a hand-written list, and an unregistered code reads as UNCLASSIFIED, | ||
| so a schema refusal inside a job body burns the whole retry policy re-proving an answer no attempt | ||
| can change. | ||
@@ -175,0 +188,0 @@ `timing-safe-equal.ts` holds the one constant-time string comparison `@ultimat3/auth` and |
+5
-3
| { | ||
| "name": "@ultimat3/core", | ||
| "version": "17.0.0", | ||
| "version": "18.0.0", | ||
| "description": "Ultimate's foundation: errors, context, env, config, clock, ids, logging, telemetry, lifecycle", | ||
@@ -33,3 +33,3 @@ "license": "MIT", | ||
| "engines": { | ||
| "bun": ">=1.3.0" | ||
| "bun": ">=1.4.0" | ||
| }, | ||
@@ -40,3 +40,5 @@ "scripts": { | ||
| }, | ||
| "dependencies": {} | ||
| "dependencies": { | ||
| "@ultimat3/schema": "18.0.0" | ||
| } | ||
| } |
+12
-0
@@ -33,2 +33,3 @@ # 🧱 @ultimat3/core | ||
| | `defineConfig()` for `app.config.ts` | `config.ts` | | ||
| | the `pwa` block — what an install needs, and the boot refusal when it is not there | `config-pwa.ts` | | ||
| | the closed route vocabulary every renderer names | `route-vocabulary.ts` | | ||
@@ -491,2 +492,13 @@ | runtime roles + `ROLE` resolution | `roles.ts` | | ||
| The **refusal** is bounded for the same reason the cache is. `X_LOCALE_INVALID` answers 400 and | ||
| `@ultimat3/http`'s `toProblem` copies a `cause` into the response `detail` **and** into the log | ||
| line, so a tag echoed verbatim is a stranger-chosen string of unbounded length in a shared log | ||
| index. `localeInvalid` quotes back the first `MAX_LOCALE_EXCERPT` (35) code points — RFC 5646 | ||
| §4.4.1's own figure for the longest tag the registry can form — and appends | ||
| `(truncated at 35 characters)` when there were more, so a cut value is never read as a whole one. | ||
| `describeValue` is deliberately **not** used here: "a 5-character string" deletes the only | ||
| actionable content in a sentence whose job is to name the tag. The whole tag rides in | ||
| `meta.locale`, which is the half that gives a redactor a key to address; a value spliced into | ||
| prose has none. | ||
| ## One flight layer — wait, classify, share, bound, fence | ||
@@ -493,0 +505,0 @@ |
+74
-25
@@ -5,2 +5,3 @@ // Single responsibility: `app.config.ts` — the one config file. Deeply optional with real | ||
| import { CURRENCY_CODE_PATTERN } from '@ultimat3/schema'; | ||
| // Same rule for the same reason: `app.config.ts` CONSUMES the cache tier names, it does not own | ||
@@ -10,8 +11,7 @@ // them. Declaring them here is what let `cache.tiers` and the ladder `@ultimat3/cache` orders by | ||
| import { CACHE_TIERS, type CacheTierName } from './cache-vocabulary'; | ||
| import type { PwaConfig } from './config-pwa'; | ||
| import { PWA_FIX, pwaIssues } from './config-pwa'; | ||
| import { describeValue } from './error-render'; | ||
| import { ConfigInvalidError } from './errors'; | ||
| import { ROLES, type Role } from './roles'; | ||
| // `app.config.ts` CONSUMES the route vocabulary; it does not own it. Declaring `OfflineStrategy` | ||
| // here is what made it copyable — `render`, `manifest` and `pwa` each wrote their own rather than | ||
| // import a name that reads like a config key. | ||
| import type { OfflineStrategy } from './route-vocabulary'; | ||
| import { isIanaZoneName } from './time-zone-name'; | ||
@@ -46,14 +46,34 @@ | ||
| /** | ||
| * `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. | ||
| * Retention for `x_notify_inbox`, and the one framework table whose window the framework may not | ||
| * pick. Every other one holds bookkeeping whose job ENDS — an idempotency key, a rate-limit | ||
| * bucket, an auth challenge, a delivery claim — and a sweep is unambiguously right. An inbox row | ||
| * is a message a person has not read yet, so when it disappears is a product decision, which is | ||
| * axiom 8: Ultimate ships mechanism, your app ships convention. | ||
| * | ||
| * BOTH DEFAULT TO `undefined`, meaning never swept, and that is the only safe default here: the | ||
| * failure mode of keeping rows is a table that grows, and the failure mode of guessing a number is | ||
| * a notification the recipient never got to read. | ||
| * | ||
| * TWO WINDOWS RATHER THAN ONE, because the axiom-8 objection is only about UNREAD messages. An app | ||
| * that wants read notices gone in a month and unread ones kept forever must be able to say exactly | ||
| * that, and `SQL_NOTIFY_INBOX_MARK_READ` already stamps the time that makes it expressible. | ||
| * | ||
| * Milliseconds and not a `DurationInput`: that type lives in `@ultimat3/jobs` (tier 3) and this | ||
| * package is tier 0, which is the same reason `cache.defaultTtlMs` and `jobs.visibilityTimeoutMs` | ||
| * are spelled this way. | ||
| */ | ||
| export interface PwaConfig { | ||
| readonly enabled: boolean; | ||
| readonly offline: OfflineStrategy; | ||
| readonly backgroundSync: boolean; | ||
| readonly push: boolean; | ||
| export interface NotifyConfig { | ||
| /** Age of a row's `read_at`, not its `created_at` — a row ages from when it was READ. */ | ||
| readonly inboxReadRetentionMs: number | undefined; | ||
| /** Age of an unread row's `created_at`. Setting this deletes messages nobody has read. */ | ||
| readonly inboxUnreadRetentionMs: number | undefined; | ||
| } | ||
| /** | ||
| * Every window `validate` screens, derived from nothing — a third one added to `NotifyConfig` | ||
| * without a row here is a window an app can set to `-1`, and `config.test.ts` asserts the two | ||
| * lists agree so the omission is a red test rather than a silent hole. | ||
| */ | ||
| export const INBOX_RETENTION_KEYS = ['inboxReadRetentionMs', 'inboxUnreadRetentionMs'] as const; | ||
| /** | ||
| * Deliberately thin. `urlEnv`, `poolSize` and `schema` were removed 2026-08 because **nothing | ||
@@ -172,2 +192,3 @@ * read them** — the only reader of any `config.database.*` field in the repo was this file's own | ||
| readonly realtime: RealtimeConfig; | ||
| readonly notify: NotifyConfig; | ||
| readonly ai: AiConfig; | ||
@@ -197,2 +218,3 @@ } | ||
| readonly realtime?: Input<RealtimeConfig> | undefined; | ||
| readonly notify?: Input<NotifyConfig> | undefined; | ||
| readonly ai?: AiConfigInput | undefined; | ||
@@ -220,14 +242,9 @@ } | ||
| /** | ||
| * A deliberate duplicate of `CURRENCY_CODE_PATTERN` in `packages/schema/src/money-value.ts`, which | ||
| * is the framework's ONE declaration of what an ISO 4217 code looks like and the source | ||
| * `isCurrencyCode`, the published OpenAPI `pattern` and `@ultimat3/entity`'s Postgres CHECK all | ||
| * derive from. This file cannot import it: `core` and `schema` are both tier 0 and `core → schema` | ||
| * is not in `SIDEWAYS_ALLOW` (`scripts/lib/tiers.ts`), the same wall that makes `describeValue` a | ||
| * character-for-character copy in `error-render.ts`. | ||
| * | ||
| * So keep the two identical, and keep the pattern inside the syntax ECMAScript, JSON Schema and | ||
| * POSIX ERE spell identically — a `defaultCurrency` this accepts and `t.money` refuses is an app | ||
| * whose configured currency cannot be written to a row. | ||
| * Built from `@ultimat3/schema`'s `CURRENCY_CODE_PATTERN`, the framework's ONE declaration of what | ||
| * an ISO 4217 code looks like — the same source `isCurrencyCode`, the published OpenAPI `pattern` | ||
| * and `@ultimat3/entity`'s Postgres CHECK all derive from. It was a character-for-character copy | ||
| * here until the `core -> schema` edge was declared (`scripts/lib/tiers.ts`), held equal only by a | ||
| * pin test in `@ultimat3/cli`. | ||
| */ | ||
| const CURRENCY_RE = /^[A-Z]{3}$/; | ||
| const CURRENCY_RE = new RegExp(CURRENCY_CODE_PATTERN); | ||
@@ -250,3 +267,10 @@ function isLocale(value: string): boolean { | ||
| auth: { signInPath: null }, | ||
| pwa: { enabled: false, offline: 'network-only', backgroundSync: false, push: false }, | ||
| pwa: { | ||
| enabled: false, | ||
| offline: 'network-only', | ||
| backgroundSync: false, | ||
| push: false, | ||
| name: '', | ||
| colors: undefined, | ||
| }, | ||
| roles: [...ROLES], | ||
@@ -263,2 +287,3 @@ database: { driver: 'postgres', ssl: false }, | ||
| realtime: { enabled: false, transport: 'memory', urlEnv: undefined }, | ||
| notify: { inboxReadRetentionMs: undefined, inboxUnreadRetentionMs: undefined }, | ||
| ai: { mcp: { expose: true, path: '/mcp' } }, | ||
@@ -295,2 +320,4 @@ }; | ||
| const tierFix: string[] = []; | ||
| // Same shape again: carried only when an installable app is missing what an install needs. | ||
| const pwaFix: string[] = []; | ||
@@ -325,2 +352,23 @@ if (!NAME_RE.test(config.name)) { | ||
| } | ||
| // BOTH RETENTION WINDOWS OR NEITHER — `undefined` is a real value here (never swept) and the | ||
| // only other legal one is a positive, finite count of milliseconds. Zero is refused rather than | ||
| // read as "immediately": a sweep at age 0 deletes every row the instant it is written, which is | ||
| // an inbox that silently receives nothing, and nobody types it on purpose. `describeValue` | ||
| // rather than `${…}`: this validator is the boundary a JS app's untyped config crosses, so the | ||
| // value here is `unknown` in practice however the interface types it. | ||
| for (const key of INBOX_RETENTION_KEYS) { | ||
| const ms: unknown = config.notify[key]; | ||
| if (ms === undefined) continue; | ||
| if (typeof ms !== 'number' || !Number.isFinite(ms) || ms <= 0) { | ||
| issues.push( | ||
| `notify.${key} must be a positive number of milliseconds, not ${describeValue(ms)}`, | ||
| ); | ||
| } | ||
| } | ||
| // What an install needs, asked at BOOT and not at emit — `config-pwa.ts` owns the rules and the | ||
| // remedy, because `pwa.enabled` turning four other requirements on is a question about that block | ||
| // and nothing else here. | ||
| if (pwaIssues(config.pwa, issues)) pwaFix.push(PWA_FIX); | ||
| // A rung the ladder cannot build is the defect this key had: `sortTiers` places a name by its | ||
@@ -340,3 +388,3 @@ // index in `CACHE_TIERS`, and a name missing from it sorts to `-1` — AHEAD of the request memo. | ||
| // pasted — a trailing `.` after `x verify` is a command nobody can run. | ||
| fix: [...zoneFix, ...tierFix, BASE_FIX].join('. '), | ||
| fix: [...zoneFix, ...tierFix, ...pwaFix, BASE_FIX].join('. '), | ||
| meta: { issues }, | ||
@@ -377,2 +425,3 @@ }); | ||
| realtime: section(base.realtime, merged.realtime), | ||
| notify: section(base.notify, merged.notify), | ||
| ai: { mcp: section(base.ai.mcp, merged.ai?.mcp) }, | ||
@@ -379,0 +428,0 @@ }; |
+10
-53
@@ -255,55 +255,12 @@ // Single responsibility: turn a value the framework does not control into text for an error's | ||
| * | ||
| * 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. | ||
| * DECLARED IN `@ultimat3/schema` and re-exported here, `As of 2026-08-27`. It was a | ||
| * character-for-character copy of `packages/schema/src/describe-value.ts` — the two tier-0 | ||
| * packages could not import each other — held equal by a 63-line behavioural pin in | ||
| * `@ultimat3/cli`, a TIER-5 package, that no rule required to exist. Whose function it is has not | ||
| * changed: schema's `expected()` is the other reader, and a widening that leaks a value is now one | ||
| * edit in one file rather than two files that can drift apart in silence. | ||
| * | ||
| * `charCount` went with it — the same duplication, one call deep — so the unit a message quotes | ||
| * and the unit a length rule counts in are the same code, not two files that agree today. | ||
| */ | ||
| export function describeValue(value: unknown): string { | ||
| if (value === undefined) return 'undefined'; | ||
| if (value === null) return 'null'; | ||
| switch (typeof value) { | ||
| case 'string': | ||
| return countOf(charCount(value), 'string', 'character'); | ||
| case 'number': | ||
| return describeNumber(value); | ||
| case 'boolean': | ||
| return 'a boolean'; | ||
| case 'bigint': | ||
| return 'a bigint'; | ||
| case 'symbol': | ||
| return 'a symbol'; | ||
| case 'function': | ||
| return 'a function'; | ||
| default: | ||
| break; | ||
| } | ||
| if (Array.isArray(value)) return countOf(value.length, 'array', 'item'); | ||
| // `getTime()` rather than a value: an invalid Date is the one Date fact a caller can act on. | ||
| if (value instanceof Date) return Number.isNaN(value.getTime()) ? 'an invalid Date' : 'a Date'; | ||
| return 'an object'; | ||
| } | ||
| function describeNumber(value: number): string { | ||
| if (Number.isNaN(value)) return 'NaN'; | ||
| if (value === Number.POSITIVE_INFINITY) return 'Infinity'; | ||
| if (value === Number.NEGATIVE_INFINITY) return '-Infinity'; | ||
| return 'a number'; | ||
| } | ||
| function countOf(size: number, noun: string, unit: string): string { | ||
| if (size === 0) return `an empty ${noun}`; | ||
| const article = noun === 'array' ? 'an' : 'a'; | ||
| return `${article} ${noun} of ${size} ${unit}${size === 1 ? '' : 's'}`; | ||
| } | ||
| /** | ||
| * 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; | ||
| } | ||
| export { describeValue } from '@ultimat3/schema'; |
+5
-2
@@ -68,3 +68,3 @@ // Single responsibility: the public API of @ultimat3/core. Explicit named exports only — | ||
| McpConfig, | ||
| PwaConfig, | ||
| NotifyConfig, | ||
| RealtimeConfig, | ||
@@ -75,3 +75,5 @@ RealtimeTransport, | ||
| } from './config'; | ||
| export { defineConfig } from './config'; | ||
| export { defineConfig, INBOX_RETENTION_KEYS } from './config'; | ||
| export type { PwaColors, PwaConfig, PwaSchemeColors } from './config-pwa'; | ||
| export { PWA_COLOR_KEYS, PWA_SCHEMES } from './config-pwa'; | ||
| export type { Ctx, CtxFacts, CtxInit, CtxPatch, CtxServices, ServiceBag } from './context'; | ||
@@ -466,2 +468,3 @@ export { | ||
| MAX_CACHED_FORMATTERS, | ||
| MAX_LOCALE_EXCERPT, | ||
| } from './intl-cache'; | ||
@@ -468,0 +471,0 @@ export { isJsonObject } from './json-object'; |
+47
-1
@@ -31,2 +31,12 @@ // One bounded cache, on one canonical key, and one screen, for every `Intl` formatter the | ||
| /** | ||
| * The cap on the tag an `X_LOCALE_INVALID` cause quotes back, in code points. | ||
| * | ||
| * 35 is RFC 5646 §4.4.1's own number — Figure 7 derives it as the longest tag the registry can | ||
| * form (language 8 + script 5 + region 4 + two variants 9+9), and the same section says a protocol | ||
| * with a fixed buffer "MUST allow for language tags of at least 35 characters". So every tag a | ||
| * caller could legitimately have meant fits, and nothing longer is a tag being debugged. | ||
| */ | ||
| export const MAX_LOCALE_EXCERPT = 35; | ||
| /** | ||
| * The canonical BCP 47 spelling, or `undefined` when the tag is not structurally valid at all | ||
@@ -57,4 +67,10 @@ * (`en_US`, `''`, `not a locale`). Well-formed but unknown to ICU (`zz`) is a locale — `Intl` | ||
| code: 'X_LOCALE_INVALID', | ||
| cause: `"${locale}" is not a well-formed BCP 47 language tag`, | ||
| cause: `${localeExcerpt(locale)} is not a well-formed BCP 47 language tag`, | ||
| fix: "pass a tag like 'en', 'en-GB' or 'de-DE' — screen a header-supplied value with Intl.DateTimeFormat.supportedLocalesOf([tag]) before it reaches a formatter", | ||
| // The raw tag, under a NAME. A `cause` is copied into the 400 body and into the log line by | ||
| // `@ultimat3/http`'s `toProblem`, and a logger redacts by key — so a caller's value spliced | ||
| // into prose has no key left to redact, which is the whole argument `describeValue` rests on. | ||
| // `meta` is the key, and it is machine-read, so it carries the value WHOLE: an excerpt here | ||
| // would be a value a redactor or a bug report reads as complete when it is not. | ||
| meta: { locale }, | ||
| }); | ||
@@ -64,2 +80,32 @@ } | ||
| /** | ||
| * The tag as a reader can act on it: the first `MAX_LOCALE_EXCERPT` code points, quoted, and SAID | ||
| * to be cut when there were more. | ||
| * | ||
| * `describeValue` is the usual answer for a caller-supplied value in a `cause` and it is the wrong | ||
| * one here — it renders `en_US` as "a 5-character string", deleting the only actionable content in | ||
| * a sentence whose entire job is to say WHICH tag was refused. Bounding it keeps the diagnostic and | ||
| * removes the part a stranger chooses: without a cap the whole of an `Accept-Language` value — | ||
| * megabytes, at the caller's option — became the error's `message`, its 400 body and its log line. | ||
| * | ||
| * Code points, never `slice`: cutting between a surrogate pair leaves a lone surrogate, which is | ||
| * not text and survives no encoder between here and the log index. The string iterator is lazy, so | ||
| * a megabyte tag costs 35 steps rather than a megabyte-long array. | ||
| * | ||
| * No escaping here: `UltimateError`'s constructor runs `singleLine` over every line-bearing field | ||
| * exactly once, which is what keeps a newline in a tag from writing a second line an operator reads | ||
| * as a genuine framework message. A second pass at this call site would be a second place that has | ||
| * to be right. | ||
| */ | ||
| function localeExcerpt(locale: string): string { | ||
| let head = ''; | ||
| let taken = 0; | ||
| for (const char of locale) { | ||
| if (taken === MAX_LOCALE_EXCERPT) return `"${head}" (truncated at ${taken} characters)`; | ||
| head += char; | ||
| taken += 1; | ||
| } | ||
| return `"${head}"`; | ||
| } | ||
| /** | ||
| * The canonical spelling of a well-formed tag, or `X_LOCALE_INVALID`. The ONE screen a | ||
@@ -66,0 +112,0 @@ * caller-supplied BCP 47 tag passes before it reaches an `Intl` constructor. |
| // Single responsibility: register `@ultimat3/schema`'s error codes so their titles render for any | ||
| // process that imports `@ultimat3/core` — not just the CLI. `@ultimat3/schema` is tier 0 alongside | ||
| // this package, so it cannot call `registerErrorCodes()` itself (that would mean importing core, | ||
| // a same-tier import) and this package cannot import schema to read its declarations back (same | ||
| // reason, the other direction). The codes below are a deliberate, tested duplicate of | ||
| // `SCHEMA_ERROR_CODES` in `packages/schema/src/errors.ts` — `schema-error-codes-pin.test.ts`, in a | ||
| // package that may legally import both (`@ultimat3/cli`), asserts them equal so a title edited in | ||
| // one place and not the other fails the build instead of quietly disagreeing at runtime. | ||
| // this package and cannot call `registerErrorCodes()` itself: that would be `schema -> core`, an | ||
| // import this package's own dependency-free promise forbids in that direction and always will. | ||
| // The other direction is DECLARED (`core -> schema`, `scripts/lib/tiers.ts`), so the titles are | ||
| // READ from schema rather than restated here. | ||
| // | ||
| // They were a hand-kept duplicate until 2026-08-27, held equal by `schema-error-codes-pin.test.ts` | ||
| // in `@ultimat3/cli` — a tier-5 package pinning a tier-0 invariant, which nothing required to | ||
| // exist. It is deleted with this change. | ||
| import { SCHEMA_ERROR_CODES } from '@ultimat3/schema'; | ||
| import { registerErrorCodes } from './error-codes'; | ||
| import { registerErrorRetry } from './error-retry'; | ||
| /** Mirrors `SCHEMA_ERROR_CODES` in `packages/schema/src/errors.ts`. Keep the titles identical. */ | ||
| export const SCHEMA_ERROR_CODE_TITLES: Readonly<Record<string, string>> = Object.freeze({ | ||
| X_VALIDATION_FAILED: 'value did not match its schema', | ||
| X_SCHEMA_UNSUPPORTED: 'the active schema provider cannot do this', | ||
| X_SCHEMA_DISCRIMINANT_INVALID: 'a discriminated union member can never be dispatched to', | ||
| X_SCHEMA_DEFAULT_UNSHAREABLE: 'a schema default cannot be copied per parse', | ||
| }); | ||
| /** | ||
| * Schema's own declarations, projected to titles. Kept as an export because it is public API | ||
| * shipped since 1.0; it is now DERIVED and can no longer disagree with its source. | ||
| */ | ||
| export const SCHEMA_ERROR_CODE_TITLES: Readonly<Record<string, string>> = Object.freeze( | ||
| Object.fromEntries( | ||
| Object.entries(SCHEMA_ERROR_CODES).map(([code, declared]) => [code, declared.title]), | ||
| ), | ||
| ); | ||
@@ -24,8 +29,4 @@ // Registered here rather than in `error-codes.ts`'s `CORE_CODE_TITLES` because core does not own | ||
| // raises `X_ERROR_CODE_DUPLICATE` if a package that DOES own one of them ever tries to register it | ||
| // too, which pins ownership even though the titles live in two files. | ||
| registerErrorCodes( | ||
| Object.fromEntries( | ||
| Object.entries(SCHEMA_ERROR_CODE_TITLES).map(([code, title]) => [code, { title }]), | ||
| ), | ||
| ); | ||
| // too, which pins ownership even though the registration happens here. | ||
| registerErrorCodes(SCHEMA_ERROR_CODES); | ||
@@ -45,7 +46,8 @@ // And how each is RETRIED, here for the same tier reason the titles are here. | ||
| // A value that does not match its schema does not match it on attempt five either. | ||
| registerErrorRetry({ | ||
| X_VALIDATION_FAILED: 'terminal', | ||
| X_SCHEMA_UNSUPPORTED: 'terminal', | ||
| X_SCHEMA_DISCRIMINANT_INVALID: 'terminal', | ||
| X_SCHEMA_DEFAULT_UNSHAREABLE: 'terminal', | ||
| }); | ||
| // | ||
| // DERIVED from the same set, so a code schema adds cannot arrive unclassified: the list was typed | ||
| // out here and a fifth code would have been silently missing from it, which is precisely the | ||
| // "unregistered reads as unclassified" failure the paragraph above describes. | ||
| registerErrorRetry( | ||
| Object.fromEntries(Object.keys(SCHEMA_ERROR_CODES).map((code) => [code, 'terminal' as const])), | ||
| ); |
+13
-42
@@ -1,43 +0,14 @@ | ||
| // Single responsibility: is a string an IANA zone NAME? Tier 0's statement of the one rule | ||
| // `@ultimat3/time` enforces everywhere above it — a zone is `Area/Location`, and `UTC` is the one | ||
| // exception. It is stated twice because `core` is tier 0 and may not import `@ultimat3/time`; | ||
| // `packages/time/src/zone-canonical.ts` is where the rule and its reasoning are written down. | ||
| // Single responsibility: re-export `@ultimat3/schema`'s IANA zone-name predicate, so `app.config.ts` | ||
| // and `t.timezone` judge a zone with ONE function. | ||
| // | ||
| // This file held a character-for-character copy of it until 2026-08-27 — `core` and `schema` are | ||
| // both tier 0 and neither could import the other — kept equal by a 123-line pin test in | ||
| // `@ultimat3/cli`, a tier-5 package pinning a tier-0 invariant that no rule required to exist. | ||
| // `core -> schema` is now a declared edge (`scripts/lib/tiers.ts`), so the copy is gone. | ||
| // | ||
| // The rule itself, and why a bare `new Intl.DateTimeFormat(…)` probe is not it — ICU 78 resolves | ||
| // `CET`, `EST`, `Japan`, `GMT` and `Zulu`, so `defaultTimeZone: 'CET'` passed validation at boot | ||
| // and threw `X_TIMEZONE_INVALID` on the first `format` call (issue #257) — is written down in | ||
| // `packages/schema/src/time-zone-name.ts` and in `packages/time/src/zone-canonical.ts`. | ||
| /** | ||
| * A LEADING sign is a fixed offset, which carries no DST rules. `Etc/GMT+2` keeps its `+`. | ||
| * | ||
| * Unobservable on ICU 78 — `+01:00` resolves to itself, so the slash rule below already refuses it, | ||
| * and deleting this line changes no answer this package can currently produce. It stays because it | ||
| * guards the runtime that folds an offset into `Etc/GMT-1`, which WOULD carry a slash, and because | ||
| * `packages/time/src/zone-canonical.ts` carries the same line: two statements of one rule may not | ||
| * differ, least of all in the half that is hard to test. | ||
| */ | ||
| const NUMERIC_OFFSET = /^[+-]/; | ||
| /** | ||
| * Structural, and never delegated to `Intl` — the reasoning, and why a denylist is not the | ||
| * alternative, is `packages/time/src/zone-canonical.ts`'s and is not re-derived here. What this | ||
| * file enforces is that same rule for `app.config.ts`: an identifier is `Area/Location`, `UTC` is | ||
| * the one legal exception, and a leading sign is an offset rather than a name. | ||
| * | ||
| * It exists because a bare `new Intl.DateTimeFormat(…)` probe was the second answer to that | ||
| * question, and the two stopped agreeing: ICU 78 (Bun 1.4) resolves `CET`, `EST`, `Japan`, `GMT`, | ||
| * `Zulu` and the whole `backward`/abbreviation family, so `defaultTimeZone: 'CET'` passed validation | ||
| * at boot and threw `X_TIMEZONE_INVALID` on the first `format` call — a config file accepting a | ||
| * value nothing downstream can use (issue #257). | ||
| * | ||
| * The judgement is made on the RESOLVED name, not the input, so a casing (`utc`) and a runtime that | ||
| * folds an alias into its target (`Etc/UTC` → `UTC`) both answer correctly. No cache: this is read | ||
| * once per process at config validation, never off a request header — a zone that arrives from a | ||
| * caller goes through `@ultimat3/time`'s `canonicalTimeZone`, which is bounded and canonicalizing. | ||
| */ | ||
| export function isIanaZoneName(value: string): boolean { | ||
| if (value === '' || NUMERIC_OFFSET.test(value)) return false; | ||
| try { | ||
| const resolved = new Intl.DateTimeFormat('en-US', { timeZone: value }).resolvedOptions() | ||
| .timeZone; | ||
| return resolved === 'UTC' || resolved.includes('/'); | ||
| } catch { | ||
| return false; | ||
| } | ||
| } | ||
| export { isIanaZoneName } from '@ultimat3/schema'; |
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.
601510
1.84%88
1.15%10929
1.31%621
1.97%1
Infinity%+ Added
+ Added