Upgrading - developerz-ai/ultimate GitHub Wiki
As of 2026-08. Semver applies from here. A breaking change to a documented API needs a major. Every @ultimat3/* version is pinned exactly and moves in lockstep โ never mix versions.
Twenty-six majors have shipped, and this page walks all twenty-six โ 2.0.0's 33 entries joined it As of 2026-08, and scripts/changelog-check.ts now refuses a summary row whose section the page does not carry, which is how they were missing for six releases. CHANGELOG.md is the source for the majors it still carries, and git show v<tag>:CHANGELOG.md for the ones it has archived; none ships a codemod, so every entry is a manual edit the entry itself names. One section per major, newest first โ read the ones between your pin and your target, oldest first.
| From โ to | Breaking entries | Read |
|---|---|---|
| 26.x โ 27.0.0 |
2 โ AsyncContext.exit required; /_storage and /media refuse a key outside every org/<id>/ prefix unless defineStorage({ shared }) lists its prefix, and Storage.isShared required |
the 27.0.0 section, in order |
| 25.x โ 26.0.0 |
6 โ an OAuth login that verifies the id token's signature against the provider's published key set by default; Apple out of the OAuth built-ins; ServerHandle.describe() deleted; no api/**/route.ts file kind; recover: 'agent' deleted and scraping at tier 4; one CLI verb per command (list, show, delete) |
the 26.0.0 section, in order |
| 24.x โ 25.0.0 |
40 โ Bun 1.4.2 as the floor; a closed app.config.ts that refuses an undeclared key and every deleted one by name, the locales in defineCatalogs(); assertEnvExample, resolveSpeculation and normalizeAuditRecord removed, AuditRecord.name / .primitive required, every value imported from the package that declares it; one spelling per factory, never create<Thing>; one Page (entity's Page and storage's ListPage removed), http.drainTimeoutMs and http.buildIdHeader removed, a bearerMount catch-all; one MCP projection (toolFrom), one page shape (nextCursor, hasMore), a per-action rate limit that is the action's own, a redaction-keeping IdempotencyStore, SQL_* statements off the barrels, the NATS job driver stub deleted, required WebhookLedger.isDisabled and ChangeEvent.write; one MCP block in core with McpExposure and its name removed, contentHash removed, a scopes map that names every tool, a required DigestAppend.appender; no model chosen for an app โ it registers its own and names one; a widened Menu aria-controls, a private FakeElement.listeners, offline scrapes that state their robots reason, nearest removed, a --cdp-url capture that fails closed, a page that may not import a repo, ai's contentHash renamed promptHash, one meaning per value name, every job ack counted; a per-queue jobs.concurrency, the framework's own secrets in .env.example, an e2e app that inherits no DATABASE_URL; a two-argument resolveGrants, WORKER_QUEUES as the exact set, a jobWorker default of 8 slots, and ChangeEvent.table
|
the 25.0.0 section, in order |
| 23.x โ 24.0.0 |
202 โ a calendar check on t.date, t.url refusing what the parser would cut, plain objects only, a default its own schema must accept, decimal-only coercion, a stricter defineConfig, an unknown LOG_LEVEL refused, retry and createFlightGate refusing a bound that is not one, a child context that aborts with its parent, compound credential names redacted, error meta under extra.meta in the monitor envelope, per-signal OTLP headers, a sampler that ignores a leftover ratio, wildcard host rules that stop at the network edge, an empty cursor secret counted as unset; then tier 1 โ t() always interpolating, interval crons through both passes of a fall-back hour, exact cron names, formatRelative requiring a zone, a transaction that rejects when its body swallowed a failed statement, X_DB_COMMIT_UNKNOWN, nested transaction options refused, sibling nested scopes run in turn under a 30 s wait, a changed-primary-key drift kind, introspect() reporting catalog types, flag expiries that must be ISO; then an e:<entity> purge key on every tagged response, WebP-only responsiveImage() by default, promoteAttachment requiring its policy, a required StorageDriver.stat(), an optional lastModified, a get() ceiling, image variant keys that keep the source extension, v2 signed URLs that name their disk; then a required Driver.transactor(), dbDrift leaving entity, a preload ceiling, assertAllowed throwing the decision's own code, a 401 for a denial with no actor, a malformed policy decision that denies; then ctx.peer behind its own switch, anonymous browser writes held to same-origin, failed sign-ins metered to a 429, health bodies trimmed for strangers, a max-age treated as a shared-cache offer, a body refused without a content-type, an awaited browser close(); then auth โ a sealed MFA secret that needs x auth seal-mfa run once, a retired x_auth_failures table, a reservation-shaped AuthLimiter, API keys bound to their owner and its grants, X_MFA_REQUIRED carrying a challenge, eight more required AuthAdapter members, oauthLogin requiring APP_URL, an e2e offline() that rejects when a page refuses the switch; then compareValues removed from query, a .limit() that bounds every page, a single read answering one row over MCP, admin:* declared by defineAdmin() rather than by import; then mutator() requiring idempotent: true, a regenerated openapi.json and schema dump, idempotent actions and cache busts that settle with the commit, a manifest step that fails on a stale openapi.json; then jobs โ a final-attempt lapse buried instead of re-claimed, cancel refusing finished jobs, step writes fenced on the claim, a uuid-only runId, an awaited purgeExpired(), x_job_events swept; then realtime โ three operator steps: APP_URL on the sync role as the whole origin allow-list, REPLICA IDENTITY FULL for channels with params, and a replication connection that refuses a weak password without TLS โ rows revived across the bus, five interfaces with new required members, stricter channel re-authorization, and an offline queue abandoned when the principal changes; then a catch-all route that answers its bare prefix, a zero-length ISR ttl refused, NativeReason gaining 'unparsable', CSS-module classes scoped in selectors only with every scoped name changed once, a positional CompiledPattern.specificity, and a notification tap that opens this app only; then ui โ two required ThemeEnv members, a fixed theme.defaultMode that a "System" pick and an OS change no longer override, the deprecated theme inline script removed, a Popover trigger whose aria-controls can be absent, and file controls that post only the files they accepted; then a required peek on every rate-limit store and an address refused on required routes once its failed credentials are spent; then mcp โ a 5xx cause hidden from a remote caller, db.query refusing SQL run as text, a meta tool without destructive listed as a query, list calls held to their whitelist, failed tokens metered per address; then a send() that waits for its transaction's commit and one recipient rule on every mail driver; then x_notify_digests in the schema dump, queries publishing input, a non-finite manifest fact refused, and a dropped NOT NULL default classed as breaking; then ai โ an atomic BudgetStore.take, a required Gateway.callLedger(keys), gateway actor and org ceilings that count every model call's caller, a 5xx tool cause hidden from the model, fnv1a removed and HashEmbedder vectors changed, an unknown finish reason read as truncated, an unscoped vector read refused inside an org's request, a required VectorStore.prune, chunk() owning source, and numericTolerance refusing a bound that is not one; then testing โ a coded app() before boot, assertDeterministic by canonical form, frozenClock announcing its moves, one module instance per island mount; then scraping โ a proxy switch in launch args refused, a required browser-level target(), a jar-less browser refused, burnSession comparing savedAt, maxDrop held to a fraction, credential-shaped URL redaction, a declared header that replaces the session's, wildcard hosts resolved and pinned, robots redirects screened per hop, and an undated recording read as stale; then admin โ a nullable keyset bound, edit forms that post _version and a 409 for a stale edit, a required AdminFormProps.version, a 'stale' CrudResult, creates and updates decided on what they write, tenant-scoped audit screens, actions and batches that commit with their audit entry, ?scope=* for no scope, and stricter list filters; then a split sitemap's parts under /sitemaps/, a permission's declaration site restored with it, then cli โ an export directory a build must prove is its own, a static build that fails on a module that does not load, unread build flags refused, a --dry-run that is the write plan, generator names and API bindings refused before writing, an unowned path read as root-wide, test discovery anchored at the root, guards and app entry files under filesize and errors, SEO answers kept an hour, a scaffold pinned to the repository's solid-js and typechecking the root program, a permission only defineAdmin() declared refused by the policy step, an unrecorded REPLICA IDENTITY FULL refused by the drift step, and a registry snapshot that carries permission sites; then a handler's own CSP kept beside the app's, x routes --json in every route fix, serving roles that leave the framework schema to ROLE=migrate, decimal-only ports and proxy hops, stored files served as sandboxed downloads, no readiness grace off the listening roles, a replicator ready only while its stream runs, and cli commands that exit 1 on an unknown name or any finding, a /_x SQL panel that runs only a same-origin POST from this machine, and stricter x doctor, x jobs, x shot, x dev, x verify merge, tests.run, x mcp serve and x routes; then checkErrorCodesThrown waiving only the codes a list names; then a chart that renders a NetworkPolicy per role, a bounded /tmp and a Secret every role must have, and a sync listener on its own port at port 0 |
the 24.0.0 section, in order |
| 22.x โ 23.0.0 |
66 โ an image line that prebuilds the island store, a worker that imports less of the app, a committed schema dump, a stated coverage floor, step deadlines, raw browser requests refused by the gate, a typed-handle repo with list(limit) and a generated query with no orgId input, admin label keys the i18n step now checks, every hand-written job driver and store fenced on its claim, runJobs through a real worker, a framework-served admin that replaces the host's pages and now serves the jobs dashboard, an async AuditLog, admin writes held to the row scope, and sealed scraping sessions that discard what was stored before |
the 23.0.0 section, in order |
| 21.x โ 22.0.0 |
23 โ two date readers that refuse a non-ISO string instead of reading it in the host's zone, a helm release named after the app, channel() requiring a policy, a per-mutation outbox, a sync role that refuses to boot with nothing to deliver, boot-owned auth tables, x shot on raw CDP with no puppeteer-core, realtime.transport deciding the bus, and removed exports: Result, realtime's backoffDelay, the e2e driver's move to @ultimat3/testing, startLiveReplicator leaving it, unreferenced package internals and 236 of the CLI's, a one-time x db gen for a re-stamped schema hash, and a query that filters on a column its loader never selected refusing instead of answering []
|
the 22.0.0 section, in order |
| 20.x โ 21.0.0 |
27 โ AsyncState's import path, custom(merge) over rows rather than outputs, realtime's second conflict vocabulary removed, isSuperseded widened, one error path for every typed client, the record envelope on actions that return entity rows, the service worker's outbox flush replaced by a message to open tabs, a third client-scope answer, last-write-wins refused without a clock, the realtime client rebuilt around one page store and one read hook, Compose requiring SYNC_URL, x verify's duration as wall time, and channels served by declaration only. The client data layer, one entry per removed surface |
the 21.0.0 section, in order |
| 19.x โ 20.0.0 |
2, both @ultimat3/ui component behaviour and neither a type change โ a DataTable that keeps its rows while reloading, and a Button whose loading no longer sets the native disabled. Nothing fails to compile; what changes is what a screen does |
the 20.0.0 section, in order |
| 18.x โ 19.0.0 | 2, both from the same hole โ the service worker had no build behind it, so the config key that steers it and the route it falls back to both had to move | the 19.0.0 section, in order |
| 17.x โ 18.0.0 |
5 โ a runtime floor that was a minor behind what the CLI emits, two PWA config surfaces that had to grow before an app could be installable, a --json shape, and one scraping interface |
the 18.0.0 section, in order |
| 16.x โ 17.0.0 |
3, all one sweep โ a numeric option that used to accept NaN refuses it, at boot or at the call boundary rather than mid-request |
the 17.0.0 section, in order |
| 15.x โ 16.0.0 |
1 โ matches(/re/) refuses a construct the two regex engines read differently, at entity() time |
the 16.0.0 section, in order |
| 14.x โ 15.0.0 |
1 โ DriftKind gains a member, so an exhaustive switch with no default stops compiling |
the 15.0.0 section, in order |
| 13.x โ 14.0.0 | 8 โ two are security fixes with a behaviour change (a frame verb, a session key), four are types that refused what the runtime already did, and two remove API nothing called | the 14.0.0 section, in order |
| 12.x โ 13.0.0 |
2, both narrow: a service factory's parameter type, and one deleted PageLike member with zero call sites anywhere in the repository |
the 13.0.0 section, in order |
| 11.x โ 12.0.0 | 16, from the widest sweep since 4.0.0 โ a keyset defect that dropped rows, a name that reached the DDL unchecked, and eight interfaces that gained a member | the 12.0.0 section, in order |
| 10.x โ 11.0.0 | 7 | the 11.0.0 section, in order |
| 9.x โ 10.0.0 | 19 | the 10.0.0 section, in order |
| 8.x โ 9.0.0 | 5 | the 9.0.0 section, in order |
| 7.x โ 8.0.0 | 6 | the 8.0.0 section, in order |
| 6.x โ 7.0.0 | 4 | the 7.0.0 section, in order |
| 5.x โ 6.0.0 | 7 | the 6.0.0 section, in order |
| 4.x โ 5.0.0 | 2, over six surfaces, each a declaration that promised what the code did not do | the 5.0.0 section, in order |
| 3.0.0 โ 4.0.0 | 25, from a sweep that closed every known gap | the 4.0.0 section, in order |
| 2.0.0 โ 3.0.0 | 10, all from a five-agent bug sweep | the 3.0.0 section, in order |
| 1.x โ 2.0.0 | 33 | the 2.0.0 section, in order |
| 11.x โ 20.0.0 | 47 | every major section CHANGELOG.md still carries, oldest first |
An entry is a line CHANGELOG.md marks BREAKING โ. The count is derived, never curated:
grep -cE '^(- \*\*|### )BREAKING โ' <(awk '/^## /{u = ($0 == "## [Unreleased]")} !u' CHANGELOG.md)
# 413 As of 2026-10-09 โ every RELEASED section, which is the sum of every row above whose section
# the changelog still carries. `[Unreleased]` is cut by the awk deliberately: a bare whole-file
# grep agrees with this number only while that section is empty, so it moved on every PR that
# landed a breaking change and moved BACK when the release promoted the section โ a count that can
# only be right between merges, whose repair is a number the next release invalidates. Corrected
# 2026-08-26, after it failed exactly that way.
#
# The count is SMALLER than the number of entries this page walks, and that is the archive, not a
# discrepancy: `CHANGELOG.md` keeps the recent releases and `git show v10.0.0:CHANGELOG.md` has the
# rest. THIS page is not truncated with it โ a reader upgrading across four majors needs every
# walkthrough in order, so every major keeps its row and its section here. `changelog-check` reads
# the oldest `## X.Y.Z` heading still in the changelog as the retention boundary and stops asking
# for a count below it. At a TAGGED commit
# `[Unreleased]` holds none, and that is the state the rule below checks โ a `BREAKING โ` line left
# there at a tag is
# X_DOC_CHANGELOG_UNRELEASED_BREAKING, and the release promotes the section rather than appending one.
# Scope the count to one section to read a single row. The range is that section's own heading line
# to the line before the next `## `, and `grep -n '^## ' CHANGELOG.md` prints both โ
# sed -n '<start>,<end>p' CHANGELOG.md | grep -cE '^(- \*\*|### )BREAKING โ'
# Line numbers are deliberately not written here: every release moves them.
# `bun run changelog-check` compares both directions: each row against its OWN section, and the
# line above against the file.Each entry changes a surface the table below covers.
Move to whatever
latestis โ only the footer stamps the number, because a version written into a page goes stale on the next tag. All 31 workspaces resolve at one version โ 29@ultimat3/*plus the unscopedcreate-ultimate,@ultimat3/scrapingand@ultimat3/flagsincluded โ and every tarball since 3.0.0 was published by the release workflow with a provenance attestation. Resolve before you pin, never take it from this page:
| Check | Command | Answer that means "go" |
|---|---|---|
what latest is |
npm view @ultimat3/core version |
the version you are pinning |
| that a package resolves at it | npm view @ultimat3/scraping@<version> version |
that version, not E404
|
| that the tarball is attested | npm view @ultimat3/core dist.attestations |
a provenance object |
| every name that must move together | bun run scripts/release-workflow.ts --json |
the 30 derived names โ check each |
Two entries โ CHANGELOG.md's 27.0.0 breaking entries in their order, grouped by
package, lowest tier first. No legacy path, no codemod, no shim. Entry 2 can 404 a read your app
relies on: find every un-scoped key a page links before the deploy (upgrade step 2). A later
slice appends its rows below the last one and never renumbers; each row's CHANGELOG.md line names
it as (#N) after the dash.
| # | Do | What you see until you do | Entries |
|---|---|---|---|
| 1 | pin every @ultimat3/* to the one new version, bun install
|
nothing yet โ a mixed install is untested | โ |
| 2 |
before the deploy: find apps -type f -exec grep -nE '/_storage/|/media/|signedUrl\(' {} + โ for each key that is NOT built with scopedKey(orgId, โฆ) (no org/<id>/ prefix) and that readers must still fetch, add its prefix to defineStorage({ shared: { <disk>: ['<prefix>/'] } }); leave everything else unshared |
404 X_STORAGE_KEY_UNSHARED on that link |
2 |
| 3 |
bun run typecheck โ a hand-written AsyncContext or Storage object |
TS2741 naming exit or isShared
|
1, 2 |
| 4 | x verify |
green, or a finding whose fix: is the edit |
โ |
Tier 0 โ @ultimat3/core (1). Tier 1 โ @ultimat3/storage, with @ultimat3/cli's routes (2).
| # | Surface | Costs you an edit if |
|---|---|---|
| 1 | AsyncContext.exit(fn) |
you implement AsyncContext yourself rather than calling asyncContext(): TS2741. Add exit: (fn) => fn() for a context with no store, or your store's own exit
|
| 2 |
GET /_storage/:disk/*key, GET /media/*key, StorageConfig.shared, Storage.isShared
|
a page, island or client reads a key outside org/<id>/ through either route โ a brand asset on a disk, a shared upload. 26.x served it to every holder of storage:read; 27.0.0 answers 404 X_STORAGE_KEY_UNSHARED unless defineStorage({ disks, shared: { uploads: ['brand/'] } }) lists its prefix for that disk (whole segments ending in /, never org/, never the whole disk โ X_CONFIG_INVALID at boot otherwise). Or move a truly public file to apps/web/site/assets/ and name it with asset(). A hand-built Storage adds isShared(disk, key) (TS2741). Tenant-scoped reads are unchanged |
Six entries โ CHANGELOG.md's 26.0.0 breaking entries in their order, grouped by
package, lowest tier first. No legacy path, no codemod, no shim. Entry 1 can refuse sign-ins on
a host whose egress cannot reach the provider's jwks_uri โ check it before the deploy (upgrade
step 2). A later slice appends its rows below the last one and never renumbers; each row's
CHANGELOG.md line names it as (#N) after the dash.
| # | Do | What you see until you do | Entries |
|---|---|---|---|
| 1 | pin every @ultimat3/* to the one new version, bun install
|
nothing yet โ a mixed install is untested | โ |
| 2 |
before the deploy, an app with an OIDC sign-in: from a production pod, curl -sS -m 5 https://www.googleapis.com/oauth2/v3/certs (or the jwks_uri of each provider you registered); where it cannot be reached, return 'token-endpoint-tls' for that provider only from oauthLogin(auth, { idTokenKeys }) (entry 1 shows the function) |
X_OAUTH_EXCHANGE_FAILED (stage jwks) on every sign-in through that provider |
1 |
| 3 |
bun test โ a test that feeds exchangeOAuthCode, completeOAuthLogin or oauthLogin an unsigned id token: sign it and serve the key set at the provider's jwksUri from the injected fetch, or pass keys / idTokenKeys: 'token-endpoint-tls'
|
X_OAUTH_TOKEN_INVALID where the test signed in; an extra key-set URL in a recorded endpoint list |
1 |
| 4 |
bun run typecheck for the removed exports: APPLE_PROVIDER, ServerHandle.describe(), AgentRecovery, Recovery, ROUTE_FILENAME.api
|
TS2305 / TS2339 at each site | 2, 3, 5 |
| 5 |
find apps -path '*/api/*' \( -name route.ts -o -name page.tsx \) โ move each into an action, or a plain route in runtime.routes
|
X_ROUTE_FILE_INVALID at boot |
4 |
| 6 |
grep -rnE "x (jobs (ls|rm)|(actions|queries|entities) describe|db branch (ls|drop))\b" .github scripts bin package.json docs and swap each for list, delete, show
|
X_CLI_UNKNOWN_COMMAND naming the new verb |
6 |
| 7 | x verify |
green, or a finding whose fix: is the edit |
โ |
Tier 2 โ @ultimat3/auth (1โ2), @ultimat3/http (3). Tier 4 โ @ultimat3/render (4),
@ultimat3/scraping (5). Tier 5 โ @ultimat3/cli (6).
| # | Surface | Costs you an edit if |
|---|---|---|
| 1 |
exchangeOAuthCode, completeOAuthLogin, oauthLogin โ the id token's signature |
your egress cannot reach a provider's jwks_uri, or a test hands the exchange an unsigned id token. 25.x accepted any id token read off the token endpoint over TLS; 26.0.0 checks its signature against the provider's published key set whenever the provider has a jwksUri (providerJwks, cached per provider). X_OAUTH_TOKEN_INVALID for a bad signature, X_OAUTH_EXCHANGE_FAILED (stage jwks) for an unreachable key set. Opt one provider out with oauthLogin(auth, { idTokenKeys: (provider) => provider === 'bigco-sso' ? 'token-endpoint-tls' : undefined }) โ undefined keeps the default; providerJwks(providerFor(id)) there throws for GitHub, which publishes no jwks_uri; completeOAuthLogin(auth, { idTokenKeys }) and exchangeOAuthCode(โฆ, { keys }) take the value directly |
| 2 |
APPLE_PROVIDER, BUILTIN_OAUTH_PROVIDERS, BUILTIN_OAUTH_PROVIDER_IDS
|
you import APPLE_PROVIDER (TS2305) or list 'apple' in defineAuth({ providers }) (an unknown provider). Apple answers a form_post and the callback is a GET, so it never completed a login. Delete it, or registerOAuthProvider your own |
| 3 | ServerHandle.describe() |
you call it. TS2339; read x routes --json, or call describeRoutes(table) from @ultimat3/http
|
| 4 |
api/**/route.ts, ROUTE_FILENAME, RouteEntry.surface
|
a route file lives under api/: X_ROUTE_FILE_INVALID (was X_ROUTE_MODE_INVALID). Make it an action, or a plain route in runtime.routes. ROUTE_FILENAME is Record<RouteSurface, string>; type a surface with the new RouteSurface export of @ultimat3/render
|
| 5 |
recover: 'agent', AgentRecovery, Recovery
|
a scrape passes 'agent' (refused at scrape(); 25.x threw X_NOT_IMPLEMENTED on the first moved selector) or imports a removed type. Write a RecoveryHook function and call your own llm() from it; import type { Recovery } becomes RecoveryHook. @ultimat3/scraping is tier 4 now โ nothing to edit in an app |
| 6 |
x jobs ls, x jobs rm, x actions|queries|entities describe, x db branch ls, x db branch drop
|
a script runs an old verb: X_CLI_UNKNOWN_COMMAND with the new one in its fix. list, delete, show, list, delete; a bare x jobs still lists, and --json is unchanged |
Forty entries โ CHANGELOG.md's 25.0.0 breaking entries in their order: Bun
first, then grouped by package, lowest tier first. No legacy path, no codemod, no compatibility
shim โ every break is a build error or an X_* error naming the rewrite. Most of the major is
deletion: a second spelling, a second projection or a second page shape removed, and the keys and
re-exports a deprecation promised to remove. Entries 2 and 8 refuse the boot of an app that
still writes a deleted or an undeclared key. Entry 12 is silent: a per-action bucket in configureHttp stops
limiting anything, so move it before the deploy โ upgrade step 9. Entries 22โ24 refuse every AI
call until the app registers its own models and names one โ upgrade step 8. Entries 31โ40 landed
after the cut, so they sit below 30 rather than with their packages. Entry 38 is silent: a worker
Deployment that set WORKER_QUEUES stops claiming every queue it does not list โ upgrade step 12. A later slice
appends its rows below the last one and never renumbers; each row's CHANGELOG.md line names it
as (#N) after the dash.
| # | Do | What you see until you do | Entries |
|---|---|---|---|
| 1 |
bun upgrade to 1.4.2 and move the app's own image to a 1.4.2 base |
X_BUN_VERSION from x and x doctor
|
1 |
| 2 | pin every @ultimat3/* to the one new version, bun install
|
nothing yet โ a mixed install is untested | โ |
| 3 | delete the removed keys from app.config.ts and each overlay โ the seven of 25.0.0, the ten of earlier majors, and any key the refusal says is not one (fix the typo it suggests); declare the locales in defineCatalogs (packages/i18n/src/index.ts); move a non-/mcp ai.mcp.path to the first defineAppMcp({ path }) in apps/<app>/mcp.ts; move drainTimeoutMs to drain: { deadlineMs } and delete buildIdHeader from configureHttp
|
TS2353 in a typed config; X_CONFIG_INVALID naming the key and its replacement, or the nearest real key, at the first import |
2, 8 |
| 4 |
bun run typecheck for the renames and removed exports: factory spellings (every create<Thing> and new of a class now a type), every value a second package re-exported, the renamed value names, checkEnvExample, fingerprint, nearestName, promptHash, entity's Page, SQL_*, createNatsDriver and createRedisDriver imports |
TS2305 / TS2724 at each import; TS1485 / TS1362 at a class now exported as a type | 3, 5โ7, 14, 15, 20, 28, 31, 32 |
| 5 |
bun run typecheck for the MCP projection, the MCP block and the page shape: .tool() and toolFromAction โ toolFrom, QueryMcp / McpExposure / McpAnnotationHints โ core's McpExposureDeclaration family, mcp.name โ the primitive's name, endCursor / hasNextPage โ nextCursor / hasMore, a storage page's objects / truncated / cursor โ rows / hasMore / nextCursor
|
TS2339 / TS2305 / TS2353 at each site | 7, 10, 11, 18 |
| 6 |
bun run typecheck for the members a custom implementation owes: AuditRecord.name / .primitive, IdempotencyStore.keepsRedaction and settle(โฆ, redacted), WebhookLedger.isDisabled, ChangeEvent.write, ChangeEvent.table for .entity, DigestAppend.appender, a StorageDriver.list() that answers Page<StorageListEntry>; drop counted from every ack; widen a Menu trigger's aria-controls; read listenerFor instead of listeners; narrow a read of config.jobs.concurrency with typeof โฆ === 'number'; pass an OAuthGrantContext where code calls a ResolveOAuthGrants
|
TS2741 / TS2554 / TS2322 / TS2339 / TS2353 at each site | 4, 7, 13, 16, 17, 21, 25, 26, 33, 34, 37, 40 |
| 7 |
bun run typecheck for ai: pass models to each anthropicProvider(โฆ), replace OPENAI_MODEL_IDS with the app's ids |
TS2554 / TS2305 at each site | 23 |
| 8 |
before the deploy, where the app calls a model: registerModel every model it names (by convention in its own models.ts), and name one on each declaration or as providerGateway({ defaultModel }); a test registers its rows in beforeEach and passes model to echoProvider()'s requests |
X_AI_MODEL_UNKNOWN before any provider call; X_AI_MODEL_UNRESOLVED at a call that names none |
22โ24 |
| 9 |
before the deploy: move each rateLimit.buckets.<actionName> from configureHttp to that action's rateLimit: { limit, windowMs }
|
X_CONFIG_INVALID at boot, naming the bucket and the action |
12 |
| 10 |
x env example (an app exporting envSchema), x manifest, commit .env.example, x.manifest.json and openapi.json
|
X_ENV_EXAMPLE_DRIFT / X_MANIFEST_STALE from the manifest step |
11, 35 |
| 11 |
x verify --only boundaries,unit,contract,e2e and fix what it fails: move an app wildcard off a bearerMount prefix, list every projected tool under a scope, give each offline scrape a robots: { ignore } reason, move a page's repo read into a query, pass an e2e's external DATABASE_URL / S3_ENDPOINT in startE2eApp({ env })
|
X_ROUTE_CONFLICT; X_MCP_SCOPE_UNCOVERED at boot; X_SCRAPE_ROBOTS_DISALLOWED; X_BOUNDARY_ROUTE_TO_DB; an e2e that read the external database's rows fails against the embedded one |
9, 19, 27, 30, 36 |
| 12 | read each script or CI job that runs x shot --cdp-url against a hosted browser, passes --to or --dry-run to x jobs drain, or reads data.next from x jobs list --json; before the deploy, read each worker Deployment that sets WORKER_QUEUES and list every queue it must serve; pass concurrency: 5 to a hand-built jobWorker that must keep 5 slots |
X_CDP_CALL_FAILED where a capture used to proceed; X_CLI_BAD_FLAG; undefined where a cursor was; jobs.worker.queue-unserved at a worker's boot and jobs left ready on the queues it no longer claims; 8 jobs at once where 5 ran |
7, 15, 29, 38, 39 |
| 13 | x verify |
green, or a finding whose fix: is the edit |
โ |
Every package (1). Tier 0 โ @ultimat3/core (2โ5). Tier 1 and up โ the factory spellings of
db, cache, storage, auth, action, jobs, realtime, mail, notify, ai and
testing (6). Tier 2 โ @ultimat3/entity (7),
@ultimat3/http (8โ9). Tier 3 โ @ultimat3/action and @ultimat3/query (10โ14),
@ultimat3/jobs (15โ16), @ultimat3/realtime (17). Tier 4 โ @ultimat3/mcp (18โ19),
@ultimat3/manifest (20), @ultimat3/notify (21), @ultimat3/ai (22โ24), @ultimat3/ui (25).
Tier 5 โ @ultimat3/testing (26), @ultimat3/scraping (27), @ultimat3/cli (28โ30). After
the cut โ @ultimat3/ai (31), the value names of every package (32), @ultimat3/jobs (33).
Sweep 12c โ @ultimat3/core (34), @ultimat3/cli (35), @ultimat3/testing (36); its review round โ
@ultimat3/auth (37), @ultimat3/cli (38), @ultimat3/jobs (39), @ultimat3/realtime (40).
| # | Surface | Costs you an edit if |
|---|---|---|
| 1 |
engines.bun, x, x doctor
|
you run Bun 1.4.0 or 1.4.1. X_BUN_VERSION; bun upgrade, and a 1.4.2 base image in the app's own Dockerfile |
| 2 |
app.config.ts: any key it does not declare, at any depth; keys removed: locales, defaultLocale, defaultTimeZone, defaultCurrency, theme.tokens, jobs.driver, ai.mcp.path, and from earlier majors realtime.heartbeatMs, database.urlEnv, database.poolSize, database.schema, pwa.installPrompt, auth.afterSignInPath, ai.modelEnv, cache.driver, cache.urlEnv, realtime.tier
|
a layer writes one. X_CONFIG_INVALID naming the path and either its replacement or, for a key that never existed, the nearest real one (did you mean drain?); TS2353 in a typed config; TS2339 where code reads config.locales. The locales and their default are defineCatalogs({ default, locales }) in packages/i18n/src/index.ts; the zone is passed per call, the currency per Money; the theme is defineTheme in apps/web/shared/theme.ts; the driver is setJobDriver(postgresJobDriver({ executor })). Delete ai.mcp.path: every MCP endpoint, #0 included, mounts at its own defineAppMcp({ path }) (default /mcp) โ if yours was not /mcp, pass it as path to the first defineAppMcp in apps/<app>/mcp.ts. McpConfig is { expose }. Of the ten older keys only cache.driver was read (โ cache: { tiers: [...] }); the rest were read by nothing โ delete the line, and the fix: names what does the job (REMOVED_CONFIG_KEYS, packages/core/src/config-removed.ts) |
| 3 |
assertEnvExample, EnvExampleDriftError, resolveSpeculation
|
you import one. checkEnvExample(โฆ) returns findings instead of throwing โ or let x verify --only manifest run it; speculation is defineConfig({ navigation: { speculation } })
|
| 4 |
AuditRecord, normalizeAuditRecord, NormalizedAuditRecord
|
a sink or test reads record.action or builds a record without name and primitive. TS2339 / TS2741. Read and write name and primitive; no DDL changed |
| 5 |
Deprecation, DeprecationField, DeprecationRender, recordDeprecatedCall, renderDeprecation; the values createClientFlight (now clientFlight), DEFAULT_CLIENT_RETRY, isSuperseded, isTransientFailure, getAuditSink, setAuditSink, resetAuditSink; every other value a second package re-exported โ the Entry 5 table below, including formatIssues (mcp), MAX_MONEY_SCALE (money), ULTIMATE_ERROR_BRAND (core), JOB_STATES and TEST_TYPES (cli) |
you import one from a package that is not its home. TS2305; change the module specifier per the table โ @ultimat3/core unless the row names another, and under core's own name where the row gives one. action and query re-export types only; scraping's throwIfAborted(signal) is signal.throwIfAborted()
|
| 6 | every createMemory*, createPg*, pgSchedulerState, createPostgresClient; a Memory* / InMemory* / Pg* / BuiltinAdapter class new-ed as a value; every create<Vendor>Driver; storage's memoryDriver; every other create<Thing> factory; ai's new AnthropicProvider / EchoProvider / HashEmbedder / RemoteEmbedder / BudgetLedger, mcp's new McpServer, query's new Builder
|
you call one. TS2305 / TS2724; TS1485 / TS1362 where the class is now a type only. Rename per the table below. Arguments are unchanged; a type a factory takes or builds is renamed to the factory's spelling (TS2724 at the old name โ the type-renames table below), so import type { BuiltinAdapter } is PostgresAuthAdapter and MemoryAdapter is MemoryAuthAdapter. A budget ledger is the gateway's (gateway.callLedger(keys), gateway.scope(โฆ)); a query builder is from(โฆ)'s. createBranch, createSession and createApp keep their names: they are verbs |
| 7 |
Page from @ultimat3/entity
|
you annotate a findMany or .page() result with it. Import Page from @ultimat3/core (or @ultimat3/query): findMany answers it, so the page now carries hasMore too. A hand-written Repo builds its page with core's pageOf(rows, nextCursor) (TS2741 / TS2322 at a { rows, nextCursor } literal). And ListPage from @ultimat3/storage: list() answers Page<StorageListEntry> โ read rows / hasMore / nextCursor for objects / truncated / cursor (null on the last page), pass cursor: page.nextCursor; a custom driver returns pageOf(entries, nextCursor). And a script reading x jobs list --json: data.next is data.nextCursor, beside data.hasMore
|
| 8 |
configureHttp({ drainTimeoutMs }), defineHttpConfig({ drainTimeoutMs }); the same two with buildIdHeader
|
you pass one. X_CONFIG_INVALID, refused by key so a spread is caught too. drainTimeoutMs: declare drain: { deadlineMs } in app.config.ts โ every role, web included, drains on it. buildIdHeader: delete it; the header is core's BUILD_ID_HEADER (x-ultimate-build), the only one any client sends |
| 9 | bearerMount |
the app declares a wildcard at the mount's own <prefix>/*rest. X_ROUTE_CONFLICT at registration; the mount answers every method under its prefix |
| 10 |
.tool(), toMcpTool(s), toQueryTool(s), isExposed, toolFromAction, toolFromQuery, McpToolDescriptor, McpInvokeOptions, QueryTool*
|
you project or call a tool by hand. toolFrom(x) for an action or a query (toolFromAction renamed, no alias), toolListEntry(tool) for the tools/list entry, invoke(x, input, { ctx, surface: 'mcp' }) to call, (await sourceFor(q, input, { surface: 'mcp' })).execute() to read, isMcpExposed(x.mcp) from @ultimat3/core
|
| 11 | query Page, the ?_first= envelope, openapi.json
|
a client reads endCursor or hasNextPage. Read nextCursor and hasMore; pass nextCursor as after (_after= on the wire). Run x manifest. And a client keeps the LAST page's cursor โ to poll for rows appended after it, or because it reads nextCursor without hasMore: the last page's nextCursor is now null (it is null exactly when hasMore is false, on every surface), so a while (page.nextCursor) loop no longer fetches an empty page past the end. To tail a listing, keep the cursor of the last page that had one, or re-read the first page |
| 12 | http.rateLimit.buckets.<actionName> |
you limited an action through a named bucket. It is refused at boot with X_CONFIG_INVALID (a bucket keyed by a mounted action's or query's name): delete it from configureHttp and declare rateLimit: { limit, windowMs } on the action |
| 13 | IdempotencyStore |
you wrote your own. Declare keepsRedaction: true, keep settle's 4th argument redacted and return it as IdempotencyRecord.redacted
|
| 14 |
SQL_* exports of action, jobs, notify, admin
|
you import a statement other than a table DDL (SQL_AUDIT_TABLE, SQL_IDEMPOTENCY_TABLE, SQL_JOBS_TABLE, SQL_NOTIFY_*_TABLE, SQL_ADMIN_AUDIT_TABLE) โ SQL_CLAIM, SQL_OUTBOX_RELEASE and SQL_NOTIFY_INBOX_MARK_READ included. Call the store that runs it, or copy the text into the app |
| 15 |
createNatsDriver, NatsDriverOptions, createRedisDriver, RedisDriverOptions; the --to and --dry-run flags of x jobs drain
|
you import one โ every method threw X_NOT_IMPLEMENTED. Postgres is the durable driver: postgresJobDriver({ executor }). Or a script passes either flag: X_CLI_BAD_FLAG; x jobs drain is still planned (X_NOT_IMPLEMENTED), so delete the call |
| 16 | WebhookLedger |
you wrote your own. Add isDisabled(endpointId): Promise<boolean>
|
| 17 | ChangeEvent |
a custom change feed builds one. Pass write: null where it has no write name |
| 18 |
McpExposure and its name; QueryMcp, QueryMcpAnnotations, QueryListParams, QueryListFilterOp (query); McpAnnotationHints (action) |
a hand-built primitive handed to toolFrom sets mcp: { name } โ rename the primitive instead. Or you name one of the types: import McpExposureDeclaration, McpAnnotationHints, McpListParams, McpListFilterOp from @ultimat3/core. An action's block is ActionMcp, Omit<McpExposureDeclaration, 'listParams'>
|
| 19 | defineAppMcp({ scopes }) |
the map leaves a projected tool out โ include: 'exposed' and hand-written tools such as whoami included. X_MCP_SCOPE_UNCOVERED at boot; list each tool under a scope |
| 20 |
contentHash from @ultimat3/manifest
|
you import it. fingerprint(body) from @ultimat3/core, the same 16 hex characters |
| 21 |
DigestAppend, DigestStore
|
you wrote a store. Key its replay on the required appender
|
| 22 |
DEFAULT_MODEL, a call with no model |
a declaration, its prompt and the gateway all leave the model out. X_AI_MODEL_UNRESOLVED naming the three places; echoProvider() needs model too, and so does a request to a provider called directly (anthropicProvider, openAiProvider), which runs request.model and nothing else. describeAgents() answers model: null there |
| 23 |
anthropicProvider() (24.x: new AnthropicProvider()), ANTHROPIC_MODEL_IDS, OPENAI_MODEL_IDS
|
you construct a provider without models or with the built-in list. TS2554 / TS2305; an empty list is X_AI_REQUEST_INVALID. Pass the ids the app registered |
| 24 | the built-in catalogue, registerOpenAiModels()
|
the app names a model it never registerModel-ed. X_AI_MODEL_UNKNOWN before any provider call. Register each model at boot, with its source and date beside each number |
| 25 |
Menu trigger props |
you type aria-controls as string. It is string | undefined; a closed menu carries none |
| 26 | FakeElement.listeners |
a test reads it. element.listenerFor(name), or island.fire(selector, type)
|
| 27 | an offline scrape() test with fakeBrowser / fixtureBrowser
|
it declares no robots: { ignore }. X_SCRAPE_ROBOTS_DISALLOWED under bun test; write the reason the run is permitted |
| 28 |
nearest from @ultimat3/cli
|
you import it. nearestName from @ultimat3/core
|
| 29 | x shot --cdp-url |
the provider refuses the browser-target attach. X_CDP_CALL_FAILED; point at a browser that allows it, or drop the flag |
| 30 | the boundaries step |
a page, layout or route imports a slice's repo. X_BOUNDARY_ROUTE_TO_DB; read through a query |
| 31 |
contentHash from @ultimat3/ai
|
you import it. promptHash(input), the same argument and the same hash; contentHash is @ultimat3/render/server's byte hash only |
| 32 | a value name two packages declared: forbidden, unauthenticated, json, text, validate, money, isEnabled, describeRoutes, channel, guard, guardBeforeInput, resetRegistry, explain, actorLabel, allowed, denied, relationsFor, invariant, uuid, cacheKeyFor, DEFAULT_RETRY, NO_TENANT, chunk, normalize, tokenize, introspect, isDestructive, assertNoDrift, safeUrl, formatBytes (ui), resetIdCounter, srcsetFor, frozenClock, testClock (scraping), sessionExpired
|
you import one. TS2305 / TS2724; rename per the Entry 32 table below โ same arguments, same answer, no alias. Scraping's testClock is testScrapeClock: @ultimat3/testing's testClock is the former createTestClock, a different clock |
| 33 | AckOptions.counted |
a custom JobDriver.ack reads it, or a caller passes it. TS2353 at a literal; delete it โ every ack counts |
| 34 |
JobsConfig.concurrency (JobsConcurrency) |
code reads config.jobs.concurrency as a number. It is number | Readonly<Record<string, number>>, slots per queue; narrow with typeof concurrency === 'number', and read a queue's row as concurrency[queue] ?? JOBS_CONCURRENCY_DEFAULT (both exported by @ultimat3/core). An empty table, a blank queue name or a count below 1 is X_CONFIG_INVALID
|
| 35 |
.env.example, x env example
|
the app exports envSchema. The projection now ends in a Framework section โ ULTIMATE_CURSOR_SECRET, STORAGE_SIGNING_SECRET โ so the committed file is X_ENV_EXAMPLE_DRIFT in the manifest step; run x env example and commit it. A key the schema already declares is not repeated |
| 36 | startE2eApp |
an e2e relied on an exported DATABASE_URL or S3_ENDPOINT reaching the app. Neither is inherited any more; pass it: startE2eApp({ root, env: { DATABASE_URL: '<the external url>' } })
|
| 37 | ResolveOAuthGrants |
code calls a ResolveOAuthGrants-typed function with one argument โ typically an app's unit test of its grants function. TS2554; pass an OAuthGrantContext (provider, tokens, idTokenClaims, userinfo()) as the second argument. A one-argument implementation still assigns to the type |
| 38 | WORKER_QUEUES |
a worker Deployment sets it and relied on also serving jobs.queues and every registered job's queue (24.x read nothing by that name). It is now exactly the listed queues: jobs on any other queue stay unclaimed by that worker, which warns jobs.worker.queue-unserved at boot naming them. List every queue the Deployment must serve, or unset the variable for the union. An empty entry (banks,,mail) is X_CONFIG_INVALID
|
| 39 | jobWorker({ concurrency }) |
you build a jobWorker by hand and pass no concurrency, or a table that leaves a queue out. That queue runs JOBS_CONCURRENCY_DEFAULT (8, @ultimat3/core) slots, was 5; pass concurrency: 5 to keep it. A worker x boots is unchanged |
| 40 |
ChangeEvent.entity (@ultimat3/realtime) |
a custom ChangeFeed, test or consumer builds or reads a change event. Rename entity: to table: โ it holds the relation (table) name on every producer. TS2353 / TS2339. A sync node still reads a 24.x replicator's entity off the bus, so the rolling deploy needs no ordering |
Entry 6, the renames:
| Package | Was | Is |
|---|---|---|
db |
createPostgresClient |
postgresClient |
cache |
createMemorySemanticCache |
memorySemanticCache |
jobs |
createMemoryDriver, createPgDriver
|
memoryJobDriver, postgresJobDriver
|
jobs |
createPgLeader, createPgLeaseLeader, pgSchedulerState
|
postgresLeader, postgresLeaseLeader, postgresSchedulerState
|
jobs |
createPgEventBus, createPgOutboxStore
|
postgresEventBus, postgresOutboxStore
|
jobs |
createMemoryEventBus, createMemoryOutboxStore, createMemoryStepStore
|
memoryEventBus, memoryOutboxStore, memoryStepStore
|
jobs |
createMemoryLeaseStore, createMemorySchedulerState, createMemoryBackfillLedger
|
memoryLeaseStore, memorySchedulerState, memoryBackfillLedger
|
mail |
createMemoryDriver |
memoryMailDriver |
notify |
createMemoryDeliveryLedger, createMemoryDigestStore, createMemoryInboxStore, createMemoryPreferenceStore
|
memoryDeliveryLedger, memoryDigestStore, memoryInboxStore, memoryPreferenceStore
|
notify |
createPgDeliveryLedger, createPgDigestStore, createPgInboxStore
|
postgresDeliveryLedger, postgresDigestStore, postgresInboxStore
|
storage |
memoryDriver, MemoryDriverOptions
|
memoryStorageDriver, MemoryStorageDriverOptions
|
auth |
new BuiltinAdapter(โฆ), new MemoryAdapter(โฆ)
|
postgresAuthAdapter(โฆ), memoryAuthAdapter(โฆ)
|
action |
new MemoryIdempotencyStore(โฆ) |
memoryIdempotencyStore(โฆ) |
realtime |
new MemoryLocalStore(), new MemoryQueueStore()
|
memoryLocalStore(), memoryQueueStore()
|
realtime/server |
new InMemoryAdvisoryLock(โฆ), new PgAdvisoryLock(โฆ)
|
memoryAdvisoryLock(โฆ), postgresAdvisoryLock(โฆ)
|
realtime/server |
new InMemoryChangeFeed(โฆ), new PgLogicalReplicationFeed(โฆ); PgOutputDecoder
|
memoryChangeFeed(โฆ), postgresChangeFeed(โฆ); not exported โ the feed decodes |
mail |
createSmtpDriver, createResendDriver, createLogDriver, createUnconfiguredDriver
|
smtpMailDriver, resendMailDriver, logMailDriver, unconfiguredMailDriver
|
ai |
new PgVectorStore(โฆ), new MemoryVectorStore(โฆ), new MemoryBudgetStore()
|
postgresVectorStore(โฆ), memoryVectorStore(โฆ), memoryBudgetStore()
|
testing |
createSubscribeDriver() |
subscribeDriver() |
auth |
createAuthLimiter, createJwksClient, createKdfGate, createPkce, createTotpReplayGuard
|
memoryAuthLimiter, jwksClient, boundedKdfGate, pkcePair, memoryTotpReplayGuard
|
cache |
createCacheStack, createCdnTier, createLruTier, createMemoTier, createRedisTier
|
cacheStack, cdnTier, lruTier, memoTier, redisTier
|
core |
createClientFlight, createContext, createFence, createFlightGate
|
clientFlight, ctxOf, generationFence, flightGate
|
core |
createLogger, createRaster, createSingleFlight
|
structuredLogger, blankRaster, singleFlight
|
db |
createPgliteClient, createRecordingClient
|
pgliteClient, recordingClient
|
http |
createPipeline, createRateLimiter, createRequestContext, createRouter, createServer
|
httpPipeline, rateLimiter, requestContext, httpRouter, httpServer
|
i18n |
createTranslator |
catalogTranslator |
jobs |
createJobsFacade, createLimiter, createOutboxRelay, createPacer
|
outboxJobsFacade, concurrencyLimiter, outboxRelay, backfillPacer
|
jobs |
createScheduler, createStepRunner, createWorker
|
jobScheduler, stepRunner, jobWorker
|
realtime |
createOutbox, createSyncNode
|
localOutbox, syncNode
|
realtime/server |
createEntry, createFrameRouter, createReplicator
|
queryEntry, frameRouter, changeFeedReplicator
|
mcp |
createDevServer, createMcpServer, new McpServer(โฆ)
|
devMcpServer, mcpServer, mcpServer(โฆ)
|
pwa |
createInstallController |
installController |
render |
createIslandCollector, createIsrController
|
islandCollector, isrController
|
ai |
new AnthropicProvider(โฆ), new EchoProvider(โฆ), new HashEmbedder(โฆ), new RemoteEmbedder(โฆ)
|
anthropicProvider(โฆ), echoProvider(โฆ), hashEmbedder(โฆ), remoteEmbedder(โฆ)
|
ai |
createGateway, CreateGatewayInput; new BudgetLedger(โฆ)
|
providerGateway, ProviderGatewayInput; gateway.callLedger(keys) or gateway.scope(โฆ)
|
ui |
createFocusTrap, createFormBinding, createRovingTabindex, createToastStore
|
focusTrap, formBinding, rovingTabindex, toastStore
|
testing |
createTestClock, createTestMail, createTestNetwork, createTestStatements
|
testClock, testMail, testNetwork, testStatements
|
testing |
createRunJobs, createLiveNode, createTemplateSql
|
testJobs, liveNode, templateSql
|
scraping |
createArtifactWriter, createPacer, createPrompt, createRing
|
artifactWriter, scrapePacer, scrapePrompt, boundedRing
|
scraping |
createRobotsGate, createSecretBag, createUsageMeter
|
robotsGate, secretBag, usageMeter
|
query |
new Builder(โฆ) |
from(โฆ) |
Each class above stays nameable as a type (import type { MemoryVectorStore }) โ under the
factory's spelling where the two differed. Entry 6, the type renames (TS2724 at the old name):
| Package | Was | Is |
|---|---|---|
jobs |
PgDriverOptions, MemoryDriverOptions, PgEventBusOptions
|
PostgresJobDriverOptions, MemoryJobDriverOptions, PostgresEventBusOptions
|
jobs |
PgOutboxOptions, MemoryOutboxOptions, PgLeaseLeaderOptions
|
PostgresOutboxStoreOptions, MemoryOutboxStoreOptions, PostgresLeaseLeaderOptions
|
mcp |
CreateMcpServerInput, CreateDevServerInput
|
McpServerInput, DevMcpServerInput
|
ai |
PgVectorStore, PgVectorStoreInput, CreateGatewayInput
|
PostgresVectorStore, PostgresVectorStoreInput, ProviderGatewayInput
|
auth |
BuiltinAdapter, MemoryAdapter
|
PostgresAuthAdapter, MemoryAuthAdapter
|
notify |
PgDigestStore, PgDigestStoreOptions, PgInboxStore, PgInboxStoreOptions
|
PostgresDigestStore, PostgresDigestStoreOptions, PostgresInboxStore, PostgresInboxStoreOptions
|
notify |
PgDeliveryLedger, PgDeliveryLedgerOptions, MemoryLedgerOptions
|
PostgresDeliveryLedger, PostgresDeliveryLedgerOptions, MemoryDeliveryLedgerOptions
|
realtime/server |
InMemoryAdvisoryLock, PgAdvisoryLock, PgAdvisoryLockOptions
|
MemoryAdvisoryLock, PostgresAdvisoryLock, PostgresAdvisoryLockOptions
|
realtime/server |
InMemoryChangeFeed, InMemoryChangeFeedOptions, PgLogicalReplicationFeed, PgLogicalReplicationOptions
|
MemoryChangeFeed, MemoryChangeFeedOptions, PostgresChangeFeed, PostgresChangeFeedOptions
|
storage |
MemoryDriverOptions |
MemoryStorageDriverOptions |
Entry 5, each value at its home โ @ultimat3/core unless the right-hand column names another:
| Was imported from | Values | Import from |
|---|---|---|
action |
BUILD_ID_HEADER, IDEMPOTENCY_HEADER, pluralize, actorOf
|
@ultimat3/core |
action |
toBucket |
@ultimat3/http |
action, query
|
admitsAnonymous, policyPermissions, policyCapability
|
@ultimat3/policy |
query |
CursorInvalidError, actorOf; derivePath
|
@ultimat3/core; as queryPath
|
query |
MAX_PAGE_SIZE |
@ultimat3/entity |
http |
escapeHtml, readCookie, REQUEST_TIMEOUT_HEADER, isCanonicalWebhookField, WEBHOOK_ID_HEADER, WEBHOOK_SIGNATURE_HEADER, WEBHOOK_SIGNATURE_VERSION, WEBHOOK_TOPIC_HEADER
|
@ultimat3/core |
jobs |
WEBHOOK_FIELD_MAX, webhookHeaders, webhookMac, webhookSignature, webhookSigningString, classifyThrown; types WebhookMacInput, WebhookSigningInput
|
@ultimat3/core |
flags ยท time ยท auth, storage
|
fnv1a ยท localeInvalid ยท timingSafeEqual
|
@ultimat3/core |
i18n |
DEFAULT_LOCALE, directionOf, isRtl, localeSegment; FRAMEWORK_CATALOG_LOCALE
|
@ultimat3/core; as DEFAULT_LOCALE
|
mail |
escapeHtml; MAIL_CATALOG_LOCALE
|
@ultimat3/core; as DEFAULT_LOCALE
|
ai |
MAX_SEMANTIC_CACHE_SCOPES |
@ultimat3/core, as MAX_CACHED_FORMATTERS
|
cache |
TIER_ORDER; createSingleFlight, types FlightJoin, SingleFlight
|
@ultimat3/core, as CACHE_TIERS; as singleFlight
|
render |
formatBytes, RENDER_MODES, HYDRATE_STRATEGIES, OFFLINE_STRATEGIES, THEME_STORAGE_KEY, CLIENT_PATH_STYLE_META, CLIENT_PERSIST_META, CLIENT_SCOPE_META, CLIENT_BUILD_META, CLIENT_SYNC_META, CLIENT_SYNC_WORKER_META
|
@ultimat3/core |
render |
NAVIGATION_HEADER, NAVIGATION_SURFACE_HEADER, NAVIGATION_SCOPE_HEADER, NAVIGATION_LOCATION_HEADER
|
@ultimat3/core, as CLIENT_NAVIGATION_HEADER, CLIENT_NAVIGATION_SURFACE_HEADER, CLIENT_NAVIGATION_SCOPE_HEADER, CLIENT_NAVIGATION_LOCATION_HEADER
|
render |
tagKeys |
@ultimat3/cache |
pwa |
formatBytes, BUILD_ID_HEADER; BUILD_ID_META; APP_UPDATE_AVAILABLE
|
@ultimat3/core; as CLIENT_BUILD_META; as APP_UPDATE_MESSAGE
|
ui |
THEME_STORAGE_KEY |
@ultimat3/core |
admin |
REDACTED; ADMIN_AUDIT_TABLE, SQL_ADMIN_AUDIT_TABLE
|
@ultimat3/core; @ultimat3/admin/schema
|
realtime |
isJsonObject |
@ultimat3/core |
scraping |
ANY_HOST, hostDecision, hostMatches; types HostRule, HostDecision
|
@ultimat3/core |
scraping |
DEFAULT_CONTENT_TYPE |
@ultimat3/storage |
scraping |
throwIfAborted(signal) |
nowhere: signal.throwIfAborted()
|
mcp ยท money ยท core
|
formatIssues ยท MAX_MONEY_SCALE ยท ULTIMATE_ERROR_BRAND
|
@ultimat3/schema |
cli |
JOB_STATES |
@ultimat3/jobs |
cli |
TEST_TYPES |
@ultimat3/testing |
Two re-exports stay, pinned in scripts/lib/package-reexports.ts: schema's t through the eight
primitive barrels (action, ai, entity, jobs, mail, mcp, notify, query), and core's
describeValue and isIsoDateTime over the declared core โ schema edge. bun run flight-copies
refuses any other (X_HELPER_COPY), and a blind export * in shipped package source.
Entry 32, one name per meaning:
| Package | Was | Is |
|---|---|---|
auth |
forbidden, unauthenticated
|
authForbidden, authUnauthenticated
|
policy |
forbidden |
policyForbidden |
http |
json, text, validate
|
jsonResponse, textResponse, validateBody
|
money ยท pwa ยท render ยท notify
|
money() ยท isEnabled ยท describeRoutes ยท channel
|
fromMinor() ยท hasCapability ยท describePages ยท deliveryChannel
|
action |
guard, guardBeforeInput, resetRegistry
|
guardAction, guardActionBeforeInput, resetActions
|
query |
guard, guardBeforeInput, resetRegistry, explain
|
guardQuery, guardQueryBeforeInput, resetQueries, explainQuery
|
admin |
actorLabel, allowed, denied, relationsFor
|
adminActorLabel, adminAllowed, adminDenied, relationDataFor
|
core |
invariant, InvariantOptions, uuid
|
assertCoded, AssertCodedOptions, uuidV7
|
ai |
cacheKeyFor, DEFAULT_RETRY, NO_TENANT
|
promptCacheKey, DEFAULT_GATEWAY_RETRY, NO_VECTOR_TENANT
|
ai |
chunk, normalize, tokenize
|
chunkDocument, normalizeVector, wordTokens
|
db |
introspect, isDestructive, assertNoDrift
|
introspectSchema, isDestructiveMigration, assertNoSchemaDrift
|
mail |
safeUrl |
safeMailHref |
ui |
formatBytes, resetIdCounter, srcsetFor
|
formatFileSize, resetUseIdCounter, variantSrcset
|
testing |
frozenClock(now, body) |
withFrozenClock(now, body) |
scraping |
testClock, sessionExpired
|
testScrapeClock, scrapeSessionExpired
|
@ultimat3/i18n's t('key') and @ultimat3/schema's t keep one name on purpose: no file imports
both (scripts/factory-names-pins.ts holds the measurement).
Entry 2 โ app.config.ts, and where each key went:
export const config = defineConfig({
name: 'my-app',
- locales: ['en', 'es'],
- defaultLocale: 'en',
- defaultTimeZone: 'UTC',
- defaultCurrency: 'USD',
- theme: { defaultMode: 'system', tokens: { brand: '#0a84ff' } },
- jobs: { driver: 'postgres' },
- ai: { mcp: { expose: true, path: '/agents' } },
+ theme: { defaultMode: 'system' },
+ ai: { mcp: { expose: true } },
+ drain: { deadlineMs: 25_000 }, // entry 8: was configureHttp({ drainTimeoutMs })
});
// apps/web/mcp.ts โ an endpoint's path is its own; /mcp needs no line
+export const mcp = defineAppMcp({ path: '/agents', /* name, tools, resolveToken โฆ */ });
// packages/i18n/src/index.ts โ the one declaration of the locales and their default
+export const catalogs = defineCatalogs({ default: 'en', locales: { en, es } });
// at each format call โ there is no ambient zone
-formatDate(at, { locale });
+formatDate(at, { locale, zone: 'Europe/Paris' });Entry 10 โ the one MCP projection:
-import { toMcpTool, isExposed } from '@ultimat3/action';
-import { toQueryTool } from '@ultimat3/query';
+import { invoke } from '@ultimat3/action';
+import { isMcpExposed } from '@ultimat3/core';
+import { toolFrom, toolListEntry } from '@ultimat3/mcp';
+import { sourceFor } from '@ultimat3/query';
-const entry = publishPost.tool(); // or toMcpTool(publishPost)
+const entry = toolListEntry(toolFrom(publishPost)); // an action or a query
-await toMcpTool(publishPost).invoke(input, { ctx });
+await invoke(publishPost, input, { ctx, surface: 'mcp' });
-await toQueryTool(postList).read(input);
+await (await sourceFor(postList, input, { surface: 'mcp' })).execute();
-if (isExposed(publishPost)) โฆ
+if (isMcpExposed(publishPost.mcp)) โฆEntry 11 โ a page:
const page = await client.postList({ _first: 20 });
-if (page.hasNextPage) next = page.endCursor;
+next = page.nextCursor; // null exactly when !page.hasMore โ the last page carries no cursorEntry 12 โ a per-action limit:
-configureHttp({ rateLimit: { buckets: { publishPost: { capacity: 10, refillPerSecond: 0.1 } } } });
export const publishPost = action({
input: t.object({ id: t.uuid }),
+ rateLimit: { limit: 10, windowMs: 60_000 },
โฆ
});Entries 4, 13, 16, 17 and 21 โ the members a custom implementation owes:
-const record = { action: 'publishPost', /* โฆ */ };
+const record = { name: 'publishPost', primitive: 'action', /* โฆ */ };
const store: IdempotencyStore = {
+ keepsRedaction: true,
- settle: (key, value, reservationId) => โฆ,
+ settle: (key, value, reservationId, redacted) => โฆ, // keep it; return it as record.redacted
};
const ledger: WebhookLedger = {
+ isDisabled: (endpointId) => Promise.resolve(disabled.has(endpointId)),
};
-await feed.emit({ entity, op, before, after, lsn, txid, orgId, at });
+await feed.emit({ entity, op, before, after, lsn, txid, orgId, at, write: null });Entries 22โ24 โ the app brings its models. Every number below is a placeholder: copy the vendor's
published limits and prices into the app's own models.ts, with the source and the date beside
each, as examples/dummy/apps/web/app/models.ts does.
import { anthropicProvider, configureAi, providerGateway, registerModel } from '@ultimat3/ai';
const usd = (minor: number) => ({ minor, currency: 'USD' }) as const;
export const appModel = registerModel({
id: 'your-model-id',
family: 'your-vendor',
contextWindow: 200_000,
maxOutput: 32_000,
inputPerMillion: usd(300),
outputPerMillion: usd(1_500),
cacheMinimumTokens: 1_024,
reasoning: { effort: true, adaptive: true, disableThinkingUpTo: undefined },
});
configureAi({
gateway: providerGateway({
providers: [anthropicProvider({ models: [appModel.id] })],
defaultModel: appModel.id,
}),
});-new AnthropicProvider()
+anthropicProvider({ models: [appModel.id] }) // entry 6: a factory, not a class
-openAiProvider({ apiKey, models: [...OPENAI_MODEL_IDS] })
+openAiProvider({ apiKey, models: [appModel.id] })
-beforeEach(() => registerOpenAiModels());
+beforeEach(() => registerModel(testModelRow));
-new EchoProvider().generate({ prompt })
+echoProvider().generate({ prompt, model: appModel.id })
-createGateway({ providers, defaultModel })
+providerGateway({ providers, defaultModel })Entries 6 and 32 โ a factory name and three value names, after the edit:
import { structuredLogger, uuidV7 } from '@ultimat3/core';
import { jsonResponse } from '@ultimat3/http';
import { fromMinor } from '@ultimat3/money';
const log = structuredLogger(); // was createLogger()
export function quote(): Response {
const id = uuidV7(); // was uuid()
const price = fromMinor(1_999, 'USD'); // was money(1_999, 'USD')
log.info('quoted', { id, minor: price.minor });
return jsonResponse({ id, price }); // was json({ id, price })
}Entry 7 โ a storage page and the x jobs list --json page:
let cursor: string | null = null;
do {
const page = await disk.list({ prefix: 'avatars/', cursor });
- for (const entry of page.objects) seen.push(entry.key);
- cursor = page.truncated ? page.cursor : null;
+ for (const entry of page.rows) seen.push(entry.key);
+ cursor = page.nextCursor; // null exactly when !page.hasMore
} while (cursor !== null);
-x jobs list --json | jq -r '.data.next'
+x jobs list --json | jq -r 'select(.data.hasMore) | .data.nextCursor'| Surface | What changed |
|---|---|
x shot, the PWA manifests, the service worker, the prerender, the speculation rules, the sitemap, x g --locales, x i18n, the e2e default locale |
read the locales from the app's defineCatalogs() through one reader, appLocaleSet(root) (@ultimat3/i18n/app-catalogs), never app.config.ts โ an app with no packages/i18n/src/index.ts is en alone on every one of them (UNDECLARED_LOCALES) |
x jobs list |
a full last page no longer prints --after <cursor> to an empty page |
| Helm grace periods | sized from drain.deadlineMs (the worker's from drain.workerDeadlineSeconds when > 0, since 26.0.0), which x deploy --method helm passes on every upgrade |
x jobs drain |
still planned: X_NOT_IMPLEMENTED before it boots the queue or leases a job; it takes no --to or --dry-run (entry 15), since no durable second driver ships |
describeAgents() |
model and modelFrom are null where nothing names a model; 'built-in-default' is gone |
| fix lines | no fix: names a vendor's model as the default |
pwa.offline.fallback, .image, .font
|
must be paths on this origin: //host, /\host and absolute URLs are X_CONFIG_INVALID at config load |
a raw MCP tool with no destructive
|
billed to the write bucket and listed as (action); set destructive: false on a hand-registered read |
BarChart |
renders a <figure> around <svg role="img">; class lands on the figure |
x shot --matrix |
directories are <theme>-<w>x<h>, were <theme>-<w>
|
/admin home |
counts a resource only when it declares count: true
|
x env check, x doctor
|
report ULTIMATE_CURSOR_SECRET and STORAGE_SIGNING_SECRET off one list, the published development cursor key counted as unset; x env check judges a table as the boot does (a table naming no environment is deployed), x doctor only where this machine's environment is named staging or production; neither reports the storage key when S3_ENDPOINT selects object storage. X_CURSOR_SECRET_DEV at boot prints the same fix, export ULTIMATE_CURSOR_SECRET="$(openssl rand -hex 32)"
|
ROLE=worker on SIGUSR2 |
retires: stops claiming, finishes every held job, aborts nothing, logs jobs.worker.retiring / jobs.worker.retired, drains, exits 0. Both Helm charts send it from a worker preStop when roles.worker.retireSeconds > 0 (default 0, off) and add those seconds to the worker's grace period. A no-op on Windows |
| a table two producer kinds publish | a sync node keeps the kind it read first for that table and drops the other's changes, logged once as X_REALTIME_PRODUCER_CONFLICT
|
x dev restart |
retires the worker first and waits, on stderr, for its running jobs to finish |
grep -rnwE "locales|defaultLocale|defaultTimeZone|defaultCurrency|tokens|driver|drainTimeoutMs|buildIdHeader|heartbeatMs|urlEnv|poolSize|schema|installPrompt|afterSignInPath|modelEnv|tier" app.config.ts apps packages --include=*.ts
grep -n "mcp" app.config.ts
grep -rnwE "createMemory[A-Z]\w*|createPg[A-Z]\w*|pgSchedulerState|createPostgresClient|create[A-Z]\w*Driver|(Nats|Redis|Memory)DriverOptions|memoryDriver|MemoryIdempotencyStore|(Pg|Memory)VectorStore|MemoryBudgetStore|(Builtin|Memory)Adapter|Memory(Local|Queue)Store|(InMemory|Pg)AdvisoryLock|InMemoryChangeFeed|PgLogicalReplicationFeed|PgOutputDecoder" apps packages --include=*.ts --include=*.tsx
grep -rnwE "assertEnvExample|EnvExampleDriftError|resolveSpeculation|normalizeAuditRecord|NormalizedAuditRecord|contentHash|nearest" apps packages scripts --include=*.ts --include=*.tsx
grep -rnwE "create(AuthLimiter|JwksClient|KdfGate|Pkce|TotpReplayGuard|CacheStack|CdnTier|LruTier|MemoTier|RedisTier|ClientFlight|Context|Fence|FlightGate|Logger|Raster|SingleFlight|Pipeline|RateLimiter|RequestContext|Router|Server|Translator|JobsFacade|Limiter|OutboxRelay|Pacer|Scheduler|StepRunner|Worker|Outbox|SyncNode|Entry|FrameRouter|Replicator|DevServer|McpServer|InstallController|IslandCollector|IsrController|Gateway|PgliteClient|RecordingClient|FocusTrap|FormBinding|RovingTabindex|ToastStore|TestClock|TestMail|TestNetwork|TestStatements|RunJobs|LiveNode|TemplateSql|ArtifactWriter|Prompt|Ring|RobotsGate|SecretBag|UsageMeter)|CreateGatewayInput|AnthropicProvider|EchoProvider|HashEmbedder|RemoteEmbedder|BudgetLedger|McpServer|Builder|authLimiter|totpReplayGuard" apps packages --include=*.ts --include=*.tsx
grep -rnwE "PgDriverOptions|MemoryDriverOptions|PgEventBusOptions|PgOutboxOptions|MemoryOutboxOptions|PgLeaseLeaderOptions|CreateMcpServerInput|CreateDevServerInput|PgVectorStore(Input)?|BuiltinAdapter|MemoryAdapter|Pg(DigestStore|InboxStore|DeliveryLedger)(Options)?|MemoryLedgerOptions|InMemoryAdvisoryLock|InMemoryChangeFeed(Options)?|PgAdvisoryLock(Options)?|PgLogicalReplication(Feed|Options)" apps packages --include=*.ts --include=*.tsx
grep -rnwE "BUILD_ID_HEADER|IDEMPOTENCY_HEADER|pluralize|toBucket|admitsAnonymous|policyPermissions|policyCapability|actorOf|CursorInvalidError|MAX_PAGE_SIZE|derivePath|escapeHtml|readCookie|REQUEST_TIMEOUT_HEADER|isCanonicalWebhookField|WEBHOOK_[A-Z_]+|webhook(Headers|Mac|Signature|SigningString)|Webhook(Mac|Signing)Input|classifyThrown|fnv1a|localeInvalid|timingSafeEqual|DEFAULT_LOCALE|directionOf|isRtl|localeSegment|FRAMEWORK_CATALOG_LOCALE|MAIL_CATALOG_LOCALE|MAX_SEMANTIC_CACHE_SCOPES|FlightJoin|SingleFlight|TIER_ORDER|formatBytes|RENDER_MODES|HYDRATE_STRATEGIES|OFFLINE_STRATEGIES|THEME_STORAGE_KEY|CLIENT_[A-Z_]+_META|NAVIGATION_(SURFACE_|SCOPE_|LOCATION_)?HEADER|tagKeys|BUILD_ID_META|APP_UPDATE_AVAILABLE|REDACTED|(SQL_)?ADMIN_AUDIT_TABLE|isJsonObject|ANY_HOST|host(Decision|Matches)|Host(Rule|Decision)|throwIfAborted|DEFAULT_CONTENT_TYPE|formatIssues|MAX_MONEY_SCALE|ULTIMATE_ERROR_BRAND|JOB_STATES|TEST_TYPES" apps packages --include=*.ts --include=*.tsx
grep -rnwE "forbidden|unauthenticated|json|text|validate|money|isEnabled|describeRoutes|channel|guard|guardBeforeInput|resetRegistry|explain|actorLabel|allowed|denied|relationsFor|invariant|InvariantOptions|uuid|cacheKeyFor|DEFAULT_RETRY|NO_TENANT|chunk|normalize|tokenize|introspect|isDestructive|assertNoDrift|safeUrl|resetIdCounter|srcsetFor|frozenClock|testClock|sessionExpired" apps packages --include=*.ts --include=*.tsx | grep "@ultimat3/"
grep -rnwE "McpExposure|QueryMcp|QueryMcpAnnotations|QueryListParams|QueryListFilterOp|McpAnnotationHints|ListPage|truncated|counted" apps packages --include=*.ts --include=*.tsx
grep -rnE "(Deprecation\w*|recordDeprecatedCall|renderDeprecation|createClientFlight|DEFAULT_CLIENT_RETRY|isSuperseded|isTransientFailure|(get|set|reset)AuditSink).*from '@ultimat3/(action|query)'" apps packages --include=*.ts --include=*.tsx
grep -rnE "\.tool\(\)|toMcpTools?\b|toQueryTools?\b|isExposed\b|toolFromAction|toolFromQuery|McpToolDescriptor|McpInvokeOptions|QueryTool(Descriptor|ReadOptions|Answer)" apps packages --include=*.ts --include=*.tsx
grep -rnwE "endCursor|hasNextPage|SQL_[A-Z_]+|AuditRecord|IdempotencyStore|WebhookLedger|ChangeEvent|DigestAppend|DigestStore|FakeElement" apps packages --include=*.ts --include=*.tsx
grep -rnE "Page\b.*from '@ultimat3/entity'|bearerMount\(|scopes:|buckets:|aria-controls|robots:|fakeBrowser\(|fixtureBrowser\(" apps packages --include=*.ts --include=*.tsx
grep -rnwE "DEFAULT_MODEL|ANTHROPIC_MODEL_IDS|OPENAI_MODEL_IDS|registerOpenAiModels|AnthropicProvider|EchoProvider|defaultModel|registerModel" apps packages --include=*.ts --include=*.tsx
grep -rnE "cdp-url|x shot|jobs drain|jobs (ls|list)|data\.next\b" .github scripts package.json bin
grep -rnE "jobs\.concurrency|startE2eApp\(|jobWorker\(|ResolveOAuthGrants|resolveGrants" apps packages --include=*.ts --include=*.tsx
grep -rn "WORKER_QUEUES" . --include=*.yaml --include=*.yml --include=*.env --include=Dockerfile* --exclude-dir=node_modulesThe typecheck step finds 3โ7, 10, 11, 13โ18, 20, 21, 23, 25โ28, 31โ34, 37 and 40 at the import or
the member, and 2 in a typed app.config.ts. Entries 2 and 8 also refuse at the first import of the
config (X_CONFIG_INVALID), 19 at boot, 9 at route registration and 22 and 24 at the first model
call. The manifest step finds 11's stale openapi.json and 35's stale .env.example, boundaries finds 30, and the unit and
e2e suites find 27. Nothing finds 12 โ a bucket keyed by an action's name is read by nobody โ or 29
until a capture runs; read each configureHttp for 12 and each x shot --cdp-url caller for 29.
A shell script that parses x jobs list --json (7) or passes --to to x jobs drain (15) fails only
when it runs: the last grep above finds it. Nothing finds 36 but an e2e that reads the external
database's rows; the startE2eApp grep finds each caller. Nothing finds 38 or 39 either โ a worker
leaves an unlisted queue's jobs ready with one boot warning, and a hand-built jobWorker simply
runs more at once โ so the last two greps list every WORKER_QUEUES and every jobWorker( to read.
Entry 1 is x doctor's first line.
Two hundred and two entries โ CHANGELOG.md's 24.0.0 entries in their order: grouped by package, lowest tier first. No legacy path, no
codemod, no compatibility shim โ every break is a build error or an X_* error naming the rewrite.
As of 2026-10 slices 01โ10 have landed: @ultimat3/schema and @ultimat3/core; tier 1 โ
i18n, time, db, flags; then cache, seo, storage and one render entry; then slice 04, complete โ tier 2's entity, policy and http, with one testing entry; then slice 05, auth; then slice 06, complete โ query, mcp, admin, then action and the manifest step; then slice 07, jobs; then slice 08, realtime; then slice 09, complete โ render and pwa, with one http entry, then ui; then slice 10, complete โ http again, mcp, mail, notify and manifest, then ai; then slice 11, complete โ testing,
scraping and admin; then slice 12a โ cli's build, generator, test-selection and policy half, with
seo, policy and testing; then slice 12b โ cli's runtime and commands, with http and the
chart; then slice 13a, the repository's guards, with one cli entry; then slice 13b, CI and the
chart; then slice 14, one home per helper โ render, cli, schema and core. Realtime has three operator steps: entries
88โ90, upgrade steps 32โ34. A deployment with MFA-enrolled users has an
operator step: entry 58, upgrade steps 18โ20. A gateway that declares actor or org
ceilings: entry 123, upgrade step 49. The policy and drift steps go red on an
existing app: entries 169โ170, upgrade step 61. Every deploy runs ROLE=migrate before its
serving roles: entry 174, upgrade step 65. A chart upgrade needs its values read first:
entries 192โ194, upgrade step 72. A later slice appends
its rows below the last one and never renumbers; each row's CHANGELOG.md line names it as
(#N) after the dash.
| # | Do | What you see until you do | Entries |
|---|---|---|---|
| 1 | pin every @ultimat3/* to the one new version, bun install
|
nothing yet โ a mixed install is untested | โ |
| 2 |
bun run typecheck, then x verify --only typecheck,unit
|
X_SCHEMA_DEFAULT_INVALID or X_CONFIG_INVALID at the first import of the file that declares it |
4, 6 |
| 3 | read the deploy environment: LOG_LEVEL, ULTIMATE_CURSOR_SECRET, OTEL_EXPORTER_OTLP_TRACES_HEADERS, OTEL_EXPORTER_OTLP_METRICS_HEADERS, OTEL_TRACES_SAMPLER
|
a boot that exits on X_INVARIANT or X_CURSOR_SECRET_DEV; a collector that rejects one signal; every root trace sampled where a leftover ratio thinned them |
7, 13, 14, 16 |
| 4 |
x verify --only unit,contract,e2e and fix the tests it fails |
a date, URL, object or query number that validated and is now refused; a redacted field a test read | 1โ3, 5, 8โ11 |
| 5 | repoint error-monitor rules from extra.<key> to extra.meta.<key>; add an exact host rule for each internal address a wildcard used to admit |
a saved search that matches nothing; a refused request to 127.0.0.1
|
12, 15 |
| 6 |
bun run typecheck again for the tier-1 types: add zone to each formatRelative call, wildcardTime to a hand-built CronExpression, case 'changed-primary-key': to a DriftKind switch, .expression to a CatalogColumn.generated read |
TS2741 / TS2339 at each site | 18, 20, 26, 28 |
| 7 | load the app once (x verify --only unit) and fix each cron and flag declaration it refuses |
X_CRON_INVALID, X_FLAG_EXPIRY_INVALID at the first import of the declaring file |
19, 30 |
| 8 |
x db gen where the database holds an object entry 29 lists; commit packages/db/schema/
|
a dump that no longer matches the one the gate regenerates | 29 |
| 9 |
x verify --only unit,contract,job,live and fix the tests it fails; read every catch and every Promise.all inside a withTransaction body |
X_DB_TRANSACTION_ABORTED where a call used to resolve; X_DB_SIBLING_SCOPE_TIMEOUT after a 30 s wait; โฆnameโง in a rendered string; a relative date counted in calendar days |
17, 20โ25, 27, 31 |
| 10 |
bun run typecheck for the storage types: add policy to each promoteAttachment call, stat() to a hand-written StorageDriver, a guard to each lastModified read, a base path to canonicalRequest / signConstraints
|
TS2741 / TS18048 / TS2554 at each site | 35โ37, 41 |
| 11 | load the app once (x verify --only unit) and rename each cache tag and revalidate.tags entry it refuses |
X_ROUTE_MODE_INVALID at registration; X_CACHE_PURGE_FAILED where a tagged response is built |
33, 43 |
| 12 |
x verify --only unit,contract,e2e and fix the tests it fails; add the real media types to each upload policy; pass { formats: FORMAT_ORDER } where your driver encodes AVIF |
a header or <picture> snapshot that changed; X_STORAGE_TOO_LARGE from a get(); an upload refused for its sniffed type |
32, 34, 38, 40, 42 |
| 13 | after the deploy: list and delete the old-shape image variants; expect signed URLs minted before it to fail for 15 minutes; purge the CDN once if a collection bust must reach older edge copies | orphaned variant files; an outstanding signed URL that no longer verifies | 32, 39, 41 |
| 14 |
bun run typecheck for tier 2: add transactor() to a hand-built or wrapping Driver, import dbDrift from @ultimat3/db, retype a 403 denial status as DenialStatus
|
TS2741 / TS2305 / TS2322 at each site | 44, 45, 48 |
| 15 |
x verify --only unit,contract,policy and fix the tests it fails; read every catch around assertAllowed and every policy predicate's return |
X_UNAUTHENTICATED and a 401 where a test expected X_FORBIDDEN and 403; a denial where a malformed decision allowed; X_INVARIANT_VIOLATED from a preload over 10,000 rows |
46โ49 |
| 16 | set the http config the deploy needs: trustClientCertHeader: true where ctx.peer is read behind a proxy that strips the header, healthDetailPeers for an off-box health reader, cors.origins for a cross-origin browser form, hostname for an embedder |
ctx.peer is null; a health body of three fields; X_CSRF_BLOCKED on an anonymous form post |
50, 51, 53, 56 |
| 17 |
x verify --only unit,contract,e2e and fix the tests it fails: await every browser close(), send content-type with every body, write private, max-age=N where a handler meant a per-user lifetime |
X_BODY_INVALID; a rewritten cache-control; 429 after repeated 401s; a leaked Chrome |
52, 54, 55, 57 |
| 18 |
before the deploy, where any user has MFA: x secrets init, or set ULTIMATE_SECRETS_KEY in the deploy |
X_SEAL_KEY_MISSING at login() for an enrolled user |
58 |
| 19 |
bun run typecheck for auth: a custom AuthLimiter implements reserve / refund; a custom AuthAdapter implements the eight members; callers of verifyApiKey read .record; delete imports of redeemRecoveryCode, mfaRequired, authNotImplemented; enrolment writes through saveTotpSecret; set APP_URL
|
TS2741 / TS2305 / TS2339 at each site; X_ENV_MISSING from oauthLogin
|
60, 61, 64, 65, 67 |
| 20 | deploy, then x auth seal-mfa --json once |
X_MFA_SECRET_UNSEALED (500) at the second factor for every enrolled user |
58 |
| 21 |
x verify --only unit,contract,e2e; move the second-factor client to meta.challenge and completeMfa; list each API key's scopes and pass grantsOf where users hold roles |
a key that resolves with fewer scopes or 401; a client still reading meta.userId; X_AUTH_WRITE_FAILED where a test matched X_DB_UNIQUE_VIOLATION; X_CDP_CALL_FAILED from an offline() a page refused |
61โ63, 66, 68 |
| 22 | once every replica runs this release: x doctor --json, then psql "$DATABASE_URL" -c 'drop table if exists x_auth_failures'
|
X_FRAMEWORK_TABLE_ORPHANED from x doctor
|
59 |
| 23 |
bun run typecheck for slice 06: delete compareValues, pass kindsOf(shape.entity) to compareRows / matchesFilter / isAfterKey, replace adminPermissions with ...ADMIN_PERMISSIONS in definePermissions([...])
|
TS2305 / TS2554 at each site | 69, 72 |
| 24 |
x verify --only unit,contract,e2e; drop .limit() from each read a client pages to the end; read the row, not .rows, from a single: true MCP tool |
a listing that ends at its limit; X_CURSOR_INVALID once for a cursor minted before the deploy; an MCP answer of a different shape |
70, 71 |
| 25 | add idempotent: true to every mutator({ โฆ }); on more than one replica also configureIdempotency({ scope: 'shared' }). A hand-built postgresIdempotencyStore adds origin and reclaimAfterMs
|
TS2741 at each site; X_MUTATOR_NOT_IDEMPOTENT at declaration from untyped code |
73, 77 |
| 26 |
x manifest, commit x.manifest.json and openapi.json
|
the manifest step is X_MANIFEST_STALE
|
74, 79 |
| 27 |
x db gen, commit packages/db/schema/
|
the drift step is X_SCHEMA_DUMP_DRIFT for x_idempotency
|
75 |
| 28 |
x verify --only unit,contract,job; read each test that rolls back an idempotent action or asserts a cache bust inside withTransaction
|
a record left in-flight after a rollback; a bust that fires at commit; X_IDEMPOTENCY_RESERVATION_LOST (409) on a slow attempt |
76, 78 |
| 29 |
bun run typecheck for jobs: a custom EventBus returns a promise from purgeExpired(), a custom EventLookup adds now(), a hand-built Lease adds abandon(), a hand-built RetentionStores adds events; a caller of SQL_CLAIM / SQL_SCHEDULER_FIRE binds the extra parameter |
TS2741 / TS2322 at each site; a statement that fails to bind | 81, 85, 87 |
| 30 | read every job with retry.attempts: 1, every script that cancels a finished job, and every runId passed to enqueue
|
a job buried dead after its worker died; X_JOB_NOT_CANCELLABLE; X_ID_INVALID
|
80, 82, 84, 86 |
| 31 |
x verify --only unit,job and fix the tests it fails |
X_JOB_LEASE_LOST from a step write after a lapsed lease; a dead letter where a test expected a re-run |
80, 83 |
| 32 |
before the deploy: make APP_URL on the sync role the origin the pages are served on โ a declared origin is the whole allow-list; add it to .env.production for an older compose file |
every websocket upgrade is 403 X_SOCKET_ORIGIN_REFUSED
|
88 |
| 33 |
where a channel is declared with params: x db gen "replica identity full", then x db migrate
|
replication.channel_identity_partial in the replicator log; deletes on those channels not announced |
89 |
| 34 |
before the deploy: check the replication role's password method and the connection's sslmode; set ?sslmode=verify-full, move the role to scram-sha-256, or state ?sslmode=disable
|
X_REPLICATION_FAILED when the replicator dials |
90 |
| 35 | roll replicator and sync together โ they must run the same major; point an off-box health reader of the sync role at healthDetailPeers
|
rows that do not parse across a mixed fleet; a health body of three fields | 91, 93 |
| 36 |
bun run typecheck for realtime: a custom Transport adds onReconnect, a custom AdvisoryLock adds onLost and abandon, a custom ChangeFeed adds abandon, an UpgradeTarget adds requestIP, UpgradeDeps adds healthDetailPeers; replace .hasOwnProperty( on decoded records with Object.hasOwn(
|
TS2741 at each site; a TypeError on a null-prototype object |
92, 96 |
| 37 |
x verify --only unit,live,e2e and fix the tests it fails |
X_TOPIC_FORBIDDEN where a channel kept delivering; X_OFFLINE_QUEUE_ABANDONED for a write queued across a sign-out |
94, 95 |
| 38 |
bun run typecheck for render: add case 'unparsable': to an exhaustive switch over NativeReason
|
TS2322 / TS2366 at the switch | 99 |
| 39 | load the app once (x verify --only unit) and give each isr route whose only trigger is a zero-length ttl a positive ttl or tags
|
X_ROUTE_MODE_INVALID at registration, naming the file |
98 |
| 40 |
x build, then x verify --only unit,contract,e2e; handle path === '' in each catch-all handler, read an escaped CSS-module class by its unescaped name, replace a stored specificity with a comparison, point each push url at this origin |
a 200 where a test expected a 404 at a catch-all's bare prefix; a snapshot holding the old scoped class name; styles['w-1'] undefined; a tap that opens the app root |
97, 100โ102 |
| 41 |
bun run typecheck for ui: add current() and appDefault() to a hand-built ThemeEnv; delete each import of THEME_INLINE_SCRIPT / themeInlineScript*, its hand-inlined <script> and its sha256-โฆ in your CSP; widen a Popover trigger's aria-controls to string | undefined
|
TS2741 / TS2305 / TS2322 at each site | 103, 105, 106 |
| 42 |
x build --target static, then x verify --only unit,e2e,budgets; set theme: { defaultMode: 'system' } where the page must follow the OS; read each upload test that posts a file the control refuses |
X_BUDGET_UNMEASURED on every budgeted route until the build; a "System" pick that stays dark; a refused file missing from the request |
104, 107 |
| 43 |
bun run typecheck for http: a hand-written RateLimitStore adds peek(key, bucket, nowMs), a hand-written RateLimiter adds peek(key, bucketName)
|
TS2741 at each site | 108 |
| 44 |
before the deploy: list every probe, monitor, script or agent that sends a credential that can fail โ to an auth: 'required' route or to the MCP endpoint โ from an address real users or agents share; raise rateLimit.defaultBucket or rateLimits.unauthenticated, or move it |
429 X_RATE_LIMITED / 429 X_MCP_RATE_LIMITED for a right credential after a run of wrong ones |
109, 114 |
| 45 |
x manifest, then x db gen; commit x.manifest.json, openapi.json and packages/db/schema/. Where x manifest refuses X_MANIFEST_FACT_INVALID, make the declaration at meta.path finite and run it again |
X_MANIFEST_STALE from the manifest step; X_MANIFEST_BREAKING for jobs.mail.send.input from contract-diff; X_SCHEMA_DUMP_DRIFT for x_notify_digests from drift
|
116โ119 |
| 46 |
x verify --only unit,contract,job,e2e and fix the tests it fails; read each MCP client that parses a 5xx cause, each db.query an agent runs, each hand-registered tool's destructive, each manage_resource list call, each send() inside withTransaction and each recipient a mail is sent to |
a fixed cause where the server's was; X_MCP_QUERY_REJECTED; a writing tool listed as query; X_INPUT_INVALID for a list key; a mail sent only after the commit; X_MAIL_ADDRESS_INVALID or X_VALIDATION_FAILED at send()
|
110โ113, 115, 116 |
| 47 | before dropping a default from a NOT NULL column: bump the app's major in package.json, or keep the default |
X_MANIFEST_BREAKING from contract-diff naming โฆhasDefault
|
120 |
| 48 |
bun run typecheck for ai: a hand-written BudgetStore adds an atomic take(key, tokens, limit); a hand-written or wrapping Gateway implements callLedger(keys); a hand-written VectorStore adds prune(filter, keep); replace each fnv1a import with core's fingerprint
|
TS2741 / TS2554 / TS2305 at each site | 121, 122, 125, 129 |
| 49 |
before the deploy, where createGateway({ budget }) declares actor or org: size each ceiling for every llm(), agent() and hive member a caller makes โ every anonymous caller shares one counter โ and key each gateway.scope() with budgetKeysFor(actor)
|
X_AI_BUDGET_EXCEEDED where the call ran |
123 |
| 50 | load the app once (x verify --only unit,eval); give each numericTolerance a finite, non-negative bound; bind a tenant on each vector store read inside an org's request, or open the backfill store with scope: UNSCOPED
|
X_INVARIANT at the eval file's import; X_VECTOR_UNSCOPED (500) at the read |
128, 131 |
| 51 |
x verify --only unit,eval,e2e and fix what it fails: re-index every store HashEmbedder filled; give each app code an agent tool throws for the model to read a 4xx registerErrorStatus row or a public cause; move a source passed to chunk() under another key; make a fixture's invented finish_reason stop
|
a ranking or vector snapshot that changed; a tool result reading CODE: and a fixed sentence; X_LLM_TRUNCATED where an answer was repaired |
124, 126, 127, 130 |
| 52 |
bun run typecheck for scraping: a hand-written launcher or test double's browser adds target() answering createCDPSession(), whose session has send and on; a burnSession call passes the version of the record the run used |
TS2741 / TS2554 at each site | 137, 139 |
| 53 | load the app once (x verify --only unit) and write each expect.maxDrop as a fraction |
X_INVARIANT at scrape(), naming the scrape |
140 |
| 54 |
x verify --only unit,e2e and fix the tests it fails: read app() inside a test body, set up per mount the state a test carried between island mounts |
X_TEST_APP_NOT_BOOTED where a test expected ReferenceError; a mount that no longer sees the last one's module state; an assertion on key order or a count of clock announcements that changed |
132โ135 |
| 55 |
x verify --only unit,job and fix the scraping runs it fails: move a proxy switch out of localBrowser({ options: { args } }) into proxy or egress; add an exact allowHosts rule for each internal host a wildcard reached and for each host a robots.txt redirects to; re-record each fixture with no readable recordedAt
|
X_SCRAPE_LAUNCH_ARGS_INVALID; X_SCRAPE_HOST_BLOCKED; X_NOT_IMPLEMENTED from a jar-less browser; X_SCRAPE_FIXTURE_STALE; a redaction snapshot or a header that changed; robots rules that no longer apply |
136, 138, 141โ145 |
| 56 |
bun run typecheck for admin: a custom AdminRepo#list handles a null keyset.value; an AdminForm rendered by hand passes version; an exhaustive switch over CrudResult handles 'stale'
|
TS2345 / TS18047 / TS2741 at each site; a switch that no longer compiles | 146, 148, 149 |
| 57 | load the app once (x verify --only unit) and rename a list scope declared as *
|
X_ADMIN_FILTER_INVALID where the resource is declared |
153 |
| 58 |
x verify --only unit,contract,e2e and fix the tests it fails: post the rendered _version with each hand-posted edit form; read each policy rule over input for what an admin create or update writes; read each test that expects an action's or a batch's write to outlive a failed audit entry, or another org's audit rows; fix each link that sends a blank, hex, exponent or off-calendar filter |
a 409 where an edit used to save; a denial on a create or update; a rolled-back write with a failed entry; an audit list without another org's rows; X_ADMIN_FILTER_INVALID
|
147, 150โ152, 154 |
| 59 |
bun run typecheck for slice 12a: a hand-built ProcessRegistrySnapshot adds permissionSites; a one-argument restorePermissions(names) call passes the permissionDeclarationSites() captured with the names |
TS2741 at each snapshot; TS2554 at each restorePermissions call |
156, 171 |
| 60 |
where a sitemap is split (past maxUrls, 50,000 by default): x build --target static, upload the new sitemaps/ folder, delete or redirect each old sitemap-<n>.xml, re-submit /sitemap.xml. A running web role answers SEO from a one-hour memo โ restart it to publish a change sooner |
a search console reporting the old parts as 404; a sitemap change that shows an hour late | 155, 166 |
| 61 |
x verify --only policy,drift: add each permission it names to the app's own definePermissions([...]); x db gen "record replica identity full", then x db migrate
|
X_PERMISSION_BORROWED naming the file; X_DB_SCHEMA_UNMIGRATED for replica identity full
|
169, 170 |
| 62 |
x verify --only filesize,errors,i18n,unit,live,e2e and fix what it fails |
a guard or an apps/*/server.ts / prerender.ts over 500 lines or with an unrunnable fix:; a test under a slice named build, examples or dummy that never ran and now fails |
164, 165 |
| 63 | read each build and CI script: empty a custom --out once or build to .x/static; drop --tag / --out / --no-preflight where the target never reads it; fix each module x verify --only manifest --json lists; read ok and the findings of x g --dry-run; rename a generator name it refuses; expect more work from --affected after a change under scripts/ or guards/
|
X_BUILD_OUT_UNSAFE; X_BUILD_FAILED; X_CLI_BAD_FLAG; X_GENERATE_CONFLICT; a CI job that runs every workspace |
157โ163 |
| 64 | optional, to match a fresh scaffold: pin solid-js 1.9.15; set "typecheck": "tsc --noEmit -p ../../tsconfig.json" in apps/web/package.json and apps/admin/package.json
|
an app typecheck that now reports what the gate already refused | 167, 168 |
| 65 |
before the deploy: make ROLE=migrate run to completion before every serving role. The shipped chart's pre-install/pre-upgrade hook and docker-compose.prod.yml already do; a deploy of your own adds the step |
every web, sync, worker, scheduler and replicator pod refuses to boot: X_FRAMEWORK_SCHEMA_UNAPPLIED
|
174 |
| 66 | read the deploy environment and the monitors: TRUSTED_PROXY_HOPS decimal 1โ64, PORT and METRICS_PORT decimal; repoint an alert on X_PORT_INVALID for hops to X_TRUSTED_PROXY_HOPS_INVALID, and a match on x routes list --json to x routes --json
|
a boot that exits on X_PORT_INVALID or X_TRUSTED_PROXY_HOPS_INVALID; an alert that never fires |
173, 175, 176 |
| 67 | roll the chart: docker/helm carries the replicator probe; a chart x new wrote copies the two edits from docker/helm/templates/_helpers.tpl and service.yaml; read each alert on a not-ready replicator |
a replicator at 0/1 while its stream is down; a worker that stops claiming at SIGTERM | 178, 179 |
| 68 |
bun run typecheck: a hand-built DoctorProbe adds appUrl
|
TS2741 at each site | 182 |
| 69 |
x verify --only unit,contract,e2e and fix what it fails: read each handler that sets its own CSP, each link to an uploaded non-media file, each script or test of /_x/db, x routes list, x verify merge parts and MCP tests.run
|
a page under two policies; a download where a tab opened; 405, 421 or X_CSRF_BLOCKED from /_x; X_CLI_UNKNOWN_COMMAND; X_VERIFY_MERGE_INPUT; failed โฅ 1
|
172, 177, 183, 187, 188, 190 |
| 70 | read each script and CI job that calls x: x help, x i18n add|sync, x db seed, x db backfill, x doctor behind a PORT, x jobs with an id, x shot --out, the x dev and x mcp serve port fixes |
exit 1 where 0 was; a probe of PORT; X_APP_URL_PORT_MISMATCH; X_JOB_UNKNOWN; a screenshot in the cwd; X_PORT_IN_USE
|
180โ182, 184โ186, 189 |
| 71 | where a repository of your own calls checkErrorCodesThrown: pass the codes it retired as the third argument |
X_ERROR_CODE_UNTHROWN for a code whose row says "not thrown"; X_ERROR_CODE_UNTHROWN_STALE for a listed code that is constructed again |
191 |
| 72 |
before the chart upgrade: read the values โ a scraper outside the cluster in networkPolicy.metricsFrom (or networkPolicy.enabled: false); tmp.sizeLimit for a role that writes more than 512Mi; existingSecret or roles.<role>.existingSecret for every role. A chart x new wrote copies templates/_volumes.tpl, templates/networkpolicy.yaml and the two values blocks from a fresh scaffold |
helm template failing on tmp.sizeLimit or on a role with no Secret; a scraper that reads nothing; a pod evicted for /tmp
|
192โ194 |
| 73 | read each test or tool that runs x dev --port 0 or startRoles({ port: 0 }) and dials the sync node at the web port + 1: read syncUrl from the boot's answer |
a refused connection to web + 1 | 195 |
| 74 | x verify |
green, or a finding whose fix: is the edit |
โ |
Tier 0 โ @ultimat3/schema (1โ5), @ultimat3/core (6โ16). Tier 1 โ @ultimat3/i18n (17),
@ultimat3/time (18โ21), @ultimat3/db (22โ29), @ultimat3/flags (30โ31), @ultimat3/cache
(32โ33), @ultimat3/seo (34), @ultimat3/storage (35โ42). Tier 4 โ @ultimat3/render (43).
Tier 2 โ @ultimat3/entity (44โ46), @ultimat3/policy (47โ49), @ultimat3/http (50โ56). Tier 5 โ
@ultimat3/testing (57). Tier 2 โ @ultimat3/auth (58โ67). Tier 5 โ
@ultimat3/testing again (68). Tier 3 โ @ultimat3/query (69โ70), with @ultimat3/mcp (71). Tier 5 โ
@ultimat3/admin (72). Tier 3 โ @ultimat3/action (73โ78). Tier 5 โ @ultimat3/cli (79).
Tier 3 โ @ultimat3/jobs (80โ86). Tier 5 โ @ultimat3/cli again (87). Tier 3 โ
@ultimat3/realtime (88โ96). Tier 2 โ @ultimat3/http again (97). Tier 4 โ @ultimat3/render
(98โ101), @ultimat3/pwa (102), @ultimat3/ui (103โ107). Tier 2 โ @ultimat3/http again
(108โ109). Tier 4 โ @ultimat3/mcp (110โ114), @ultimat3/mail (115โ116), @ultimat3/notify
(117), @ultimat3/manifest (118โ120), @ultimat3/ai (121โ131). Tier 5 โ @ultimat3/testing
(132โ135), @ultimat3/scraping (136โ145), @ultimat3/admin (146โ154). Tier 1 โ
@ultimat3/seo (155). Tier 2 โ @ultimat3/policy (156). Tier 5 โ @ultimat3/cli (157โ170),
@ultimat3/testing (171). Tier 2 โ @ultimat3/http (172โ173). Tier 5 โ @ultimat3/cli
(174โ190), and again (191). Deploy โ the chart, with @ultimat3/cli's scaffold (192โ195).
Tier 4 โ @ultimat3/render (196). Tier 5 โ @ultimat3/cli (197). Tier 0 โ @ultimat3/schema
(198โ199), @ultimat3/core (200โ202).
| # | Surface | Costs you an edit if |
|---|---|---|
| 1 |
t.date, isIsoDateTime, fromIso, timestamp() columns, feed dates, DateTime
|
a fixture, seed, import or client sends a day its month does not have ('2026-02-30', '2026-04-31'), month 00/13, day 00/32. Refused at validation; it used to roll over into the next month. Correct the date at its source |
| 2 | t.url |
a value has a leading or trailing space or control character, or a tab, CR or LF anywhere. value.trim() before validating; an interior tab, CR or LF survives .trim() โ strip or percent-encode it at the source. A stored row that already holds one fails the next time it is validated |
| 3 |
t.object, t.record, t.money
|
you pass a Map, a Date or a class instance. Pass a plain object: { ...instance }, Object.fromEntries(map). Null-prototype objects are still accepted |
| 4 | .default(v) |
v fails the schema it is declared on (t.number.min(5).default(1)). X_SCHEMA_DEFAULT_INVALID at the first import of the file; the cause quotes the rule. Edit the default, or relax the rule |
| 5 | HTTP query and form coercion | a client sends 0x10, 0b11 or 0o17 for a number. It stays a string and fails as expected a number. Send decimal |
| 6 | defineConfig |
app.config.ts or an overlay holds: an unknown roles entry, jobs.backoff, database.driver or theme.defaultMode; a non-boolean database.ssl, realtime.enabled or ai.mcp.expose ('false' from an env variable read as on โ write process.env.X === 'true'); auth.signInPath with no leading / (ai.mcp.path, then also checked here, is deleted in 25.0.0); cache.tiers: []; an empty or non-string jobs.queues entry; one locale twice (['EN', 'en']); a section set to null. X_CONFIG_INVALID names each key |
| 7 | LOG_LEVEL |
a deploy sets a value that is not trace, debug, info, warn, error, fatal or silent, lower-case โ DEBUG and verbose included. The process exits at import (X_INVARIANT); it used to log at info. Unset and empty are unchanged |
| 8 |
retry(), retryDecision()
|
attempts can be NaN, infinite, negative or a fraction, or timeBudgetMs NaN or infinite โ typically Number(process.env.X) on an unset variable. X_INVARIANT before the first try. Parse and default the value before passing it. attempts: 0 still runs once |
| 9 | createFlightGate |
maxConcurrent or maxQueued is NaN, infinite, negative or a fraction (X_INVARIANT at construction), or maxConcurrent is 0 and you expected callers to wait: each is refused with X_FLIGHT_GATE_OVERLOADED
|
| 10 | withChildContext({ signal }) |
the child's work was meant to survive the request. The child's signal now aborts when the parent's does. Move that work to a job |
| 11 | redaction: logs, audit rows, the error monitor | a test, a log query or an audit reader expects the value of a field named like a credential โ currentPassword, mfaSecret, resetToken, recoveryCode, webhookSecret, passwordHash, tokenHash, keyHash. It reads [redacted]. A name ending in token (NPM_TOKEN, confirmationToken), qualified key material (privateKey, AWS_ACCESS_KEY_ID) and a value embedding a credential (connectionString, databaseUrl) read [redacted] too. Ask isRedactedKey('<name>') for any other name. idempotencyToken, continuationToken, maxTokens, cacheKey and code are unchanged |
| 12 | the Sentry envelope | a monitor rule, alert or saved search reads an error's meta key at extra.<key>. It is extra.meta.<key>. scope.extra keys stay at extra.<key>, except fix, docs, stack, requestId and actorId, which the framework's own values now win |
| 13 |
OTEL_EXPORTER_OTLP_TRACES_HEADERS, OTEL_EXPORTER_OTLP_METRICS_HEADERS
|
a deploy sets either one and also OTEL_EXPORTER_OTLP_HEADERS. The per-signal variable was ignored; it now replaces the generic one for that signal. Put every header the signal needs in it, or unset it |
| 14 | OTEL_TRACES_SAMPLER=parentbased_always_on |
OTEL_TRACES_SAMPLER_ARG is also set. The ratio is ignored and every root is sampled. For a ratio: OTEL_TRACES_SAMPLER=parentbased_traceidratio
|
| 15 |
hostDecision, every allowHosts list |
a '*' or '*.suffix' rule was how a request reached a loopback, private, link-local or metadata address literal. Add the exact rule: allowHosts: ['*', '127.0.0.1']. Hostnames are unaffected, including one that resolves inward |
| 16 | ULTIMATE_CURSOR_SECRET |
a compose file or chart sets it to the empty string. Outside local development the boot is X_CURSOR_SECRET_DEV: x secrets set ULTIMATE_CURSOR_SECRET. Cursors issued under the empty key stop verifying โ clients restart from page one |
| 17 |
t(key) with no vars |
a caller relied on the template coming back raw: a key whose message holds {name} or {{. It renders โฆnameโง and {. Pass the vars, or read the template with t.raw(key)
|
| 18 | interval crons, CronExpression
|
a task with * or */n in its minute or hour field must not run twice in a fall-back hour โ make the body idempotent for that hour, or give it a fixed time. A hand-built CronExpression adds wildcardTime (TS2741) |
| 19 | cron expressions | one spells a name loosely (mond, thurs, sept) or has an extra - or / part (1-5-7, 1/2/3). X_CRON_INVALID where the task is declared. Write mon or monday; write one range, one step |
| 20 | formatRelative |
always: add zone (TS2741). A test that asserted on "tomorrow" / "in N days" re-reads its expectation โ the count is midnights crossed in that zone |
| 21 |
addDaysInZone, formatDuration, formatDurationIso, plainDateUtc, addPlainDays, plainDateIn
|
a computed argument can be a fraction, NaN, infinite, or a date outside years 0000โ9999. X_SCHEDULE_INVALID or X_INVARIANT at the call. Round or default the number before passing it |
| 22 | withTransaction |
a body catches a statement error and carries on. X_DB_TRANSACTION_ABORTED; it used to resolve with nothing stored. await withTransaction(() => fallible()).catch(fallback) around the statement, or rethrow |
| 23 |
X_DB_COMMIT_UNKNOWN, onRollback
|
you retry on X_DB_UNAVAILABLE from a commit, or an onRollback undo had to run when the socket died mid-COMMIT. Neither list runs now. In a psql "$DATABASE_URL" session, select a row the transaction wrote; re-run only when it is absent |
| 24 | a nested withTransaction(fn, options)
|
it passes isolation, readOnly: true, deferrable: true or another client (X_INVARIANT). Move the first three to the outermost call; run the other client's work after the outer scope returns |
| 25 | sibling nested withTransaction scopes |
a nested body awaits another scope under the same parent (X_DB_SIBLING_SCOPE_TIMEOUT after 30 s โ it is a cycle), code relied on two siblings interleaving under Promise.all, or the first sibling runs longer than 30 s. Await them in sequence: await withTransaction(first); await withTransaction(second). For a long first sibling pass { siblingWaitMs }; 0 removes the deadline |
| 26 |
DriftKind, the drift check |
you switch over DriftKind exhaustively: add case 'changed-primary-key':. A table re-keyed by hand now fails the check; the finding's fix: is one psql "$DATABASE_URL" -c 'โฆ' command to run against that database, then x db migrate re-checks |
| 27 | introspect() |
you compare ColumnDescription.dataType to 'numeric', 'ARRAY' or 'USER-DEFINED', or assume every IndexDescription.columns entry is a column name. Compare to numeric(12,2), text[], the enum's name; skip entries in parentheses |
| 28 |
CatalogColumn.generated from @ultimat3/db/schema-dump
|
you read it as a string. Read generated.expression; generated.storage says stored or virtual
|
| 29 | the schema dump's unrendered.sql
|
the database holds extended statistics, forced row security, non-default column storage, an unpopulated materialized view, or a trigger on a partitioned table. x db gen, commit the directory |
| 30 | a temporary flag's expiresAt
|
it is not ISO-8601 ('December 1, 2026', '12/01/2026') or names a day its month lacks. X_FLAG_EXPIRY_INVALID at declaration. Write '2026-12-01'
|
| 31 | configureFlags({ reportEveryMs }) |
the value can be NaN, infinite, negative or a fraction โ Number(process.env.X) unset. X_INVARIANT. Parse and default it first; 0 is legal |
| 32 |
Surrogate-Key / Cache-Tag from cacheHeaders()
|
a test asserts the header value, or a CDN rule parses it: each tagged response adds e:<entity>. After the deploy a collection bust misses edge copies cached before it until their s-maxage passes โ purge the CDN once if that window matters |
| 33 | cache tags | a tag's entity or id holds whitespace or a comma. X_CACHE_PURGE_FAILED from cacheHeaders(), surrogateKeys() and the CDN tier's bust. Rename it in its declareTags(...) call |
| 34 |
responsiveImage(), usableWidths
|
your image driver encodes AVIF and you relied on the default offering it: pass { formats: FORMAT_ORDER }. A snapshot of <picture> markup loses its AVIF <source>. A source wider than 8192 gets ?w=8192 as its widest candidate. An intrinsic width that is NaN, zero or negative is X_INVARIANT
|
| 35 | promoteAttachment |
always: add policy (TS2741; X_INVARIANT from untyped code) โ the uploadPolicy() the upload was granted under. An upload over policy.maxBytes is X_STORAGE_TOO_LARGE and stays under pending/
|
| 36 | a hand-written StorageDriver
|
you ship one: add stat(key): Promise<StorageObject | undefined> (TS2741) |
| 37 |
StorageListEntry.lastModified, StorageObject.lastModified
|
you read it without a guard (TS18048), or relied on sweepOrphans deleting objects with no reported date. Handle undefined
|
| 38 | disk.get(key) |
an object can exceed the disk's maxPutBytes (10 MB unless set) โ a presigned upload, a file written by another tool. X_STORAGE_TOO_LARGE. Use disk.stream(key), or set maxGetBytes on the driver |
| 39 | stored image variants | always, if variants exist: keys are <source key>@<transform>.<ext> with the source's extension kept. Old-shape variants are orphaned; delete them with the commands below. A stored reference to a variant key is recomputed with variantKey()
|
| 40 |
variantKey, fitDimensions
|
a width or height can be NaN, zero, negative or a fraction (X_INVARIANT): round it first. A caller that relied on contain upscaling a small source gets the source's size |
| 41 | signed URLs on the local and memory disks | always: URLs minted before the deploy stop verifying (15 minutes by default) โ a client retries with a fresh grant. A caller of canonicalRequest / signConstraints adds the base path: signedUrlBasePath(baseUrl)
|
| 42 | upload policies | a policy allowed video/mp4 and took AVIF, HEIC, MOV, M4A or 3GP under it. Add image/avif, image/heic, video/quicktime, audio/mp4, video/3gpp as needed |
| 43 | a route's revalidate.tags
|
a tag holds whitespace or a comma. X_ROUTE_MODE_INVALID at registration, naming the file. Rename the tag |
| 44 | a hand-built Driver, a hand-built SealedMeta
|
you ship or wrap an entity driver: add transactor() โ a wrapper writes transactor: () => inner.transactor() (TS2741). A hand-built SealedMeta adds plaintext. A hand-built driver kept in a file under the app's own packages/db/ moves the schema hash when it gains transactor(): x verify answers X_DB_DRIFT, and x db gen "<name>" re-records the hash and writes no migration |
| 45 |
dbDrift, ENTITY_ERROR_CODES from @ultimat3/entity
|
you import dbDrift from entity: import { dbDrift } from '@ultimat3/db' (TS2305). A list built from ENTITY_ERROR_CODES no longer holds X_DB_DRIFT
|
| 46 | preload(relation) |
one page attaches more than 10,000 related rows. X_INVARIANT_VIOLATED; pass the bound the relation really has: .preload('comments', { max: 50000 }), or page the relation itself. The last { max } stated on a chain wins. The refusal's fix: is x entities show <entity> --json; the edit is in its cause |
| 47 | assertAllowed |
a catch or a test expects X_FORBIDDEN for a caller with no actor, or for a denied(reason, code) with the app's own code. It is X_UNAUTHENTICATED, or that code, as a PolicyDenialError
|
| 48 |
HttpDenial.status, problem.status, problem.title
|
you type either status as 403, or assert on them: no actor is 401, and the title is the code's registered title. Type them DenialStatus
|
| 49 | policy predicates, definePolicy({ check })
|
one returns something other than a boolean or a PolicyDecision โ { allowed: 'yes' }, an object from another library, nothing. It denies now. Return true, false or denied(reason, code)
|
| 50 | ctx.peer |
you read the client certificate identity behind a proxy. It is null until configureHttp({ trustClientCertHeader: true }) โ set it only where the proxy strips or overwrites x-forwarded-client-cert
|
| 51 | anonymous POST / PUT / PATCH / DELETE from a browser |
a page on another origin posts to this app without a session โ an embedded sign-up form, a marketing site on its own domain. X_CSRF_BLOCKED (403). List the origin: configureHttp({ cors: { origins: ['https://www.example.com'] } }). Requests with no Origin and no sec-fetch-site are unaffected |
| 52 | repeated 401s | a client, probe or test sends many requests that fail auth: 'required' from one address: past rateLimit.defaultBucket they are 429 with Retry-After. Authenticate the probe, point it at /healthz, or raise the bucket |
| 53 |
/healthz, /readyz bodies on the web role |
a monitor off the box reads buildId, the in-flight count or the check names. It gets { state, ready, role }. configureHttp({ healthDetailPeers: ['loopback', 'private'] }), or an exact address. Probes that read only the status need nothing. The sync role's routes are unchanged for now |
| 54 |
cache-control set by a handler |
it says max-age=N, must-revalidate or proxy-revalidate with no private: the response is rewritten as a shared-cache offer would be. For a per-user lifetime write private, max-age=N
|
| 55 | request bodies | a client sends a non-empty body with no content-type. X_BODY_INVALID (422). Send content-type: application/json; a handler that wants raw bytes reads bodyBytes()
|
| 56 | defineHttpConfig |
an embedder relied on HOSTNAME to choose the bind address: pass hostname. A config with buildId: null and BUILD_ID set now runs with skew detection off |
| 57 |
E2eBrowser.close(), LaunchedBrowser.close(), CdpLaunchFailedError
|
a test or script calls close() without await, or reads closed: await browser.close(). A hand-built CdpLaunchFailedError passes { executable, attempts }
|
| 58 |
operator action โ x_users.mfa_secret
|
any user has MFA enrolled. Do the three steps under the table, in order. Until x auth seal-mfa has run, each enrolled user gets X_MFA_SECRET_UNSEALED (500) at the second factor. App code that writes the secret calls saveTotpSecret(auth, userId, secret)
|
| 59 |
x_auth_failures, x_auth_lockouts
|
always, after the rollout completes: x doctor reports X_FRAMEWORK_TABLE_ORPHANED; run psql "$DATABASE_URL" -c 'drop table if exists x_auth_failures'. The new x_auth_lockouts columns are added at boot. Failure counts restart; live lockouts carry over |
| 60 | a custom AuthLimiter
|
you implement one: replace assertAllowed / recordFailure with reserve(key) and refund(reservation) (TS2741). A caller of either removed method calls reserve before the check and refund on success |
| 61 |
verifyApiKey, apiKeyActor
|
you call either: read .record off the result and pass a store with findUserById. A key whose owner is disabled or gone stops working. A key issued with a userId that is not an x_users id: reissue it without userId
|
| 62 | API key scopes | a key holds * or <res>:*, or more than its owner is granted: the extra scopes are dropped. issueApiKey with a wildcard is X_CONFIG_INVALID โ list the scopes. Where users hold roles, pass apiKeyResolver(store, { grantsOf }) or their keys resolve with no scopes |
| 63 | X_MFA_REQUIRED |
a client or handler reads meta.userId to run the second factor. Read meta.challenge and call completeMfa(auth, challenge, code) within 5 minutes |
| 64 |
redeemRecoveryCode, mfaRequired, authNotImplemented
|
you import any (TS2305). A recovery code goes to completeMfa; throw the challenge with mfaChallengeRequired(auth, userId)
|
| 65 | a custom AuthAdapter
|
you ship one: implement the eight members TS2741 names. BuiltinAdapter and MemoryAdapter are the references |
| 66 |
BuiltinAdapter write errors |
you match X_DB_UNIQUE_VIOLATION on a user write. Match X_AUTH_WRITE_FAILED and read meta.column (email, external_id, id) |
| 67 | oauthLogin |
neither baseUrl nor APP_URL is set. X_ENV_MISSING at the start leg: set APP_URL to the app's public origin |
| 68 | E2eSession.offline() |
an e2e test toggles offline while a page refuses the script โ a page mid-navigation, a crashed tab. X_CDP_CALL_FAILED where the call used to resolve and leave that page online. Wait for the page to settle, or close it, before offline()
|
| 69 |
compareValues, compareRows, matchesFilter, isAfterKey from @ultimat3/query
|
you wrote a custom SqlSource or matcher: delete compareValues (TS2305) and pass kindsOf(shape.entity) as the kinds argument of the other three (TS2554) |
| 70 | a read with .limit()
|
a client pages it past the limit with .page() / ?_first= and a cursor. The listing now ends at the limit: the last page inside it answers hasMore: false and nextCursor: null. Drop the .limit() to page to the end. A cursor a client held across the deploy on a limited read is X_CURSOR_INVALID once โ restart from the first page |
| 71 | the MCP tool of a single: true read |
an agent, prompt or test reads { rows } from it. It answers the row itself, or X_NOT_FOUND; its outputSchema is the row |
| 72 |
adminPermissions, permissions declared by importing @ultimat3/admin
|
you import adminPermissions (TS2305), or your own closed permission set relies on the import having declared admin:* before defineAdmin() runs: add ...ADMIN_PERMISSIONS to definePermissions([...])
|
| 73 | mutator({ โฆ }) |
always, for every mutator: add idempotent: true (TS2741; X_MUTATOR_NOT_IDEMPOTENT from untyped code). x g mutator writes it. Running more than one replica: configureIdempotency({ scope: 'shared' })
|
| 74 | openapi.json |
always, if the file is committed: the Problem schema gained instance, requestId, issues, meta and lost the code pattern. x manifest, commit. A generated client that validated code against the pattern regenerates |
| 75 | packages/db/schema/ |
always, if the dump is committed: x_idempotency gains tx_bound. x db gen, commit. The column itself is added at boot |
| 76 | an idempotent action inside withTransaction
|
a test or caller expects the idempotency record settled after a rollback: it is in-flight, and the key can be retried. A retry that overtakes a slow first attempt makes that attempt fail X_IDEMPOTENCY_RESERVATION_LOST (409) |
| 77 | postgresIdempotencyStore |
you build the store yourself: add origin: () => client and reclaimAfterMs: requestDeadlineMs (TS2741). The framework's boot needs nothing |
| 78 |
cache.invalidates, bustAfterCommit
|
a test reads the cache inside the transaction and expects the bust to have happened, or uses bustAfterCommit's return value unguarded: it is undefined when deferred to the commit |
| 79 | the manifest step, x manifest --check
|
a script reads X_MANIFEST_DRIFT for a missing x.manifest.json โ it is X_MANIFEST_MISSING; or relied on contract-diff to report a stale openapi.json โ the manifest step does, as X_MANIFEST_STALE
|
| 80 | a job whose worker can die mid-run | it declares retry.attempts: 1, or relied on being re-claimed after every lapse. A lease that lapses on the final attempt buries the row dead (failed with retry.deadLetter: false), lastError LEASE_LAPSED_FINAL_ATTEMPT. Raise retry.attempts; requeue a buried row with x jobs retry <id>
|
| 81 | a custom JobDriver, SchedulerState or Lease; a caller of SQL_CLAIM / SQL_SCHEDULER_FIRE
|
you ship one: claim buries a final-attempt lapse and reports it through onExhausted, honouring dropExhausted; fire lands watermarkMs; Lease adds abandon(). SQL_CLAIM binds a fifth parameter and returns buried rows; SQL_SCHEDULER_FIRE binds $4
|
| 82 |
cancel on a finished job |
a script or admin action cancels a dead, failed, done or cancelled job: X_JOB_NOT_CANCELLABLE. To remove a dead letter: x jobs delete <id>
|
| 83 | step writes after a lapsed lease | a job body runs longer than its visibility timeout: its next step write is X_JOB_LEASE_LOST. Raise the job's visibility timeout or split the step. A custom StepStore.put accepts the second argument |
| 84 | enqueue(..., { runId }) |
you pass a runId that is not a lowercase uuid โ an upper-case uuid, a slug, a number as text. X_ID_INVALID before anything is staged. Pass crypto.randomUUID(), or omit it |
| 85 | a custom EventBus / EventLookup
|
you ship one: purgeExpired() returns Promise<number> and rejects on failure; the lookup adds now(): Promise<number> (TS2741 / TS2322) |
| 86 | list cursors, backfills.list({ runId }), retry.attempts
|
a cursor's id is not a uuid (X_JOB_PAGE_INVALID), a ledger runId is not one (X_ID_INVALID), or retry.attempts is Infinity or a fraction. Take the cursor from the previous page; declare a whole number of attempts |
| 87 | a hand-built RetentionStores, a reader of PurgeReport.swept
|
you build the stores yourself: add events (TS2741). swept has a sixth entry, x_job_events โ read it by name, not by position |
| 88 | operator action โ the sync websocket's origin rule | always, where APP_URL is set on the sync role or allowedOrigins is passed: a declared origin is the whole allow-list, so it must be the origin the pages are served on. Otherwise every socket is 403 X_SOCKET_ORIGIN_REFUSED; its fix: is the export APP_URL=โฆ that admits the origin that asked. Compose requires APP_URL on sync; the Helm chart sets it from ingress.host; x dev adds its own web origin to the list. Undeclared, the node admits the origin it was reached on. A hand-built SocketOriginRefusedError passes { reason, asked, admitted }
|
| 89 | operator action โ channels declared with params | the app declares one: x db gen "replica identity full", then x db migrate. Until then deletes on those channels are not announced, and the replicator logs replication.channel_identity_partial
|
| 90 | operator action โ the replication connection | the server asks the replication role for a cleartext or md5 password and the URL's sslmode is prefer (the default) or allow: X_REPLICATION_FAILED. Append ?sslmode=verify-full (or verify-ca), move the role to scram-sha-256, or state ?sslmode=disable. require also connects, but verifies no certificate โ it protects the password from a passive listener only |
| 91 |
parseEnvelope, parseChange, the bus wire |
you read change rows off the bus: timestamp() columns are Date, bytes() columns Uint8Array (base64 on the wire). A fleet with replicator and sync on different majors does not interoperate โ deploy both together |
| 92 | a custom Transport, AdvisoryLock, ChangeFeed, UpgradeTarget, UpgradeDeps
|
you ship one: add Transport.onReconnect(listener), AdvisoryLock.onLost(listener) and AdvisoryLock.abandon(), ChangeFeed.abandon(), requestIP(request), healthDetailPeers (TS2741). abandon() is synchronous and writes no goodbye |
| 93 |
/healthz, /readyz bodies on the sync role |
a monitor off the box, or any reader whose request carries Forwarded, X-Forwarded-For, X-Forwarded-Host, X-Forwarded-Proto, X-Real-IP or Via, reads more than { state, ready, role }. List the reader in healthDetailPeers and reach the node directly. Status codes are unchanged |
| 94 | channel guards and subscribes | a subscriber with no actor sat on a channel that declares row, or a guard throws a tenancy refusal: the seat is denied (X_TOPIC_FORBIDDEN). A guard that throws anything else suspends the seat until a later pass. A channel param value over 128 characters is refused, and a socket holding maxTopicsPerSocket latched denials gets no new topic |
| 95 | the offline queue | a write is queued, in flight or being flushed while the principal changes โ sign-out, switch of org. It rejects with X_OFFLINE_QUEUE_ABANDONED and is not sent. Handle the rejection as any outbox refusal; useMutation already rolls its optimistic row back. Not covered: the server does not bind a replayed write to its principal โ see Known gaps
|
| 96 | decoded records.adopt / records.remove / channel params
|
you call .hasOwnProperty(โฆ) or rely on Object.prototype members on them: use Object.hasOwn(value, key)
|
| 97 | a catch-all route (/docs/*path) |
a test or client expects a 404 at the bare prefix (/docs, /docs/), or the handler assumes a non-empty rest: it is reached with path: ''. Handle path === '', or declare a static /docs, which still wins the prefix |
| 98 | revalidate: { ttl } |
an isr route's only trigger is a zero-length string โ '0s', '0ms', '0m'. X_ROUTE_MODE_INVALID at registration; the page never expired. Write a positive ttl, or tags. parseTtlMs('0s') is null
|
| 99 | NativeReason |
you switch over it exhaustively: add case 'unparsable': โ an href or form action no URL parses, left to the browser |
| 100 | CSS modules (*.module.css, *.module.scss) |
always, after a rebuild: every scoped class name changes once โ the suffix hashes the compiled CSS. A snapshot holding one updates. An escaped class (.w-1\.5) is one key, styles['w-1.5']; it was styles['w-1']. A word in a declaration or comment (local(Inter.Regular)) is no longer a key of classes
|
| 101 |
CompiledPattern.specificity, compilePattern
|
you store or compare against a number: it is a positional rank, every value changed, higher still wins โ compare two patterns' specificity. The regex now also matches a catch-all's bare prefix and a percent-encoded literal |
| 102 | push notification url
|
a sender links a tap to another host or a non-http scheme: the app root opens instead. Link to a page of the app. Only an app handing generateServiceWorker a vapid key emits the handler |
| 103 | a hand-built ThemeEnv
|
you build one โ a test fake, a server env: add current() (the document's data-theme) and appDefault() (its data-theme-default), TS2741. A fake that answers null for both keeps the OS rule; a wrapper spreads browserThemeEnv()
|
| 104 |
clearTheme(), watchOsTheme(), resolveTheme(), ThemeToggle
|
theme.defaultMode is 'light' or 'dark' and a user or test expected "System" or an OS change to follow the OS: it returns to that mode and ignores the OS. Set defaultMode: 'system' for an OS-following app. A ThemeEnv that overrode prefersDark with the booted theme โ the bootedEnv() an x new app carries in apps/web/shared/theme-toggle.island.tsx โ must drop it: under 'system' it freezes the OS answer at mount. Delete it and render <ThemeToggle mode="toggle" /> with no env
|
| 105 |
THEME_INLINE_SCRIPT, themeInlineScriptTag, themeInlineScriptHash, themeInlineScriptCspSource
|
you import any (TS2305). Delete the import, the hand-inlined <script> and its sha256-โฆ in script-src; the boot already inlines themeScript and admits its hash |
| 106 |
Popover's trigger
|
the callback annotates 'aria-controls': string, or hands it to a prop typed string (TS2322). It is string | undefined, absent while closed; spread the control object, or widen the type |
| 107 |
Dropzone, FileInput
|
a server, an e2e test or a handler expects to receive a file the control refused by accept, maxBytes or maxFiles: the input holds only accepted files, and a pick that refused everything empties it โ a required control then blocks the submit. Read the refusal from the onSelect selection's rejected
|
| 108 | a hand-written RateLimitStore or RateLimiter
|
you ship one: add peek (TS2741). A store answers peek(key, bucket, nowMs) from the state it keeps โ rateLimitPeek(bucket, refilledTokens(bucket, stored, lastMs, nowMs)), or rateLimitPeek(bucket, bucket.capacity) for a key it holds nothing for โ and writes nothing. A limiter forwards peek(key, bucketName) to its store |
| 109 |
auth: 'required' routes behind one address |
a probe, a test or a client sends failing credentials from an address others share. Once rateLimit.defaultBucket under unauthenticated|ip:<address> is spent, every request from that address to a required route is 429 X_RATE_LIMITED until it refills โ a valid credential included, signed-in users behind the same NAT included. Point the probe at /healthz, authenticate it, give it its own address, or raise the bucket |
| 110 | MCP error text from defineAppMcp (errorAudience: 'caller') |
an agent, prompt or test reads the cause of a 5xx code core's hasPublicCause does not list โ X_DB_STATEMENT_FAILED above all. It reads a fixed sentence pointing at the logs, and fix: x errors explain <CODE> --json unless the error carries a callerFix; resources/read error data likewise. Read the cause in the log line or the error monitor. createMcpServer defaults to 'developer' and is unchanged |
| 111 | db.query |
an agent's query quotes an identifier as U&"โฆ", or calls query_to_xml*, cursor_to_xml*, table_to_xml*, schema_to_xml*, database_to_xml*, ts_stat, ts_rewrite or pg_import_*: X_MCP_QUERY_REJECTED. Spell the identifier plainly; run the inner statement as the query |
| 112 |
surface: 'meta' and a hand-registered tool |
the tool omits destructive and writes: it is listed as query, was action. Declare destructive: true
|
| 113 |
manage_resource list calls |
a call passes an optional input key the query's listParams leaves out (includeDeleted, status_in): an isError result carrying X_INPUT_INVALID. Add it to mcp.listParams, or call the query's own tool. Required input keys are still admitted |
| 114 | the MCP endpoint, mcpHttpRoute, x mcp serve --transport http
|
a client sends missing or wrong bearer tokens from an address an agent also uses: past 20 failures a minute the address is 429 X_MCP_RATE_LIMITED before resolveToken runs, a right token included. Set rateLimits: { read, write, unauthenticated: <n> } on defineAppMcp or mcpHttpRoute. A host of its own that calls route.handle(request) without { address } is not metered |
| 115 |
send() inside withTransaction
|
a handler relied on the mail going out though the transaction rolled back, or a test reads the queue before the commit: the mail.send job is staged and published after COMMIT, and a rollback sends nothing. A facade installed with mode: 'required' refuses a send() outside a transaction (X_OUTBOX_NO_TX) โ wrap the call in a transaction, or pass { sync: true }
|
| 116 | mail recipients, unsubscribeUrl, the mail.send job |
a to, cc, bcc or replyTo holds a control character, </> in the mailbox, or a non-ASCII mailbox: X_MAIL_ADDRESS_INVALID at send() on every driver โ Resend and memory delivered it. Write the domain as punycode; a non-ASCII local part needs another mailbox. An unparseable unsubscribeUrl is X_VALIDATION_FAILED. Always, where x.manifest.json is committed and the app sends mail: contract-diff reports jobs.mail.send.input until x manifest
|
| 117 | packages/db/schema/ |
always, if the dump is committed: the boot creates x_notify_digests and its indexes. x db gen, commit |
| 118 | x.manifest.json |
always, if the file is committed: every query gains input. x manifest, commit. The first diff against a file without it reports no change |
| 119 | manifest facts | a declaration publishes NaN, Infinity or -0 โ a budget, a rate limit, a retry count read from an unset variable: X_MANIFEST_FACT_INVALID at x manifest, naming meta.path. Replace -0 with 0, and NaN or Infinity with a finite number |
| 120 | an entity column's default | a release drops the default of a NOT NULL column: contract-diff reports โฆhasDefault as breaking, so the gate is X_MANIFEST_BREAKING without a major bump. Keep the default, or bump the major in package.json
|
| 121 | a hand-written BudgetStore
|
you ship one: add take(key, tokens, limit) (TS2741) โ add tokens only when the total stays at or under limit, in one atomic step on the store (a Redis EVAL, one conditional update), answering { taken, spent } with spent read before the take. Never spent then add: that pair is the race. Another answer is X_INVARIANT at the reservation |
| 122 | a hand-written or wrapping Gateway
|
you ship one: implement callLedger(keys: BudgetKeys) (TS2741), or pass keys at a callLedger() call (TS2554). A wrapper forwards keys to the wrapped gateway's |
| 123 | createGateway({ budget: { actor, org } }) |
the ceiling is declared: every llm(), agent() and hive member with no scope() open counts against actor:<kind>:<id> and org:<orgId> of its caller, so a caller past it is X_AI_BUDGET_EXCEEDED where the call ran. Every anonymous caller is one counter. Raise the ceiling or drop it; key each gateway.scope() with budgetKeysFor(actor) so a scoped and an unscoped call of one actor share a counter. A hive's members also run under the gateway's request ceiling now |
| 124 |
agent() tool failures |
the model, a prompt or a test reads the cause of a tool's 5xx code that core's hasPublicCause does not list โ an app code with no registerErrorStatus row counts as 500. It reads CODE: and a fixed sentence, plus a declared callerFix. Give a refusal the model should act on a 4xx (registerErrorStatus({ X_ORDER_LOCKED: 409 })) or registerProblemMeta({ X_ORDER_LOCKED: { publicCause: true } })
|
| 125 | fnv1a |
you import it from @ultimat3/ai (TS2305). Use core's fingerprint; a 32-bit number is Number.parseInt(fingerprint(text).slice(0, 8), 16)
|
| 126 | HashEmbedder |
a vector store, a seeded database or a fixture holds vectors it wrote, or a test pins a ranking or a vector: every vector changes. Re-run indexDocument over each document; update the snapshot |
| 127 | an OpenAI-format finish_reason
|
a provider, fake or recorded fixture sends one this build does not know: it reads max_tokens, was end_turn (or no finish, streamed). An answer that also fails its schema is X_LLM_TRUNCATED with no repair turn; .stream() throws it. Send stop from a fake |
| 128 | vector reads in an org's request | a search, searchText or hybrid runs on a store with no tenant bound โ no scope, or an allow list alone โ while the ambient actor has an orgId: X_VECTOR_UNSCOPED (500). store.scoped({ tenant: ctx.actor.orgId }); a deliberate cross-tenant read opens the store with scope: UNSCOPED or calls .scoped(UNSCOPED). A comparison against UNSCOPED's old {} fails โ it is { crossTenant: true }
|
| 129 | a hand-written VectorStore
|
you ship one: add prune(filter, keep) (TS2741) โ delete every in-scope row filter matches except the ids in keep
|
| 130 |
chunk() metadata |
a caller passes its own metadata.source: it is overwritten with the document id, the key indexDocument prunes by. Store the value under another key |
| 131 | numericTolerance |
the bound is NaN, infinite or negative โ often read from an unset variable: X_INVARIANT where it is called. Pass a finite bound of at least 0; 0 is exact match |
| 132 |
describeApp's app()
|
a test reads it while the describe block registers, before beforeAll, and asserts toBeInstanceOf(ReferenceError): it is X_TEST_APP_NOT_BOOTED. Assert toBeUltimateError('X_TEST_APP_NOT_BOOTED'), or read app() inside a test body |
| 133 | assertDeterministic |
a test relied on two results differing only in key order failing X_TEST_NONDETERMINISTIC: it passes. Results compare by core's canonicalJson; a BigInt, Date or Map result compares instead of throwing TypeError, a cyclic one by Bun.deepEquals. A top-level undefined, function or symbol compares by its type: undefined then null is X_TEST_NONDETERMINISTIC. Assert the order itself |
| 134 | frozenClock(now, body) |
a test counts onClockMoved announcements: each call adds two, entering and restoring, so frozen-scheduler lease renewals fire inside and after the body. A listener that throws on entry rejects the call, the body does not run, and the earlier instant is restored. Update the count |
| 135 | mountIsland |
a test relied on module state carrying from one mount to the next: each mount loads from its own directory, a fresh module instance. Set the state up in each mount |
| 136 | localBrowser({ options: { args } }) |
args holds --proxy-server, --proxy-bypass-list, --proxy-pac-url, --proxy-auto-detect or --no-proxy-server (- or --, any case), or is not a list of strings: X_SCRAPE_LAUNCH_ARGS_INVALID before anything launches, exit configured or not. Set the exit with localBrowser({ proxy }) or scrape({ egress }); the caller's other args are kept and the exit appended after them |
| 137 | CdpBrowserLike.target() |
you ship a launcher or a test double that hands back a browser without it (TS2741): add target() answering { createCDPSession() }, whose session has send and on (CdpBrowserSessionLike). Puppeteer has it. A browser that refuses browser-level Fetch.enable fails open() with X_SCRAPE_BROWSER_UNREACHABLE
|
| 138 | a CDP browser with no cookies() or setCookie()
|
session() on it, or restore() of a session holding cookies: X_NOT_IMPLEMENTED, was an empty jar or dropped cookies. Use a puppeteer-core that exposes both, or set auth: { reuse: false } on the scrape() definition |
| 139 | burnSession(plan, seen) |
you call it with one argument (TS2554): pass recordVersion(restored), the version of the record the run used. A record another run saved since is kept. A hand-written ScrapeSessionStore keeps the new optional SessionState.version it is handed. A refused login now also tombstones over a record the run found but did not reuse (reuse: false, past maxAge) |
| 140 | expect.maxDrop |
the value is 1 or above (50 for 50%) or negative: X_INVARIANT at scrape(). Write a fraction in [0, 1) โ 0.5
|
| 141 |
urlSecretValues, redactSecrets
|
a log, artifact or snapshot expected every query value and the whole path concealed: only credential-shaped ones are โ a query key whose words (split on _, -, ., camelCase) include a credential word (apiKey does, keyboard does not), a 16+ character value mixing letters and digits, or the id after /devtools/<browser|page>/. redactSecrets also redacts each secret's percent-encoded (hex in either case), form-encoded and HTML-escaped spellings. Update the snapshot |
| 142 | headers on the HTTP leg | a declared header shares a name with the session's in another case and a request relied on the two being joined (BrowserUA, Mine): the declared one replaces it. Declare the whole value |
| 143 |
allowHosts wildcards on the HTTP leg and the robots read |
a host admitted only by a wildcard resolves to an address that is not public: X_SCRAPE_HOST_BLOCKED, and the connection is pinned to the approved address. Add an exact rule for the internal host. Exact rules and proxied runs are unchanged; the browser leg is not pinned |
| 144 | the robots read, createRobotsGate, robotsFetcher
|
a site's robots.txt redirects more than 5 times, or to a host off allowHosts โ every hop screened, the first included โ or, on a gate built by hand with no allowHosts, to any hostname other than the starting one (a subdomain included; a scheme upgrade, its port change and a moved path are followed): the hop is never requested and the read is "no robots", so no rule applies. List the host it redirects to; pass allowHosts to a gate built by hand. A scrape() run always passes its own |
| 145 | X_SCRAPE_FIXTURE_STALE |
a recorded fixture's recordedAt is not a date and maxAgeMs is set: the recording is stale, was fresh. Re-record it |
| 146 |
KeysetBound.value, AdminCursor.value
|
you ship an AdminRepo whose list reads keyset.value as a string (TS2345 / TS18047): it is string | null, null for a NULL sort value, was ''. NULL sorts as the largest value โ last ascending, first descending |
| 147 | the admin edit form, _version
|
a script, a test or a hand-built page posts an edit without the _version the form rendered, or after the row changed: 409, nothing written, a failed entry with admin.error.row-changed. Post rowVersion(row) of the row the edit was made against. adminUpdate with no version โ MCP, a script โ is unchanged |
| 148 | AdminFormProps.version |
you render AdminForm yourself (TS2741): pass rowVersion(row) for an edit, null for a create |
| 149 | CrudResult |
a switch over kind is exhaustive: handle 'stale' โ row as it is now, version, audit โ which adminUpdate(โฆ, { version }) answers when the version no longer matches |
| 150 | create and update policies | a rule reads the subject's input: an admin create or update is decided again after validation with the written values as input (an update beside row), so the rule now fires there. A write it refuses is denied and audited as one |
| 151 |
/admin/audit, a row's history card |
an actor with an orgId โ a test, an operator โ expected another org's entries: only its own org's are listed. An actor with no orgId still reads every tenant's |
| 152 | an admin action, a set-based matching call, a queued batch |
a test expects the work to stay written when its audit entry cannot be: on postgresAuditLog the work rolls back with the entry, and the log holds one failed entry. A queued batch's id is derived from its content, so a retry dedupes onto chunks still live |
| 153 |
?scope=*, listHref({ scope: null })
|
a list scope is declared as *: X_ADMIN_FILTER_INVALID, rename it. listHref({ scope: null }) writes ?scope=* and opens every row; it opened the default scope |
| 154 | admin list filters | a link or bookmark sends a number filter as a blank, hex (0x10), an exponent (1e3) or Infinity, or a date the calendar lacks (2026-02-30): X_ADMIN_FILTER_INVALID. Send a decimal or a real day |
| 155 | split sitemap parts | a CDN, an object store, a search console or a link holds /sitemap-<n>.xml: the parts are /sitemaps/<n>.xml (SITEMAP_PARTS_DIR), served by the web role, the index unchanged at /sitemap.xml. Rebuild, upload sitemaps/, delete or redirect the old files, re-submit the index |
| 156 | restorePermissions(names, declaredAt) |
you call it with one argument (TS2554): capture permissionDeclarationSites() with knownPermissions() and pass both back. The one-argument call restored names with no declaration site, which the policy step never judges โ X_PERMISSION_BORROWED was silently off for them. Pass {} only when you mean no provenance |
| 157 | x build --target static --out <dir> |
the directory exists, is not empty and holds no .x-export: X_BUILD_OUT_UNSAFE, was deleted. An --out that is the app root or holds it is X_BUILD_OUT_UNSAFE, was X_CLI_BAD_FLAG. Empty it once or build to .x/static (exempt); every build writes the marker |
| 158 | a static build | a module of the app fails to import: X_BUILD_FAILED, the last export left as it was; the build succeeded without that page. x verify --only manifest --json lists each module |
| 159 |
x build flags |
--tag off the docker target, --out on it, or any of --tag, --out, --no-preflight on prebuilt: X_CLI_BAD_FLAG, was ignored. Drop the flag |
| 160 | x g --dry-run |
a script reads ok or data.files: ok: false with the real run's conflicts, and data.files lists only what would be written |
| 161 |
x g and apps/web/api/index.ts
|
the primitive's camelCased file name is already bound in the index โ another feature's module, api, defineApi, health: X_GENERATE_CONFLICT before anything is written. Run the fix: โ the same command under a feature-prefixed name |
| 162 |
x g names |
a plural resource or entity name, a non-ASCII letter, or a name whose type the generated code uses (promise, omit, partial, row): X_CLI_BAD_FLAG. Run the fix: โ x g resource post, x g entity uber, x g resource promise-resource
|
| 163 |
x affected, x test --affected
|
a CI job relies on a change under scripts/, guards/, bin/ or to tsconfig.base.json selecting nothing: any non-doc path no workspace owns is root-wide โ every workspace, every test file |
| 164 | test discovery | an app has a directory named build, examples or dummy below its root: its tests run under x test and on the gate's steps, live and e2e included, and may fail. Fix them; only the root's are skipped |
| 165 |
filesize, errors, i18n
|
a guards/*.ts, apps/*/server.ts or apps/*/prerender.ts is over 500 lines or has a fix: that is not one runnable command: the gate fails. Split it or make the fix a command. The i18n step no longer reads guards/
|
| 166 |
/robots.txt, /sitemap.xml, /sitemaps/<n>.xml on a running web role |
a test or an operator expects a change to the app's routes or SEO settings on the next request: each origin's answer is kept an hour, 8 origins at most. Restart the process |
| 167 |
x new's solid-js
|
a fresh scaffold pins 1.9.15, was 1.9.14. An existing app moves its pin by hand |
| 168 |
x new's app typecheck scripts |
a fresh scaffold's apps/web and apps/admin run tsc --noEmit -p ../../tsconfig.json, the root program: slower, and it reports a type error another workspace's declaration causes. An existing app keeps its scripts until it changes them |
| 169 | the policy step |
an app action, query or route guard requires a permission only a package declared โ defineAdmin()'s <entity>:read|write|delete: X_PERMISSION_BORROWED. Add the name to the app's own definePermissions([...]); the finding names the file |
| 170 | the drift step |
a params channel's records table or a live query's subscribes: table needs REPLICA IDENTITY FULL and no migration records it: X_DB_SCHEMA_UNMIGRATED. x db gen "record replica identity full", then x db migrate. A subscribes: name no entity declares is X_QUERY_SUBSCRIBES_UNKNOWN
|
| 171 | ProcessRegistrySnapshot |
you build one by hand (TS2741): add permissionSites โ permissionDeclarationSites(), or {}
|
| 172 | a handler's own content-security-policy
|
a handler sets one โ to loosen the app's, or a test reads the header: it is kept and the app's is added beside it, both enforced. Loosen through security.csp.extend; read both policies in the test. The report-only header follows the same rule |
| 173 | the fix: of X_ROUTE_NOT_FOUND, the duplicate-route and mount-conflict errors |
a monitor, script or test matches x routes list --json: it reads x routes --json. Codes unchanged |
| 174 | a serving role's boot, ROLE=migrate
|
a deploy starts web, sync, worker, scheduler or replicator on an external DATABASE_URL before ROLE=migrate ran this build, or never runs it: X_FRAMEWORK_SCHEMA_UNAPPLIED, the pod refuses to boot. Run ROLE=migrate (x db migrate) first โ the shipped chart's hook and docker-compose.prod.yml already do. An embedded database applies its own |
| 175 | TRUSTED_PROXY_HOPS |
a malformed value: X_TRUSTED_PROXY_HOPS_INVALID, was X_PORT_INVALID. Write decimal digits, 1 to 64; repoint an alert on the old code |
| 176 |
PORT, METRICS_PORT, ROLE
|
a port written 0x1F90, 8e3 or +80: X_PORT_INVALID, was read as a number. Write decimal digits. A blank ROLE now boots web, where it was refused |
| 177 |
/_storage, /media
|
a link opens an uploaded file that is not a raster image, audio or video โ PDF, SVG, HTML, XML โ in a tab: it downloads, with content-security-policy: sandbox. Embed images, audio and video as before; link to a viewer page for the rest |
| 178 |
worker, scheduler, replicator on SIGTERM |
a process relied on drain.readinessGraceMs before it stopped claiming: it stops at once. web and sync keep the grace |
| 179 | the replicator's readiness, the chart | an alert reads a not-ready replicator as an outage, or a chart was written by x new: the replicator is ready only while its stream runs (/readyz?deep=1 on the metrics port), and its metrics Services publish not-ready addresses. Copy the two edits from docker/helm/templates/_helpers.tpl and service.yaml into a scaffolded chart |
| 180 | x help <name> |
a script asks for a name no command has: X_CLI_UNKNOWN_COMMAND, exit 1 โ was the catalogue and 0. Use the name the fix names |
| 181 |
x i18n add, x i18n sync, x db seed, x db backfill
|
a run reports a finding โ an app module that will not import, included: exit 1, was 0. Fix the module the finding names |
| 182 |
x doctor, DoctorProbe
|
PORT is set and --port is not: the probe reads PORT, was always 3000. A loopback APP_URL on another port is X_APP_URL_PORT_MISMATCH, a malformed one X_CONFIG_INVALID. A hand-built DoctorProbe adds appUrl: string | undefined (TS2741) |
| 183 | the /_x dev dashboard |
a script runs GET /_x/db?sql=, posts from another origin, or reaches x dev under a Host that is not loopback: 405 X_METHOD_NOT_ALLOWED, X_CSRF_BLOCKED, 421 X_DEV_HOST_REFUSED. curl -sS -H 'accept: application/json' --data-urlencode 'sql=select 1' http://localhost:3000/_x/db
|
| 184 | x jobs show|retry|cancel|rm|promote <id> |
the id is not a uuid: X_JOB_UNKNOWN, was X_DB_STATEMENT_FAILED on Postgres. Pass the job's id |
| 185 | x shot --out <relative path> |
a script ran it outside the app root: the file lands relative to the cwd, was the app root. Pass the path from where you run it |
| 186 |
x dev's X_PORT_IN_USE
|
a script parses the fix: it suggests --port N+2, was N+1
|
| 187 | x verify merge |
a part's step has no numeric durationMs or malformed findings: X_VERIFY_MERGE_INPUT. Sharded files that repeat a file or do not hash to corpusHash: red. Shard every job by one rule โ all with --timings, or none |
| 188 | MCP tests.run
|
an agent or test read failed: 0 from a run with an error outside any test, or a non-zero exit: each counts as a failure |
| 189 | x mcp serve --transport http |
the port is taken: X_PORT_IN_USE, with a neighbouring port as the fix, was X_CLI_UNEXPECTED
|
| 190 | x routes |
a script passes a word โ x routes list --json: X_CLI_UNKNOWN_COMMAND, exit 1. x routes --json, --surface <s> to filter |
| 191 |
checkErrorCodesThrown (@ultimat3/cli) |
a repository of your own calls it and relied on a reference row saying "not thrown" or "thrown by nothing" to waive a code: the code is X_ERROR_CODE_UNTHROWN. Pass the retired codes as the third argument โ checkErrorCodesThrown(root, page, new Set(['X_MY_RETIRED'])); it defaults to this repository's UNTHROWN_CODES. A listed code that is constructed again is X_ERROR_CODE_UNTHROWN_STALE
|
| 192 | the chart's NetworkPolicy | the cluster's CNI enforces policies and something outside the cluster scrapes /metrics, or reaches a port other than http: refused. List the scraper in networkPolicy.metricsFrom, narrow networkPolicy.httpFrom to the ingress controller if you want, or set networkPolicy.enabled: false
|
| 193 | tmp.sizeLimit |
a role writes more than 512Mi to /tmp: the pod is evicted. A values file that drops tmp.sizeLimit does not render. Raise it per your measurement |
| 194 |
existingSecret, roles.<role>.existingSecret
|
the release-wide existingSecret is empty and a role names none of its own: helm template fails naming the role. Set one or the other for every role; the migrate Job reads migrate.existingSecret
|
| 195 | the sync listener at port 0 | a test or tool runs x dev --port 0, startRoles({ port: 0 }) or a scratch server and dials the web port + 1: the sync node is on its own kernel-chosen port. Read syncUrl from the boot's answer; pages dial /_x/sync on their own origin and are unaffected |
| 196 |
budget.lcp on defineRoute
|
a route declares lcp: a type error, and X_ROUTE_MODE_INVALID at registerRoute. Delete it from every budget: { โฆ }. A reader of lcp in x routes --json, of budget.lcp in x.manifest.json or of lcpMs in .x/build-stats.json drops it |
| 197 | x test --worker I |
a script passes it: X_CLI_BAD_FLAG, exit 1. x verify --only unit --shard i/n
|
| 198 |
toWireSchema, toOutputSchema (@ultimat3/mcp), mcpSchemaOf (@ultimat3/action), toMcpInputSchema (@ultimat3/schema) |
you import one: not exported. import { toWireSchema, toWireOutputSchema } from '@ultimat3/schema'; for a full draft-07 document toJsonSchema(s, { dialect: 'draft-07', includeDialect: false })
|
| 199 | a tool schema: .tool().inputSchema, @ultimat3/ai's JsonSchema
|
you read format off it, or build a ProjectableAction with additionalProperties: <schema>: no format is published, and additionalProperties is a boolean โ write true. The action's parse still enforces the format |
| 200 |
defineConfig's name
|
the config is JavaScript or untyped and has no string name: X_CONFIG_INVALID at load. defineConfig({ name: 'my-app', โฆ })
|
| 201 |
storageNotImplemented, dbNotImplemented, scrapeNotImplemented, JobsNotImplementedError, CliNotImplementedError, NotImplementedError (@ultimat3/pwa, @ultimat3/realtime) |
you import one, or catch X_NOT_IMPLEMENTED as instanceof StorageError or another package base: not exported, and no longer an instance. import { NotImplementedError } from '@ultimat3/core', or match err.code === 'X_NOT_IMPLEMENTED'
|
| 202 |
readCookie (@ultimat3/auth), escapeAttribute (@ultimat3/seo) |
you import one: not exported. import { escapeHtml, readCookie } from '@ultimat3/core'
|
Entry 58, the one step an operator must not skip. A deployment with MFA-enrolled users:
| Order | Do | Until it is done |
|---|---|---|
| 1 | make the master key exist: x secrets init, or set ULTIMATE_SECRETS_KEY in the deploy |
X_SEAL_KEY_MISSING at login() for an enrolled user |
| 2 | deploy this release | โ |
| 3 |
x auth seal-mfa --json, once |
X_MFA_SECRET_UNSEALED (500) at the second factor for every enrolled user; x doctor reports the count |
Step 3 answers { sealed, alreadySealed, skipped }. It is idempotent, and skipped counts rows
that changed while it ran โ run it again and they are alreadySealed. A custom adapter calls
sealMfaSecrets({ adapter }). A deployment with no MFA users needs none of this.
Entry 39, the orphaned variants. These commands were not run against a real disk or bucket for
this page โ list first, read the list, then delete. The pattern matches a variant of either shape,
so new-shape variants already written are deleted too and regenerate on the next request. A key
of your own that ends in such a suffix (photos/[email protected]) matches as well.
# local disk โ list, then repeat with -delete in place of -print
find <root> -type f -regextype posix-extended -regex '.*@(full|((w|h|q)[0-9]+|cover|contain)(-((w|h|q)[0-9]+|cover|contain))*)\.(avif|webp|jpg|png)' -print
# their sidecars live under <root>/.meta/ as <key>.json โ the same pattern with \.json appended
find <root>/.meta -type f -regextype posix-extended -regex '.*@(full|((w|h|q)[0-9]+|cover|contain)(-((w|h|q)[0-9]+|cover|contain))*)\.(avif|webp|jpg|png)\.json' -print
# s3 โ list, then `aws s3 rm s3://<bucket>/<key>` for each key
aws s3 ls s3://<bucket> --recursive | grep -E '@(full|[whq][0-9]+|cover|contain)[^/]*\.(avif|webp|jpg|png)$'| Surface | What changed |
|---|---|
| a union in HTTP coercion | every member is tried: t.union(t.literal('auto'), t.number) reads '12' as 12. A string a member accepts as a string is never converted |
SchemaError#toJSON() |
carries retry: 'terminal'; a bigint or a cycle in meta no longer throws |
| image errors | a header declaring a zero, negative, fractional or NaN size is X_IMAGE_DECODE_FAILED, was X_IMAGE_TOO_LARGE. A HEIC (mif1 only) is no longer sniffed as AVIF |
| OTLP export | an endpoint with a query string keeps it after the signal path; a NaN or infinite attribute is dropped |
X_VERIFY_STEP_TIMEOUT |
names the test file still running and its fix: runs it; --json findings gain meta
|
fix: lines |
X_REGISTRAR_MISSING / X_REGISTRAR_CONFLICT name the owning package; X_SECRETS_KEY_INVALID names the key file when the file is what is wrong |
x db gen and a changed primaryKey
|
the migration is written โ drop <table>_pkey, add the new key, drop not null for a declared-nullable column leaving it. X_MIGRATION_IRREVERSIBLE for a key another table's foreign key references, and for a new key over a column the same migration adds with no default, or a null one: add and backfill the column in one migration, change the key in the next |
x tasks |
next, last and upcoming are unchanged in form; they come from isoInZone in @ultimat3/time
|
| db error codes | a syscall error (EPIPE, E2BIG) is X_DB_UNAVAILABLE, was X_DB_STATEMENT_FAILED; a ragged array or Invalid Date parameter is X_INVARIANT on both drivers, was X_DB_UNAVAILABLE
|
addBusinessDays, businessDaysBetween
|
the wall time survives a DST day; a date the zone skipped is not counted |
Accept-Language |
a q that is not a plain decimal ranks 0, not 1 |
formatMoney({ trimZeroFraction: true }) |
1250 USD is $12.50, was $12.5; a whole amount still drops .00
|
robots.txt |
seo.robots.disallow is in every group, and a User-agent: * group is emitted when none is declared |
| ISR documents | carry Surrogate-Key and Cache-Tag from revalidate.tags; a response rewritten to private or no-store drops both |
| feeds | Atom gains the channel <author>, <rights>, <icon>; RSS an item author and <media:content> for an item image
|
| storage error codes | a refused write is X_STORAGE_PUT_FAILED, a refused read X_STORAGE_READ_FAILED, a local-disk key under another key X_STORAGE_KEY_CONFLICT โ each was a bare error. An empty org is X_STORAGE_ORG_MISMATCH (404), was X_STORAGE_PATH_UNSAFE (400) |
GET /_storage/:disk/*key |
no Last-Modified when the disk reports no date |
| the Redis cache tier | one extra round trip before each load() and one after the SET; a fill whose load outlives 60 s is not written to Redis |
transition(column, id, move) |
an undefined or null id is X_NOT_FOUND and moves no row; on Postgres it moved every row in the from state |
entity tests on memoryDriver()
|
a unique declared as invariant(name, c.unique([...])) is enforced (X_DB_UNIQUE_VIOLATION); bigint() refuses a value outside int8; trimmed() strips spaces only; url() refuses what the column's CHECK refused โ each as Postgres already did |
update / updateWhere under an app-only invariant |
on Postgres a refused write is rolled back; it used to stay written |
seed dryRun
|
runs every verb in a transaction and rolls back, so its metrics are a real run's and a seed that would fail fails |
| drift findings | each fix: is one runnable command, mostly psql "$DATABASE_URL" -c 'โฆ' then x db migrate; for a table outside public it sets search_path first, and unexpected tables and objects are schema-qualified |
X_CSRF_BLOCKED |
title is "an unsafe request that did not prove same-origin"; code and status unchanged |
| 5xx problem documents | a withheld cause carries callerFix or x errors explain <CODE> --json, never the developer fix:
|
requestTimeoutMs, x-request-timeout-ms
|
above 2,147,483,647 the config is X_CONFIG_INVALID and the header is ignored; both used to time every request out at once |
| redirects | a non-http(s) target is never handed to the client router; the locale-prefix redirect stays on this origin |
Set-Cookie |
two set in one request both reach the wire |
X_CDP_LAUNCH_FAILED |
Chrome gets a 60 s launch deadline and one retry; the error lists each attempt in cause and meta.attempts
|
disableUser |
also revokes the user's live API keys; the result gains apiKeysRevoked
|
updatePrivileges with a passwordHash
|
ends the user's other sessions; the result gains sessionsRevoked
|
| OAuth route failures | the body carries one fixed cause and x errors explain <CODE> --json; the detail is the auth.oauth.refused log line |
| OAuth discovery and JWKS | a document answering for another issuer is refused; a key set with no importable key keeps the cached keys |
x doctor |
two auth probes: unsealed second-factor secrets, and an orphaned framework table |
X_CDP_TIMEOUT |
meta.reading says target-gone, lost-in-transport or no-answer, with the frames seen since the call |
search() refusals |
X_INPUT_INVALID (400), were X_INVARIANT (500) |
entity tests on memoryDriver(), again |
decimal kinds compare by value: '2.5' matches a stored '2.50', 10 matches a bigint() column โ as Postgres does |
| the typed read client | a Date input goes as its ISO instant; an omitted required array reads []
|
transition() |
declared idempotent; its id input follows the entity's key instead of always t.uuid, so the published schema of a transition on a non-uuid key changes |
.contract() |
checks the registry-wide OpenAPI document; an unregistered .named() twin of a registered route is X_CONTRACT_DRIFT
|
ERROR_STATUS |
unchanged for importers; the table now lives in per-tier files |
requeue of an unknown id |
X_JOB_NOT_FOUND (404) on both drivers; was X_INVARIANT on memory, X_DRIVER_UNAVAILABLE on Postgres |
BulkResult.remaining, stats()
|
remaining counts only what a second call would move; queues are ordered by code unit on both drivers |
| worker logs | new lines jobs.claim.exhausted, jobs.worker.slot-renewal-failed, jobs.step.failure-unrecorded; the claim loop no longer spins during a database outage |
x_job_events |
swept by the hourly x.purge; no DDL changed in this slice |
| the replicator | a stream that ends is restarted on retryDelayMs; a web+replicator process answers /readyz 503 while its stream is down or a change is still being refused by the bus; ReplicatorStats gains restarts and failure
|
| the page socket | beats every 10 s by default, was 15 s; the node names the real beat in hello.heartbeatMs
|
realtime fix: lines |
nine Postgres-client fixes are x doctor --json; a bad NATS_URL is named without echoing it |
| records channels | a bus reconnect or a sequence gap sends replay-gap on every open records channel, not only to live queries |
| the outbox | a store that cannot be opened or wiped warns X_LOCAL_STORE_UNAVAILABLE and queues in memory instead of rejecting for the rest of the tab |
| a zone that is not a string |
X_TIMEZONE_INVALID from every zoned function in @ultimat3/time, was a bare TypeError
|
| a static route outside ASCII | matches its percent-encoded request (/precios-espa%C3%B1a), was a 404; a server declaring one starts, where Bun.serve threw |
| ISR regeneration | a render that never settles frees its path after regenerateDeadlineMs (default 30,000); a render slower than that overlaps the next one, and its late page is dropped when a newer one started |
| ISR route lookup | a stored path is filed under the most specific matching route, as the request router picks it, not the first in table order |
| the client router | a reload or full-load Back lands at the saved offset; the page can enter the back/forward cache; <a href="#"> is the browser's; form line breaks go as CRLF; an aborted navigation's stylesheets are retired. Router script +774 B raw |
| render refusals | a non-text prerender() item or param is X_PRERENDER_FAILED, asset() of a non-string X_ASSET_MISSING, a non-string island resolver answer X_ISLAND_INVALID โ each was a bare TypeError
|
display_override |
follows display: fullscreen โ fullscreen, standalone, minimal-ui; minimal-ui โ minimal-ui; browser โ none; standalone unchanged |
| the service worker | a non-JSON push body is the notification text; the install fill skips another build's stamped answer; route rules match encoded literals and a catch-all's bare prefix, in the request router's order โ sw.js changes once |
personalPages: 'last-member' |
a personal page declaring offline: 'precache' is kept in the member's partition; it was never kept |
| the boot's theme script | stamps data-theme-default too: 52โ54 B longer, a new script-src hash the boot admits itself. BUILD_STATS_RULES is 4, so an old .x/build-stats.json is stale until x build --target static
|
<QrCode> |
every code changes its modules โ it now decodes; each was unreadable. A snapshot of its markup updates |
DataTable |
leaves its error or empty state when the query recovers; those states now render inside the table's wrapper, which carries class
|
defineTheme() |
a role overridden in light only is answered with the shipped dark channels in the dark media block; that brand's CSS and brandStyleCspSource() hash change |
createFormBinding |
a local validate that throws or rejects is a failed submit, not a submit() that rejects and stays submitting
|
CommandPalette, Tooltip, Popover
|
centred under dir="rtl"; Popover's inline placements sit beside the anchor, and align applies to block-* placements only; the vertical Tabs indicator mirrors |
useId in an island |
<prefix>-<scope>-<n> with a per-bundle random scope; server ids unchanged |
Textarea in an island |
sets .value; the leading newline is written on the server only |
RelativeTime, BarChart, linkTarget, Meter
|
"1 hour ago", was "60 minutes ago"; bars still drawn from 201 points; //host, HTTPS://โฆ and a leading space are external; aria-valuenow clamped to the drawn bar |
| the icon generator | a non-JSON body or a malformed node is X_UI_INVALID_VALUE; bun run --filter @ultimat3/ui icons --bump moves the pin and regenerates |
a .pattern() with flags |
its JSON Schema carries x-ultimate-pattern-flags beside pattern, in OpenAPI and MCP inputSchema; the input's fact in x.manifest.json moves with it, so step 45 covers it. MCP argument validation applies the flags โ /^[a-z]+$/i accepts ABC
|
db.query |
a column named like a keyword plus digits (set2) is read, was refused as SET
|
| outgoing mail headers |
Reply-To encodes a non-ASCII display name; an ASCII one holding a special is quoted ("Doe, Jane" <[email protected]>); List-Unsubscribe is punycode and percent-encoded. X_MAIL_HEADER_INVALID gains meta.reason
|
SMTP_URL |
a % in the user or password that starts no escape is X_CONFIG_INVALID, was a bare URIError
|
registeredMails() |
ordered by code unit, as registeredMailIds() is |
| notify fan-out | a recipient id named twice is delivered once; it failed X_STEP_DUPLICATE part-way through the audience |
x.purge |
sweeps x_notify_digests windows closed longer ago than PgDigestStore.retentionMs (7 days); PurgeReport.swept has seven entries โ read them by name |
BudgetStore traffic |
an actor or org counter is written only where that ceiling is declared โ an app with no budget writes nothing. Under a declared ceiling the default MemoryBudgetStore holds one counter per caller who spent, per process (no window; a counter that returns to zero is deleted; size() reports the count). A daily or monthly ceiling needs a shared store whose keys expire |
llm() / agent() with a non-object output
|
respond's schema is { value: <output> }; the answer comes back unwrapped, and a prose answer that is not JSON is taken as the text |
| the gateway response cache | every key is a 16-hex fingerprint over the whole request, tools included, so every entry misses once; a failed cache.set is the log line ai.cache.write_failed, the answer still returned |
| the semantic cache | an entry is written under <prompt hash>:<fingerprint>; lookup is by embedding, so stored entries still answer โ unless HashEmbedder wrote them (entry 126) |
estimateTokens, reserve
|
the completion is capped at the model's maxOutput, so a maxTokens above it reserves less |
| provider failure details | read to 4 KiB, 300 characters shown, on every transport; RemoteEmbedder scrubs its key from them |
eval regressions, numericTolerance(0)
|
both sides and the drop rounded to 3 decimals, so a drop of exactly tolerance passes; an exact match at tolerance: 0 scores 1, was 0 |
chunk() |
no chunk exceeds size: the carried overlap yields to a unit it does not fit beside, so a re-index may cut differently |
| the install |
@ultimat3/ai depends on @ultimat3/http
|
| island mount cleanup | the disposeLiveIslands file-boundary hook ships with the island fixture, not @ultimat3/testing/preload: any preload that installs the leak guard runs it. Each mount's scratch directory goes on dispose, on a throw, or at the next file boundary โ none left in the temp root |
| island template parsing |
<!> and <!-- โฆ --> are one empty node; &, <, >, ", ' and numeric references are decoded once in text and attributes โ a reference to 0, a surrogate or past U+10FFFF is U+FFFD |
| a failed scrape session save | after a successful login it is the log line scrape.session.write_failed (step: 'session.save') and the attempt goes on; it failed the run |
| admin list paging | rows sharing a sort value are neither skipped nor repeated at a page boundary, and a backward page answers rows; relation labels resolve every id on the page |
| an admin datetime edit | a field left untouched is left out of the patch, so its stored seconds and sub-seconds survive |
| admin search | a repo that throws lists its resource under skipped (admin.search.skipped.failed), not a failed search |
| the timeline and cache dev panels | only DevSourceUnavailableError is drawn as "unavailable"; any other error from a source is thrown |
BATCH_QUEUED_REASON |
declared in batch-queue.ts, still exported from @ultimat3/admin
|
grep -rnE "\.default\(" apps packages --include=*.ts --include=*.tsx
grep -rnE "retry\(|retryDecision\(|createFlightGate\(|withChildContext\(|allowHosts" apps packages --include=*.ts --include=*.tsx
grep -rnE "LOG_LEVEL|ULTIMATE_CURSOR_SECRET|OTEL_(EXPORTER_OTLP_(TRACES|METRICS)_HEADERS|TRACES_SAMPLER)" docker .github apps packages
grep -rnE "(ssl|enabled|expose): *process\.env" apps packages --include=*.ts
grep -rnE "withTransaction\(" -A12 apps packages --include=*.ts | grep -E "catch|Promise\.all|isolation|readOnly|deferrable|client:"
grep -rnE "formatRelative\(|addDaysInZone\(|formatDuration(Iso)?\(|cron: |expiresAt|reportEveryMs|introspect\(|\.generated\b|DriftKind" apps packages --include=*.ts --include=*.tsx
grep -rnE "promoteAttachment\(|\.lastModified|implements StorageDriver|: StorageDriver = |canonicalRequest\(|signConstraints\(|variantKey\(|fitDimensions\(|\.get\(|allowedContentTypes|responsiveImage\(|usableWidths\(|declareTags\(|revalidate:" apps packages --include=*.ts --include=*.tsx
grep -rnE "assertAllowed\(|X_FORBIDDEN|definePolicy\(|status: 403|\.preload\(|dbDrift|ENTITY_ERROR_CODES|: Driver = |implements Driver" apps packages --include=*.ts --include=*.tsx
grep -rnE "ctx\.peer|trustProxy|cache-control|defineHttpConfig\(|healthz|readyz|\.close\(\)|\.closed\b|CdpLaunchFailedError" apps packages docker .github --include=*.ts --include=*.tsx --include=*.yml --include=*.yaml
grep -rnE "mfaSecret|redeemRecoveryCode|mfaRequired|authNotImplemented|verifyApiKey\(|apiKeyActor\(|actorFromApiKey\(|issueApiKey\(|apiKeyResolver\(|assertAllowed\(|recordFailure\(|implements AuthAdapter|: AuthAdapter = |: AuthLimiter = |X_DB_UNIQUE_VIOLATION|oauthLogin\(|meta\.userId" apps packages --include=*.ts --include=*.tsx
grep -rnE "compareValues|compareRows\(|matchesFilter\(|isAfterKey\(|adminPermissions|single: true|\.limit\(|_first=" apps packages --include=*.ts --include=*.tsx
grep -rnE "mutator\(|postgresIdempotencyStore\(|configureIdempotency\(|bustAfterCommit\(|invalidates:|X_MANIFEST_DRIFT" apps packages scripts .github --include=*.ts --include=*.tsx --include=*.yml
grep -rnE "attempts: 1\b|cancelJob\(|\.cancel\(|runId:|purgeExpired|EventLookup|SQL_CLAIM|SQL_SCHEDULER_FIRE|RetentionStores|\.swept|implements JobDriver|: JobDriver = " apps packages scripts --include=*.ts --include=*.tsx
grep -rnE "APP_URL|SYNC_URL|sslmode|allowedOrigins" docker .env.production .github 2>/dev/null
grep -rnE "NativeReason|\.specificity|compilePattern\(|ttl: *'0|/\*[A-Za-z]+|notificationclick|showNotification" apps packages --include=*.ts --include=*.tsx
find apps -type d -name '[[]...*[]]'
grep -rnF '\.' apps --include='*.module.css' --include='*.module.scss'
grep -rnE "channel\(|parseEnvelope\(|parseChange\(|implements Transport|: Transport = |AdvisoryLock|UpgradeTarget|hasOwnProperty\(|OfflineQueue" apps packages --include=*.ts --include=*.tsx
grep -rnE "ThemeEnv|prefersDark|clearTheme\(|watchOsTheme\(|THEME_INLINE_SCRIPT|themeInlineScript|aria-controls|<Dropzone|<FileInput|defaultMode" apps packages --include=*.ts --include=*.tsx
grep -rnE "RateLimitStore|RateLimiter|defaultBucket|rateLimits|errorAudience|mcpHttpRoute\(|\.handle\(|destructive|listParams|createJobsFacade\(" apps packages --include=*.ts --include=*.tsx
grep -rnE "send\(|replyTo|cc:|bcc:|unsubscribeUrl|SMTP_URL" apps packages docker --include=*.ts --include=*.tsx --include=*.yml --include=*.env*
grep -rnE "BudgetStore|callLedger|createGateway\(|\.scope\(|fnv1a|HashEmbedder|finish_reason|VectorStore|\.scoped\(|UNSCOPED|chunk\(|indexDocument\(|numericTolerance\(|agent\(" apps packages --include=*.ts --include=*.tsx
grep -rnE "describeApp\(|ReferenceError|assertDeterministic\(|frozenClock\(|onClockMoved|mountIsland\(" apps packages --include=*.ts --include=*.tsx
grep -rnE "localBrowser\(|args:|CdpBrowserLike|burnSession\(|maxDrop|urlSecretValues|redactSecrets|allowHosts|createRobotsGate\(|robotsFetcher\(|recordedAt" apps packages --include=*.ts --include=*.tsx
grep -rnE "existingSecret|sizeLimit|networkPolicy|metricsFrom|port: 0|--port 0|syncUrl" docker deploy helm charts apps packages --include=*.yaml --include=*.yml --include=*.ts --include=*.tsx
grep -rnE "ROLE=|migrate|TRUSTED_PROXY_HOPS|METRICS_PORT|PORT=|readinessGraceMs|readinessProbe|content-security-policy|/_storage|/media|/_x/db|x routes list|x help |DoctorProbe|tests\.run|x verify merge|x shot" docker .github apps packages scripts --include=*.ts --include=*.tsx --include=*.yml --include=*.yaml --include=*.tpl --include=*.sh
grep -rnE "sitemap-[0-9]|restorePermissions\(|ProcessRegistrySnapshot|--out|--tag|--no-preflight|--dry-run|--affected|solid-js|\"typecheck\"" apps packages scripts .github --include=*.ts --include=*.tsx --include=*.json --include=*.yml
grep -rnE "keyset\.value|AdminCursor|<AdminForm|CrudResult|adminUpdate\(|_version|listHref\(|scope=|scopes:|definePolicy\(" apps packages --include=*.ts --include=*.tsxTier 0: the typecheck step finds none of 1โ16 โ every entry is a value, not a type. A typed
app.config.ts already refused most of entry 6 at compile time; the ones it did not are
'/'-less paths, empty lists and a locale spelled twice. Entries 4 and 6 throw at the first import;
7 and 16 at boot; 8 and 9 where the call is made. Entries 1โ3, 5, 10 and 11 need the unit,
contract and e2e suites; 12โ15 need a read of the deploy environment and the monitor.
Tier 1: the typecheck step finds 20, and 18, 26 and 28 where a literal, a switch or a string
read exists. Entries 19 and 30 throw at the first import of the declaring file; 24 and 31 at the
call. It finds none of 17, 21โ23, 25, 27 or 29 โ run the unit, contract, job and live suites, and
read every catch and every Promise.all inside a withTransaction body.
Slice 03: the typecheck step finds 35, 36, 37 and the changed signatures of 41. Entry 43 throws
at registration and 33 where the response is built. It finds none of 32, 34, 38, 39, 40 or 42 โ
run the unit, contract and e2e suites, and do step 13 by hand after the deploy.
Slice 04, entity and policy: the typecheck step finds 44, 45 and a 403-typed status in 48. It
finds none of 46, 47 or 49, nor a test asserting on 48's status or title โ run the unit, contract
and policy suites.
Slice 04, http and testing: the typecheck step finds a read of closed and a
CdpLaunchFailedError built with detail (57). It finds none of 50โ56 and no unawaited
close() โ run the unit, contract and e2e suites, and read the deploy's proxy, probe and monitor
configuration for 50, 52 and 53.
Slice 05, auth: the typecheck step finds 60, 64, 65 and the changed shapes of 61. x doctor
finds 58 (the count of unsealed rows) and 59. Nothing finds 62, 63, 66 or a missing APP_URL (67)
before a request does โ run the unit, contract and e2e suites and sign in once with an MFA user
after step 20. The e2e suite finds 68.
Slice 06, query, mcp and admin: the typecheck step finds 69 and the removed adminPermissions
of 72. It finds neither 70, 71 nor a permission set that leaned on the import โ run the unit,
contract and e2e suites.
Slice 06, action and cli: the typecheck step finds 73 and 77. The gate finds 74 and 79 (the
manifest step) and 75 (the drift step). It finds neither 76 nor 78 โ run the unit, contract
and job suites.
Slice 07, jobs: the typecheck step finds the interface members of 81, 85 and 87. It finds none
of 80, 82, 83, 84 or 86, nor an unbound statement parameter in 81 โ run the unit and job suites,
and do step 30 by reading.
Slice 08, realtime: the typecheck step finds 92. Nothing finds 88โ90 before the deploy does โ
they are steps 32โ34, done by reading the deploy's environment. Entries 91 and 93โ96 need the unit,
live and e2e suites; 91 also needs both roles deployed together.
Slice 09, http, render and pwa: the typecheck step finds 99. Entry 98 throws at registration. It
finds none of 97, 100, 101 or 102 โ run the unit, contract and e2e suites after a fresh x build,
and read each push sender's url for 102.
Slice 09, ui: the typecheck step finds 103, 105 and an annotated or string-typed trigger in 106.
It finds neither 104 nor 107 โ run the unit and e2e suites after x build --target static, and
read theme.defaultMode in app.config.ts for 104.
Slice 10, first half โ http, mcp, mail, notify and manifest: the typecheck step finds 108. The
gate finds 116's job schema and 118 (the manifest and contract-diff steps), 117 (the drift
step), 119 at the first manifest build and 120 at contract-diff the day a default is dropped.
It finds none of 109โ115 or 116's recipient rule โ run the unit, contract, job and e2e suites,
and read the deploy's probes and agents for 109 and 114.
Slice 10, ai: the typecheck step finds 121, 122, 125 and 129. Entry 131 throws where
numericTolerance is called โ an eval file's import โ and 128 at the first unscoped read in an
org's request. It finds none of 123, 124, 126, 127 or 130 โ run the unit, eval and e2e suites, read
the gateway's budget before the deploy for 123 (step 49), each agent tool's thrown codes for
124, and re-index what HashEmbedder wrote for 126.
Slice 11, testing and scraping: the typecheck step finds 137 and a one-argument burnSession
(139). Entry 140 throws at scrape() โ the declaring file's import โ and 136 when the browser
launches. It finds none of 132โ135, 138 or 141โ145 โ run the unit, job and e2e suites, and read
each allowHosts wildcard and each site's robots.txt redirect for 143 and 144.
Slice 11, admin: the typecheck step finds 146, 148 and 149. Entry 153's * scope throws where
the resource is declared. It finds none of 147 or 150โ152, nor 154's links โ run the unit, contract
and e2e suites, read each policy rule over input for 150, and each script that posts an edit form
for 147.
Slice 12a, seo, policy, cli and testing: the typecheck step finds 156 and 171. The gate finds
169 (the policy step), 170 (the drift step) and 165 (filesize, errors); 164 is a test that
now runs. x build finds 157โ159 and x g 161โ162 at the call. Nothing finds 155, 160, 163 or
166 โ read the sitemap's host, each script that reads x g --dry-run or --affected, and each
check that expects a fresh SEO answer. 167 and 168 change only a fresh scaffold.
Slice 12b, http and cli: the typecheck step finds 182's DoctorProbe. Entries 174โ176 stop the
boot โ 174 on every serving pod until ROLE=migrate ran, which is step 65, done before the deploy.
Each command in 180, 181, 183โ187, 189 and 190 refuses at the call. Nothing finds 172, 173, 177,
178, 179 or 188 โ run the unit, contract and e2e suites, and read each handler's CSP, each link to
an upload, the chart's replicator and each monitor that matches a fix string.
Slice 13a, cli: nothing finds 191 but the call โ a repository of your own that runs
checkErrorCodesThrown reads its findings once after the upgrade.
Slice 13b, the chart: helm template finds 193's missing tmp.sizeLimit and 194's role with no
Secret โ before the deploy, if it runs in CI. Nothing finds 192 but a scraper that stops reading,
nor 195 but a dial that is refused โ read each values file and each port-0 test.
Sixty-six entries, the 23.0.0 section of CHANGELOG.md, in its order โ which is the order
an existing app meets them.
Six things an agent-written app does differently from 23.0.0 on. Each is the idiom the generators now emit, so copying generated code copies it.
| Do | Instead of | Read |
|---|---|---|
read and write through the typed handle: db.<table>.where({ id }).one(), repo.list(limit)
|
sql template literals and an orgId argument in repo.ts โ the handle scopes every read to the actor's org |
Entities and migrations โ The repo |
call the server from an island through browserClient / browserQueries (apps/web/shared/browser-client.ts) |
fetch( โ the gate refuses it |
Client data |
store a credential in a text().sealed() column |
a plaintext column, or a vault of your own | Entities and migrations |
record a run's ending in onSettled
|
a status column written from the body, or a second job that polls | Jobs and workflows |
keep the tests x g emits, and raise the floor in x.verify.json when coverage rises |
deleting an emitted test, or a floor nobody states | Testing โ Coverage |
operate the queue at /admin/jobs, and give an operator a screen by declaring resources: / actions: (when, batch) on defineAdmin()
|
a hand-written jobs page, or an admin page per operation | Admin dashboard |
Run each step; the entries it closes are in the last column.
| # | Do | What you see until you do | Entries |
|---|---|---|---|
| 1 | pin every @ultimat3/* to the one new version, bun install
|
nothing yet โ a mixed install is untested | โ |
| 2 | add RUN bun node_modules/@ultimat3/cli/src/bin.ts build --target prebuilt to docker/Dockerfile, after the runtime stage's COPY . ., above ENV NODE_ENV=production
|
every web pod logs X_IMAGE_NOT_PREBUILT at boot and compiles every island and stylesheet |
1, 2 |
| 3 |
x db gen, then commit packages/db/schema/ and packages/db/.gitattributes
|
the drift step is X_SCHEMA_DUMP_DRIFT
|
3โ6 |
| 4 |
x verify --only unit --json, then paste the coverage line its finding carries into x.verify.json
|
the unit step is X_COVERAGE_FLOOR_UNSTATED
|
7โ10 |
| 5 | rewrite each raw request the boundaries step names (table under entry 11) |
X_BROWSER_TRANSPORT_BYPASS / X_BROWSER_SERVER_BARREL, one finding per site |
11, 12 |
| 6 |
git mv a defineAdmin() under apps/*/src/ to apps/admin/app/admin/admin.ts; then x manifest and commit x.manifest.json
|
the manifest step is X_ADMIN_UNSCANNED, then X_MANIFEST_DRIFT
|
13, 14 |
| 7 |
x doctor --json, then x g guard <name> for each name in data.guards.missing you adopt, and fix what x verify --only boundaries reports |
nothing โ upgrading installs no guard | โ |
| 8 | drop --feature <other> from x g resource and --live from every x g but query in your scripts; read x tasks --json rows by key; git mv apps/admin/src/pages apps/admin/app/admin/pages
|
X_CLI_BAD_FLAG |
17โ22 |
| 9 |
bun run typecheck and fix each job, driver, export, scraping, admin and type-union site it names |
TS2741 / TS2345 / TS2339 / TS2353 at each hand-built literal, driver, store or option | 23โ43, 55โ57, 63, 64 |
| 10 | delete the admin host: every page.tsx under an admin URL โ /admin/jobs included โ the AdminRepo adapter, the admin action routes, a hand-written jobs nav item; declare defineAdmin({ entities, db }). A pages: page that keeps your own shell or an island keeps its file: on 27.1.0 or later, claim it instead |
X_ADMIN_REPO_UNBOUND at load, X_ROUTE_DUPLICATE for each page file, X_ADMIN_PAGE_PATH_INVALID for a pages: entry under /jobs
|
49โ55 |
| 11 |
x verify --only policy,i18n, then grant every permission it names in the role map and add every admin key it names to each non-en catalog |
X_PERMISSION_UNGRANTED, one per permission a mounted admin route asks for; X_CATALOG_MISSING_KEYS per locale |
15, 16 |
| 12 |
x verify --only unit,job and fix the tests it fails |
a test that leaned on runJobs calling the body directly or on a lease that never renewed, an event published in beforeAll, a row action's old redirect, an admin write or action the row scope now refuses, a social tag on a noindex page |
44โ46, 48, 58โ61 |
| 13 |
x secrets init where no master key exists; correct any credential a site had refused before the first scrape |
X_SEAL_KEY_MISSING before the browser opens; every stored session is logged in again |
62, 65, 66 |
| 14 | x verify |
green, or a finding whose fix: is the edit โ X_PACKAGE_SHAPE for a published package's fixture |
47 |
| # | Surface | Costs you an edit if |
|---|---|---|
| 1 | the app image | your docker/Dockerfile predates 23.0.0. x build --target docker no longer writes .x/islands/; the store is built inside the image. Add the step-2 line. app.config.ts must import with no deployment environment while NODE_ENV is unset โ the line runs above ENV NODE_ENV=production
|
| 2 |
ROLE=worker, ROLE=scheduler
|
a job reaches a defineService, defineStorage or defineCatalogs only through a module that also imports a component. Import that module from apps/web/api/index.ts. The image needs x.manifest.json and a stamped BUILD_ID for the smaller load; without either the role imports everything and logs the fix. A gap is X_ROLE_LOAD_INCOMPLETE, at boot and in the manifest step |
| 3 | packages/db/schema/ |
the app has a migration. Step 3. A release that changes a framework table changes framework/, and this one does: x_jobs, x_outbox, x_scheduler_state, and four new tables (x_job_pauses, x_job_workers, x_job_counters, x_admin_audit). x db gen needs @electric-sql/pglite: bun add -d @electric-sql/pglite # then: x db gen
|
| 4 | an extension the embedded database does not ship (vector, postgis) |
a migration creates one. Set TEST_DATABASE_URL (or DATABASE_URL) to a server that has it, with a role that may create a database, in CI and wherever x db gen runs. Contrib extensions (citext, pgcrypto, โฆ) need nothing |
| 5 |
x db gen's exit code |
a script treats exit 1 as "nothing written". It now exits 1 after writing the migration when the dump fails; the cause names the migration โ keep it |
| 6 |
x db migrate, DriftKind
|
your dev database holds a trigger, function, view, type or sequence no migration creates (unexpected-object, X_DB_DRIFT) โ add the migration or drop the object. An exhaustive switch over DriftKind adds case 'unexpected-object':
|
| 7 |
x.verify.json coverage
|
always. Step 4. Under 95 the line needs a "why"; an exclude entry is { "glob": "โฆ", "why": "โฆ" }. Keep the floor at the measured number โ it is X_COVERAGE_FLOOR_STALE once the tree passes it by 1.5 points, and X_COVERAGE_BELOW_FLOOR names the ten worst files when it drops |
| 8 | a slow gate step | a step runs past 8 minutes (unit, contract, live, job, e2e, eval) or 5 (every other): X_VERIFY_STEP_TIMEOUT. Raise one with "stepTimeoutMs": { "unit": 900000 } in x.verify.json
|
| 9 |
x verify --json in a pipeline |
you parse 2>&1 as JSON. Read stdout only; stderr carries one {"step","ok","ms"} line per finished step |
| 10 | test-order dependence | a unit test relied on which files shared a --parallel worker. The step runs slices of at most 16 files, one process each |
| 11 |
fetch(, new WebSocket(, new XMLHttpRequest(, new EventSource( in browser code |
any *.island.tsx, anything that calls clientTransport/pageClient, or anything they import holds one. The finding names the line; rewrite with the table below |
| 12 | a value import of @ultimat3/entity / @ultimat3/query in browser code |
import @ultimat3/entity/record or @ultimat3/query/client; import type is unaffected |
| 13 | x.manifest.json |
always: step 6. It gains an admin section (each resource's filters, sorts, scopes, row scope, sections, formGroups, related, actions, and the audit store's kind), a concurrency member on a job that declares one, onSettled: true on one that declares that |
| 14 |
defineAdmin() under apps/*/src/
|
your admin is declared there โ the reference app's was. No boot imports that directory, so /admin was never mounted; the manifest step now says so: X_ADMIN_UNSCANNED. git mv apps/admin/src/index.ts apps/admin/app/admin/admin.ts, repoint its relative imports, x verify --only manifest
|
| 15 | the policy step and a mounted admin |
a role should open the admin but the role map does not grant every permission its routes ask for. Each one is X_PERMISSION_UNGRANTED, one finding per permission per mount: grant admin:read and <table>:read|write|delete โ and job:read / job:manage for the jobs dashboard, audit:read for the audit screen โ in the role map |
| 16 | the i18n step and a mounted admin |
a locale lacks a key a mounted admin renders โ resource titles, field labels, sections, scopes, columns, the nav, action labels (admin.action.<name> with no labelKey) and their input labels, the branding key: AdminApp.catalogKeys(). The step was green while the page drew โฆadmin.<table>.titleโง; it is now X_CATALOG_MISSING_KEYS naming each. en is answered by the framework's catalog; a non-en app adds the framework admin keys to its own |
| 17 |
x g entity, x g resource
|
you generate after upgrading. New repos read through db.<table>, export list(limit) (was listByOrg(orgId, limit)), and refuse a read under an actor with no org (X_TENANCY_ACTOR_ORG_REQUIRED). Existing repos keep working. The first x g entity in an older app writes packages/db/src/client.ts and exits with X_DB_HANDLE_UNREGISTERED naming the line for packages/db/src/index.ts
|
| 18 | x g resource <name> --feature <other> |
a script passes it. Drop the flag (X_CLI_BAD_FLAG) |
| 19 |
x g <kind> --live for any kind but query
|
a script passes it. It was ignored on a resource (whose list query is already live); it is X_CLI_BAD_FLAG, fix x g query <name>-feed --feature <name> --live
|
| 20 | a query x g query / x g resource generates |
you generate after upgrading. The read takes { limit } โ no orgId input, no .where({ orgId }): the org is the actor's, scoped by the typed handle. The generated can<X>Read checks only that the actor has an org. Already-generated slices keep working; a caller of a newly generated one passes { limit }
|
| 21 | x g admin:page |
a script or doc expects apps/admin/src/pages/, which the app scan never imported. It writes apps/admin/app/admin/pages/ by default: git mv apps/admin/src/pages apps/admin/app/admin/pages, then update the import in apps/admin/app/admin/admin.ts
|
| 22 | x tasks |
a script runs it with no queue reachable, or reads --json rows by position. It opens the queue when a task is declared; rows gain lastMs, last, lastFiredAtMs
|
| 23 |
<job>.run({ โฆ }) by hand |
a test calls it. Add finalAttempt: isFinalAttempt(<job>.retry, attempt) and progress: () => undefined
|
| 24 | a hand-built job literal or exhaustive switch
|
over JobOutcome ('refused', 'dropped'), QueueStats (failed), WorkerStats (refused, dropped, pollDelayMs), JobHandle, JobDescriptor, JobRecord, JobTrace. Add the member the compiler names |
| 25 | a hand-written JobDriver
|
you ship one. ack/nack take { workerId, claim } and answer Promise<boolean>: driver.ack(id, claimOf(claimed)). Increment a claim ordinal in claim(), fence both settles on it, file a row failed when nackState(options) says so, and answer an existing EnqueueRequest.id with that job and deduped: true
|
| 26 | a hand-written JobIntrospection, or a caller of list()
|
you implement one: add every member TS2741 names โ promoteMany among them, which the admin's jobs dashboard calls. A caller that asked list() for more than 200 rows walks pages with list({ after: jobCursor(lastRow) }); over 200 or a foreign cursor is X_JOB_PAGE_INVALID
|
| 27 | a hand-written LeaseStore, SchedulerState, LeaderElection, or createFleetSlots caller |
add holders(key); fire(driver, { task, occurrenceMs, jobs }); renewEveryMs; read acquire's SlotGrant
|
| 28 | a custom EventBus, createPgEventBus({ clock })
|
add stored and now(); delete the clock option. eventPrompt() on a bus with stored: false is X_DRIVER_UNAVAILABLE outside development and test |
| 29 | a staged enqueue()
|
you read its id or runId: they are now the job's own. A custom OutboxStore persists runId
|
| 30 | retry: { deadLetter: false } |
a job declares it. An exhausted run is now failed (outcome dropped) instead of re-run forever โ check what relied on the loop |
| 31 |
concurrency: 0, a negative, a fraction |
caught as X_INVARIANT. It is X_JOB_DECLARATION_INVALID
|
| 32 | lastError |
you compare it to a rendered message. It ends with โ fix: โฆ now; compare a prefix |
| 33 | the memory job driver | a test relied on a Date, an undefined member or a non-enumerable property surviving the queue. The payload is its JSON form, as on Postgres |
| 34 | exportRows({ sink }) |
always: sink is a thunk, sink: () => disk('exports'), resolved per write. A value is a type error โ at runtime definition.sink is not a function on the first part โ and evaluated disk() at module load, before boot |
| 35 |
rpc({ pathStyle }) in island code, a match on X_ROUTE_NOT_FOUND
|
delete the pathStyle โ the document carries it. A wrong style is X_CONTRACT_DRIFT (still 404) |
| 36 | a live query's source | it names an org by hand. Drop the argument: the source is read as the subscriber's tenant. LiveQueryDefinition.snapshot receives { input, tenant }
|
| 37 |
EntityCore<Row>['$schema'], a hand-built RecordProjection
|
read the type off the entity() result (typeof posts.$schema); add sealed: [] to the projection |
| 38 | x secrets rotate |
something asserts on secrets.enc.json's exact contents. It now keeps ULTIMATE_SECRETS_RETIRED_KEYS; drop a key with x secrets rotate --drop <keyId> once nothing is sealed under it |
| 39 |
interface X extends LinkProps / PaginationProps / DataTableProps<Row>
|
extend one member: TextLinkProps/ButtonLinkProps, PaginationCallbackProps/PaginationLinkProps, DataTableCallbackProps<Row>/DataTableLinkProps<Row>
|
| 40 |
guard.check(root) in a test |
pass guardSources(root) from @ultimat3/cli as the second argument |
| 41 |
checkAppBoundaries, a switch over BuildTarget
|
delete the import; add case 'prebuilt':
|
| 42 | a hand-written e2e driver | read the third argument, { timeoutMs }
|
| 43 | a hand-written E2eApp double |
you build one. E2eApp has a required log(): string โ the spawned app's bounded output, which a failed e2e test now prints (its last 40 lines). Add log: () => ''
|
| 44 |
runJobs in a test |
a test relied on it calling the body directly. Each pass is a real worker's tick(): keyed concurrency waits, whenBusy: 'fail' refuses with X_JOB_KEY_BUSY, and runs claimed in one pass run concurrently. A test that expected two runs of one key to both complete sees a refusal or a wait |
| 45 |
clock.advance() past the visibility timeout in a runJobs test |
a run is in flight while the test advances. runJobs renews its lease on the test clock as a real worker does, so the lapse is X_JOB_LEASE_LOST, as it would be in production. A test that cancels a running job then awaits runJobs.drain() adds clock.advance(1) after the cancel |
| 46 | the jobs event bus in a test | a test published an event in beforeAll and read it in a test. The app's test preload resets the bus before every test: publish inside the test |
| 47 | a non-private workspace package with a src/**/*-fixture.ts
|
you publish one. Its files must carry !src/**/*-fixture.ts, and a fixture reachable from an entry point is X_PACKAGE_SHAPE naming the file to rename. Private packages โ every generated app's โ are exempt |
| 48 |
og:* / article:* / twitter:* on a robots: { index: false } page |
a test or a crawler expected them. A document that may not be indexed carries none |
| 49 | defineAdmin |
always: defineAdmin({ entities, db }) (adminEntitiesOf(db) lists every entity on the handle). Delete each page.tsx under an admin URL, the AdminRepo adapter and the admin action routes. Without db: X_ADMIN_REPO_UNBOUND; a leftover page: X_ROUTE_DUPLICATE
|
| 50 |
guardedPage, AdminRouteConfig.component, RegisteredRepo, adminRouteConfig(route)
|
guardedScreen(app, route, body) or route.respond(โฆ); adminRouteConfig(app, route). The admin view components take no handlers and no loading
|
| 51 |
AdminList, pageRequestOf, adminList
|
you render the list yourself: hrefFor={(location) => listHref(basePath, resource, location)} plus request, scope, counts. An unknown URL parameter is X_ADMIN_FILTER_INVALID, not ignored |
| 52 | the tenant column in the admin | a form posted it. It is stamped from the actor; the posted value is ignored |
| 53 |
describeRoutes(), Stylesheet
|
you list routes or build a Stylesheet by hand: mounted routes carry mount, a stylesheet carries claimed
|
| 54 |
/admin/jobs/*, a hand-written jobs page or nav item |
always: every defineAdmin() now serves the jobs dashboard โ resources x_jobs, x_job_queues, x_job_tasks, x_job_workers and the overview at /admin/jobs. Delete the page and the nav item; a pages: entry at those paths is X_ADMIN_PAGE_PATH_INVALID. A test that renders /admin/jobs installs a queue first: setJobDriver(createMemoryDriver())
|
| 55 |
defineAdmin({ jobs }), AdminApp.jobs
|
delete both: the dashboard reads the queue itself. TS2353 names the site |
| 56 | a hand-written AuditLog, or a caller of entries()
|
entries(query) is async and takes AuditQuery (entity, entityId, actorId, orgId, limit, before, changes): write await log.entries(โฆ). A custom log adds atomic(run) and kind
|
| 57 | InvokeResult |
you read decision off a failure. Failures carry kind: 'denied' | 'not-applicable' | 'invalid'; narrow on kind === 'denied' first |
| 58 | a posted row action's redirect | a test asserts its Location. It is the row (303), not the list |
| 59 | a row-scoped resource's create and update | a write put a row outside the actor's rows(actor). It is refused before the repo is called and audited as admin.error.row-out-of-scope. A text gt/lt row scope cannot be decided here and refuses every write |
| 60 | an action on a row the actor cannot see | it ran. A row that is gone or outside rows is refused, when or none: X_ADMIN_ACTION_NOT_APPLICABLE, 409, the same over MCP |
| 61 | permissionsForOperation('admin', op) |
you read a second element. It answers one permission (['admin:read']), not the same one twice |
| 62 | stored scraping sessions | always. Step 13. Sessions and refusal markers stored before 23.0.0 are deleted on first load, so every run logs in again, and a credential the site had already refused is presented once more. Correct it first on a site that locks an account after repeated failures |
| 63 |
storageSessionStore(disk), artifacts: { storage }
|
pass a thunk: storageSessionStore(() => disk('sessions'))
|
| 64 | a hand-built PromptRequest, AuthContext, ScrapeSecrets, ScrapeReport, SessionInit, a createPrompt call |
add the members TS2741 names; createPrompt({ scrape, handler, page, runId, clock, โฆ })
|
| 65 |
meta.cdpUrl of X_SCRAPE_CDP_ATTACH_FAILED
|
you read the full URL from it. It is wss://host:port
|
| 66 |
localBrowser({ proxy }) with credentials |
your launcher's page has no authenticate() (X_SCRAPE_EGRESS_UNSUPPORTED). Upgrade the launcher or drop the credentials from the URL |
Entry 11, the rewrite for each raw request:
| It was | Write |
|---|---|
fetch('/api/โฆ', { method: 'POST' }) โ an action |
await browserClient.<action>(input) |
fetch('/_x/query/โฆ') โ a read |
useQuery(<QUERY_REF>, input); outside a component, await browserQueries.<query>(input)
|
new WebSocket(โฆ) / new EventSource(โฆ)
|
useQuery(<QUERY_REF>, input) for rows, useChannel(<CHANNEL_REF>, params, { onEvent }) for events |
an upload, fetch or XHR to a signed URL |
await uploadFile({ file, grant, onProgress }) from @ultimat3/storage
|
| anything else |
await clientTransport({ method: 'GET', url }) from @ultimat3/core/page
|
An app scaffolded before 23.0.0 has no apps/web/shared/browser-client.ts; write it as
Client data shows, and add export type Api = typeof api to apps/web/api/index.ts.
grep -rnE "fetch\(|new (WebSocket|XMLHttpRequest|EventSource)\(" apps packages --include=*.tsx --include=*.ts | grep -v -E "route\.ts|\.test\.ts"
grep -rnE "\.run\(\{|ack\(|nack\(|E2eApp|createPgEventBus|storageSessionStore\(|createPrompt\(|guardedPage|adminRouteConfig\(|checkAppBoundaries|pathStyle|exportRows\(|\.entries\(|InvokeResult|permissionsForOperation\('admin'" apps packages --include=*.ts --include=*.tsx
grep -rlE "page\.tsx" apps/admin
grep -rlE "defineAdmin\(" apps/*/src
ls apps/admin/src/pages 2>/dev/null
grep -rnE "x g [a-z:]+ .*--(live|feature)" scripts package.json .github 2>/dev/nullThe typecheck step finds entries 23โ29, 34, 37, 39โ43, 50, 51, 55โ57, 63 and 64. The gate finds
3, 4, 7, 11โ16, 47 and 54; x db migrate finds 6; x g finds 18 and 19. It does not find entry 2
(a worker that misses a side-effect registration logs it at boot), 20, 21, 32, 33, 36, 44โ46, 48,
58โ61 or 62 โ run the unit, job, live and scraping suites, and read the first boot's log.
A 22.x console that mounted each pages: entry in its own <path>/page.tsx โ the app's frame
around adminRouteFor(โฆ).component, islands declared in the mount โ does not have to fold into the
admin's layout. Upgrade to 27.1.0 or later (an earlier 23โ27 has no seam: the island is
X_ISLAND_NOT_HYDRATED, the file X_ROUTE_DUPLICATE), keep every file, and make one edit per
mount plus one in the shared helper:
| In | 22.x | 27.1+ |
|---|---|---|
each <path>/page.tsx
|
const mounted = adminRouteFor(admin, path) (or your mountedPage(page)) FIRST, the island() calls after it |
the island() calls first, then export const config = claimAdminRoute(admin, path, { hydrate, budget, navigation, load }) LAST โ its defineRoute drains them |
each <path>/page.tsx
|
defineRoute({ render, offline, policy: mounted.policy, meta, cache, load: () => โฆmounted.component({ ctx, params, url })โฆ }) |
nothing: render, offline, policy, meta, private no-store and the guarded body are the claim's. Drop cache: 'no-store' (the default for a gated page) and meta
|
| the page component |
props.data = your load's object, body from route.component
|
props.data: AdminClaimData<YourShellData> โ body, status, denied (the body is the refusal, with its own <h1>), titleKey, nav (admin.navFor(ctx)), and app = what your load returned |
| the shared mount helper | a load that resolved the actor, called the guarded component, computed denied with decideAll, read navFor, and withStatus(403 | 404, โฆ)
|
only the shell's own data: load: ({ ctx, url, denied }) => ({ staffRole, mfaEnrolled, locale, path }). Return withStatus(404, data) when the body asked for a 404; the 403 is the admin's |
/admin/page.tsx (your own home) |
a route at the base path |
claimAdminRoute(admin, admin.basePath, { load: () => loadHome() }) โ the dashboard is claimable; data.app is your home's data |
pages: stays the source of the path, title, nav item and permission pair; only a pages: entry
or the dashboard can be claimed, and only by the config claimAdminRoute returned for that exact
path (Admin dashboard โ An admin page in your own shell).
Twenty-three entries, the 22.0.0 section of CHANGELOG.md. Entries 4, 7, 9, 10, 11, 12, 13, 14, 18, 20 and 21 are compile
errors, and so are parts of 3 (planDeploy's fourth argument), 6 (a declaration with no policy)
and 19 (selectTransport's second argument, 'redis'). Entry 15 throws when a sync pod boots,
entry 19 when any realtime role boots with a transport and NATS_URL that disagree, and entry 6
when a channel module loads. Entries 1, 2 and 5 compile unchanged and refuse a string they used to
accept; entry 17 changes a meta.code, and entry 23 throws where a query silently answered [].
Entry 3 also installs a new helm release beside the old one unless you name it. Entry 16 is a
dependency you can drop, entry 22 is one x db gen if drift asks for it, and entry 8 needs no
edit. 22.0.0 is plan 101's deep sweep: every framework package, both tracked apps and the deploy
path.
| # | Surface | Costs you an edit if |
|---|---|---|
| 1 | t.date |
you send a date that is not ISO-8601 in shape ('March 14, 2026', '3/14/2026', '12'). It is refused at validation now (coerceQuery no longer rewrites it). new Date read it at the host's local midnight, so the stored day depended on TZ. Send '2026-03-14'. Epoch-ms numbers and Date objects are unchanged |
| 2 |
timestamp() columns |
you write a string with no Z and no offset ('2026-03-14T09:00') or a non-ISO one. Refused now; it used to be parsed in the host's zone. Write '2026-03-14T09:00:00Z' or a Date
|
| 3 |
x deploy --method helm, planDeploy
|
you deployed with helm before. The release is now named after app.config.ts's name, not the literal app, so the next deploy installs a second release beside the old one. Keep the old one with x deploy --method helm --release app, or helm uninstall app after the new one is up. The deploy now waits for the rollout (default --timeout 15m) and reports data.rollout. planDeploy(image, 'helm', root) needs a fourth argument, { release, namespace, timeout }. --release, --namespace and --timeout are refused on --method compose
|
| 4 | invokeAdminAction({ โฆ expectedConfirmation }) |
you pass expectedConfirmation. Delete it; the gate derives the token from the action's entity and subject.id, and the browser still echoes confirmationToken(entity, id) in confirmation. With both fields omitted a destructive action used to run unconfirmed; it is refused now |
| 5 |
@ultimat3/ui DateTime, toDate
|
you pass a non-ISO string ('August 14, 2026 09:00', '8/14/2026'). Pass an ISO date, a date-time with Z or an offset, or a Date
|
| 6 |
channel(name, { โฆ }) with no policy
|
a declaration omits it. It is a type error and throws X_CHANNEL_DECLARATION_INVALID at load. A public channel says so: policy: allow('public'). ChannelDescription.policy is string, no longer string | null โ delete a null branch |
| 7 | a custom QueueStore or LocalStore
|
you implement one. QueueStore.save(state) โ write(change: QueueChange), and LocalStore.saveQueue(scope, state) โ writeQueue(scope, change): one change per mutation, by key. Whole-queue records a 21.x page wrote are converted on first read, so queued writes survive the upgrade. The shipped stores need nothing |
| 8 | the page's SharedWorker | nothing to edit. The worker is named per build now (workerName(scope, buildId), internal), so tabs of two deploys never share a socket host |
| 9 | ChangeOp |
you switch over it exhaustively. Add case 'truncate': โ a rowless change: a TRUNCATE empties the affected live windows and starts a new epoch on every open channel topic |
| 10 |
Result, Ok, Err, ok, err, map, mapErr, isOk, isErr, tryCatch, unwrap, unwrapOr from @ultimat3/core
|
you import any of them. Gone: throw an UltimateError and try/catch it |
| 11 |
X_USERS_TABLE, X_SESSIONS_TABLE, X_ACCOUNTS_TABLE, X_VERIFICATIONS_TABLE, X_API_KEYS_TABLE, X_USERS_MIGRATION_1_3 from @ultimat3/auth
|
you import them, or pasted them into a migration. Delete both: every boot applies AUTH_TABLES (the 1.3 upgrade included, as add column if not exists) |
| 12 | internals removed from package barrels | you import a runtime value that no other package used โ CHANGELOG.md's 22.0.0 section lists them per package (auth, ui, core, http, entity, query, mcp, ai, mail, notify, pwa, render, scraping, manifest). None is an error class, a code table or a documented API. Copy the constant into your app, or use the documented API it served |
| 13 | the e2e driver: installE2eDriver, e2eFixtures, startE2eApp, e2eApp, e2eBaseUrl, e2eBrowser, openE2eBrowser, cdpConnect, cdpE2eTab, findChrome, launchChrome, the Cdp*Error / E2e*Error classes and their types, from @ultimat3/cli
|
you import any of them. Import them from @ultimat3/testing; the test preload is @ultimat3/testing/e2e-preload. FRAMEWORK_SCRIPTS and FRAMEWORK_INLINE_SCRIPTS stay on @ultimat3/cli. The X_E2E_* / X_CDP_* codes are unchanged |
| 14 |
entityRow, camel from @ultimat3/realtime/server
|
you decode WAL tuples yourself. entityRow(physical) โ entityRow(relation, physical, 'before' | 'after'); it decodes through the registered entity (decodeRow, .column() renames and money included) and refuses a table with no registered entity (X_REPLICATION_PROTOCOL). camel is gone |
| 15 | a sync role on a real database |
it has no reachable change feed. It refuses to boot with X_REALTIME_TOPOLOGY; it used to come up healthy and deliver nothing. Set NATS_URL on web, sync and one replicator, or leave sync off. (x dev --role sync,replicator runs both in one process, for development.) The scaffolded chart (roles.sync.enabled: false) and Compose file (replicas: 0) ship sync off, with the enable recipe beside the switch; a chart or compose file you copied earlier keeps whatever it had |
| 16 |
x shot, x shot --island, the ui.* MCP tools |
you installed puppeteer-core for them. They drive Chrome over raw CDP on the e2e step's launcher now: bun remove puppeteer-core if nothing else imports it. X_SHOT_BROWSER_MISSING now means "no Chrome to launch" โ export CHROME_PATH=<binary> where Chrome is not at /usr/bin/google-chrome or the other probed paths, or pass --cdp-url. A request to a host off the allow list is refused inside the browser and recorded as refused: "host" in the verdict |
| 17 |
ui.interact step failures |
you match meta.code against X_SCRAPE_*. A failed step carries X_SHOT_ELEMENT_MISSING, X_SHOT_ELEMENT_UNREADY or X_SHOT_KEY_INVALID now |
| 18 |
backoffDelay from @ultimat3/realtime
|
you import it. Import core's: import { backoffDelay } from '@ultimat3/core', and pass attempt: n + 1 โ realtime's copy counted from 0, core's counts from 1. A failed channel catch-up now retries after the base wait, not twice it |
| 19 |
realtime.transport, NATS_URL, selectTransport
|
your deploy relied on NATS_URL alone to pick the bus. realtime.transport decides now: 'nats' dials the variable realtime.urlEnv names and refuses the boot (X_CONFIG_INVALID) when it is unset โ it used to fall back to in-process in silence โ and 'memory' with NATS_URL set refuses too. Set transport: 'nats' where the fleet shares a bus, or unset NATS_URL. realtime.enabled: false now starts no sync node and no replicator. transport: 'redis' (never built) no longer typechecks. selectTransport(env) โ selectTransport(env, { transport, urlEnv })
|
| 20 |
startLiveReplicator, LiveReplicator, LiveReplicatorOptions from @ultimat3/testing
|
you import them โ a live-query test or a hand-rolled dev boot. Import them from @ultimat3/realtime/server; @ultimat3/testing's subscribe fixture needs nothing |
| 21 |
@ultimat3/cli's barrel |
you import anything from it but newCommand, dbCommand, verifyCommand and the documented API โ 236 internals are gone (command objects, scan internals, report helpers, option types). maskLiterals / stripComments โ import { maskLiterals, stripComments } from '@ultimat3/core'. The CLI no longer depends on @ultimat3/scraping, so a workspace that reached scraping through it declares it itself |
| 22 |
x db gen's schema hash |
x verify --only drift reports drift right after the upgrade with no entity edited: the hash now uses core's canonicalJson. Run x db gen once. Neither tracked app needed it |
| 23 | a query's sql: () => from(โฆ) over rows its loader selected |
the query filters (where) or sorts (orderBy) on a column the loader's select({ โฆ }) leaves out. That read used to answer [] in silence; it throws X_QUERY_COLUMN_UNSELECTED (QueryColumnUnselectedError) now. Add the column to the loader's select, or drop the filter if the loader already applies it. The reference app's publicPostSlugs was one โ the blog prerendered no article until publishedSlugs selected status and publishedAt
|
grep -rnE "from<|from '@ultimat3/cli'|LiveReplicator|puppeteer|X_SCRAPE_|backoffDelay|selectTransport|transport: 'redis'|expectedConfirmation|planDeploy\(|\bResult<|\b(tryCatch|unwrapOr|mapErr)\(|X_(USERS|SESSIONS|ACCOUNTS|VERIFICATIONS|API_KEYS)_TABLE|X_USERS_MIGRATION_1_3|saveQueue|entityRow\(|\bcamel\b|case 'delete':" apps packages --include=*.ts --include=*.tsx
grep -rnE "from '@ultimat3/cli'" apps packages scripts --include=*.ts | grep -E "e2e|cdp|Chrome|E2e|Cdp"
grep -rnE "channel\(" apps packages --include=*.ts | grep -v policyThe typecheck step finds every removed name, planDeploy's arity and a missing policy. It does
not find a non-ISO date string in a fixture, a seed or a client โ those fail at validation, so run
your contract and e2e suites โ nor a helm release still named app.
Twenty-seven entries. Entries 1โ3, 5, 8, 9, 11โ17, 20โ23 and 27 are compile errors. Entry 24
is a stale file one rebuild replaces, entry 25 is a budgets finding, and entry 26 is capacity
planning, not code. Entry 10
throws when the module loads, and entry 18 when docker compose up starts. Entry 19 is a
number that means something else. Entries 4, 6 and 7 compile
unchanged and answer differently: a wider isSuperseded, a coded error where a bare TypeError
or SyntaxError used to arrive, and a new response body for non-framework HTTP clients. Entry 2
also changes what 'last-write-wins' keeps, and entry 8 leaves two error codes that nothing
throws. 21.0.0 is the client data layer: one record store per tab, one HTTP seam and one socket
per origin. The entries are CHANGELOG.md's 21.0.0 section.
| # | Surface | Costs you an edit if |
|---|---|---|
| 1 | AsyncState |
you import it from @ultimat3/ui. It moved to @ultimat3/core unchanged, and ui does not re-export it. import type { AsyncState } from '@ultimat3/ui' โ import type { AsyncState } from '@ultimat3/core', plus @ultimat3/core in that workspace's dependencies if it is missing. AsyncRegion, asyncBranch, AsyncBranch and AsyncFlags stay in ui |
| 2 | a mutator's conflict: custom(merge)
|
you wrote a merge. It now receives the local and server rows of the record, not the mutator's parsed outputs: custom<PostRow>((local, server) => ({ ...server, title: local.title })). The output-shaped merge never ran. Realtime's rebase holds rows, so it dropped the policy without a word and used its default. Also: resolveConflict moved to @ultimat3/core (its answer is Row, so narrow it); Conflict, CustomConflict and strategyOf are gone from @ultimat3/action, and strategyOf(c) is typeof c === 'string' ? c : c.kind. No compile error warns you that 'last-write-wins' now keeps the local row only when its updatedAt is newer than the server's. A row with no numeric clock resolves to the server's |
| 3 |
ConflictLike, custom, CustomMerge, MergeArgs or ConflictStrategy from @ultimat3/realtime
|
you import any of them. Types: import type { ConflictPolicy } from '@ultimat3/core'. Realtime's own custom(({ local, base, server }) => โฆ) becomes action's custom((local, server) => โฆ): no base, and the merge must return a row with a string id, or the rebase throws X_REBASE_CONFLICT. Delete any branch that returned null to accept a delete. The merge is no longer called when the server deleted the row or the client never held one; the server's answer lands as it is |
| 4 | isSuperseded(error) |
you took true to mean X_SUPERSEDED. It is also true for X_CLIENT_SCOPE_CHANGED, a read in flight when the page changed principal. Code that branches on isSuperseded needs nothing; code that needs the specific code reads error.code
|
| 5 |
QueryRequestFailedError, QueryProblem from @ultimat3/query
|
you import either. A query's failure is a plain UltimateError now: if (e instanceof QueryRequestFailedError) โ if (isUltimateError(e)), then switch on e.code. A query's non-2xx with no framework code is X_CLIENT_TRANSPORT_FAILED, not X_RPC_FAILED, so update a match on the old code |
| 6 | a typed client's network fault, non-JSON 2xx, or non-2xx with no framework code | you catch TypeError, SyntaxError or RpcFailedError, or match 'X_RPC_FAILED', around rpc(), .client(), queryClient() or @ultimat3/storage's uploadFile() on its no-XHR fallback (fetchSignedPut). All of them are X_CLIENT_TRANSPORT_FAILED now: if (isUltimateError(e) && e.code === 'X_CLIENT_TRANSPORT_FAILED'). RpcFailedError still exports and nothing throws it, so an instanceof check against it compiles and never matches. RemoteActionError (the server's own code) and X_CONTRACT_DRIFT are unchanged |
| 7 | the body and OpenAPI 200 of an action whose output references an entity row |
something other than an @ultimat3/* client reads it: a generated SDK, curl, a test posting with fetch. Under x-ultimate-records: 1 the output is body.data, beside records. Regenerate an SDK from the new openapi.json. rpc() and .client() strip the envelope, so typed callers need nothing |
| 8 |
@ultimat3/pwa's background sync |
you pass backgroundSync: { flushEndpoint } to generateServiceWorker, import DEFAULT_FLUSH_ENDPOINT or BackgroundSyncOptions, call backgroundSyncSource(opts), or mounted /_x/outbox/flush yourself. Delete the option and the imports, call backgroundSyncSource() with no arguments, and delete the route. The worker now posts OUTBOX_DRAIN_MESSAGE to open tabs, and the page's outbox replays. pwa.backgroundSync: true in app.config.ts is unchanged. Also delete any branch matching X_PWA_SYNC_FLUSH_FAILED or X_PWA_SYNC_INCOMPLETE: both stay registered, nothing throws them, and such a branch compiles and never runs |
| 9 | ClientScope.principal |
you narrow it with !== null and use it as a string. It is string | null | undefined now. undefined is an unscoped page, rendered for nobody, for which nothing is persisted. Test typeof principal === 'string', and give a switch an undefined arm |
| 10 | a mutator declaring conflict: 'last-write-wins'
|
its entity has no number updatedAt the server writes (a timestamp() string does not count), or its output carries no entity row. It now throws X_MUTATOR_CLOCK_MISSING when the module loads. Add updatedAt as a number column (epoch ms) that the server writes on every update, or declare conflict: 'server-wins'. Without a clock the server row already won every time, so 'server-wins' changes no behaviour |
| 11 |
setLiveClient, clearLiveClient, hasLiveClient, LiveClient, ClientSocket
|
your island builds a LiveClient. Delete the socket adapter, the sync-URL module, new LiveClient(โฆ), client.connect() and setLiveClient(client). Add installRealtime({ signal: createSignal }) in mount before the first render. hasLiveClient() โ hasPageSocket(). client.subscribe(topic, fn) โ useChannel(decl, params, { onEvent, onPresence }) or usePresence(decl, params)
|
| 12 |
useLive, LiveRows, LiveInput, liveHookFor
|
you read a live query. useLive<Row>({ name }, input) โ useQuery<Row>({ name, live: true }, input). feed.state() === 'live' โ feed().status === 'ready', the rows are feed().data, and unsubscribe() โ release(). Delete liveHookFor bindings |
| 13 |
IdentityMap, privateScope, rowKey, RowScope, RowKey
|
you import them. RecordStore, recordKey(type, key), RecordKey; read one record with useRecord(type, key)
|
| 14 |
createOpfsLocalStore, RebaseLog, reconcile, rebaseFrame, strategyName, serverRenderLiveClient; the journalling MemoryLocalStore / LocalStore
|
you pass a store, queue or log to the client, or use them in a test. Delete them: the optimistic apply needs nothing now. MemoryLocalStore and LocalStore still export, but name the persistence store now (rows, write, queue, wipe), not 20.x's journal (apply, rollback, commit), so code written against the old shape stops compiling. Durable offline writes ship: an entity declared persist: true is restored from IndexedDB by the page boot (openLocalStore, pageLocalStore), and its queued mutations replay through pageOutbox()
|
| 15 |
useMutation, useMutationQueue, MutatorLike
|
you call drain(), set entity: on a MutatorLike, or re-query after a write. Delete drain() and entity:. The call resolves with the action's output, so read it directly. Writes go over HTTP, so a mutation now persists where it used to answer X_NOT_IMPLEMENTED. With no response at all (meta.failure: 'network') the call resolves undefined and the write is queued; code that needs the output checks for it. A 'status' or 'body' failure still rejects |
| 16 | sync protocol 3; createSyncNode({ onMutate }), MutationHandler
|
you run a sync node. Delete onMutate, and redeploy clients and nodes together: a v2 client and a v3 node refuse each other with X_PROTOCOL_VERSION
|
| 17 |
topic / Topic from @ultimat3/realtime/server
|
you import it there. Import it from @ultimat3/realtime
|
| 18 | Compose deploys: docker-compose.prod.yml, the scaffold, both tracked apps |
you deploy with Compose. web now refuses to start without SYNC_URL. Set SYNC_URL=ws://<host>:3001/_x/sync in .env.production and run Compose with --env-file .env.production (x deploy does), or proxy /_x/sync to sync and set SYNC_URL=wss://<host>/_x/sync. A copied compose file adds the same line to web
|
| 19 |
x verify --json โ data.durationMs
|
you sum step times or read the total as a sum. It is wall time now; the static steps overlap the serial suites |
| 20 | the presence frame, PresenceFrame
|
you read rosters. Declare the room channel(name, { โฆ, events: true }) and read readPresence(frame.event) in its events handler; a roster is an events frame { presence: op, members, total? } now |
| 21 |
ChannelHub#guard, subscribe(socket, topic), publish(topic), publishFrame, channelFrame, TopicGuard*, nodeId, { kind: 'topic' }
|
you run a hub. Declare each channel with channel(name, { params, policy, row?, catchUp, records?, events? }). Build the hub as new ChannelHub({ transport, sockets }). guard โ the declaration's policy; publish(topic, event) โ hub.publishEvent(decl, params, event). Delete nodeId. An undeclared name is X_TOPIC_FORBIDDEN
|
| 22 |
LocalTable, LocalRow, tx.<type>.insert(row) / upsert(row) / update(id, โฆ)
|
you write rows in a local twin, or type one. Tables are addressed by record key: tx.posts.insert(post.id, post), upsert(key, row), update(key, patch), delete(key), and get(key) / all() to read. LocalRow is gone: LocalTable<Post>
|
| 23 |
cdpE2ePage, CdpE2ePageOptions from @ultimat3/cli
|
your e2e suite opens a page itself. Use openE2eBrowser() and its page tab, session.newTab() for a second tab, or cdpE2eTab({ โฆ }) on an existing connection |
| 24 | .x/build-stats.json |
you ran x build before upgrading. Every budgeted route reads X_BUDGET_UNMEASURED until x build --target static runs again, because the stats file carries the measurement rules that wrote it and v1 files are not read |
| 25 |
hasPageSocket() as a guard in page code |
a module no island imports calls it (an offline banner, an update prompt in a layout). The budgets step now reports it as X_LIVE_ROUTE_NO_ISLAND. Move the module into an island: x g island <route-dir> --at <route-dir>, import it from the island's mount(), declare island({ src })
|
| 26 | a browser's reconnect backoff | you sized a sync node on the 30 s spread of reconnects. Browsers now redial within 4 s (equal jitter from 500 ms). Check the node's AcceptBudget sheds a SIGKILL herd arriving in 2โ4 s. Nothing to change in app code |
| 27 |
LaunchedBrowser.endpoint from @ultimat3/cli
|
you launch Chrome yourself for tests. cdpConnect(browser.endpoint) โ browser.connection, which is already answering over the debugging pipe. cdpConnect(url) remains for a remote browser |
grep -rnE "import type \{[^}]*AsyncState[^}]*\} from '@ultimat3/ui'" apps packages --include=*.ts --include=*.tsx
grep -rnE "ConflictLike|ConflictStrategy|CustomMerge|MergeArgs|CustomConflict|strategyOf|resolveConflict|custom\(|QueryRequestFailedError|QueryProblem|X_RPC_FAILED|instanceof (TypeError|SyntaxError|RpcFailedError)|isSuperseded|flushEndpoint|DEFAULT_FLUSH_ENDPOINT|BackgroundSyncOptions|outbox/flush|X_PWA_SYNC_|\.principal|last-write-wins|LiveClient|useLive|liveHookFor|IdentityMap|rowKey|privateScope|LocalStore|RebaseLog|reconcile|serverRenderLiveClient|drain\(|onMutate|realtime/server|PresenceFrame|\.guard\(|publishFrame|channelFrame|TopicGuard|nodeId" apps packages --include=*.ts --include=*.tsxThe typecheck step finds every removed name for you. It does not find a custom( merge whose
type argument is still the output type: custom<TRow> takes any object type, so that merge
compiles and reads undefined for every output field that is not a column of the row. The second
grep lists every custom(. Read each one.
Both entries are @ultimat3/ui component behaviour, and neither changes a type โ so nothing
fails to compile and nothing throws. They change what a screen DOES, which is why they are a major:
a silent rendering change is worse than a build error, and you would have found these by looking.
| # | Surface | Costs you an edit if |
|---|---|---|
| 1 |
DataTable while reloading |
you relied on skeletons appearing on every load. A FIRST load (rows: []) still renders skeletons; a RELOAD now keeps the existing rows, dimmed, under aria-busy. Replacing rendered content with placeholders on every refresh is a second layout change for no news, and it is what makes a fast list feel slow โ the reader has already read those rows, and taking them away to say "loading" tells them nothing they can act on. No edit if you pass fresh rows on each load, which is the ordinary case. If you genuinely want the placeholder back on a refresh, pass rows: [] while the fetch is in flight |
| 2 |
Button with loading
|
you style button[disabled], or you assert on the disabled attribute in a test. loading no longer sets the native attribute โ it sets aria-disabled + aria-busy and refuses the click in onClick with preventDefault(). The click is still refused; the mechanism moved. A disabled control loses focus the moment it becomes disabled โ mid-flow, with no announcement โ is exempt from the WCAG contrast minimum precisely because nobody is meant to read it, which is wrong for a control you are asking someone to WAIT on, and it does not actually prevent a double submit, because that race is server-side and always was. The edit is one selector: style button[aria-disabled='true'] beside button[disabled], and assert on aria-disabled
|
defineTheme() can now refuse your brand โ X_UI_CONTRAST_INSUFFICIENT. Listed here rather
than as a third entry because it throws at declaration with the measured ratio, the required one and
the role to move, so it cannot ship silently and needs no search. An override whose RESOLVED
channels put a pairing below WCAG 2.2 AA (4.5:1 text, 3:1 focus ring, 1.4:1 border) fails at boot.
Only pairings your brand can have CHANGED are measured โ blaming an app for the framework's own
colours is how a rule gets switched off. The usual defect is half a pairing: a new accent against
the shipped white accent-fg. @ultimat3/ui exports contrastRatio and roleContrast so you can
measure a candidate before shipping it. AA and never APCA: APCA is not a standard, and AA is the
operative legal benchmark.
Nothing else in this release costs an edit. The five new app guards ship in x new only; an
existing app gains one with x g guard <name> (since 23.0.0; x doctor --json lists the shipped
ones it lacks in data.guards.missing), one file per rule, and deleting one drops that rule. x shot --all-islands is additive, and
x g island / x g resource now write a .island.states.ts beside what they generate.
Both entries cost an edit only if you had pwa.enabled: true โ the same block 18.0.0 grew, for
the same reason one release later: 18.0.0 wired the web manifest and left the service worker with
zero callers, so pwa.offline steered nothing and /offline was a path no build precached.
x dev, the container and x build --target static all emit sw.js and x-sw-register.js now,
and the config had to be able to say what they emit.
| # | Surface | Costs you an edit if |
|---|---|---|
| 1 | AppConfig.pwa.offline |
you set pwa.enabled: true. It was 'precache' | 'runtime' | 'network-only' โ an app-wide default for a field defineRoute makes required on every route, so it defaulted nothing and was read by nobody. It is now { fallback, image, font, neverCache }, and fallback is required once enabled is true: an absolute route path, screened at defineConfig rather than at x build. The edit is one line โ offline: 'runtime' โ offline: { fallback: '/offline' } โ plus a route at that path. A relative value is refused because it resolves against whatever document registered the worker, so offline served under /posts/1 is /posts/offline: a 404 cached as the answer to every offline navigation. Or set pwa.enabled: false. Per-route offline: is unchanged, and is still where the strategy is declared |
| 2 |
x new's scaffold |
you regenerate an app, or you copied the old scaffold. The offline fallback is apps/web/site/offline/page.tsx, not apps/web/app/offline.tsx: the directory is the URL and <name>.tsx is not a route file, so the old path shipped a component nothing imported and left /offline a URL that did not exist. site/ and render: 'static' deliberately โ the document that answers a lost network must render with no network, no session and no database, which app/ (ssr | stream) cannot promise. An existing app moves the file and adds offline: 'precache', hydrate: 'never' and robots: { index: false } to its defineRoute
|
x build --json renames one field: precacheWarnings โ serviceWorkerWarnings, and the
terminal row is labelled service-worker. It is listed here rather than as a third entry because
no released build ever wrote the old key โ PrecacheManifest.warnings had no reader at all until
this release. The list carries the precache byte ceiling and pwa.push: true with no VAPID key
to sign a subscription with, which is the one PWA capability still unwired.
Two of the five cost an edit only if you had pwa.enabled: true, which until this release did
nothing at all โ no Ultimate app had ever served a web manifest, so the block was a switch with no
reader. One is bun upgrade. The other two are types.
| # | Surface | Costs you an edit if |
|---|---|---|
| 1 | the Bun floor, >=1.4.0 in engines.bun and in x's own check |
you run Bun below 1.4.0. The edit is bun upgrade. The floor said >=1.3.0 while x test emitted bun test --isolate, a flag Bun introduced in 1.3.13 โ so on 1.3.0..1.3.12 the gate's dominant step died on an unknown flag and x doctor called the runtime fine. 1.4.0 rather than 1.3.13 because a floor is a claim about a runtime somebody tested: CI pins 1.4.x and both images build on oven/bun:1.4-*
|
| 2 | AppConfig.pwa |
you set pwa.enabled: true. It now also requires pwa.name and pwa.colors.light / pwa.colors.dark, each with themeColor and backgroundColor. defineConfig refuses an incomplete block at boot, not at x build, and the fix: carries the whole block. There is nothing to derive them from: app.name is a slug, so an install prompt offering ledger-demo is wrong rather than rough, and a browser paints the install splash before a stylesheet loads. Raw hex is legal here โ one of two places in an app it is, beside theme.tokens (itself removed in 25.0.0). Or set pwa.enabled: false, which is what it effectively was |
| 3 |
@ultimat3/pwa's exports |
you import PwaConfig, ThemeTokens or SchemeColors from it. PwaConfig is now WebManifestInput โ two exported types of one name with no map between them is axiom 1, and this was the one that lied: its doc said "the pwa block of app.config.ts" and it was the generator's input. ThemeTokens/SchemeColors are PwaColors/PwaSchemeColors from @ultimat3/core, which is where the config lives |
| 4 |
x test --json, and four @ultimat3/cli exports |
you read data.shards[] or data.failed, or import planShards, shardArgs, SHARD_COMMAND_PREFIX or Shard. x test runs one bun test --parallel=N now, so there are no shards to report: read data.ok instead of scanning for a failure and data.reproduce instead of rebuilding the rerun. testArgs(โฆ) builds the argv, filesIn(command) reads the file list back out. X_TEST_SHARD_FAILED still exists and is x test --worker I's alone |
| 5 |
ScrapeTarget / ScrapePage
|
you implement either interface yourself. ScrapeTarget gains setColorScheme and ScrapePage gains colorScheme; a driver of your own stops compiling until it has both. Calling @ultimat3/scraping rather than implementing it costs nothing |
x test live --workers 8 now runs one worker, and that is a fix rather than a regression. The
gate always ran live and e2e serially and the command did not, so the same files ran eight
processes under one entry point and one under the other. A logical replication slot is named at the
Postgres cluster level, so a per-worker database never isolated it. --workers is still
accepted and clamps to 1.
Three breaking entries, and all three are the same sweep: a numeric option that used to accept
NaN, ยฑInfinity, a fraction or a negative now refuses it. The refusal is at boot or at the call
boundary, never mid-request, so one bun test or one x verify surfaces every one of them at
once and each fix: line carries the edit.
An app passing real numbers is unaffected. An app that was passing NaN was not working: the
bound it declared was not being enforced, and nothing said so. ?? guards nullish, and NaN is
not nullish โ so Number(process.env.X) on an unset variable walked past the default and landed on
the bound intact. Math.max, Math.min and Math.floor all propagate NaN, and this repo was
relying on all three as guards.
| # | Surface | Costs you an edit if |
|---|---|---|
| 1 | Every numeric option in the table under 17.0.0's Changed heading โ across http, query, jobs, realtime, auth, ai, render, pwa, mail, manifest, mcp, notify, ui, scraping, testing, cli, admin, core, time
|
you pass one a value that is not finite, or is fractional or negative where the option counts things. Read the row for the floor: 0 stays legal wherever it means something โ port: 0 asks the OS for a free port, timeout: 0 is one look, seed: 0 is a seed, maxAgeSeconds: 0 is "revalidate every time", concurrency: 0 is one worker |
| 2 | http.trustedProxyHops |
you declare 0. That was the failure state, not a setting: forwarded.ts returns undefined for hops < 1, so a declared 0 silently trusted nothing while reading as configured. The domain is 1โฆ64. It is a boot-owned key, so no app can write it โ the blast radius is embedders calling defineHttpConfig directly |
| 3 | search().page(input, { first }) |
your page would cut rows. A window narrower than the read is now served when nothing is dropped, and refused โ naming both edits โ when rows would be lost. This is a widening against 16.x for the common case: limit defaults to 20 and first has no default, so search({โฆ}) + .page(input, { first: 10 }) used to mint a cursor that page two then threw on |
TRUSTED_PROXY_HOPS widened from 1โ16 to 1โ64 and is listed here only so the change is not a
surprise: it accepts strictly more than 16.x did, so no configuration that worked stops working.
One breaking entry, and it fires at entity() โ before a migration exists, before anything
reaches a database. Nothing migrates, and an app whose patterns are already portable emits exactly
what it emitted at 15.x.
| # | Surface | Costs you an edit if |
|---|---|---|
| 1 |
c.<col>.matches(/re/) refuses an unportable construct |
your pattern uses \b, ., \w, \s, a backreference, a named group, an inline flag, a POSIX class, \A/\Z, \x, a leading ] in a class, or a non-ASCII range endpoint |
At 15.x this accepted any RegExp and emitted a maybe-equivalent POSIX pattern. That is the
defect, not the refusal: matches(/\bfoo/) shipped a CHECK that compiled cleanly, errored nowhere,
and enforced a BACKSPACE โ measured on 18.4, 'foo' ~ '\bfoo' is FALSE while /\bfoo/.test('foo')
is true. Two rules under one name, with nothing anywhere to report it.
Nothing is translated now either, and that is the mechanism: pattern.source is the string
.test() runs and the string spliced into the constraint. That is only safe because every
construct where the engines disagree is refused at declaration.
How to find them: run your entities โ bun test, or x db gen. The refusal is at entity()
time, so one run surfaces every one, and each names the construct, its index, both readings, and
the repair.
| Construct | Measured on 18.4 | Write instead |
|---|---|---|
\b |
'foo' ~ '\bfoo' false โ ARE reads BACKSPACE |
a predicate: matches((v) => /\bfoo/.test(v)), app-only, sql: null
|
. |
'a\nb' ~ 'a.b' true, JS false |
[^\n\r] |
\w \W
|
'รฉ' ~ '^\w$' true โ locale alnum class |
[A-Za-z0-9_] |
\s \S
|
'\u00a0' ~ '^\s$' false, JS true |
[ \t\n\r\f\v] |
\A \Z
|
anchor in ARE, a letter in JS |
^ / $
|
\x |
'ะ' ~ '^\x414$' true โ ARE takes 3 hex, JS 2 |
a predicate |
leading ] in a class |
[]a] differs |
\] |
\1โ\9, (?<name>โฆ), (?i), [[:alpha:]], non-ASCII range endpoint |
backreference, named group, inline flag, POSIX class, collation-ordered range | a predicate |
Kept, and each measured to AGREE: literals, ^ $ | ( ) * + ?, lazy quantifiers, {n}/{n,}/{n,m},
(?: (?= (?! (?<= (?<!, \d \D \n \r \t \f \v, punctuation escapes, bracket expressions
with ASCII ranges, non-ASCII members, and \uwxyz at exactly four hex digits.
Two of those earn a note. \d is in by measurement, not by reading โ POSIX pins [[:digit:]]
at the ten ASCII digits, and 'ูฃ'/'๏ผ' are false on both sides. \uwxyz is in because Bun
escapes a regex literal's non-ASCII characters (/^รฉ$/.source is ^\u00E9$), so refusing it
would have refused every i18n pattern written the ordinary way.
The subset is falsifiable, not asserted. One live test runs every refused construct against a
real server and asserts the two engines still disagree; another runs the kept subset and asserts
they agree. A future Postgres that grows JavaScript's \b turns those red rather than leaving a
stale exclusion in place.
x db gen now emits drop index / drop constraint for a recorded index no entity declares.
At 15.x it emitted nothing, so those objects stayed on the database while the next sidecar stopped
recording them โ and drift was green over it. Your first generation after upgrading may carry
drops you did not expect; read them, they are objects your entities genuinely no longer declare.
A recorded unique CONSTRAINT gets a drop constraint โฆ if exists before the drop index,
because drop index on a constraint-backed index is 2BP01 and if exists does not suppress it.
One breaking entry, and it is a compile error only for code that exhaustively switches over a type it does not own. Nothing changes at runtime and no data migrates.
| # | Surface | Costs you an edit if |
|---|---|---|
| 1 |
DriftKind gains missing-check
|
you switch over DriftKind with no default
|
The same shape 4.0.0 recorded when it gained changed-foreign-key. Add a default, or handle the
new member:
case 'missing-check':
// the snapshot names a CHECK constraint the catalog does not hold
break;It compares conname and never the definition, and that is not an optimisation โ it is the
only comparison that can work. pg_get_constraintdef answers Postgres' own rewriting:
status in ('draft','published') comes back as
CHECK ((status = ANY (ARRAY['draft'::text, 'published'::text]))). Comparing that text against a
generated predicate would report a correct database as drifted, forever.
Only the declared side is judged, so a NOT NULL (contype='n' on PG17+), an enumerated()
column's old anonymous constraint and an extension's own constraints are all silent.
Eight breaking entries. Four are compile errors the moment you upgrade (3, 4, 5, 8). Two are security fixes that change behaviour with nothing failing to compile (1, 2). One changes the text of a validation issue path (6), and one is a driver interface (7).
No app.config.ts key moves. No codemod: each entry names its own manual edit.
| # | Surface | Costs you an edit if |
|---|---|---|
| 1 | a frame verb acts on the frame, not the parent document | never โ a fix. frame(โฆ).fill() used to clear the parent's same-id field and append to the frame's |
| 2 | a session key hashes each segment | never an edit โ but every stored session logs in once more, see below |
| 3 |
$migration(), toSql, invariantsToSql, constraintName removed |
you called one โ nothing in this repo did, and $migration() rendered ALTER TABLE "<entity name>", a relation that does not exist |
| 4 |
t.number.int() demands a safe integer |
you relied on 2^53 passing a boundary that the row write then refused |
| 5 |
and() / or() refuse an empty clause list |
you build a policy from a list that can filter to empty โ and() used to answer allowed
|
| 6 | a t.record issue path names the entry by position
|
you parse issue paths and expect the caller's key. The key is caller data and reached the log line |
| 7 |
ScrapeTarget.setOfflineMode required; CdpTargetInit.ringCapacity deleted |
you implement ScrapeTarget yourself |
| 8 |
Repo.insert/insertAll/upsertAll take RowWrite<T>
|
you implement Repo โ otherwise this accepts more than before |
Segments used to be sanitised by collapsing every run of non-[a-zA-Z0-9._-] to a single -, so
[email protected] and alice-corp.com produced one key. The browser then loaded account A's
cookies, auth.validate() answered true โ the session is valid, for the wrong account โ and
A's rows were stored under B's tenant.
Nothing to run. A stored record is not found under the new spelling, a miss reads as "no session", so the run logs in again and writes the new key: one extra login per stored session, no error, and the old objects are orphaned until your bucket's lifecycle rule collects them.
// before โ answered ALLOWED, and admitsAnonymous() agreed, so http did not 401 first
policy: and(...requiredCaps.map(can)) // requiredCaps filtered to empty
// after โ refused where it is written
policy: requiredCaps.length === 0 ? allow('public') : and(...requiredCaps.map(can))allow('public') is the explicit spelling for "no clauses required"; deny('<reason>') is the one
that carries a reason. Both are refused because a combinator with no clauses states nothing, and
and() stated the opposite of what its author meant.
They took the row type where money's write type belongs, so Repo.insert demanded a
MoneyValue while narrowMoney exists precisely to narrow a bigint handed to a driver โ a
compile error on public API for a value the framework documents, implements and stores correctly.
RowWrite<T> accepts both spellings. If you implement Repo, widen those three signatures; if you
only call them, this accepts strictly more than before and costs you nothing.
Two breaking entries, and both are compile errors the moment you upgrade. Nothing changes at runtime, no data migrates, no cursor or protocol moves. This major is wide in what it ADDS โ notifications, full-text search, webhooks, exports, state machines, form binding โ and narrow in what it breaks.
No app.config.ts key moves. No codemod: each entry names its own manual edit.
| # | Surface | Costs you an edit if |
|---|---|---|
| 1 |
ServiceFactory receives CtxFacts, not Ctx
|
your defineService factory annotates its parameter (ctx: Ctx), or reads a sibling service off it |
| 2 |
PageLike.content() is deleted |
you called it in an e2e test, or implement PageLike yourself |
defineService's factory is handed the framework's own facts โ actor, now(), clock, tz,
locale, requestId โ and not the app's augmented services. It always worked this way; the
type said otherwise.
// before โ compiles, and is circular: the factory that BUILDS ctx.posts
// declares that ctx.posts must already exist
export const posts = defineService('posts', (ctx: Ctx) => ({ โฆ }));
// after โ the documented form, which cannot drift from the signature
export const posts = defineService('posts', (ctx) => ({ โฆ }));Drop the annotation. If you genuinely need to name the type, CtxFacts is now exported from
@ultimat3/core โ it was the parameter type all along and the barrel never re-exported it, so no
app could name what its own factory was handed.
Reading a sibling service inside a factory no longer typechecks. That was already documented as
unsupported โ factories run in registration order and a sibling may not exist yet โ but the type
permitted it. Move the read into the method that needs it, where useService() resolves at call
time.
PageLike's comment claimed "every member is one the reference app's e2e suite already calls".
An audit found that false for three of eleven: content() had zero call sites anywhere in the
repository, and title() / reload() are named only by x g route's generated template, which
nothing executes.
content() is gone. title() and reload() stay, with the caveat recorded on each. If you drive
a browser yourself, delete content from your PageLike implementation; if you called it, read the
DOM through evaluate() instead.
Sixteen breaking entries, in three groups. Eight are compile errors the moment you upgrade
(4, 5, 6, 8, 9, 13, 14, 16). Four need an action before or at the deploy and nothing fails to
compile (1, 2, 3, 11) โ every persisted pagination cursor stops working, rows sharing a sort value
change order, one index migration, and sync nodes and browser clients must ship together. The
last four are visible only to a caller at runtime (7, 10, 12, 15).
No app.config.ts key moves, because the surface this major opens never had one: AppConfig has
never carried an http member, so configureHttp() is an addition and not a migration โ see the
last table. No codemod: every entry names its own manual edit.
| # | Surface | Costs you an edit if |
|---|---|---|
| 1 | every cursor minted before 12.0.0 is X_CURSOR_INVALID
|
you persisted a cursor โ in a URL, a job payload, a client store. Nothing fails to compile |
| 2 | the primary-key tiebreak takes the last declared key's direction | you depended on the order of rows sharing a sort value. Nothing fails to compile |
| 3 | an index declaring where or order is renamed <table>_<cols>_<hash8>_idx
|
you declared one โ a migration, before the deploy |
| 4 | a physical column or table name must be [a-z_][a-z0-9_$]*, at most 63 bytes |
you wrote .column('createdAt') or an entity(name) / table that is not lower-snake |
| 5 |
Repo gains aggregate and approximateCount; ReadBuilder gains five terminals |
you implement Repo or Driver yourself |
| 6 |
Operator gains contains, contained-by, overlaps, has-key
|
you switch exhaustively over Operator
|
| 7 |
introspect() returns app tables only |
you read its output, or assert how many catalog queries it issues |
| 8 |
rateLimitKey is deleted; RateLimitConfig.tenantBucket is required |
you called rateLimitKey, or built a full RateLimitConfig
|
| 9 |
Ctx gains a required deadlineAt
|
you hand-build a Ctx โ a test fixture, a custom host |
| 10 |
traceHeaders() sends the remaining request budget |
never for the caller; a downstream service now receives x-request-timeout-ms
|
| 11 |
PROTOCOL_VERSION 1 โ 2, and five realtime exports are deleted |
you imported one โ and every deployment redeploys clients and sync nodes together |
| 12 | MCP rate limits are enforced, 120 read / 20 write per minute per actor | an agent exceeded them; it was previously unmetered |
| 13 |
memoryAuditSink() is bounded at 1,000 and discards oldest-first |
you used it as a system of record, or implement MemoryAuditSink
|
| 14 |
RouteBudget.css, .cls and .tbt are deleted |
you declared one โ it was ignored, and now it does not compile |
| 15 |
claim({ queues: [] }) is refused; the memory driver's claim is async
|
you call a JobDriver directly |
| 16 |
DoctorProbe gains a required database()
|
you implement DoctorProbe
|
4. Spell every physical name lower-snake. X_INVARIANT_VIOLATED at entity(), before any
statement runs.
- createdAt: timestamp().column('createdAt'),
+ createdAt: timestamp().column('created_at'),A derived name is unaffected: columnName is meta.name ?? snake(property) and snake()
lower-cases, so createdAt: timestamp() still writes created_at. What changed is that the derived
branch is now checked too โ for three majors only meta.name was, so a property named
n" , "x" text); drop table t; -- put a real drop table inside a generated create table.
Quoting is not a defence against a value that can close the quote. entity(name) and table go
through the same assertion.
5. Implement the two new Repo members, or stop hand-rolling one. TS2739.
const repo: Repo<Post> = {
findById, findMany, insert, update, delete: remove, count, countBy,
+ aggregate: (fn, column, args) => driverAggregate(fn, column, args),
+ approximateCount: async () => null, // `null` is "never analysed", and always legal
};approximateCount may answer null unconditionally โ that is the documented value for a table
nobody has ANALYZEd, and every caller already handles it. aggregate cannot be stubbed the same
way: a wrong number is worse than no number, so raise X_AGGREGATE_UNSUPPORTED if you will not
implement it.
6. Widen the switch. TS2366, or a silent fallthrough if it had a default.
case 'is-not-null': return sql`${col} is not null`;
+ case 'contains': return sql`${col} @> ${bind}`;
+ case 'contained-by': return sql`${col} <@ ${bind}`;
+ case 'overlaps': return sql`${col} && ${bind}`;
+ case 'has-key': return sql`${col} ? ${bind}`;The four exist so a declared json() or arrayOf() column is no longer write-only from the query
language.
8. rateLimitSpends answers a LIST, not a key. TS2305.
- const key = rateLimitKey(route, ctx);
- await limiter.assert(key, bucket);
+ for (const spend of rateLimitSpends(route, ctx, config)) {
+ await limiter.assert(spend.key, spend.bucket);
+ }One request spends the caller's bucket and then the tenant's, stopping at the first refusal. The old
builder answered actor else org else ip, exclusively โ and the anonymous actor answers
null for both of the first two โ so no HTTP request ever spent an org bucket. RateLimitConfig
gains a required tenantBucket: string | null; null is "this app has no per-tenant allowance",
which is the default and the previous behaviour.
9. Add deadlineAt to a hand-built Ctx. TS2741.
const ctx: Ctx = {
requestId, traceId, locale, tz, buildId, role, actor, now,
+ deadlineAt: null, // null is "no deadline", which is what a job or a test has
};createContext({ โฆ }) already defaults it, so only a literal pays. It is what
remainingBudgetMs(ctx) reads and therefore what entry 10 propagates.
13. memoryAuditSink() is not a system of record. The interface break is TS2739 on
size/dropped; the behaviour break is silent.
- setAuditSink(memoryAuditSink());
+ setAuditSink(postgresAuditSink({
+ executor: { query: (text, values) => db().query({ text, values }) },
+ }));Past 1,000 records the memory sink drops the oldest on every write, so an audited action can run,
succeed, be recorded and leave nothing behind. { maxRecords } raises the bound and does not remove
it. dropped is non-zero exactly when the sink is telling you it is the wrong one. x_audit is
applied at boot beside the jobs, idempotency and rate-limit tables, so there is no migration to write.
14. Delete the budget key. TS2353.
budget: {
js: '40kb',
- css: '12kb',
- cls: 0.1,
},There is nothing to replace them with. All three were declared on the route contract, flattened away
by registerRoute โ which projects a budget to budgetJs + budgetLcp and nothing else โ and read
by no consumer anywhere, so a declared CSS budget was ignored while the budgets step reported
green. A new budget key is now a build error until the descriptor projects it
(_EveryBudgetKeyIsProjected, packages/render/src/type-pins.tsx). budget.lcp survives and is
published, not enforced: nothing in the build observes a paint.
16. Implement DoctorProbe.database(). TS2741. Only a hand-written probe pays โ x doctor's
own is unchanged.
const probe: DoctorProbe = {
bunVersion, root, port, production, devCursorSecret, devStorageSecret,
+ database: () => probeDatabase(process.env['DATABASE_URL']),
};x doctor answered "shippable" while probing the web port alone: a wrong password or a database that
does not exist accepts the socket and refuses the session, which a port check cannot see. It now
probes both ports and the database.
1. Drop every persisted cursor. Nothing fails to compile; a stored cursor is refused at decode
with X_CURSOR_INVALID.
| Where a cursor lives | Do this |
|---|---|
| a URL a client holds | nothing โ request the first page (after: null) and re-mint |
a job payload, a resumable inBatches() position |
re-enqueue from the start, or from a business key of your own |
| a client store, a saved view | clear it on the version bump |
Two changes make an old cursor unreadable, and both were forced by one defect. A timestamp() sort
key is now carried as a microsecond epoch rather than an ISO string, because ORDER BY evaluates
timestamptz at microsecond precision while the seek treated a whole millisecond as one equality
class โ two different equality classes over one page boundary. Reproduced against Postgres 16: three
rows inside one millisecond, uuid-v7 ids, orderBy('createdAt','desc').limit(1) returned 1 of 3
rows and stopped, every time. And every entry is tagged โ ~ for an absent value, ! before a
present one โ so an absence can be told from the text that spells it, which is what nullable sort
keys need.
The seek is now a plain < / > / = against $n::timestamptz; nextMillisecond and the
>= v and < v + 1ms window are gone. A read ordered by a timestamp() column carries one extra
output column on the wire, "<col>$US" โ a name no entity can declare, stripped by decodeRow, so
it reaches no caller's row. Only a test that asserts SQL text sees it, which is exactly the suite
that hid this defect for three majors.
2. The default total order is uniform-direction.
- ORDER BY created_at DESC, id ASC
+ ORDER BY created_at DESC, id DESCtotalOrder appends the primary key in the last declared key's direction rather than always
ascending, so orderBy('createdAt','desc') runs created_at desc, id desc. Rows sharing a sort
value come back in the opposite order to before, and no page is lost either way โ the seek matches
the order it is built from.
Two things follow. A mixed-direction order was un-indexable by this framework's own index DSL, so the
default one could never be served by a declared index. And a uniform order is now emitted as a row
comparison (a, b) < ($1, $2), measured on PG16 as an Index Only Scan against BitmapOr + Sort
for the or-chain. The or-chain remains for an order you wrote as mixed yourself, and for one whose
keys are not all NOT NULL: a row comparison has no null ordering, so a NULL on either side makes
the whole comparison unknown.
3. Rename the indexes that declare where or order. A declared index is matched by name, so
the old one is not dropped and the new one is not created until you say so.
x db gen "rename partial and ordered indexes" # then read the emitted up/down before applying-- what the generated migration looks like, one pair per affected index
alter index posts_author_id_idx rename to posts_author_id_9f2c1ab4_idx;The discriminator is sha256("<order>|<where>"), first 8 hex. Plain and unique() names are
unchanged, deliberately: Postgres names a column-level unique() index <table>_<column>_key
itself, so a discriminator there would make the generator emit a second create unique index for an
index that already exists (42P07), and a foreign key's own index is deduped against a hand-declared
one by the plain name.
Without it, two different partial indexes on one column were one name โ posts_author_id_idx for
both where status = 'published' and where status = 'draft' โ and the second was dropped with no
error, no warning and no drift finding. So this migration may create an index you declared years ago
and never had. A name over 63 bytes is now refused at declaration rather than truncated by the server
in silence.
11. Redeploy clients and sync nodes together. PROTOCOL_VERSION moves 1 โ 2 and a skewed peer
is refused with X_PROTOCOL_VERSION, in both directions โ a cursor rides the client's subscribe
and the node's snapshot, and the deleted fields were decoded through str() / num(), which throw
on absence. There is no rolling window in which the two versions interoperate.
- import { digestOf, DIGEST_UNVERIFIED, fnv1a } from '@ultimat3/realtime';
+ // nothing replaces themLiveCursor.digest and LiveCursor.count are gone with them. Every snapshot ran canonicalJson
over every row and hashed it for a value no code path read โ a full serialize-and-hash of every
result set, per live query, per reconnecting socket, in the restart storm this package is benchmarked
on. count would have been wrong had it ever gained a reader: advance seeds its set from the
already-truncated ids, so a delete past CURSOR_ID_LIMIT never decremented it. @ultimat3/flags
and @ultimat3/ai keep their own fnv1a and are untouched.
7. introspect() returns app tables only.
| Relation | Before | Now |
|---|---|---|
| an ordinary or partitioned table | returned | returned |
| a view, a materialised view, a foreign table | returned | excluded |
anything Postgres records as extension-owned (pg_depend, deptype = 'e') |
returned | excluded |
| a table someone created by hand | returned | returned, and still unexpected-table
|
IntrospectOptions.exclude no longer decides the set on its own โ it narrows what survives the rule
above, and cannot bring an excluded relation back. This is what makes a stock managed Postgres
deployable: create extension pg_stat_statements in public is the CNPG, RDS, Supabase and Neon
default, and its view read as unexpected-table with x db gen "add pg_stat_statements" as the
printed fix โ so every deploy failed terminally and following the fix would have written an
extension's internal view into the app's migration set. Ownership rather than a name prefix, because
that rule covers the view and misses PostGIS's spatial_ref_sys.
Edit only if a test asserts the statement count: it issues four catalog queries where it issued three.
10. A downstream service now receives x-request-timeout-ms. No edit on the calling side โ
traceHeaders() is spread by both typed clients before your own headers, so an explicit value still
wins.
| Situation | Header sent |
|---|---|
| in a request with 12s left of its budget | x-request-timeout-ms: 12000 |
| in a request whose budget is spent | none โ never 0, which the far side reads as "the caller asked for nothing" |
| in a job, a test, a browser | none; there is no ambient deadline |
The receiving end may only be shortened by it: resolveTimeoutMs takes the minimum of its own
configured budget and the header. Before this, a 30s gateway budget already spent to t=29 handed the
next service a fresh 30s, so work ran for another half minute holding a pool slot and a vendor
connection after the caller's socket had already been answered X_TIMEOUT.
12. MCP callers are metered. 120 read and 20 write per minute, per actor, per class โ
X_MCP_RATE_LIMITED, 429, with Retry-After.
mcpHttpRoute({ server, resolveToken, rateLimits: { read: 600, write: 60 } });
// or defineAppMcp({ โฆ, rateLimits: { read: 600, write: 60 } })A tools/call naming a destructive: true tool spends write; so does any call this server
cannot resolve, fail-closed, so a probing client never gets the cheap bucket. Everything else,
initialize included, spends read โ a coarse per-route rule would have thrown an agent off on its
handshake. The key names the actor and never reaches the caller.
Behind more than one replica, pass the store too โ the default counts per process, which is
honest for x mcp serve and a lie for N replicas behind one URL, each enforcing the full allowance
on its own:
mcpHttpRoute({ server, resolveToken, rateLimitStore: postgresRateLimitStore({ executor }) });X_MCP_RATE_LIMITED is its own code and not X_RATE_LIMITED because the knob differs: that one's
fix: names the HTTP pipeline's buckets, which do not govern this route, so raising them would run
and change nothing.
15. Name the queue. X_JOB_CLAIM_QUEUES_EMPTY from both drivers.
- await driver.claim({ queues: [], limit, visibilityTimeoutMs, workerId });
+ await driver.claim({ queues: ['default'], limit, visibilityTimeoutMs, workerId });An empty list named no queue and meant two different things: every queue on the memory driver,
the default queue on Postgres, with ClaimOptions.queues documenting neither. Each meaning is
silently wrong in the other's deployment โ one takes work this worker was never configured for, the
other drains nothing and reads as an idle queue. There is no third meaning to pick.
createWorker passes exactly one queue per pass, so only an embedder calling a driver directly is
affected. The memory driver's claim is now async to raise the refusal, so a caller that read its
return synchronously gets a Promise.
Read these if you built a workaround for one.
| Change | What it means |
|---|---|
| read replicas |
DATABASE_REPLICA_URL plus a withReplicaReads(fn) scope. Opt in twice and byte-identical when unconfigured. Read-your-writes is the rule, not an option: one write at any depth pins the rest of the scope to the primary, and a transaction is always the primary's. Three consecutive replica failures park it for ten seconds. The URL must name a read-only standby
|
configureHttp() |
the entire HTTP tuning surface โ CORS origins, body limit, request timeout, max in-flight, rate-limit buckets โ was reachable from no app config key that existed. AppConfig has never had an http member, so every fix: line naming http.<key> in app.config.ts resolved against nothing. Call it at module scope in a file under apps/*/. rateLimit.scope stays boot-owned |
| a durable audit sink |
postgresAuditSink({ executor }), append-only, no purge โ retention is a legal question with a different answer per app. The row is a fixed allow-list and never a walk of the Ctx, which on an HTTP surface carries the caller's Authorization and Cookie
|
| aggregates and containment |
sum / avg / min / max / approximateCount, and four containment operators. min/max on text is refused (Postgres orders by collation, JS by code unit); avg over money is refused, naming sum() + count()
|
| nullable sort keys order |
asc nulls last / desc nulls first, with the null position carried in the cursor. Only a nullable primary-key column is still refused โ null = null is unknown, so the tiebreak cannot break a tie. The refusal also moved to plan time: it used to fire only when a next page existed, so it was green on 15 seeded rows and X_INVARIANT_VIOLATED on the first real read |
a policy gate step, twentieth |
every permission an app grants or requires must be one it declares. the scaffold shipped an app that answered X_PERMISSION_UNKNOWN on two of its three routes โ status 500 โ under a green gate. It skips in the framework monorepo, which declares no roles |
X_MANIFEST_MISSING |
an app root with no x.manifest.json fails the manifest step. Nothing ever ran x manifest, so the file did not exist in any app x new produced while the step reported green. Run x manifest once and commit it |
scripts/declaration-readers.ts |
every leaf key of every primitive declaration needs a reader in shipped source. 173 leaves across 18 roots, ratchet at zero |
MCP minLength / maxLength count code points |
the validator counted UTF-16 code units while the schema that publishes those numbers counts code points, so an astral-character argument was passed and then refused by the action's own parse, or refused outright on a bound the agent had obeyed |
x i18n add <locale>, x dev --port N
|
a locale file that turned the gate red printing a fix that repaired nothing; and a dev server dying on port N+1 with a caught Error rendered into the cause and X_CLI_UNEXPECTED rather than a stable code |
Seven breaking entries, from a shutdown, cache and disclosure sweep. Four are compile errors the
moment you upgrade. Three are not โ and one of those changes what a CDN is allowed to store for a
signed-in visitor, so read entries 3, 4 and 5 even if nothing here fails to compile. No
app.config.ts key moves in this major, so there is no config edit to start with. No codemod.
| # | Surface | Costs you an edit if |
|---|---|---|
| 1 |
isrKey(url, locale) takes the negotiated locale, and it is part of the store key |
you call isrKey โ the document a multi-locale isr route serves changes either way |
| 2 |
IsrStore gains a required markStale(path)
|
you implement IsrStore yourself |
| 3 |
HttpConfig.drainTimeoutMs is number | null, default null
|
you read the resolved field โ and the drain budget moves 15s โ 25s for an app that declared neither |
| 4 | an unclassified 5xx problem document carries no exception text | a client reads title / detail / cause off a 500 |
| 5 | a request carrying an identity gets private, max-age=0; every shared response varies on cookie and x-timezone
|
never โ a CDN leak, closed. Personalised pages stop being shared-cacheable, which is the fix |
| 6 |
initialsOf(name, locale) takes a required locale |
you call initialsOf directly; <Avatar> is unchanged |
| 7 |
@ultimat3/pwa deletes RetryPolicy, DEFAULT_RETRY, retryDelayMs, shouldRetry and BackgroundSyncOptions.retry
|
you imported one from @ultimat3/pwa. @ultimat3/jobs exports two of those names and is untouched |
1. Pass the negotiated locale to isrKey. TS2554, one argument short.
- const served = await isr.serve(isrKey(url), () => renderPage(url));
+ const served = await isr.serve(isrKey(url, ctx.locale), () => renderPage(url));isrKey is on @ultimat3/render/server, where 9.0.0 put it. The locale rides in a reserved query
parameter โ __x_locale, exported as ISR_LOCALE_PARAM โ and not as a prefix, because routePathOf
splits a key at its ?: an es:/blog key matches no route, so descriptorFor answers undefined
and a declared revalidate: { ttl } silently becomes tag-only.
One entry per path served visitor 2 the document negotiated for visitor 1 โ <html lang>, every
t() โ for the whole TTL, and s-maxage told the CDN to do the same. toResult emits
vary: accept-language now for the CDN half; the rest of the shared key comes from @ultimat3/http's
cache-headers stage, which sees the actor this function cannot.
The time zone is deliberately not a dimension: a locale set is declared and bounded, a zone list
is not. A date on an isr page belongs in a zone the page itself names, or the page belongs in ssr.
2. Implement markStale in place. Only a custom IsrStore pays this โ memoryIsrStore() has it.
const store: IsrStore = {
get: (path) => map.get(path),
set: (entry) => { map.delete(entry.path); map.set(entry.path, entry); },
+ markStale: (path) => {
+ const entry = map.get(path);
+ if (entry === undefined) return false;
+ map.set(path, { ...entry, stale: true }); // in place: the position IS the eviction order
+ return true;
+ },
delete: (path) => { map.delete(path); },
paths: () => [...map.keys()].sort(),
};Never set({ ...entry, stale: true }) โ that read-modify-write is the defect the member exists to
end. set means "this page was just generated" and a store is entitled to order eviction by exactly
that, so marking through it made the stalest page the newest: a tag bust protected the pages that
most needed regenerating.
Second half, and it costs nothing: regenerate samples a cache fence before the render and does
not store an entry the fence invalidated. A bust landing mid-render was previously erased by
store.set({ stale: false }), and for a tag-only route isFresh is true forever โ so the process
served pre-write HTML for the rest of its life.
6. Pass the locale to initialsOf. TS2554.
- initialsOf(member.displayName)
+ initialsOf(member.displayName, useUi().locale)<Avatar> reads useUi().locale itself, so a component tree pays nothing. A bare
toLocaleUpperCase() reads the runtime's default locale โ a server's LANG, a browser's UI
language, never the request's โ so one Turkish name uppercased to ฤฐ on the server and I in the
browser, out of identical props. @ultimat3/ui has no ambient locale to fall back on, by rule.
7. Delete the import; there is nothing to replace it with. TS2305.
- import { backgroundSyncSource, DEFAULT_RETRY, type RetryPolicy } from '@ultimat3/pwa';
+ import { backgroundSyncSource } from '@ultimat3/pwa';
- backgroundSyncSource({ flushEndpoint, retry: { ...DEFAULT_RETRY, maxAttempts: 5 } });
+ backgroundSyncSource({ flushEndpoint });BackgroundSyncOptions is { flushEndpoint?: string } and nothing else โ in this release. 21.0.0
removes it as well, together with the flush route; see entry 8 of 20.x โ 21.0.0. This package schedules no
retry and never did: the one-shot sync handler rejects and the platform decides when to wake it
again. Of the policy only maxAttempts reached the emitted worker, as a SYNC_MAX_ATTEMPTS constant
nothing read, and X_PWA_SYNC_INCOMPLETE's fix: told the reader to raise
pwa.backgroundSync.retry.maxAttempts โ a key PwaConfig has never carried, because
backgroundSync is a boolean. Error codes already says so.
@ultimat3/jobs is a different package carrying two of those names. RetryPolicy and
DEFAULT_RETRY are still exported from it, still read by the worker, unchanged. Only pwa's copies are
gone, and a RetryPolicy on a job() is not one of them.
3. drainTimeoutMs is number | null, and null means "this app did not say".
| What the app declared | Drain deadline before | Now |
|---|---|---|
| nothing | 15,000ms |
25,000ms โ core's own DEFAULT_DEADLINE_MS
|
configureLifecycle({ deadlineMs: 600_000 }) |
15,000ms โ reverted by the next line of boot | 600,000ms |
defineHttpConfig({ drainTimeoutMs: 5_000 }) |
5,000ms | 5,000ms |
createServer calls configureLifecycle({ deadlineMs }) only when the app declared one
(packages/http/src/server.ts:101).
Unconditional, with defineHttpConfig defaulting the number, that line reverted the exact edit
X_SHUTDOWN_TIMEOUT's own fix: prints โ silently, in every process that serves web.
Edit only if you relied on the 15s default โ write it down:
- defineHttpConfig({ rateLimit: { scope: 'process' } })
+ defineHttpConfig({ rateLimit: { scope: 'process' }, drainTimeoutMs: 15_000 })The INPUT field is still number | undefined, so a declaration compiles unchanged. The RESOLVED
field is number | null, so const ms: number = config.drainTimeoutMs is TS2322 โ that is the
compile half, and it reaches only a caller that reads the merged config back.
4. An unclassified 5xx says nothing about the exception that caused it.
| Member | On a 5xx nobody classified | On a coded refusal |
|---|---|---|
type, status, code, fix, docs, requestId
|
unchanged | unchanged |
title |
unhandled server error |
the code's own title |
detail, cause
|
one fixed sentence pointing at this process's logs, under the request id | the authored cause |
"Unclassified" is X_INTERNAL, or a code with no row in @ultimat3/http's table and no
registerErrorStatus row โ deliberately not status >= 500, which would have blanked X_DRAINING's
one instruction. dev: true is unchanged, and the text is not lost: the error-map stage logs it as
a redactable field and reports every 5xx to the error monitor, both keyed by requestId.
A pg message quoting the rejected row, a driver message quoting the DSN, went to any non-HTML client
in production. error-page.ts had locked the browser out of exactly this, so the two audiences
disagreed about one condition.
Edit only if a client parsed those members. Match on code, correlate on requestId. A 5xx of
your own that should keep its authored cause needs a status of its own โ that is what makes it
classified:
registerErrorStatus({ X_PAYMENTS_UNREACHABLE: 502 }); // from @ultimat3/http, once at boot5. A request carrying an identity is private, max-age=0, whatever the handler declared.
- cache-control: public, max-age=0, s-maxage=30, stale-while-revalidate=300
+ cache-control: private, max-age=0The cache-headers stage reviews a cache-control the handler wrote instead of standing down: a
declaration offering the response to a shared cache (public, or an s-maxage) plus a non-anonymous
actor is replaced. immutable is the one exception โ it asserts the body is a function of the URL
alone, which a content-addressed island chunk or image is, and demoting those re-downloads every
chunk on every navigation for every signed-in user.
An anonymous shared response is unchanged except that it now carries
vary: accept-language, cookie, x-timezone. Both halves are needed: private for the identified
request, vary: cookie for the shared one.
What was happening: ssrHeaders offers any route without a policy to a CDN for 30 seconds, and
meta.auth is 'public' | 'required' โ so the commonest page in any app, public but greeting you by
name when you are signed in, is a 'public' route whose own header said s-maxage. That is the shape
x g route --surface app scaffolds.
No edit, and expect the shared-cache hit rate on personalised pages to go to zero โ that is the
fix, not a regression. A route that really is a function of the URL alone says so:
cache-control: public, max-age=31536000, immutable.
Read these if you built a workaround for one.
| Fix | What stops happening |
|---|---|
| the framework's CSP admits its own hydration runtime in production |
no island booted after deploy, anywhere the policy is enforced. script-src was 'self' 'wasm-unsafe-eval' with no hash while the runtime is an inline module โ report-only under x dev, enforced in a container. startWeb now hashes the seven HYDRATE_RUNTIME_BODIES into script-src, as it already did for styles |
a browser gets an error page, not problem+json
|
a 404 or a 500 rendering the internal cause and the author-facing fix: into a visitor's window. Copy is the catalog's errors.* keys; override per status with apps/web/site/errors/<status>.html, and x dev keeps the overlay |
worker, scheduler and sync drain in two phases, and holdUntilShutdown reaches the exit |
one accept hook spending the whole budget before "stop listening" and "stop upgrading" had been invoked at all โ 4 hooks started, none finished โ and an overrun wedging the process until the kubelet's SIGKILL, where the job lease lapsed and another worker re-ran it. Behaviour change: a job outrunning configureLifecycle({ deadlineMs }) is abandoned (jobs.worker.drain-abandoned) and the queue redelivers it, where the teardown used to hang forever with the driver open. Raise the budget past your slowest job |
a worker's fleet slot is released before the driver closes, and a renewal interval is unrefed |
a concurrency: 1 job unclaimable by the replacement pod for a full visibility window after every deploy, and a refed interval holding a drained process open until SIGKILL |
| the scheduler re-asserts leadership before every task, not once per round | the tail of a round dispatching under a lease another node already took. The occurrence key does not absorb it: SQL_ENQUEUE's conflict target is partial over the live states, so a duplicate landing after that job finished inserts a new row and the handler runs twice |
| a replayed backfill batch writes no ledger row | 4,800 x_backfills UPDATEs before a resumed 5M-row sweep read a single new row, on every attempt, inside the visibility lease |
| a server render gets a live client instead of a 500 | a page whose body reads a live query failing on the server; it renders its loading branch and the browser takes over on hydrate. mutate() / drain() there are X_LIVE_SERVER_RENDER
|
createLogger({ level: 'verbose' }) is refused at construction |
an unknown level failing open โ every level emitted |
one documented first run, and it is bin/setup
|
cd myapp && x dev failing on X_BUILD_FAILED because x new installs nothing โ which eight doc pages, and x new's own closing line, told the reader to do. wiki/Installation.md also listed six x new flags that do not exist |
Nineteen breaking entries, from a twelve-audit correctness sweep. Every one deletes or corrects
a declaration that promised something the code did not do. One app.config.ts edit. Six compile
errors. Five that no compiler will find โ read those even if nothing else here applies. Four refuse
a declaration that was already broken, and three are corrected underneath you at no cost. No
codemod.
| # | Surface | Costs you an edit if |
|---|---|---|
| 1 |
ERROR_DOCS_BASE and errorDocsUrl(code) are deleted |
you construct an UltimateError with an explicit docs:
|
| 2 | a problem document's type is urn:ultimate:error:<CODE>
|
a client matches on that string |
| 3 |
@ultimat3/time refuses a malformed locale with X_LOCALE_INVALID
|
you catch RangeError around a formatter |
| 4 |
realtime.tier and RealtimeTier are deleted |
your app.config.ts sets realtime.tier โ the one config edit in this major
|
| 5 | the WAL decoder returns parsed values, not Postgres' own text | you name PgOutputMessage, entityRow or PhysicalRow
|
| 6 | a delta resume no longer seats a pre-policy cursor | never โ a cross-tenant leak, closed |
| 7 |
verifyDigest() is deleted from @ultimat3/realtime
|
you called it, which nothing could have |
| 8 |
defineAuth({ providers }) defaults to []
|
you serve an OAuth route and name no provider |
| 9 |
@ultimat3/auth writes x_accounts.access_token / refresh_token as null
|
your own SQL reads either column |
| 10 | a multi-audience id token needs a matching azp; a future nbf is refused |
your OAuth provider issues multi-audience id tokens, or a host clock is ahead |
| 11 |
MemoryAdapter.createUser enforces x_users' two UNIQUE constraints |
a test registers one address twice |
| 12 | two admin resources may not claim one path:
|
your defineAdmin already had four screens unreachable |
| 13 |
registerLayout(name, layout) refuses a name already taken |
two modules register one layout name |
| 14 |
assertReadOnly returns a ReadOnlyVerdict
|
you call it from @ultimat3/admin/dev
|
| 15 |
generate() no longer collects a LOCAL refusal |
you catch X_AI_PROVIDER_UNAVAILABLE to mean "the model call failed" |
| 16 |
@ultimat3/render's graph-based island budget API is removed |
you imported routeJsBytes, graphFor, checkBudget, checkBudgets or assertBudget
|
| 17 |
@ultimat3/pwa's routeRules orders by specificity, wildcards last |
you ship a generated sw.js
|
| 18 |
subscriptionState takes a Clock
|
you passed epoch milliseconds |
| 19 |
StaticReport gained a required unmeasured
|
you construct one by hand |
realtime: {
enabled: true,
- tier: 'live-queries',
transport: 'nats',
urlEnv: 'NATS_URL',
},TS2353, and nothing else. RealtimeConfig is { enabled, transport, urlEnv } โ
packages/core/src/config.ts:132.
tier accepted 'channels' | 'live-queries' | 'local-first', defaulted, was documented with
per-value semantics, and was set by both tracked apps and every scaffolded app โ and nothing read
it. No comparison, no branch, no dereference. tier: 'local-first' bought exactly what
'channels' bought, and the durable local store it advertised does not exist. Which tier an app is
on is decided by what it declares: a channel() topic, a live: true query, a local store.
Leaving the line in also works, and that is the hazard. section() copies an unknown key
through, so a stale tier: still boots and still does nothing; an app that builds its config into a
variable before passing it to defineConfig loses excess-property checking and sees no error at
all. Same shape as jobs.driver in 5.0.0 and realtime.heartbeatMs in 4.0.0 โ the thirteenth
instance of that class. bun run scripts/config-readers.ts is what keeps the fourteenth out.
1. Omit docs:; do not substitute the new constant.
- import { errorDocsUrl, UltimateError } from '@ultimat3/core';
+ import { UltimateError } from '@ultimat3/core';
export class BillingDeclinedError extends UltimateError {
constructor(cause: string) {
- super({ code: 'X_BILLING_DECLINED', cause, fix: 'retry with another card', docs: errorDocsUrl('X_BILLING_DECLINED') });
+ super({ code: 'X_BILLING_DECLINED', cause, fix: 'retry with another card' });
}
}The constructor already resolves docs from the registry, so passing it by hand is a second
declaration of one fact. ERROR_DOCS_URL is exported from @ultimat3/core for a caller rendering
the link outside an error โ not as a drop-in for the deleted function.
Why: https://ultimate.dev/errors/<code> answered HTTP 404, host included, on every error this
framework has ever thrown, including the first line a new agent reads. One URL rather than one per
code, because codes live on Error codes in table rows and a row has no anchor โ
a #X_DB_DRIFT fragment would be a second dead declaration, not a fix for the first.
7. Delete the verifyDigest() call. That is the whole migration, and nobody had one to delete:
a delta-resumed cursor carries DIGEST_UNVERIFIED, so the check answered false for every cursor
drift can occur in, and identity-map.ts merges columns across queries by design โ any app with two
reads over one entity would have reported permanent drift. Drift is the server's desynced mark.
14. assertReadOnly returns a verdict, and the verdict carries the string to run.
- const refusal = assertReadOnly(sql);
- if (refusal !== null) return { refused: refusal };
- const rows = await client.query(sql);
+ const verdict = assertReadOnly(sql);
+ if (verdict.kind === 'refused') return { refused: verdict.refused };
+ const rows = await client.query(verdict.sql);ReadOnlyVerdict is { kind: 'runnable'; sql } | { kind: 'refused'; refused }. Execute
verdict.sql, never the string you passed in: every check ran on a stripped form and the verdict
is the reconciled one. The /_x panel discarded it and ran the textarea's own bytes, so two callers
of one guard disagreed about which string runs.
16. Delete the import. Removed from @ultimat3/render: routeJsBytes, graphFor,
checkBudget, checkBudgets, assertBudget and their types. Every one was exported from the
barrel and called by nothing; the budget gate that actually runs is @ultimat3/cli's and it
measures the emitted document. parseByteBudget, defaultIslandBudget and islandModuleIds are
unchanged.
18. subscriptionState takes a Clock where it took epoch milliseconds โ TS2345 on a
number. The parameter is optional and defaults to systemClock, so most callers delete an
argument:
- subscriptionState(record, lastStatus, Date.now())
+ subscriptionState(record, lastStatus)
+ subscriptionState(record, lastStatus, frozenClock(NOW)) // a test, from @ultimat3/core19. StaticReport gained a required unmeasured โ every budgeted route a build could not
weigh, with the reason, which is the list X_BUDGET_UNMEASURED's fix: cites by name and which
until now reached no x command's output at all.
const report: StaticReport = {
target: 'static', out, buildId, emitted, skipped,
+ unmeasured: [],
};Reading one costs nothing: parseStaticReport takes the field as optional and answers [] when
it is absent, so a .x/static-report.json written by an older build still parses. Only
hand-construction moves.
2. A problem document's type is a URN, per code.
- if (problem.type === 'https://ultimate.dev/errors/X_RATE_LIMITED') โฆ
+ if (problem.type === problemTypeFor('X_RATE_LIMITED')) โฆproblemTypeFor(code) is urn:ultimate:error:${code}, exported from @ultimat3/http. code is
unchanged and is the simpler match โ problem.code === 'X_RATE_LIMITED' needs no import. type
and docs used to carry the same dead link on every 4xx and 5xx; they are two values now because
they answer two questions โ type is RFC 9457's identifier for the problem kind, a URN so it has
no host left to rot, and docs is the one wiki page.
3. A malformed locale is refused with a code instead of dying as a bare RangeError.
| Tag | Before | Now |
|---|---|---|
en, en-GB, de-DE
|
formats | formats |
zz โ well-formed, unknown |
Intl falls back |
Intl falls back, still not refused
|
en_US, '', a raw Accept-Language value |
bare RangeError out of Intl, several frames from the header it came from |
X_LOCALE_INVALID, with a runnable fix:
|
Every @ultimat3/time entry point taking a locale passed the caller's raw tag to an Intl
constructor. assertLocale is the single gate now, and the list is one command:
grep -rn 'assertLocale(' packages/time/srcEdit only if you catch RangeError around a formatter; screen header input with
Intl.DateTimeFormat.supportedLocalesOf([tag]). The code's row is on Error codes.
8. defineAuth({ providers }) defaults to [], not the live OAuth registry.
export const auth = defineAuth({
adapter,
+ providers: ['github', 'google'],
});An app already passing providers: needs nothing. An app that passed none now serves no
/auth/oauth/<id> route โ name the ones you mean. The default was every provider any dependency had
registered, so the uniform 404 the option exists for could never fire, and an import decided the
app's login surface. With the credentials fix in the same release, that closed an enumeration
oracle: 500 meant registered, 404 meant not, and the 500 published the app's own *_CLIENT_ID and
*_CLIENT_SECRET names.
15. X_AI_PROVIDER_UNAVAILABLE now means one thing: the transport failed, on every provider
tried. A local refusal reaches the caller with its own code and its own runnable fix:.
| Code | Raised when | Its fix:
|
|---|---|---|
X_AI_KEY_MISSING |
no key configured and none passed |
export ANTHROPIC_API_KEY=<key>, or pass { apiKey } to the provider |
X_AI_REQUEST_INVALID |
a reasoning control the chosen model does not have | set model: on the llm() request |
X_AI_PROVIDER_UNAVAILABLE |
a non-2xx, an in-band error event, or a stream cut before message_stop
|
retry, or configure a second provider |
A catch treating X_AI_PROVIDER_UNAVAILABLE as "the model call failed" stops seeing the two
misconfigurations, which is the point: collecting one discarded its instruction and made
generate() and stream() answer one misconfiguration two ways.
17. Regenerate sw.js โ with the call, because no command writes it. x build emits no service
worker and nothing in the framework calls generateServiceWorker; the generated file's own
regenerate: header names the call for that reason.
generateServiceWorker(routes, config, buildId); // from @ultimat3/pwaThe emitted file changes for any app with a dynamic route above a static sibling. ruleFor returns
the first pattern that matches and the order was alphabetical: : (0x3A) and * (0x2A) sort
before every letter, so /posts/:id shadowed /posts/new, and a single /* shadowed the whole
table โ every PRECACHE_MANIFEST entry downloaded at install and then never looked up. Path is
still the tie-break, so identical input still emits an identical file.
If one of these fires, your app was half-broken before the upgrade โ each produced a declaration that was silently unreachable, not a rule the framework tightened for its own sake. The refusal names the finding.
| # | Refuses | Code | What had been happening |
|---|---|---|---|
| 10 | an id token naming several audiences whose azp is not this client, and an nbf in the future |
X_OAUTH_TOKEN_INVALID |
both only narrow, on the ID_TOKEN_CLOCK_SKEW_MS id-token.ts already exported to workload.ts and did not itself enforce โ an nbf ten years out verified, and a token an OP minted for another client that also lists yours verified with it (OIDC Core 3.1.3.7) |
| 11 | a second x_users row with one email, or with one external_id
|
X_AUTH_WRITE_FAILED |
MemoryAdapter is what x new scaffolds and what every test runs against, so the duplicate path was exercised only against the permissive half of the seam: two register() calls at one address made two rows, and the second was unreachable forever |
| 12 | two defineAdmin resources claiming one path:
|
X_ADMIN_PAGE_PATH_INVALID |
eight routes over four paths, with the second resource's four screens silently unreachable. The fix: hands you a path: for one of them |
| 13 |
registerLayout(name, โฆ) on a name already registered |
X_MAIL_DUPLICATE |
layouts.set answered whichever module ran last, base included, so a dependency could re-shell every framework mail in silence |
| # | What changed | What you do |
|---|---|---|
| 5 | the WAL decoder returns the values a repository row holds. PgOutputDecoder decoded a timestamptz as '2026-08-09 12:00:00+00', a text[] as '{a,b}' and a bytea as '\x0102', while the shared live window holds rows @ultimat3/entity parsed โ and compareValues normalises a Date to its epoch, so an edit to any column of any row jumped that row to the top of every orderBy('createdAt', 'desc') feed for every subscriber, carrying the raw string into the window. post.tags.map(โฆ) threw on the first patch |
nothing, unless you name PgOutputMessage, entityRow or PhysicalRow from @ultimat3/realtime/server โ after/before and entityRow's return widen to PhysicalRow. It is a wire convergence: only a real walsender diverged, because setRowObserver emits already-parsed rows and the parity test handed the same object to both sides |
| 6 | a delta resume no longer seats a pre-policy cursor. resumeFrom advanced across the retained patch list, which is pre-policy by design, so a subscriber reconnecting inside the retain window gained the id of every row inserted for every other actor while it was away โ and then received a delete frame carrying another tenant's row id |
nothing. The leak subscriber-gate exists to close, re-opened one layer up and closed again |
| 9 |
@ultimat3/auth no longer persists provider access or refresh tokens. x_accounts.access_token and refresh_token held live third-party credentials in the clear under a tables.ts header promising "no column holds a plaintext secret", and nothing ever read either one back |
nothing, unless your own SQL selected either column โ both are written null now. The type and the DDL are unchanged; keep a token you actually call out with in your own table, encrypted |
Read these if you built a workaround for one.
| Fix | What stops happening |
|---|---|
Gateway.stream() resolved the provider between reserve() and the try/finally that releases it |
a registered model no configured provider serves debited the estimate and never credited it back โ on MemoryBudgetStore, which is per process and never expires, five refused streams spent an org's whole ceiling with nothing ever sent, and every later call was X_AI_BUDGET_EXCEEDED for the life of the process (#319) |
| a successful login cleared the per-IP failure bucket | one credential the attacker owns bought unlimited stuffing โ 4 guesses, 1 login, repeat: 160 guesses from one address against a 5-attempt limit, never locked (#317) |
| the OAuth callback published uncoded internal exception text to an unauthenticated caller | connection strings and bind passwords reaching the browser, on two independent paths. The token-endpoint body is no longer reflected either โ that request carries client_secret (#318) |
readonly-sql ended a -- comment at \n only |
a CR terminated the comment for Postgres and not for the scanner, hiding the payload from all four layer-3 checks โ including select pg_advisory_lock(42), whose session lock survives ROLLBACK and outlives the read on a pooled connection (#316) |
diffRows threw on a money() column |
every adminUpdate on a money-bearing entity failed with an uncoded TypeError after repo.update() committed, landing zero audit entries (#321) |
x db gen --allow-destructive emitted a migration Postgres refuses |
drop table with no preceding FK drop, tables ordered alphabetically rather than by dependency โ SQLSTATE 2BP01 during ROLE=migrate, with a down that cannot restore |
Five breaking entries, from closing the ten findings the 8.0.0 sweep filed rather than absorbed. Four are compile errors the moment you upgrade. The fifth changes what your cache ladder is, at runtime, and it is the one to read even if nothing else here applies. No codemod.
| # | Surface | Costs you an edit if |
|---|---|---|
| 1 |
@ultimat3/render splits into . and ./server
|
you import a build-time name, or relied on importing the barrel to install the .tsx loader |
| 2 |
cache.tiers names the ladder's own rungs, and is now read |
your app.config.ts sets cache.tiers โ runtime behaviour changes even if it compiles
|
| 3 |
@ultimat3/storage renames IMAGE_FORMATS / ImageFormat
|
you imported either |
| 4 |
@ultimat3/pwa drops the forced-reload half of version-skew
|
you called updateSignal or updatePolicy
|
| 5 |
@ultimat3/core replaces CacheTier with CacheTierName
|
you named the type |
- import { defineRoute, renderToHtml } from '@ultimat3/render';
+ import { defineRoute } from '@ultimat3/render';
+ import { renderToHtml } from '@ultimat3/render/server';55 names moved: the render pipeline (renderToHtml, renderSsr, renderStatic, renderStreamHtml,
the ISR controller) and the loaders (installRenderLoader, compileStylesheet, stylesFor,
transformTsx). . keeps the authoring vocabulary โ defineRoute, h, Fragment, island,
hydrate*, the registry, the mode tables.
Second, easily-missed half: importing @ultimat3/render no longer installs the .tsx/.scss
loader. @ultimat3/render/server does. A test that did await import('@ultimat3/render') before
loading a page module must now import /server.
Why: bun build --target=browser on the barrel failed outright โ "Browser polyfill for module
node:url doesn't have a matching export named fileURLToPath", out of css-modules.ts. The
island this framework tells you to write could not be bundled.
- cache: { tiers: ['memo', 'lru', 'shared', 'isr'] }
+ cache: { tiers: ['request-memo', 'lru', 'redis'] }memo โ request-memo, shared โ redis, and delete isr โ it is a RenderMode, and the
routes that want it declare render: 'isr'. It named a cache rung that never existed.
Read this even if your config already compiles. The key was previously read by nothing:
startCacheTiers registered memo + lru unconditionally, redis on REDIS_URL, cdn on a purge
credential. An app declaring tiers: ['request-memo'] measurably got
['request-memo', 'lru', 'redis', 'cdn']. Now the ladder is the declaration, which means:
- naming a rung the environment cannot supply refuses the boot โ
rediswithoutREDIS_URL,cdnwithout a purge credential โ rather than quietly building a shorter ladder - an environment offering a rung the config does not name logs
cache.tier.unnamedand builds nothing
If you relied on the old always-on lru, or on REDIS_URL adding a tier your config never
mentioned, name it.
- import { IMAGE_FORMATS, type ImageFormat } from '@ultimat3/storage';
+ import { VARIANT_FORMATS, type VariantFormat } from '@ultimat3/storage';Both packages exported those two names over different sets, so a storage caller narrowing on
storage's type had a type saying gif cannot occur and a value from core's probe that was one. If
you were probing rather than minting variants, the six-format set is IMAGE_FORMATS from
@ultimat3/core โ which is what you actually had.
variantKey() also now refuses a format outside VARIANT_FORMATS instead of returning a key ending
.undefined.
Removed: updateSignal, updatePolicy, DEFAULT_GRACE_MS, and the types ForceReason,
UpdatePolicy, UpdatePolicyInput, UpdateSignalInput. AppUpdateAvailable narrows to
{ type, to }, losing from, forced and deadlineAt.
Nothing performed the reload they described, and no runtime could have called them: @ultimat3/http
(tier 2) and @ultimat3/realtime (tier 3) both sit below pwa (tier 4). Forcing a reload is not
a capability this framework has. Notification is, and is complete โ read
useConnection().updateAvailable, or compare the worker's posted to with detectSkew, and render
your own affordance.
The type behind entry 2. @ultimat3/cache still exports a CacheTier โ it is the tier interface,
a different thing, and it is unchanged. The two sharing one name is what made a type error about
CacheTier unreadable.
Six breaking entries, from one whole-repo bug sweep. Five are compile errors the moment you upgrade. The sixth is a silent behaviour change, and it is the one to read even if nothing else here applies to you. No codemod.
| # | Surface | Costs you an edit if |
|---|---|---|
| 1 |
@ultimat3/realtime has two entries |
you import a server name โ NATS, pg replication, the sync node, the channel hub |
| 2 |
IdempotencyStore.settle / fail take a reservation id |
you call either, or implement the interface |
| 3 |
pwa.installPrompt, auth.afterSignInPath, ai.modelEnv deleted |
your app.config.ts sets one |
| 4 |
@ultimat3/manifest drops canonical
|
you imported it |
| 5 |
@ultimat3/render drops matchRoute / RouteMatch
|
you imported either |
| 6 |
SQL_CANCEL projects its columns |
you asserted on that constant's text |
- import { ChannelHub, createSyncNode, LiveQueryRegistry } from '@ultimat3/realtime';
+ import { ChannelHub, createSyncNode, LiveQueryRegistry } from '@ultimat3/realtime/server';Client names โ useLive, liveHookFor, LiveClient, the offline queue, rebase, the wire protocol,
cursors โ are unchanged on .. A file importing both halves now writes both imports.
Why: the single barrel carried useLive beside openNatsClient, so bun build --target=browser on
an entry importing only the hook failed with "Browser build cannot require() Node.js builtin:
stream/web", out of nats. The island this framework tells you to write could not be bundled.
The two barrels are disjoint โ ./server re-exports no client name โ so which half a symbol
lives in is checkable rather than conventional. If an import stops resolving, the name moved to
./server; nothing was deleted.
- await store.settle(key, value);
+ await store.settle(key, value, reservation.record.id);reservation is what store.reserve(key, hash) answered. Same shape for fail.
Read this if you implement the interface โ it is the one silent entry in this major. A store with
the old two-parameter method still compiles, because a shorter function is assignable to a longer
signature, and it silently loses the fence. Both statements now match on id and state, so a
straggler from a slow first attempt can no longer overwrite a replacement reservation still in
flight. The fail half was the worse one: a straggler's failure marked a live replacement
failed, and the replacement's own settle was then fenced out.
- pwa: { enabled: true, offline: 'runtime', installPrompt: true },
+ pwa: { enabled: true, offline: 'runtime' },
- auth: { signInPath: '/signin', afterSignInPath: '/dashboard' },
+ auth: { signInPath: '/signin' },
- ai: { mcp: { expose: true, path: '/mcp' }, modelEnv: 'ANTHROPIC_MODEL' },
+ ai: { mcp: { expose: true, path: '/mcp' } },There is no replacement key, because there was never a behaviour. Each was declared, defaulted,
merged, and read by nothing. Use createInstallController from @ultimat3/pwa, send the visitor
from your own sign-in route, and pass model on the llm() request.
ai.modelEnv's own doc comment argued for its deletion: "an intention, not a behaviourโฆ nothing
consumes the merged valueโฆ So the exact thing this key exists to prevent โ a model string baked into
the image โ is what actually happens."
Same precedent as JobsConfig.driver in 5.0.0 and realtime.heartbeatMs in 4.0.0. All three fail at
typecheck only โ and an app that builds its config into a variable before passing it loses
excess-property checking and sees no error at all. scripts/config-readers.ts now keeps the class out.
Use canonicalJson from @ultimat3/core. It was the third of five copies of one serialiser;
manifest's fed buildId and the contract-diff equality, so a -0/NaN/Date fold could make
a breaking API change diff as "no change" and ship silently.
Two exported route matchers existed with different precedence. @ultimat3/http's trie is the live
one; render's had zero consumers repo-wide.
It fed toJobRecord, which does Number(row.run_at) โ so against a text-decoding PgExecutor every
timestamp came back NaN. Only an edit if you asserted on the constant's SQL text.
Four breaking entries, and only one of them can reach you at runtime. Three are compile errors the moment you upgrade; the fourth is a type you may never have named. None ships a codemod.
| # | Surface | Costs you an edit if |
|---|---|---|
| 1 | ScrapeTarget.pageErrors |
you implement ScrapeDriver/ScrapeTarget yourself |
| 2 | PwaRenderMode |
you import that type name |
| 3 | PwaOfflineStrategy |
you import that type name |
| 4 | PrerenderReport.skipped |
you read x build --target static --json, or the report in code |
Only a third-party driver author pays this, and nothing in an ordinary app implements
ScrapeTarget. If you build one โ the shape packages/scraping/README.md's driver-author example
builds โ construct the ring and, if your transport can observe uncaught page exceptions, push to it.
+ import { createRing, type PageErrorRing } from '@ultimat3/scraping';
const target: ScrapeTarget = {
// โฆ
+ pageErrors: createRing(200),
};A driver that cannot observe them builds the ring and never pushes โ which is exactly what the offline targets do. That is the whole migration.
Required rather than optional, deliberately. An optional ring lets a driver stay silent about
errors it can see, which is the gap this closed: nothing in @ultimat3/scraping subscribed to
pageerror at all, so an island that threw was invisible. A throw calls no console method, so
console() answered [] and a page whose script had died read as clean.
New on ScrapePage, and additive โ no edit needed to consume them: pageErrors() and
pageErrorsDropped(). The dropped count makes the list a floor, not a total.
Two type-only renames. No member changed โ only the name the type is declared under.
| Was | Is | Members, unchanged |
|---|---|---|
PwaRenderMode |
RenderMode |
'static' | 'isr' | 'ssr' | 'stream' |
PwaOfflineStrategy |
OfflineStrategy |
'precache' | 'runtime' | 'network-only' |
- import type { PwaRenderMode, PwaOfflineStrategy } from '@ultimat3/pwa';
+ import type { RenderMode, OfflineStrategy } from '@ultimat3/pwa';@ultimat3/pwa re-exports both under the canonical name, so the import path does not have to move โ
@ultimat3/core is where they are declared and is equally correct.
Why the alias existed and why it could not stay. Tier 4 may not import tier 4, so @ultimat3/pwa
wrote its own copy of a set @ultimat3/render already had. That copy is what kept spa mapped to
cache-first after spa was deleted in 6.0.0 โ the one strategy that gives an app/ route a
shared cache entry, i.e. one signed-in member's HTML served to the next. The vocabulary is now
declared once at tier 0, and bun run scripts/render-modes.ts --json refuses a second declaration
anywhere in packages/*/src.
readonly string[] โ readonly SkippedRoute[], where a SkippedRoute is
{ route, surface, render, reason, why }. PrerenderedPage also gains route, the declared path a
concrete URL came from.
- for (const path of report.skipped) console.log(`skipped ${path}`);
+ for (const skipped of report.skipped) console.log(`skipped ${skipped.route}: ${skipped.why}`);x build --target static --json now returns emitted and skipped, and the human path prints the
same rows.
Why it changed. .x/static/ held a partial site and said nothing about the difference: app/
routes exist only through the server, so a tool pointed at the directory filed "the island did not
mount" against a route that was never emitted. A list of paths cannot distinguish "not emitted
because it needs a server" from "not emitted because it is broken", and those are opposite facts.
Installable As of 2026-08-21 โ npm view @ultimat3/core version answers 7.0.0, so 6.0.0 is behind latest and every entry below is a step you take on the way to it. Run that command anyway rather than trusting this line; a version written into a page goes stale on the next tag.
Seven breaking entries, and the first is a runtime refusal with no compile error in front of it.
Every single-label timezone name except UTC is refused. isValidTimeZone answers false, canonicalTimeZone answers undefined, assertTimeZone throws X_TIMEZONE_INVALID โ and every @ultimat3/time formatter is downstream of that one call. 43 names change answer, tabulated once under the 6.0.0 section of CHANGELOG.md; that table is the source and is deliberately not copied here.
- formatDate(at, { locale, zone: 'CET' })
+ formatDate(at, { locale, zone: 'Europe/Paris' })| Class | Names | Replacement |
|---|---|---|
| geographic link โ 24 of the 43 |
Japan, GB, Hongkong, NZ, โฆ |
the Area/Location spelling: Asia/Tokyo, Europe/London, Asia/Hong_Kong, Pacific/Auckland. Textual โ identical wall clock, identical offset |
| UTC alias |
UCT, Universal, Zulu
|
UTC |
the GMT family |
GMT, GMT0, GMT+0, GMT-0, Greenwich
|
Etc/GMT, which still renders the label GMT. Not UTC, which renders UTC โ same instant, different text on any surface that prints the zone name |
| abbreviation |
CET, EET, MET, WET, EST, MST, HST, EST5EDT, CST6CDT, MST7MDT, PST8PDT
|
none, and that is the defect. An abbreviation names no jurisdiction and carries no DST rule, so only the author knows which city's clock was meant: Europe/Paris for CET, America/New_York for EST5EDT, America/Phoenix for MST
|
Etc/GMT+2 is unaffected โ only a leading sign is a bare offset, and that + sits inside a real zone name. US/Eastern and Asia/Calcutta are unaffected too: a deprecated two-label alias is still Area/Location.
TimeZone is string in @ultimat3/time, so 'CET' compiles. Nothing fails until the call runs.
| Site | Spelling | At 6.0.0 |
|---|---|---|
| a formatter, or zone arithmetic |
zone: on formatDate, formatDateTime, formatRange, zonePartsAt, โฆ |
throws X_TIMEZONE_INVALID on the first call |
| a scheduled task |
tz: on task()
|
refused where the task is declared โ task() validates through isValidTimeZone, so this one is caught at boot |
app.config.ts |
defaultTimeZone |
refused at boot โ defineConfig validates through core's own statement of the structural rule, so a stale key is X_CONFIG_INVALID naming the field, with the swap in its fix:
|
a client's x-timezone header |
any of the 43 | no error โ resolveTimeZone falls through to the configured default, so a hand-written client sending CET silently renders in your default zone. Browsers are unaffected: Intl.DateTimeFormat().resolvedOptions().timeZone is always Area/Location
|
Find every candidate:
grep -rnE "(zone|tz|defaultTimeZone): *'[^/']+'" --include='*.ts' --include='*.tsx' .Run it from the app root. Every hit is a single-label zone; 'UTC' is the only one already correct.
Intl answers "can I format this", never "is this an IANA zone", and at ICU 78 the two stopped agreeing: Bun 1.4 resolves CET, EST, GMT and MST where ICU 75 threw. A runtime upgrade alone therefore reopened the "no date without an explicit IANA zone" rule โ silently, and in the direction that fails dangerous, because an abbreviation carries no DST rule. The judgement is now structural instead of delegated: an identifier is Area/Location, and UTC is the one legal exception. That refuses the single-label backward links along with the abbreviations, and is meant to โ no structural rule keeps CET out while letting Japan in, both being one label, and the alternative is a denylist that grows with every tzdata release. #251, and Timezones and dates for the rule it restores.
| Fix | What changes for you |
|---|---|
island JSX compiles through babel-preset-solid
|
client-side Solid reactivity inside an island works at all. An island containing JSX compiled to React.createElement and threw ReferenceError: React is not defined on first interaction, with the gate green. Two build-time dependencies join @ultimat3/cli; zero bytes reach your client bundle (#243) |
@ultimat3/core loads in a browser bundle |
core's three module-scope AsyncLocalStorage constructions โ the request context, the active span, the impersonation reason โ moved onto one lazy seam, so @ultimat3/ui no longer throws TypeError: undefined is not a constructor at module evaluation (#244). Six more constructions outside core were untouched at 6.0.0 and carry the same defect โ @ultimat3/db, @ultimat3/entity, @ultimat3/ai; they are [Unreleased], along with the guard that makes the rule a build error (#255) |
Rebuild to pick either up.
Two breaking entries over six surfaces, one of which needs an edit. There is no codemod, and there does not need to be: the whole migration is deleting one line, and only if you wrote it.
jobs: {
- driver: 'postgres',
queues: ['app-default'],
concurrency: 8,
},jobs.driver accepted 'postgres' | 'redis' | 'nats' and had no reader anywhere. Boot always
built createPgDriver, so setting it to redis did not throw, did not warn and did not boot Redis
โ it changed nothing and you silently got Postgres. If you were relying on it doing something, it
was not: you were on Postgres the whole time.
Which driver runs is setJobDriver(driver), and only that:
setJobDriver(createPgDriver({ executor })) // production
setJobDriver(createMemoryDriver()) // a testJobsDriver (the type) goes with it. JobsConfig.driver was its only use.
Leaving the line in also works. A spread carries a key no type names, so a stale
app.config.ts still boots and the field still does nothing โ packages/core/src/config.test.ts
pins exactly that. TypeScript will flag it; the runtime will not.
They are @ultimat3/testing's subscribe fixture, which was declared and had no driver โ so
nothing could have been implementing these types. They changed because they described an API that
could not work: LiveTarget was { name, queryHash }, and a node keys a subscription by
(name, input); a hash is the input already thrown away.
| Was | Is |
|---|---|
Subscribe = (target) => Promise<LiveFeed> |
(target, input, actor?) => Promise<LiveFeed> |
LiveTarget = { name, queryHash } |
{ name } โ the query itself |
LiveFeed had no reconnect()
|
it has one |
DRIVER_FIXTURE_NAMES held subscribe
|
FRAMEWORK_FIXTURE_NAMES does; the framework builds it |
A test that destructured subscribe and called it now reads:
-const feed = await subscribe(liveFeed.as(actorFor(ada), { orgId: acme.id }));
+const feed = await subscribe(liveFeed, { orgId: acme.id }, actorFor(ada));The actor is the third argument rather than baked into the target because that is where the framework puts it: the shared window is built with no subject, and every decision about an actor is per subscriber.
Error fields are escaped where they are built. UltimateError and SchemaError run
singleLine over code, title, cause, fix and docs in their constructors, so .message,
.cause, format(), toJSON() and any renderer you write are one line by construction. Measured
over every shipped cause:/fix: literal: none contains a newline, so no framework message
changed. If you build error text from a value a CALLER controls, you no longer have to remember โ
and if you were already escaping, singleLine is idempotent, so nothing doubles.
One fix: line changed text. X_REPLICATION_FAILED on SQLSTATE 42704 said
x db replication init, which is not a command โ x db takes gen, migrate, reset, seed,
studio, branch, backfill. It now names the CREATE PUBLICATION an operator can paste.
Not a change in this release โ a defect it made visible. If a query({ live: true }) declares an
orderBy on a column its rows do not carry (a projection that omits it), every change to a row
reads as a move, and the re-delivered row is the raw entity row rather than the projection. Columns
you left out of the projection reach the subscriber. #230,
and Known gaps carries it. Until it is fixed, order a live query by a column its rows
carry.
Twenty-five BREAKING โ entries. Most are one of two shapes: a declaration nothing read, deleted rather than implemented, and a surface that answered the wrong thing, corrected. Full rationale per row in CHANGELOG.md's 4.0.0 section.
Start here โ these three change behaviour whether or not you edit anything:
| Surface | The edit |
|---|---|
on delete now reaches the generated SQL. Any app that ever declared references(โฆ, { onDelete }) generates different DDL
|
run x db gen and read the diff before migrating. Every add constraint this framework had ever emitted dropped the rule, so the database has been refusing deletes under a declared cascade. Drift also gains changed-foreign-key, whose fix: hands over a drop constraint / add constraint pair โ add constraint alone is 42710 on a name already taken |
llm()'s cache.semantic.scope receives { input, ctx } and defaults to the calling actor, not 'global'
|
scope: (input) => input.orgId โ scope: ({ ctx }) => ctx.actor.orgId ?? 'none', or delete scope and take the default. A semantic lookup is a cosine nearest-neighbour with no tenant predicate, so the old shared store answered one tenant with another tenant's completion โ reproduced at similarity 1.0. A deliberately shared cache must now say so |
reapBranches() skips branches whose base is not current_database()
|
none, and re-read it if you run two Ultimate apps on one Postgres: listBranches() walks pg_database for the whole server, so one nightly sweep was dropping the other app's branches. A pre-4.0 marker records no base and is now skipped rather than dropped; the next createBranch writes it down, so it self-heals with no migration |
Deleted because nothing read them โ in every case the edit is "delete the option":
| Surface | The edit |
|---|---|
CaptureOptions.timeoutMs and CaptureRequest.timeout (@ultimat3/scraping) |
delete them. The port required a timeout, page-over-target.ts threaded it, and no driver honoured it
|
ScrapeTarget.click's index parameter |
delete it. It was unreachable from the public vocabulary โ ScrapeFrame.click takes (selector, options?) and has no index โ and the two drivers disagreed on it |
PrecacheAsset.critical (@ultimat3/pwa) |
delete it. buildPrecacheManifest never copied it, and the documented promise ("critical assets are precached even if large") was vacuous โ there is no size filter at all |
PERIODIC_SYNC_TAG, BackgroundSyncOptions.periodicMinIntervalMs (@ultimat3/pwa) |
delete them. Periodic Background Sync was never implemented in any sense: no listener, no registration, no capability flag |
realtime.heartbeatMs (RealtimeConfig) |
delete the key โ RealtimeConfig is now { enabled, tier, transport, urlEnv }. The socket beat is new LiveClient({ heartbeatMs }) (browser code, which cannot read server config) and the presence beat is derived. There is no runtime refusal: section() copies unknown keys through, so a stale key is silently inert (25.0.0 refuses it at boot) |
@ultimat3/seo no longer exports extensionOf
|
delete the import; parseImageQuery reads the format off the query |
@ultimat3/realtime no longer exports qidOf or canonicalJson
|
change the import: queryHash from @ultimat3/query, canonicalJson/fingerprint from @ultimat3/core. No live subscription re-keys โ the two spellings differed only on values JSON cannot carry |
Corrected, because they answered the wrong thing:
| Surface | The edit |
|---|---|
adminResource no longer pluralises an entity name |
set path: explicitly if you relied on the doubled URL. Every entity in both tracked apps is already named plural, so entity('orgs') was served at /admin/orgses. Which plural a name takes is an app's convention, not a mechanism the framework can own (axiom 8) |
| A local disk's signed URLs carry the registered disk name, not the driver kind | none, if you use defineStorage โ it calls registerAs(diskName) at boot. A disk registered as uploads used to 404 every signature it had just written |
ordinal(value) takes no locale |
delete the second argument. It picked the plural category with your locale and appended the English suffix regardless, so ordinal(1, 'de') was '1th'
|
registerFrameworkCatalog() and registerMailCatalog() take no locale
|
delete the argument. defineCatalogs called them once per locale, seating the English-only catalog under every locale an app declared โ an app shipping only es served English chrome with isMiss reading false, which is a fallback locale chain the i18n package forbids by name |
t.date refuses a date-time with no offset and no Z
|
send 2026-08-19T10:00:00Z. 2026-08-19T10:00 resolved against the host process's zone, so one wire value meant a different instant on each pod โ reachable from a request through coerceQuery, and published as format: 'date-time', which RFC 3339 requires an offset for |
in with a non-array operand matches no rows on both drivers |
pass an array. It matched one row in Postgres (the scalar was wrapped) and none in memory; in with a NULL in the list disagreed in the other direction, and the SQL now emits (col in (โฆ) or col is null)
|
isValidCron / parseCron refuse an unsatisfiable day/month pair ('0 0 30 2 *') |
fix the expression; the refusal names the pair. It used to parse clean and then burn ~184ms of blocking CPU per tick in the scheduler's leader loop before throwing |
createRateLimiter({ now }) โ createRateLimiter({ clock })
|
{ config, now: () => t } โ { config, clock: { now: () => new Date(t) } }. Callers that passed neither are unaffected |
requiresApp is enforced by the dispatcher |
none, unless a script matched on the old message. Outside an app, x secrets set and its siblings now answer X_NOT_IN_APP
|
NackOptions.countsAsAttempt: false no longer files a job suspended
|
none. "Do not burn an attempt" and "this is a step.sleep suspension" were one flag, so the worker's limiter and job.concurrency sheds pushed rows out of ready โ and queue_depth / queue_oldest_ready_seconds under-reported because of it |
A read whose input carries a Date, Map or Set gets a new cache key and cursor scope, once
|
none. Object.keys(date) is [], so every date rendered {} and one key answered for every date window a read ever served. Affected cursors answer X_CURSOR_INVALID once with "request the first page again"; ordinary inputs are byte-identical |
Type-level, for hand-built literals and exhaustive switches:
| Surface | The edit |
|---|---|
ColumnDescription / ReferenceDescription gain onDelete: OnDelete | null
|
add the field to hand-built description literals (a test fixture, a custom generator). null is Postgres' no action and is the old behaviour |
DriftKind gains changed-foreign-key
|
a switch over DriftKind with no default no longer compiles |
BranchInfo gains base: string | null
|
re-type if you built the shape by hand |
| Five generators write typed test filenames | re-run the generator, or rename by hand. x verify selects a suite by filename, so a generated contractTest(โฆ) inside a plain *.test.ts ran under unit while x test contract answered X_TEST_NO_FILES โ a step that passed by having nothing to run. x g action/x g mutator now also write <name>.contract.test.ts, x g query --live writes <name>.live.test.ts, and x g job/x g task/x g backfill write <name>.job.test.ts
|
One migration to run: the x_jobs idempotency index gains the tenant. It was (name, idempotency_key) while the row already carried tenant_id. x db migrate applies it.
Ten BREAKING โ entries, all from one bug sweep. Each was a documented surface that did nothing, or did the wrong thing; the fix is the edit named beside it. Full rationale per row in CHANGELOG.md's 3.0.0 section.
| Surface | The edit |
|---|---|
defineAuth({ mfa: { required: true } }) โ refused at boot (X_CONFIG_INVALID), and AuthMfaPolicy.required narrowed to the literal false
|
delete mfa.required; enforce the requirement in your own enrolment flow. Nothing ever read the flag, so a user who never enrolled got a fully-privileged session under it |
enrolTotp(input) โ enrolTotp(auth, input); input.issuer is now optional |
pass the auth you built with defineAuth. The configured issuer never reached the otpauth:// URI before |
@ultimat3/http no longer exports appErrorStatus()
|
read your own registration module. registerErrorStatus() and statusFor() are unchanged |
SyncSocket.lastSeenAt โ lastSeenMonotonicMs, on Clock.monotonic()
|
rename the read. If you were formatting it as a date you were already wrong โ the rename makes new Date(...) a compile error |
SQL_OUTBOX_RELEASE and SQL_OUTBOX_MARK_PUBLISHED take one more parameter each (1 โ 2, 2 โ 3): the claimant |
pass the claimant. OutboxStore.release/markPublished take it as an optional trailing argument, so an unfenced store still compiles |
SocketRegistry.sweepIdle() โ idle(), which returns the over-budget sockets and removes nothing |
call idle() and evict through the node, or set the budget with createSyncNode({ idleTimeoutMs })
|
DESCRIPTION_MIN_LENGTH deleted from @ultimat3/seo
|
delete the import. There is no replacement and no minimum description length is checked โ the constant was documented as enforced and was read by no validator |
A metric redeclared with different bounds or a different observe is refused (X_METRIC_NAME_INVALID) |
make the second declaration state the same bounds/observe, or fetch the handle without options โ gauge(name) is unchanged |
Seed.run() resolves with SeedRun instead of void
|
re-type the result if you typed it void. Awaiting it for the side effect alone is unaffected |
SeedContext.insert skips a stored row instead of overwriting it |
expect skipped, not an overwrite. upsert is the verb for a row the table keys |
cachedFormatter and canonicalLocale moved from @ultimat3/time to @ultimat3/core and are re-exported from time, so no import breaks โ it is listed here because the move is real, not because it costs an edit.
Thirty-three BREAKING โ entries โ the largest major this project has shipped, and the first one semver covered. Written up here As of 2026-08; the page carried a row pointing at this section for six releases and never carried the section. Full rationale per entry in CHANGELOG.md's 2.0.0 section โ the numbers below are that section's own order. No codemod.
Two things are not compile errors and are the ones to read first: the seven behaviour changes under Start here, and the one migration every app with a money() column owes.
| # | Surface | Costs you an edit if |
|---|---|---|
| 1 |
x db branch takes a verb |
you ever ran the bare-name form, which created a database |
| 2 |
x new writes no migration |
you scaffold a new app, or your app carries a hand-written 0000_initial.sql
|
| 3 | an MCP tool is named by its export name, verbatim | you read a tool name off openapi.json, describe().mcp.tool or .tool().name
|
| 4 |
selectMailDriver refuses with no mail credential |
you send mail from staging or production
|
| 5 | a lapsed fleet slot cannot be renewed by its holder | a job run outlives its slot lease |
| 6 |
@ultimat3/query ships no read-cache seam of its own |
you called setReadCache, invalidateQueryTags, or imported ReadCache
|
| 7 |
@ultimat3/auth drops requireRole / requireScope
|
you gate a route with either |
| 8 |
@ultimat3/db drops readOnly() and its four companions |
you imported any of them |
| 9 |
@ultimat3/seo drops the performance-budget surface |
you imported checkBudgets, parseBytes, a Budget* type, or set RouteRecord.budget
|
| 10 |
@ultimat3/seo drops renderLd
|
you called it |
| 11 | seven zone and locale helpers are gone | you imported attachTimeZone, timeZoneOf, attachLocale, localeOf, negotiateLocale, isValidTimeZone or resolveTimeZone
|
| 12 |
@ultimat3/seo drops renderHeadTags
|
you called it |
| 13 | a derived BudgetLedger bills its parent |
you already sit at an llm() budget ceiling โ no signature changed |
| 14 |
idempotencyKeyFor takes the actor, required and third |
you call it, or you hold idempotency records written before the deploy |
| 15 |
Idempotency-Key is enforced at 255 characters |
a client sends a longer key |
| 16 |
@ultimat3/action's fingerprint is SHA-256/16 |
you enqueue an action job across the deploy boundary |
| 17 |
markReady() throws X_LIFECYCLE_DRAINED after a drain |
a test or a process drains and then starts a role |
| 18 | a drain is bounded at 25s, and a hook that outruns it is abandoned | your drain legitimately takes longer |
| 19 |
cacheKeyFor takes a fourth, required authority
|
you call it directly |
| 20 | the query fingerprint is SHA-256/16 hex | you hold cursors minted before the deploy |
| 21 |
semantic.remember refuses a TTL the tiers refuse |
you passed a non-finite or negative lease |
| 22 |
OutboxRelay.stop() returns Promise<void>
|
you await teardown, or implement the interface |
| 23 |
TierFailure.tier is TierLabel
|
you switch over it with no default
|
| 24 |
hello carries no cursors |
you build a hello frame by hand, or read FRAME_LIMITS.resume
|
| 25 | three more @ultimat3/realtime surfaces move |
you call qidOf, read a mutation's status after a drain, or implement SyncNode
|
| 26 | four projection changes โ what a value becomes when it leaves the process | you have a money() column (a migration), read schema.nullable, pass an unclonable .default, or take a nested object as query({ input })
|
| 27 |
EPOCH is gone; call epoch()
|
you imported it, or declare a 6-field cron |
| 28 |
job() and backfill() require a tenant
|
you declare any job or backfill |
| 29 | one resolveEnvironment, and it is @ultimat3/core's |
you imported seo's, or wrote 'preview'
|
| 30 | the NATS wire client is [email protected], behind the same transport seam |
you imported a hand-rolled NATS name, or faked a byte stream in a test |
| 31 |
@ultimat3/cli exports checkSourceDrift, not checkDrift
|
you imported the CLI's |
| 32 |
invariants is a function, and invariant() takes a built expression |
you declare an entity with invariants |
| 33 | the framework's version is a call, not a constant | you imported FRAMEWORK_VERSION, DEFAULT_SERVER_INFO or CLI_VERSION
|
| # | What changes | What you do |
|---|---|---|
| 4 | with neither SMTP_URL nor RESEND_API_KEY, staging and production install a driver that rejects every send with X_MAIL_CREDENTIAL_MISSING. development and test are unchanged, and an app that sends no mail still boots โ the refusal is on the send, not at boot |
set one of the two env keys in every environment that sends. The SMTP Message-ID, and so SendResult.id, is now content-derived and stable across attempts of one send |
| 5 |
SQL_LEASE_RENEW fences on expires_at > now() as well as holder, matching the memory store. A run whose slot lapsed is cancelled with X_JOB_SLOT_LOST instead of running on uncapped past job.concurrency
|
nothing, unless a handler holds a slot longer than its lease โ raise the lease, or shorten the run. This is what the documented contract already said and what x dev already did |
| 13 | a derived ledger bills its parent, so a call that used to slip past a request ceiling can throw X_AI_BUDGET_EXCEEDED, and gateway.spent() returns a larger โ correct โ number |
raise the ceiling, or accept the refusal. Listed as breaking because it is observable to an app already at its limit, even though it makes "derive can only tighten" true for the first time |
| 15 |
Idempotency-Key is enforced at 255 characters. The OpenAPI operation published maxLength: 255 all along and nothing checked it |
shorten the key. A client sending longer keys worked by accident and now gets a 400 |
| 17 |
markReady() throws X_LIFECYCLE_DRAINED on a drained lifecycle instead of declining in silence |
call resetLifecycle() between a drain and the next start โ which is what three test files were already doing by hand. A process that drains and then starts a role now fails at the mistake rather than binding a socket that answers 503 forever |
| 18 | a drain is bounded at 25s by default and a hook that outruns it is abandoned, not stopped โ it is still running when the process exits. drainDeadlineMs() returns a number always, and remainingBudget() is a number rather than number | undefined
|
if your drain legitimately takes longer, say so โ and move the pair together, or you have only relocated the kill |
| 21 |
semantic.remember puts its TTL through assertTtl like every other write, with jitterFraction: 0
|
pass a finite, non-negative lease. It used to compute ttlMs itself and hand a tier a value no other write path can produce |
Entry 18's pair, both sides or neither:
configureLifecycle({ deadlineMs: 600_000 }); // and terminationGracePeriodSeconds >= 600jobs and realtime are the two roles that most need a bound and declared none, so before 2.0.0 they drained unbounded โ a worker pod holding a long job past terminationGracePeriodSeconds is SIGKILLed by the kubelet mid-statement, which is the failure the deadline exists to prevent.
A money() property is three physical columns, not two: <p>_minor, <p>_currency and the new <p>_scale. Every existing app needs a migration โ without the column, every read of that table names a column it does not have.
alter table "<t>" add column "<p>_scale" integer check (<p>_scale is null or (<p>_scale >= 0 and <p>_scale <= 15));Byte-for-byte what generateMigration's columnClause emits. NULL is the right value for every existing row: it means the currency's own minor unit, which is what those rows always meant, where 0 would mean whole units. examples/dummy/packages/db/migrations/0002_money_scale.sql is the worked example, hand-written because x db gen answers X_MIGRATION_SNAPSHOT_MISSING in an app whose 0001 records no snapshot.
The other three projections in the same entry:
| Was | Now |
|---|---|
t.nullable(x) emitted { โฆconverted, nullable: true }
|
{ anyOf: [<converted>, { type: 'null' }], โฆannotations }. nullable is an OpenAPI 3.0 keyword no later draft defines, so every validating consumer rejected null. A hand-written consumer reading schema.nullable reads schema.anyOf instead |
.default(value) accepted any value |
a default structuredClone refuses โ a function, a class instance, a Proxy โ throws X_SCHEMA_DEFAULT_UNSHAREABLE at the first import of the file that declares it. Pass a plain value, or a factory the handler calls |
query({ input }) accepted any schema |
an input that cannot survive a query string is refused at query() with X_QUERY_INPUT_UNENCODABLE, in the declaring file. A read is GET /_x/query/<name>, so its input is characters: flatten the nested object, or make it an action
|
No edit for most apps, and each is a one-time cost worth knowing before it is a support ticket.
| # | What goes cold, or re-runs | Why, and what to do |
|---|---|---|
| 6 | a cached query is cold once |
@ultimat3/query no longer ships its own read-cache seam. Removed: setReadCache, getReadCache, invalidateQueryTags, MemoryReadCache, DEFAULT_READ_CACHE_MAX_BYTES, and the types ReadCache and ReadCacheEntry; DEFAULT_READ_CACHE_TTL_MS stays. A Redis deployment's read path changes in both directions โ it was the Redis tier alone, so every cached read was a network round trip; it is now read-down/promote-up across request-memo โ lru โ redis, and concurrent misses of one key share a single load |
| 14 | an in-flight idempotency record is unreachable | the stored key's shape changed with the signature, so on the shared Postgres store a retry crossing the deploy boundary finds no record and re-runs the handler, inside the 24h window. truncate x_idempotency after deploying makes that state honest rather than half-reachable. The memory store dies with its process and is unaffected |
| 16 | an action job does not dedupe against its pre-deploy row |
@ultimat3/action's fingerprint is SHA-256/16, so job-handle.ts's dedupe key action:<name>:<fingerprint> changed. Action idempotency itself is unaffected in practice, because the key changed too |
| 20 | a cursor minted before the deploy is rejected once | the query fingerprint is SHA-256/16 hex where it was FNV-1a/32 โ 4ร10โน values, brute-forceable offline in seconds, and a fingerprint here is a sharing key over client-chosen input. The canonical form is unchanged, so only the hash moved; X_CURSOR_INVALID's fix: is already request the first page again
|
An app that installed its own read cache registers it where every other cached surface already took one:
- setReadCache(myCache);
- invalidateQueryTags(tags);
+ registerTier(myTier); // from @ultimat3/cache
+ invalidateTags(tags); // literally the same callA process that registers no tier reads uncached rather than filling a store no fan-out can see.
x db branch takes a verb. The argument was the branch name and the dispatcher fell through to it, so x db branch with the argument ls (the verb is list since 26.0.0) โ the fix: line the planned x branch command hands out โ cloned the database into one called ls. A stray database is not a typo an agent can see: it is a copy of production-shaped data with a name nobody will recognise a week later.
- x db branch feat-new-billing
+ x db branch create feat-new-billing| Verb | What it does |
|---|---|
x db branch create <name> |
the old bare-name form, said out loud |
x db branch list |
name, location, created-at, size |
x db branch delete <name> |
what only dropBranch('<name>', { force: true }) could do before |
Every verb is itself a legal branch name, so verb-first is the only shape where a name cannot be read as a subcommand. A word outside that set is X_CLI_UNKNOWN_COMMAND, and its fix: hands your own word back inside the command that still creates it. delete (drop until 26.0.0) takes no confirmation flag deliberately โ it may only remove what list shows. branchSql is removed with the psql shell-out it was the text for; an external clone now runs through @ultimat3/db's createBranch(), which is what makes list work at all โ the old path wrote the database and no marker comment, so every branch the CLI made was invisible to the only lister the framework has. Branches created by the old path carry no marker and are listed and dropped by neither.
x new writes no migration: the 0000_initial.sql it wrote under packages/db/migrations/, and its .hash, are gone from the scaffold, and x db gen is that directory's single writer (axiom 1). A hand-written first migration could not carry the .snapshot.json only the generator produces. A scaffold that declares an entity is therefore red on x verify's drift step until the first generate runs, and that is correct behaviour:
x db gen "initial"
x db migratebin/setup runs both for you, generating only when the directory holds no .sql.
snake_case tool names are gone, and so is toToolName. One primitive was reachable under one name and published under another โ the served name has only ever been the export name, while three publishers spelled the same tool publish_post. So an agent handed openapi.json called tools/call { name: "publish_post" } and got ToolNotFound: the catalog it was given was the wrong one.
| Was | Now |
|---|---|
publishPost.tool().name โ 'publish_post'
|
'publishPost' |
openapi.json โ "x-ultimate": { "mcpTool": "publish_post" }
|
"mcpTool": "publishPost" |
publishPost.describe().mcp.tool โ 'publish_post'
|
'publishPost' |
import { toToolName } from '@ultimat3/action' / '@ultimat3/query'
|
removed from both โ there is no derivation left to call |
Nothing that worked moves: a tools/call, a scopes: entry and a visibleTo list were already spelled verbatim, and a snake_case scopes: entry was already X_MCP_SCOPE_UNKNOWN at boot. What moves is everything read off the published contract โ run x manifest to regenerate openapi.json, then re-point any agent prompt, saved tool allowlist, generated client or test that took its tool name from x-ultimate.mcpTool, describe().mcp.tool or .tool().name. x.manifest.json is unaffected: its mcp fact never carried a tool name.
- import { EPOCH } from '@ultimat3/time';
+ import { epoch } from '@ultimat3/time'; // 27 โ call it: epoch()
- import { resolveEnvironment } from '@ultimat3/seo';
+ import { resolveEnvironment } from '@ultimat3/core'; // 29
- import { checkDrift } from '@ultimat3/cli';
+ import { checkSourceDrift } from '@ultimat3/cli'; // 31 โ same signature, same findings
- import { FRAMEWORK_VERSION } from '@ultimat3/core';
+ import { frameworkVersion } from '@ultimat3/core'; // 33 โ call it: frameworkVersion()| # | Why the spelling had to move |
|---|---|
| 27 |
EPOCH was one shared mutable Date exported from a tier-1 package, so any consumer calling EPOCH.setUTCFullYear(...) corrupted it for every other consumer in the process, permanently and silently. A Date cannot be frozen โ Object.freeze does not close setTime โ so it could not be fixed in place. instant() also returned the caller's own object and now does not, and describeCron refuses a 6-field expression with X_CRON_NOT_DESCRIBABLE where it used to return a wrong sentence |
| 29 | the name existed in @ultimat3/core and @ultimat3/seo with different parameters and different return unions โ the axiom-1 violation the 1.1.0 notes named and deferred. Core's takes an options object, resolveEnvironment({ env }), and throws X_ENVIRONMENT_INVALID on a typo'd ULTIMATE_ENV; tryResolveEnvironment() is the caller that must answer rather than fail |
| 31 | two functions named checkDrift answered two different questions. @ultimat3/db's keeps its name and its meaning โ the live database against the ledger. The CLI's is the entity source hashed against what x db gen recorded, no database. Nothing an app writes calls either |
| 33 | read at module scope, the version resolved before main in every process that imported core, so x build --target binary produced an executable that threw at import. @ultimat3/mcp's DEFAULT_SERVER_INFO becomes defaultServerInfo() and @ultimat3/cli's CLI_VERSION becomes cliVersion() for the same reason โ a constant holding the result is the module-scope read again, one import away |
Entry 29 also renames one environment across seo's surface. isIndexable() and RobotsConfig.environment take core's Environment, so 'staging' is accepted and 'preview' is a compile error; no robots.txt body changes, because neither spelling was ever indexable and only the # environment: comment line moves.
- buildRobots({ environment: 'preview' })
+ buildRobots({ environment: 'staging' })
- import type { SeoEnvironment } from '@ultimat3/seo';
+ import type { Environment } from '@ultimat3/core';30. The NATS wire client is [email protected], and the transport seam did not move
@ultimat3/realtime hand-rolled the protocol โ framing, parser, PING/PONG, TLS upgrade, inbox muxing and reconnect, 1,019 LOC plus a 431-line fake nats-server to test it. All of it is deleted, on docs/idea/18-build-vs-wrap.md's criterion: own what must join the transaction, context and error machinery; wrap a wire protocol with a dominant maintained client, because an agent knows that client's semantics from training and can never know a reimplementation. nats is the first external runtime dependency any @ultimat3/* package has taken, pinned exact, importable from exactly one file.
Transport, NatsTransport, NatsTransportOptions and selectTransport are the same seam and cost no edit. The test seam moved up one level, from an injected byte stream to an injected client:
- new NatsTransport({ url, bucket, open: (target) => Promise.resolve(stream) });
+ new NatsTransport({ url, bucket, connect: fakeNatsConnect(broker) });| Direction | Names |
|---|---|
| removed |
NatsConnection, NatsConnectionOptions, NatsConnectOptions, NatsProtocolParser, NatsOperation, NatsServerInfo, NatsStream, natsStreamOver, bunNatsStream, FakeNatsServer, fakeNatsStream
|
| added |
NatsClient, NatsConnect, NatsClientOptions, NatsRequestOptions, NatsRequestManyOptions, openNatsClient, FakeNatsBroker, fakeNatsConnect
|
unchanged, moved to nats-client.ts
|
NatsHeaders, NatsMessage, NatsMessageHandler, NatsSubscription, NatsTarget, parseNatsUrl
|
The JetStream KV layer stays ours: this client's KV abstraction expresses neither per-message TTL nor a batch multi_last direct get.
Every one had zero callers in the framework and in both tracked apps. In each case the edit is delete the import, and the replacement โ where there is one โ is named beside it.
| # | Gone | Instead |
|---|---|---|
| 7 |
requireRole / requireScope (@ultimat3/auth) |
declare the rule as a Policy โ can('admin:access'). They decided a 403 outside @ultimat3/policy, so a route gated that way reported policy: null in x routes, in framework.manifest.json and in openapi.json, and x policy list reported its permission unenforced. requireActor / currentActor stay โ those assert authentication
|
| 8 |
readOnly(), assertReadOnly(), inspectStatement(), MutationVerdict, ReadOnlyOptions, readonlyViolation() (@ultimat3/db); X_READONLY_VIOLATION is retired |
readOnly(db()).query(f) โ readOnlyQuery(text, { role: await ensureReadOnlyRole() }), which reports which defences engaged. The deleted lexer judged statement keywords and nothing else, so select pg_sleep(60) and select pg_read_file('/etc/passwd') both read as reads |
| 9 |
checkBudgets, assertBudgets, parseBytes, DEFAULT_BUDGET, BUDGET_UNITS, the four Budget* types, budgetExceeded(), RouteBudget, RouteRecord.budget (@ultimat3/seo); X_SEO_BUDGET_EXCEEDED is retired |
nothing to call โ the gate that runs is @ultimat3/cli's, raising @ultimat3/render's X_BUDGET_EXCEEDED. seo is tier 1 and cannot see a build's bytes, so it was never the package that could answer. The retired code's row moves under Reserved codes so an old log line still resolves |
| 10 |
renderLd (@ultimat3/seo) |
ld.* and meta.ld โ renderMeta already emits one <script type="application/ld+json"> per node, and an app calling both emitted its graph twice |
| 11 |
attachTimeZone, timeZoneOf (@ultimat3/time), attachLocale, localeOf (@ultimat3/i18n), negotiateLocale, isValidTimeZone, resolveTimeZone (@ultimat3/http) |
write the zone with createContext({ tz }) or withChildContext({ tz }), read it with currentTimeZone(), and take the other three from the packages that own them. HttpConfig.locale and HttpConfig.tz hold header and cookie names only |
| 12 |
renderHeadTags (@ultimat3/seo) |
renderHead(headFromMeta(meta, seoRenderers())). It escaped </ and nothing else and had no caller, while renderHead โ the path every x dev and every build takes โ escaped nothing at all: two serializers, the unused one weaker and the used one vulnerable. It could not borrow render's escapers, because xml.ts escapes into entities, which is right for XML and exactly wrong inside a raw-text element |
| 24 |
HelloFrame.resume and FRAME_LIMITS.resume
|
drop the key. A cursor rides its own subscribe frame, which is where resume was always decided โ the node replied resume: [] and read the field from nobody, so every reconnect shipped each cursor twice, up to 512 ids per subscription, during the exact restart storm the herd bound exists to flatten |
PROTOCOL_VERSION was deliberately not bumped for entry 24: decode builds a whitelist, so a new node drops an old client's resume and an old node reads a new client's omission as the empty list it always received. Both skews are readable, and bumping would refuse every in-flight client on a rolling deploy to buy nothing โ the version guards incompatibility, not novelty.
Entry 11 also brings a stricter zone rule with it: CET, EST5EDT, +01:00 and '' are refused, and a resolved zone comes back canonically spelled, so one zone is one formatter-cache key. The supported locale set and fallback are defineCatalogs({ locales, default }); the fallback zone is configureTime({ defaultZone }). TimeZoneSources gains cookie, and the default order is user, cookie, query, header โ explicit before inferred.
- cacheKeyFor(name, input, tags)
+ cacheKeyFor(name, input, tags, readAuthority(ctx.actor, 'actor'))readAuthority(actor, scope) is the only thing that produces the value, and 'actor' keeps 1.2.0 behaviour for a per-caller read. The forgotten authority is a cross-tenant read, which is why it is positional and required rather than an option with a default. Entry 14 is the same argument on idempotencyKeyFor(name, input, actor), where the forgotten one is a cross-actor replay.
Entry 28 puts one new line on every job() and every backfill():
export const notifySubscribers = job({
input: t.object({ postId: t.uuid, orgId: t.uuid }),
idempotencyKey: ({ postId }) => `notify:${postId}`,
+ tenant: ({ orgId }) => orgId,
retry: { attempts: 5, backoff: 'exponential' },
async run({ input, ctx }) { /* โฆ */ },
});A definition with no tenant is X_JOB_TENANT_REQUIRED at declaration. tenant: 'none' is the other legal answer and means the opposite thing on each side of the factory: on a job() it declares the body touches no tenant-scoped table, because every scoped read then fails closed with X_TENANCY_ACTOR_ORG_REQUIRED; on a backfill() โ which forwards tenant verbatim โ it is how a sweep declares it spans every tenant, and backfillPass opens the bounded crossTenant scope for it, never the author.
In the same slice, on the read primitive, a bare boolean policy bypass gains a reason:
- sourceFor(target, input, { ctx, enforce: false })
+ sourceFor(target, input, { ctx, unenforced: 'explain returns no rows' })The reason is required, a blank one is refused before the source is built, and one query.policy.unenforced audit line is written at debug.
| # | Was | Now |
|---|---|---|
| 22 |
OutboxRelay.stop() returned void
|
Promise<void>. It cleared the timer and returned underneath the pass in flight, so a role shutdown that awaited it resumed while a publish and its markPublished were still running โ a torn write against a closing pool. Callers ignoring the return value keep compiling and keep the old race |
| 23 |
TierFailure.tier was TierName
|
TierLabel = TierName | 'query-read', because @ultimat3/query's read tier degrades through the same bestEffort wrapper and had nowhere to report as. A switch over it needs a 'query-read' arm |
| 25 |
qidOf(name, input) was <name>:<fnv1a 32-bit>
|
<name>:<first 16 hex of SHA-256>. A qid is a sharing key โ a hit hands back the seated window, carrying the first subscriber's input and rows โ and input is client-chosen, so 32 bits is a collision found offline in seconds and one client served out of another's window. A rolling deploy costs one bounded snapshot per subscription |
| 25 |
queue.drain(send) marked each mutation acked when send resolved |
a drained mutation stays inflight until the server settles it with ack/fail or requeueInflight() returns it. DrainReport.remaining is now what is still sendable; a UI rendering unsynced should read pending(), which is unchanged and still counts both |
| 25 |
SyncNode had one teardown, stop()
|
it also declares stopAccepting(), called by the SIGTERM accept phase โ additive for a createSyncNode caller, breaking for anything implementing the interface structurally. SyncNode.websocket no longer carries publishToSelf
|
| 32 | invariants: [ invariant(name, (c) => โฆ) ] |
invariants: (c) => [ invariant(name, โฆ) ] โ see the diff below |
SyncSocket.subscribeTopic / unsubscribeTopic no longer call Bun's ws.subscribe / ws.unsubscribe either: every channel message is one filtered send per socket through SocketRegistry.deliver, because a native publish cannot be refused per socket, cannot report the frame it dropped and cannot mark a subscriber desynced.
Entry 32 is mechanical โ move the [ to after (c) => , drop each (c) => inside invariant(), drop every !:
- invariants: [
- invariant('post_title_not_blank', (c) => c.title!.trimmed().minLength(1)),
- invariant('post_price_non_negative', (c) => c.price!.minor.atLeast(0)),
- ],
+ invariants: (c) => [
+ invariant('post_title_not_blank', c.title.trimmed().minLength(1)),
+ invariant('post_price_non_negative', c.price.minor.atLeast(0)),
+ ],The defect it fixes is why every generated entity needed a !: InvariantColumns was an index-signature type, so under noUncheckedIndexedAccess every c.title was ColumnExpr | undefined. It is now a mapped type over the declared columns, so c.title is a ColumnExpr and c.titel is TS2551: Property 'titel' does not exist โฆ Did you mean 'title'?. unique() and satisfies() take keyof C & string, so a typo in a column list is caught too. indexes[].where is unchanged โ it was already a callback, and its c is now typed too.
| Surface | From |
|---|---|
X_* error codes |
already stable forever โ a shipped code never changes meaning and is never reused |
| The eight primitive shapes |
entity, policy, action, mutator, query, job, route, task and their declared fields โ 1.0.0 |
The x CLI surface |
commands, flags, exit codes, and --json output shape โ 1.0.0 |
| The import tier table | which package may import which โ 1.0.0 |
app.config.ts field names |
renaming or removing a field is a major โ 1.0.0 |
| Bump | Means | Examples |
|---|---|---|
| major | a covered surface changed incompatibly | a removed config field, a renamed CLI flag, a changed primitive field, a narrowed tier |
| minor | additive, old code still compiles and still passes x verify
|
a new optional field, a new command, a new driver behind an existing interface |
| patch | no surface change | a bug fix, a perf change, a corrected fix: line |
1.0.0 means a stable API under semver. It is not a claim about your infrastructure.
| Rule | Detail |
|---|---|
| Pinned exact versions | no ^, no ~, in the framework or in a generated app. A range is a silent upgrade |
| Lockstep releases | one release bumps all 30 packages โ 29 @ultimat3/* plus the unscoped create-ultimate โ to the same version. One version, one commit, one tag. A mixed set is unsupported |
| Published with provenance | npm via OIDC trusted publishing. Every tarball from 3.0.0 onward carries an attestation โ verified through 5.0.1; 2.0.0's do not, that release went out by hand. Per version: npm view @ultimat3/core@<version> dist.attestations
|
| Breaking changes land with the edit named |
no release has shipped a codemod and x upgrade is not implemented, so every BREAKING โ entry names the manual edit itself. A section of this page walks it |
| Dependency upgrades are framework work | Solid is pinned to 1.9.14, the stable line โ Solid 2 is still prerelease (2.0.0-beta.N, DOM renderer split into @solidjs/web) and every app inherits whatever core this repo pins. Bumping it is a framework release, never an app-level bun update. There is no ArkType or Drizzle pin to carry: @ultimat3/schema ships dependency-free builtin validators (ArkType is an optional provider you adapt yourself) and @ultimat3/entity ships its own postgresDriver()
|
| Bun floor |
>=1.4.0, target 2.0. Below the floor โ X_BUN_VERSION. It was >=1.3.0 until 2026-08-27, while x test emitted bun test --isolate โ a flag Bun added in 1.3.13 โ so the declared floor named runtimes the CLI could not run on |
Not shipped As of 2026-08, behind the interfaces that ship today |
realtime tier 3 (persist: true, local-first), the plugin API, multi-region replication, and the Redis/NATS job drivers โ the last throw X_NOT_IMPLEMENTED with a runnable fix: rather than pretending to work |
Do not upgrade a transitive dependency of a @ultimat3/* package by hand. Open an issue instead โ the pin is deliberate.
As of 2026-08 this command exits X_NOT_IMPLEMENTED (packages/cli/src/cmd-planned.ts). Its own fix: line names what to run instead, and that is the upgrade path today:
bun update --latest && x verify # the shipped path
Everything the manual path skips, you do yourself: bump every @ultimat3/* pin to one exact version, then x manifest and x verify. There are no codemods to run, because no release has shipped one yet.
The design below is what the command will do, kept here because the classes of breakage it automates are real today and the next section is how each one is detected with or without it.
| # | Step | Detail |
|---|---|---|
| 1 | Resolve the target release | all @ultimat3/* at one version; refuses a partial set |
| 2 | Bump package.json pins |
every workspace, exact versions |
| 3 | Run codemods | per-release, idempotent, AST-based. Each prints the files it touched |
| 4 | Regenerate x.manifest.json
|
routes, entities, actions, jobs, policies, tags, budgets |
| 5 | Regenerate openapi.json
|
HTTP surface from action/query declarations |
| 6 | Run x verify
|
the gate. Not green = the upgrade is not done |
--dry-run, once it exists, performs 1, 3 (in-memory), and reports the diff without writing. Output carries every changed file, every codemod name, and every check that would fail.
Nothing here relies on you reading a changelog carefully. Each class is a build error.
| Class | Detected by | Code | Fix |
|---|---|---|---|
| Action/query contract change |
x verify's contract-diff step, against the committed x.manifest.json
|
X_MANIFEST_BREAKING |
x verify --json to read the finding, then bump the major version or restore the input/output shape |
| Breaking published surface | manifest contract diff, breaking subset | X_MANIFEST_BREAKING |
bump the app's major version; old clients keep the old shape |
| Schema vs migrations | schema introspection vs migration history | X_DB_DRIFT |
x db gen "<message>" then x db migrate
|
| Stale generated facts | manifest freshness check | X_MANIFEST_STALE |
x manifest |
| Import-tier change |
scripts/boundaries.ts re-run over the new tier table |
X_BOUNDARY_VIOLATION |
move the import down a tier or invert the dependency |
| Budget ratchet | a release lowering a default budget | X_BUDGET_EXCEEDED |
fix the regression, or set an explicit budget on the route |
| Config field rename/removal | config schema parse, and the compiler before it โ an unknown key is an excess property on Input<AppConfig>, so it fails typecheck rather than reaching a runtime parse |
X_CONFIG_INVALID |
the cause names the field. No codemod has shipped yet, so this is a manual edit |
| Env schema change | typed env parse at boot | X_ENV_MISSING |
add the key; fails in ~40ms, not as a later 500 |
| Renamed job step | duplicate/unknown step names in one run
|
X_STEP_DUPLICATE |
renaming a step invalidates its stored result โ treat as a new step |
Budgets ratchet down across releases. That is intentional: a framework release that makes bundles smaller should not leave your app's slack unclaimed.
A client running build A requesting an asset from build B is the failure mode that actually breaks PWAs โ not caching strategy.
| Mechanism | Behavior |
|---|---|
| Immutable build ID | content hash of the build, stamped into the HTML, every asset path, x.manifest.json and sw.js's cache names โ the worker half was emitted by no build until #390, As of 2026-08. Never a timestamp, never latest
|
| Client sends its build ID |
X-Ultimate-Build on RPC, query, and WS handshake โ so the server answers "you are stale" instead of guessing |
| N-deploy asset retention | the last 3 builds' assets stay served โ retentionPlan(deploys, keep = 3) in packages/pwa/src/version-skew.ts. A count of deploys, with no time component: there is no 7-day half, and no pwa.retention field โ PwaConfig was { enabled, offline, installPrompt, backgroundSync, push } at 7.0.0 (installPrompt is deleted in 8.0.0). Pass keep at the call site to hold more |
AppUpdateAvailable signal |
a Solid signal flips when the server reports a newer build. Your app renders its own "Update available โ reload". No forced navigation, no lost form state |
| Forced reload |
not a capability this framework has, As of 2026-08. 9.0.0 deleted updateSignal, updatePolicy, DEFAULT_GRACE_MS and their types โ they computed a grace, a forced: flag and a deadlineAt, and nothing performed the reload, nor could: @ultimat3/http (tier 2) and @ultimat3/realtime (tier 3) both sit below pwa (tier 4). Notification is complete โ read useConnection().updateAvailable, or compare the worker's posted to with detectSkew, and render your own affordance. x deploy --critical was removed in 4.0.0 for the same reason: echoed into the deploy plan, read by nothing |
| Skew is observable | the /_x live panel reports the build-ID distribution of connected clients. x status --json is planned, not shipped |
| The realtime wire is versioned separately |
PROTOCOL_VERSION is a small integer in @ultimat3/realtime, 2 As of 2026-08-24. It is not the build id and it does not move per release: it moves only when a frame one side writes is a frame the other cannot read. A mismatch is X_PROTOCOL_VERSION on the frame โ clients and sync nodes are redeployed together across a bump, because a cursor rides both the client's subscribe and the node's snapshot, so the skew breaks resume from either end (Realtime) |
Server behavior on a stale build ID:
| Request | Response |
|---|---|
| Asset within retention | serve it |
| Asset outside retention |
410 Gone + X-Ultimate-Build-Current; the SW serves the fallback and flips AppUpdateAvailable
|
| Action / query | executed if the contract is compatible; otherwise X_BUILD_SKEW with a fix: line |
| WS handshake | accepted, then an update-available frame carrying the server's buildId โ signal flips. The socket is not killed |
Full detail: PWA and offline.
There is no jobs.driver field. 5.0.0 deleted it, because it selected nothing: boot always built the Postgres driver, so jobs: { driver: 'redis' } gave you Postgres in silence; since 25.0.0 the key is refused by name (X_CONFIG_INVALID). Which driver runs is setJobDriver(driver) at boot, and only that.
There is no driver migration to perform, As of 2026-10. Postgres is the only durable driver โ 25.0.0 deleted the NATS and Redis stubs, whose every method threw X_NOT_IMPLEMENTED โ and x jobs drain is planned: it exits X_NOT_IMPLEMENTED before the queue boots, pointing at x jobs list --json. Its --to and --dry-run flags went with the drain body in 25.0.0 and are now X_CLI_BAD_FLAG. Before that a drain onto either stub leased the pending batch for five minutes, failed every enqueue and nacked it back; and the memory target, refused by name since 2026-09, acked every durable row off the source and lost the copy when the command exited โ a target that dies with the command is not a migration.
The procedure for the day a second durable driver ships, with the drain itself planned โ no invocation of it ships, so none is written here:
| Order | Step |
|---|---|
| 1 | deploy with the old driver still installed |
| 2 | drain the old queue onto the new driver โ planned (X_NOT_IMPLEMENTED today) |
| 3 | change the setJobDriver(โฆ) call at boot, x verify, deploy |
| 4 | confirm with x jobs list --json that the old queue is empty before removing its infra |
Job code never changes across a driver: steps is a driver member, so step persistence is identical on all of them. The outbox table stays the transactional record. At-least-once delivery is preserved; atomicity is not negotiable (Jobs and workflows).
| From โ to | Change | Notes |
|---|---|---|
| tier 1 โ tier 2 |
live: true on the query |
needs a replicator role and orderBy + limit on the sql
|
| tier 2 โ tier 3 |
persist: true on the entity
|
shipped in 21.0.0, opt-in per entity. No new mutators, no new authz, no new server code (Realtime) |
memory โ nats transport |
realtime.transport: 'nats', and realtime.urlEnv โ the env key name, not a URL (default NATS_URL). There is no realtime.url field |
roll sync and replicator; clients reconnect with server-directed backoff. realtime.transport decides the bus (selectTransport(env, { transport, urlEnv })), As of 2026-09-23 (in the tree, not yet tagged): 'nats' with its variable unset refuses the boot, and so does 'memory' with NATS_URL set (X_CONFIG_INVALID). On 21.x the presence of NATS_URL decided it and the config field was never read (Configuration) |
| Source | Contents |
|---|---|
CHANGELOG.md |
Keep a Changelog format, Added / Changed / Removed. A BREAKING โ entry names its manual edit inline; there is no per-entry Migration block convention and never a codemod name โ grep -c '\*\*Migration' CHANGELOG.md answers 9 As of 2026-08-23, against 111 breaking entries |
docs/idea/14-roadmap.md |
the twelve milestones, 0โ10 shipped. Milestone 11's two-platform deploy proof is the one item still open |
docs/idea/15-risks.md |
what could still change shape โ the sync engine is roughly 70% of total effort |
docs/architecture/19-cutting-a-major.md |
how this page is maintained: one section per major, written when the first breaking change lands. Maintainer-facing โ read it if you are opening a PR against the framework, not if you are upgrading an app |
x.manifest.json |
generated, per build. Diff two manifests to see exactly what a release changed in your app |
Read the changelog backwards from your current pin to the target, and read the BREAKING โ entries only โ the rest is regenerated for you.
git revert <the upgrade commit> # or redeploy the previous image tag
x verify --json > verify.json
| Situation | Do |
|---|---|
| Prod is already rolling | redeploy the previous image tag. Assets from the previous build are inside the retention window, so sessions survive |
| An entry's named edit did not compile | keep the diff. It is the most useful part of the bug report, and it is the entry that is wrong |
x verify fails on one check |
read that step's findings from x verify --json, then reproduce it with the command its fix names |
| Cause is unclear | x errors explain <CODE> --json |
File an issue with verify.json attached, your previous and target versions, and the entry you were following. The JSON is the report โ do not paraphrase the terminal.
Symptom-first fixes: Troubleshooting. Code index: Error codes.