# 29.x → 30.0.0, entry by entry

Released 2026-11-07: `CHANGELOG.md`'s `## 30.0.0` breaking entries in their order, grouped by package, lowest tier first. No legacy path, no codemod, no shim. Rows are never renumbered; each row's `CHANGELOG.md` line names it as `(#N)` after the dash.

### The upgrade, top to bottom

| # | Do | What you see until you do | Entries |
|---|---|---|---|
| 1 | commit, then `bunx @ultimat3/cli@latest upgrade --json`: it pins every `@ultimat3/*`, raises the Bun floor, installs and runs the gate | `X_UPGRADE_DIRTY_TREE` on a dirty tree | — |
| 2 | `x db gen "same-tenant keys"`, commit `packages/db/migrations` and `packages/db/schema/`. A validate failing `X_DB_FOREIGN_KEY_VIOLATION` found rows already pointing across tenants: decide those rows, or state `crossTenant: '<reason>'` on an edge that means it | `X_DB_DRIFT` from `drift`, and `X_SCHEMA_DUMP_DRIFT` for the new framework columns. An entity whose `{ table }` differs from its name moves its keys to the table they always meant in the same migration. `X_DB_DRIFT` from a dump column no snapshot records is an edited migration: restore it | 8, 25, 46, 53 |
| 3 | `x verify --only typecheck` and close every error: a `Date` off a step result, `step.waitForEvent` with no `schema`, a hand-written `TotpReplayGuard`, `DeliveryLedger`, `JobDriver` or `StorageDriver`, a hand-built `ReferenceDescription` or `PoolProfile`, a `Record<StageName, …>`, `'queue-depth'`, `REPLAYED_HEADER` from `@ultimat3/action`, `requestedUrls()`, `suspenseBoundaries`, `csrfBlocked` with two arguments, a read of `DownloadFile.body`, `ERROR_PAGE_LINKS.homepage`, `FormSchema` or its siblings from `@ultimat3/ui`, `JSX_NODE` from `@ultimat3/render`, a hand-written `JobHandle` or `JobsFacade`, an exhaustive `switch` or `Record` over `ErrorSource`, a hand-built `AppConfig`, `retentionPlan` / `RetentionPlan` / `Deploy` from `@ultimat3/pwa`, `retainBuildIds` passed to `generateServiceWorker`, `input.prompt` off a `DefineEvalInput`, `MCP_PROTOCOL_VERSION`, an exhaustive `switch` over `McpAuthRefusal` | a compile error at each site | 8, 16, 21, 22, 25, 30, 32, 35, 39, 40, 43, 44, 45, 47, 50, 57, 58, 61, 62, 65, 66, 67, 68, 69, 70, 73 |
| 4 | `x verify --only manifest`: correct a misspelt name in `mcpConfirmations({ tools })`, or expose the action, or build a hand-written server at module scope in `apps/<app>/mcp.ts`. For a role load, add the import its fix names to `apps/web/api/index.ts`, or move the declaration into a module that imports no `.tsx` and no stylesheet | `X_MCP_CONFIRMATION_TOOL_UNKNOWN`, `X_ROLE_LOAD_INCOMPLETE` | 18, 27 |
| 5 | `x verify --only unit,contract,job`: move each fixture the new refusals name | `X_DB_TRANSACTION_CLOSED`, `X_INVARIANT_VIOLATED`, a memory-driver fixture wiring two tenants refused, `X_N_PLUS_ONE_QUERY` from a memory-backed loop now counted, `X_MAIL_KEY_MISSING` from a mail test, an `enqueue` spy on a spread `memoryJobDriver()` that `enqueueMany` or the relay no longer calls | 3, 4, 8–14, 19, 20, 41, 48, 63 |
| 6 | `grep -rnE "render: '(isr\|static)'" apps/`: a page varying per visitor becomes `ssr`, and every parameter a `load` or page reads goes in `revalidate: { query: ['<name>'] }` | nothing fails: a signed-in `load` sees nobody, and a query the key dropped serves one document for every value | 23, 24 |
| 7 | `grep -rnE "upsertAll\|seed\.upsert" apps/ packages/`: name each column a collision must overwrite. A wide `insertAll` that relied on partial progress splits its own batch | nothing fails: a timestamp stops moving on re-upsert, or a failed batch leaves no rows | 11, 12, 13 |
| 8 | read `mcpConfirmations({ tools })` against every `agent({ tools })` and every MCP server: the agent's `output` reports the pending confirmation id, and a tool that must run unconfirmed leaves `tools` | the write answers `X_MCP_CONFIRMATION_PENDING` and writes nothing | 17, 26 |
| 9 | **before the next column drop:** declare it `retired`, deploy, then drop | `X_MIGRATION_ROLLING_UNSAFE` from `x db gen` | 5 |
| 10 | **before the deploy:** move a CDN rule keyed on `public, immutable` for `/media`; re-upload through the server an XML or `+xml` object over 64 KiB that awaits promotion | a CDN miss; `X_STORAGE_TYPE_REJECTED` at promotion | 6, 7, 15 |
| 11 | `grep -rnE "onShutdown\|retry\(" apps/`: reorder a close hook that depends on running first; wrap a retry or a write that must outlive its caller in `runToCompletion(fn)` | nothing fails: a write or a retry stops with its caller | 1, 2, 3 |
| 12 | `grep -rn "'null'" apps/ packages/`, and read each list built from `URL.parse(…).origin`: list real origins with their scheme | `X_CORS_CONFIG_INVALID` at boot | 28 |
| 13 | read each `HeadTag` with a non-http(s) `href`: serve an inline SVG favicon from `site.favicon`, or a same-origin path under `assets/` | nothing fails in production; `head.attribute_dropped` once in dev | 29 |
| 14 | `x manifest`, commit `openapi*.json` and `x.manifest.json`, once, after every other edit | `X_MANIFEST_STALE` from `manifest` | 38, 42, 49 |
| 15 | `x verify --only manifest` for `X_JOB_OUTLASTS_DRAIN`: raise `drain.workerRetireMs` or lower the job's `timeout` | `X_JOB_OUTLASTS_DRAIN` naming the job, its limit and the budget | 52 |
| 16 | `x verify --only i18n`, then `x i18n sync <locale>` per locale it names, and translate what it seeds. Add the framework's `mail.*` keys `meta.keys` lists to `packages/i18n/catalogs/<locale>.json` by hand. A subject interpolates only the payload's top-level scalars: put each `{slot}` there | `X_CATALOG_MISSING_KEYS`; `X_MAIL_KEY_MISSING` from `send()` | 33, 34, 48 |
| 17 | `x verify --only boundaries`: move each `packages/*` file's runtime import of `apps/*` down. A scaffold from before 30.0.0 has two: `packages/mcp/src/index.ts` (`export const appMcp = () => defineAppMcp({ name, include: 'exposed' })`, no import of `apps/web/api/health`) and `packages/db/src/seed.ts` (`const DEMO_ORG_LABEL = 'org:demo';`) | `X_BOUNDARY_PACKAGE_TO_APP` | 55 |
| 18 | `x g query` / `x g backfill` take `--feature <slice>`; a new table is `x g resource <name>` | `X_FEATURE_REQUIRED`, zero files written | 54 |
| 19 | every upload action's input carries `size: file.size`; every `disk.signedUrl(key, { method: 'PUT', maxBytes })` on s3 adds `contentLength: file.size`; `S3_FORCE_PATH_STYLE` is `0`, `1` or blank in every environment | `X_STORAGE_UPLOAD_SIZE_UNDECLARED`; `X_CONFIG_INVALID` at boot; a 403 `SignatureDoesNotMatch` for a client PUTting another length | 36, 37, 56 |
| 20 | **before the deploy:** the autoscaler adapter gains `max by (queue) (queue_demand)` and an app's own chart its worker HPA metric; set `drain.workerRetireMs` (or `0` for the old stop) and re-deploy with `x deploy --method helm`. A scaffolded chart's `preStop` arrives by editing `docker/helm/templates/_helpers.tpl`: `const end = Date.now() + {{ int $cfg.retireSeconds }} * 1000; for (let n = 1; Date.now() < end; n++)` | the HPA reads a metric nothing publishes; an old worker Terminating up to an hour while it holds a job | 30, 51 |
| 21 | **before the deploy:** a transaction's slow outbound await moves before `withTransaction`; a pod behind a pooler sets `DATABASE_POOLER` and both bounds with `alter role`, as `X_DB_POOLER_OPTIONS_REFUSED` prints; size `DATABASE_POOL_MAX` from the pool gauges | `X_DB_STATEMENT_FAILED` (`25P03`) on an idle transaction; `X_DB_POOL_EXHAUSTED` where a statement used to queue | 31, 32 |
| 22 | `x verify --only budgets`: it now builds when `.x/build-stats.json` is missing or older than your source. A route it finds over budget: shrink it, or raise the budget by the measured figure with the reason. Raise `stepTimeoutMs.budgets` in `x.verify.json` if the build outlasts the 5-minute step deadline | `X_BUDGET_EXCEEDED` on a route stale stats hid | 59 |
| 23 | optional: delete `bin/check.ts` and the `check` script, and run `bun run verify` (`x verify`) locally and in CI. A script that killed `x dev` by pid or `pkill` calls `x stop --json` | nothing fails: `bun run check` still works and does not build twice | 60 |
| 24 | a compose deploy of an app scaffolded before 30.0.0: in `docker/docker-compose.prod.yml`'s `x-image` block, `tmpfs: [/tmp, '/app/.x:uid=1000,gid=1000']` (1000 is the `USER` of `docker/Dockerfile`), then `x env init --production` if the app has no `.env.production` | `X_STORAGE_UNWRITABLE … EACCES` at boot on every role; `X_DEPLOY_ENV_MISSING` before the first step | — |
| 25 | `grep -rn "outboxRelay(" apps/ packages/`: a relay that runs the row loop (a driver without `enqueueMany`, such as `redisJobDriver`) against very slow publishes passes `batchSize: 100` | nothing fails: 500 rows must publish inside the 30 s claim lease, or they are claimed again | 64 |
| 26 | optional, a repository that copies `ci.yml`'s `tested` job ([CI-Parallel-Gate](/ci-parallel-gate#a-push-a-green-pr-run-already-tested)): give each reused job, the verdict job included, `needs: tested` and `if: ${{ !cancelled() && needs.tested.outputs.tested != 'true' }}` and no other `if` | without `!cancelled()`, a red or cancelled `tested` skips every job instead of running them | — |
| 27 | an MCP client: a browser page on another origin calling `POST /mcp` lists that origin in `http.cors.origins`; a script reaching `x mcp serve` through a LAN name or a tunnel uses `localhost`; a test pinning `initialize`'s `2025-06-18` answer offers that revision | `403` `X_MCP_ORIGIN_REFUSED`; `421` `X_MCP_HOST_REFUSED`; `protocolVersion: '2025-11-25'` | 71, 72, 74 |
| 28 | `x verify --only typecheck`: add `createRoot` to each `setSolidRuntime` call, `ui.first` to a `Record<UiKey, string>`, `errors` to a hand-built descriptor, `raw` to a hand-built `WorkloadToken`; drop each read of `QueryPoll.ms`. Add `failureCapture` and `time` to a hand-built `AppConfig`, `detectedCookie` to a full `TimeZoneConfig`, `queryGraph` to a `DevHost`, `mailTemplates` to a hand-built `DevSources`, and the caller to each direct `read` of an MCP resource. Add the 48 keys entry 122 lists to a `Record<UiKey, string>`, a `null` check to each read of `ChartPoint.value` and each `barRects()` rect, `none` to a direct `chartTable()` call, and `async` to a hand-built `refetch`; make an `interface` that extends `OpenGraph`, `BarChartProps` or `TabsProps` a `type … = … & { … }` | TS2345, TS2741, TS2339, TS2554, TS18047, TS2312, TS2322, TS2375 | 75, 77, 82, 83, 86, 111, 114, 115, 116, 117, 119, 121, 122, 123, 125 |
| 29 | `x verify --only contract`: give each guarded action and query an `exercise`, and fix what its generated input finds. A keyset list that drew a disabled Previous takes `firstHref` | `X_CONTRACT_EXERCISE_UNDECLARED`, `X_CONTRACT_DEFECT`, `X_CONTRACT_EXERCISE_UNREACHED`, `X_UI_INVALID_VALUE` | 76, 78, 80, 81 |
| 30 | `grep -rn "refresh()" apps/`: a caller that wanted focus on `<main>`, the title read out, or a navigation cancelled says so itself | nothing fails: focus stays where it was | 79 |
| 31 | `x verify --only e2e`: name what the audit lists — visible text, `aria-label`, `<label for>`, `alt` (`alt=""` for a decorative image), a `<title>`, `<html lang>`, nothing focusable under `aria-hidden` — or give that test `{ a11y: { allow: [{ role, selector, because }] } }`. A raw-tab test (`browser.session.newTab()`) names what it lists too, or calls `allowA11y([{ role, selector, because }])` before the navigation. A hand-written `E2eBrowserPage` implements or forwards `accessibilityTree(request)` | `X_E2E_A11Y_VIOLATION` naming each element; `X_TEST_FIXTURE_UNAVAILABLE` from every `e2eTest` on a page without the method | 88 |
| 32 | `x verify --only unit,contract`: a test that asserted a `factoriesFor` value by its old name rule asserts the declared one, or builds it with `defineFactory`; a test that relied on a memory write surviving a failed `memoryTransactor().run` writes it outside the transaction | an assertion on an old name-derived value (`<column>-<index>`, `new Date(0)`, `'USD'` in a `…Currency` text column) fails; `X_SEED_GRAPH_UNSATISFIED`; a row gone after a rolled-back transaction | 89, 90 |
| 33 | `x exercise --json`: paste each printed `fix:` entry into `apps/web/exercise.ts`'s `defineExercise({ … })` — an actor its policy allows, an `input` binding a real row — or give the primitive `{ off: '<why>' }`; then `x exercise <name> --json` for that one. An app with no `apps/web/exercise.ts` writes `import { defineExercise } from '@ultimat3/testing'; export const exercise = defineExercise({});` first | `X_CONTRACT_EXERCISE_UNREACHED` from the `exercise` step, one per primitive | 91 |
| 34 | for each entity `exercise` names: `orgId: uuid().tenant()` in its columns, `x db gen '<table> tenant'`, and on a table that already holds rows `x g backfill <name> --feature <slice>` to fill it; or `entity('<name>', { shared: '<why it belongs to no tenant>' })` | `X_ENTITY_TENANT_UNDECLARED` from the `exercise` step | 92 |
| 35 | `x verify --only unit,contract,job`: await every write inside a `memoryTransactor().run`; a test that asserted an `onCommit` effect right after a nested `run` asserts it after the root's | `X_DB_TRANSACTION_CLOSED`; an effect not yet run where the old one had run | 93 |
| 36 | `x db gen`, commit `packages/db/schema/`; `x verify --only seo` and add each `description` it names (or `seo: { llms: false }`); then `x exercise --json` and close each plan finding with the `indexes:` line and `x db gen` it prints, or a stated `scan`, `expectedSeqScan` or `expectedQueryLoop` reason | `X_SCHEMA_DUMP_DRIFT`; `X_SEO_LLMS_DESCRIPTION_MISSING`; `X_QUERY_UNINDEXED`, `X_QUERY_SORT_UNSERVED`, `X_QUERY_SCALES_WITH_ROWS` | 112, 113, 118 |
| 37 | `x verify --only unit,contract,e2e`: mock each module in the file that reads it, and move a test pinning a palette order, an empty chart gap cell or `deltaOf(x, 0) === undefined` to the new answer. Then `x build --target static`: put each image a finding names under `apps/web/site/assets/` and pass `asset('assets/<file>')`, or drop `avif` from that route's `formats`. Read each served `responsiveImage()` over `asset()` for its own `widths` or `formats`, and each `defineTheme()` brand for `chart-1` and `chart-5` against `bg-soft`. A site route whose `prerender()` or `load` reads the app's own query passes `entries: report.entries, llmsPages: report.llmsPages` to `siteSeo({ … })` in `apps/web/prerender.ts` | `X_TEST_MODULE_MOCK_UNRESTORABLE`, or a test that reads the real module; `X_UI_INVALID_VALUE`; `X_IMAGE_SOURCE_UNREADABLE`, `X_IMAGE_UNSUPPORTED`; 404 `X_ASSET_VARIANT_UNDECLARED`; `X_UI_CONTRAST_INSUFFICIENT`; `X_CLIENT_TRANSPORT_FAILED` from the export | 120, 124, 126–130 |
| 38 | `x verify --only boundaries`: end each `.island.tsx`'s `mount` with `return render(() => <Island {...props} />, el)` and type it `(): () => void`; a plain-DOM island returns a function that removes what it added | `X_ISLAND_DISPOSER_MISSING` | 131 |
| 39 | `x verify --only typecheck,boundaries,contract-diff`: give an out-of-tree `AuthAdapter` the nine `PasskeyStore` methods; read a function `limit` from `descriptor.rateLimitPerCall`; pass `decide` the signature and give a custom `McpConfirmation` its `quorum`, `approvers`, `approvals`; move a hand-rolled websocket to `deviceSocket({ path, resolveToken, onFrame })`; then `x manifest` and commit the new `meter` field | TS2420, TS2339, TS2554; `X_SOCKET_HAND_ROLLED`; `X_MANIFEST_DRIFT` | 132–136 |
| 40 | give a custom `PushDeviceStore` `noteBadToken` and `clearBadToken` and pass `TokenCache.invalidate` the refused token; with `history: true` on Postgres, `x db gen "history exact images"`, and parse a history reader's `bigint`/`decimal` value as a string | TS2420, TS2554; `X_DB_DRIFT` until the migration exists | 137, 138 |
| 41 | pass the grant's server-side `policy` to every `signPart` and `completeUpload`; give a hand-written `LiveHandle` `retry()` and a custom event bus's `find` the event `id` and `afterId`; replace `enqueueInTx` with `handle.enqueue`; a client calling with cached Basic credentials across sites sends a Bearer token or comes from a listed origin | TS2345, TS2741, TS2305; `X_INVARIANT`; `X_CSRF_BLOCKED` | 139–143 |

### Entry by entry

Tier 0 — `@ultimat3/core` (1–2). Tier 1 — `@ultimat3/db` (3–5), `@ultimat3/storage` (6–7). Tier 2 — `@ultimat3/entity` (8–14), `@ultimat3/http` (15), `@ultimat3/auth` (16). Tier 3 — `@ultimat3/action` (17–20), `@ultimat3/jobs` (21–22). Tier 4 — `@ultimat3/render` (23–24), `@ultimat3/notify` (25). Appended after the tier order: `@ultimat3/mcp` (26–27), `@ultimat3/http` (28), `@ultimat3/render` (29). Appended by the second wave, by tier: `@ultimat3/core` (30), `@ultimat3/db` (31–32), `@ultimat3/i18n` (33–34), `@ultimat3/storage` (35–37), `@ultimat3/http` (38–40), `@ultimat3/entity` (41), `@ultimat3/query` (42–43), `@ultimat3/action` (44), `@ultimat3/jobs` (45–46), `@ultimat3/render` (47), `@ultimat3/mail` (48), `@ultimat3/manifest` (49), `@ultimat3/testing` (50), `@ultimat3/cli` (51–56). Appended by the third wave, by tier: `@ultimat3/http` (57), `@ultimat3/ui` (58), `@ultimat3/cli` (59–60). Appended by the wave 3 gate fix: `@ultimat3/render` (61). Appended by the fourth wave: `@ultimat3/jobs` (62–64), `@ultimat3/core` (65–66), `@ultimat3/pwa` (67–68), `@ultimat3/ai` (69), `@ultimat3/mcp` (70–73), `@ultimat3/cli` (74). Appended by the fifth wave: `@ultimat3/ui` (75–78), `@ultimat3/render` (79), `@ultimat3/action` (80–82). Appended by wave 5b: `@ultimat3/auth` (83), `@ultimat3/cli` (84), `@ultimat3/mcp` (85), `@ultimat3/realtime` (86), `@ultimat3/core` (87). Appended by wave 6a: `@ultimat3/testing` (88–89), `@ultimat3/entity` (90). Appended by wave 6b: `@ultimat3/cli` (91), `@ultimat3/entity` (92–93). Appended by wave 7: `@ultimat3/cli` (94), `@ultimat3/testing` (95), `@ultimat3/auth` (96–97), `@ultimat3/jobs` (98–100), `@ultimat3/scraping` (101), `@ultimat3/db` (102), `@ultimat3/ai` (103). Appended by wave 8: `@ultimat3/cache` (104), `@ultimat3/db` (105–106), `@ultimat3/core` (107), `@ultimat3/db` (108), `@ultimat3/cli` (109–110). Appended by wave 9: `@ultimat3/core` (111), `@ultimat3/db` (112), `@ultimat3/seo` (113), `@ultimat3/http` (114), `@ultimat3/mcp` (115–116), `@ultimat3/admin` (117), `@ultimat3/cli` (118). Appended by wave 10: `@ultimat3/seo` (119), `@ultimat3/auth` (120), `@ultimat3/realtime` (121), `@ultimat3/ui` (122–127), `@ultimat3/testing` (128), `@ultimat3/cli` (129–131). Appended by the issues PR (#990–#999): `@ultimat3/auth` (132), `@ultimat3/action` (133), `@ultimat3/mcp` (134), `@ultimat3/manifest` (135), `@ultimat3/cli` (136). Appended by sweep 1: `@ultimat3/notify` (137), `@ultimat3/db` (138). Appended by sweep 2: `@ultimat3/storage` (139), `@ultimat3/realtime` (140), `@ultimat3/http` (141), `@ultimat3/jobs` (142–143).

| # | Surface | Costs you an edit if |
|---|---|---|
| 1 | `close`-phase `onShutdown` order | a close hook relies on running before one registered after it: hooks now run last-registered first, so register in the order you want released. `accept` and `inflight` are unchanged |
| 2 | `retry()` | a retry must outlive its caller: it now stops when the ambient `ctx.signal` aborts and never waits past `ctx.deadlineAt`. Write `runToCompletion(() => retry(work, policy))`. Outside a request context nothing changes |
| 3 | a write past `requestTimeoutMs` or a job's cancellation | the work must land anyway: it now rolls back. Wrap the whole of it, `runToCompletion(() => withTransaction(fn))`: wrapped inside the transaction, the statement lands and the COMMIT is still refused. It drops the deadline for what runs inside it, so give a raw `fetch` in there its own. A keyed `idempotent` action already runs in that scope, up to `configureIdempotency({ scope, ceilingMs })`, twice `requestTimeoutMs` by default; a handler past it is aborted and its key replays `X_IDEMPOTENCY_RUN_CEILING` |
| 4 | a statement on a finished `DbTx` | one outlives its scope: it is `X_DB_TRANSACTION_CLOSED`, not an autocommit. `await` it inside `withTransaction(fn)`, or use `outsideTransaction(fn)` for work that must outlive the scope |
| 5 | dropping a column | you drop in one release: set `retired: ['<c>']` and `x db gen "retire <c>"`, deploy, then remove it and `x db gen "drop <c>" --allow-destructive`. One instance that stops before it starts: `--rolling-unsafe` |
| 6 | `promoteAttachment` | a pending object's bytes do not match its type: it reads the first 64 KiB and refuses. An XML or `+xml` object over 64 KiB refuses too: upload it through the server. An object overwritten during promotion refuses `X_STORAGE_OBJECT_CHANGED`: promote again once the upload has finished |
| 7 | the content sniff | a file opens with over 4,096 B of whitespace: refused everywhere. Trim it before upload |
| 8 | `references()` between two tenant-scoped entities | always once: `x db gen "same-tenant keys"` adds one unique index per referenced table and swaps the key online. A row already crossing tenants fails the validate: decide it, or state `crossTenant: '<reason>'` on an edge that means it, including two tenant columns meaning different things. A memory test wiring a parent and child across tenants moves its fixture. `onDelete: 'set null'` needs Postgres 15+. A null tenant column skips the check. A hand-built `ReferenceDescription` needs `sameTenant: null` and `targetTable`. An entity declared with a `{ table }` that differs from its name, as a target: `X_DB_DRIFT` until `x db gen` |
| 9 | `update`, `updateWhere`, `transition` | a value `insert` would refuse reached them: it is refused before the statement, not as `X_DB_STATEMENT_FAILED`, and a number written to a text column is an error, not a coercion. Pass the column's type |
| 10 | `orderBy` on a `json`, `bytes` or array column | you order by one: refused at plan time (it paged wrongly or threw past page 1). Order by a scalar |
| 11 | `db.x.upsertAll`, `seed.upsert` | you relied on a collision resetting unnamed columns: name the column, `{ ...row, label: 'default', createdAt: now }`. A timestamp expected to move on re-upsert (`reviewedAt`) is named or declared `onUpdateNow()`. Both `X_INVARIANT_VIOLATED`: rows naming different columns (give each row the same columns), and rows naming only the conflict target or primary key, which answer `would write nothing`: `db.blocks.upsertAll([{ blockerId, blockedId }], { onConflict: ['blockerId', 'blockedId'] })` takes `onMatch: 'nothing'` when "already there" is fine, or a named column to overwrite |
| 12 | `repo.upsertAll` on a hand-held repository | one row holds `label: undefined` and another a value: `X_INVARIANT_VIOLATED`. Give every row a value, or none |
| 13 | a wide `insertAll` / `upsertAll` | you relied on partial progress when one failed: it now leaves nothing. Split the batch yourself and call once per part |
| 14 | a soft delete under a JS-only invariant | the stamped row breaks the rule: refused on Postgres too, and `deleteWhere` past `MAX_ASSERTED_ROWS` matched rows is refused. Relax the invariant for the stamped row, or delete in smaller `where` sets |
| 15 | `/media` on a shared key | a CDN rule keys on `public, immutable`: it misses from now on. Serve a public image from `apps/web/site/assets/` via `asset()`, or from an app route using `objectResponse`. Existing variants under old keys go unread and are re-minted once |
| 16 | a custom `totpReplay` | you wrote one: replace `isUsed`/`remember` with `claim(subject, step, at): Promise<boolean>`, answering `true` the first time, `false` for a replay, and rejecting when its store is down. A framework-booted app needs no edit: drop the option and the boot installs `postgresTotpReplayGuard`, whose table `x_totp_steps` is applied at boot (`ROLE=migrate` on an external database) |
| 17 | a tool in both `mcpConfirmations({ tools })` and `agent({ tools })` | an agent calls it: it now waits for a person. Have the agent's `output` report the pending confirmation id, or remove the tool from `tools` to let the call run unconfirmed. With two deciders on one tool, the first registered answers in-app |
| 18 | a decision action's module that reaches a document | `manifest` goes red with `X_ROLE_LOAD_INCOMPLETE`: add the import its fix names to `apps/web/api/index.ts`, or move the declaration into a module that imports no `.tsx` and no stylesheet |
| 19 | `asProjectableAction(handBuilt)` | you compare by identity: it returns a copy behind the gate, and projecting that copy, or a spread of it, passes the gate once. Compare a hand-built tool by `name` |
| 20 | a failed agent tool's text | a test pins the rendered `(fix: …)` of an error that declares `callerFix`: the model now sees `callerFix`. Move the assertion with it |
| 21 | `step.run`'s answer | you read a value JSON changes: a `Date` is a compile error at the read, so write `new Date(value)`; a miss stored from `undefined` is `null`, so compare `=== null`; a value JSON cannot hold (a `bigint`, a cycle, `NaN`, an infinity, an invalid `Date`, a NUL, a lone surrogate) is `X_STEP_OUTPUT_UNSERIALIZABLE`; a keyed action answering such text is `X_IDEMPOTENCY_OUTPUT_UNSERIALIZABLE`. A step's object keys read in `jsonb`'s order on every attempt. Runs in flight across the upgrade replay as they always did |
| 22 | `step.waitForEvent` | you wait on an event: `schema` is required and `<T>` is gone. Pass a schema |
| 23 | `isr` and `static` routes | a `load` reads the actor, a cookie, `Authorization` or any other inbound header: it now sees none, `ctx.ip` and `ctx.peer` are `null`, `ctx.tz` is the app's default, and `ctx.url` is the document's address, with no undeclared query. A page that varies per visitor is `render: 'ssr'`. With no declared origin, a process stores `isr` pages for 16 request origins: set `APP_URL` if you serve more |
| 24 | an `isr` route with no `revalidate.query` | its `load` or page reads a query parameter: the route now serves one document for every value, and nothing can see that from the key alone. List it in `revalidate: { query: ['<name>'] }` |
| 25 | `x_notify_deliveries`, `DeliveryLedger` | always, for the column: `x db gen`, commit `packages/db/schema/`. An app with its own ledger also takes the table below. A channel whose send can outlast five minutes sets `reclaimAfterMs` above its slowest send, or a reclaimer overlaps it |
| 26 | a tool in `mcpConfirmations({ tools })` on a server you handed no `confirmations` | a caller relied on it running at once: it now waits. To let a call run unconfirmed, remove the tool from `tools` |
| 27 | a name in `mcpConfirmations({ tools })` | no exposed action, exposed query, MCP tool or in-process tool answers to it: `X_MCP_CONFIRMATION_TOOL_UNKNOWN` at `manifest`. Correct the spelling, or expose the action. A hand-written tool counts once its `defineAppMcp` has run when the app loads, so build that server at module scope in `apps/<app>/mcp.ts` |
| 28 | `cors.origins` containing `'null'` | you list it, or build the list from `URL.parse('localhost:3000').origin` (no scheme): `X_CORS_CONFIG_INVALID` at boot, with or without credentials. List real origins with their scheme: `configureHttp({ cors: { origins: ['http://localhost:3000'] } })`. A public, credential-free API uses `origins: ['*'], credentials: false` |
| 29 | a `HeadTag` `href` | a `link` carries `data:image/svg+xml`, any other `data:` type, or a scheme outside `http`, `https`, `mailto`, `tel`, `sms`: it prints no `href` (`android-app:` and `ios-app:` stay on `rel="alternate"`; a raster `data:image/` stays). Serve the file: `site.favicon` for an icon, or a same-origin path under `assets/`. `head.attribute_dropped` names the tag and attribute in dev |
| 30 | the `ScalingSignal` `'queue-depth'` | you name it, or an adapter rule reads `queue_depth` for the worker HPA: it is `'queue-demand'`, `SCALING_METRICS['queue-demand']` is `queue_demand`. Add the adapter rule `max by (queue) (queue_demand)`. `queue_depth` stays, the ready-only gauge for a backlog alert |
| 31 | a plain statement behind a full pool | you relied on it queueing past `acquireTimeoutMs`: it answers `X_DB_POOL_EXHAUSTED`. Raise `DATABASE_POOL_MAX` or fix the slow statement. A statement unanswered past the role's bound plus 5 s is `X_DB_UNAVAILABLE` and the pool is replaced; an autocommit write may have landed, and a job worker re-runs that attempt, so make such a write idempotent |
| 32 | a transaction left idle, `PoolProfile` | a transaction awaits slow outbound work: past 30 s (`web`, `sync`) or 120 s (`worker`, `scheduler`) the server closes it, `X_DB_STATEMENT_FAILED` (`25P03`). Do that work before opening it. A hand-built `PoolProfile` adds `idleInTransactionMs`. Behind a pooler, set `DATABASE_POOLER` and both bounds with `alter role` |
| 33 | keys only a mail or a push names | `x verify`'s `i18n` step reports them: `x i18n sync <locale>`, then translate |
| 34 | an `en` key another locale lacks | always, for a key reached only through a template literal: `x i18n sync <locale>`. A `⟦…⟧` marker over such a key stays missing until replaced |
| 35 | a third-party `StorageDriver` | you wrote one: implement `streamRange(key, [first, last])`, reading no byte before `first`, and `putStream(key, stream, { maxBytes, … })`, storing nothing past `maxBytes` |
| 36 | `grantUpload` | your own action builds the grant: its input carries `size: file.size` (`uploadFile` already sends it), or `X_STORAGE_UPLOAD_SIZE_UNDECLARED`. `grant.maxBytes` is now that size, not the policy's; an s3 URL accepts exactly that many bytes, so another length is 403 `SignatureDoesNotMatch` |
| 37 | `disk.signedUrl(key, { method: 'PUT', maxBytes })` on s3 | you call it: refused. Add `contentLength: file.size`, or drop `maxBytes` for a URL that binds nothing |
| 38 | the problem document's `retry` | a golden test pins a whole body, or a middleware adds `retry: errorRetry(error)`: the member is on every document, 401 and scope 404 included. Add it to the test; delete the middleware. `x manifest` regenerates `openapi.json`'s `Problem` |
| 39 | `PIPELINE_STAGES`, `StageName` | a `Record<StageName, …>`: add `compress`. A handler whose answer must not be coded sets `no-transform` |
| 40 | `csrfBlocked(pathname, reason, ip, origin)` | you call it: pass the request's `ip` and `origin` |
| 41 | `memoryRepo` and the `statements` fixture | a memory-backed test that uses `statements` loops: it now fails `X_N_PLUS_ONE_QUERY` / `X_N_PLUS_ONE_WRITE`. `preload()`, or `expectedQueryLoop('<why>', …)` |
| 42 | a read's published `rows:` | an app declaring `rows:` finds `openapi*.json` stale: `x manifest`, commit. Later, a member removed, retyped or made optional fails `contract-diff` with `X_MANIFEST_BREAKING` until the app's major moves |
| 43 | `DownloadFile.body` | code reads the field rather than handing it over: narrow the function form, `(range?) => BodyInit`, whose `size` is required |
| 44 | `REPLAYED_HEADER` | `import { REPLAYED_HEADER } from '@ultimat3/action'`: import it from `@ultimat3/core` |
| 45 | a custom `JobDriver` | implement `demand(): Promise<readonly QueueDemand[]>`: per queue, the running rows not held by a worker whose live registry row says it drains, plus the due `ready`/`delayed`/`suspended` rows of an unpaused queue; list only queues above 0, sorted by code unit |
| 46 | `x_job_workers.build` | always, for the dump: `x db gen`, commit `packages/db/schema/` |
| 47 | `suspenseBoundaries`, `ModeSpec.needsSuspense` | you pass or read them (TS2353 / TS2339): delete the key or the read. To stream, wrap the slow read: `<Hole fallback={<Skeleton lines={3} />}>{readRows}</Hole>` from `@ultimat3/render` |
| 48 | `send()` | a mail holds a key the locale lacks, a catalog value still `⟦…⟧`, or a `{slot}` the payload's top-level scalars do not carry: `X_MAIL_KEY_MISSING`, a framework `mail.*` mail included. Add the key, translate it, or put the field at the top level |
| 49 | `QueryFact.rows`, `Manifest.mounts` | an app declaring `rows:` or a mount: `x manifest`, commit. Later, a name dropped from a mount's `scopes` or a changed `prefix` fails `contract-diff` with `X_MANIFEST_BREAKING` until the app's major moves |
| 50 | `requestedUrls()` | you call it: `requestLog().map((call) => call.url)` answers the same list. `NetworkSnapshot.seen` is `NetworkSnapshot.log` |
| 51 | the worker retire | always, on a chart: a rollout leaves an old worker Terminating while it holds a job, for up to `retireSeconds`. `drain.workerRetireMs: 0` keeps the old stop. An app's own chart keeps its values until `x deploy` sets the retire |
| 52 | a job's `timeout` | it outlasts the worker's budget: `X_JOB_OUTLASTS_DRAIN` at `manifest`. Raise `drain.workerRetireMs`, or lower the `timeout` |
| 53 | `packages/db/schema/04_tables/` against the newest snapshot | a column one holds and the other does not: an edited migration. Restore the file, then `x db gen` |
| 54 | `x g query`, `x g backfill` | you named no slice: pass `--feature <slice>`, or `x g resource <name>` for a new table |
| 55 | a `packages/*` runtime import of `apps/*` | you have one: move the code down. `import type` passes, and `packages/db/src/` may read an entity module |
| 56 | `S3_FORCE_PATH_STYLE` | it holds anything but `0`, `1` or blank: `X_CONFIG_INVALID` at boot. Write `1` |
| 57 | `ERROR_PAGE_LINKS.homepage` | you read it: it is removed, and the default error page links only the repository. `ERROR_PAGE_LINKS.repository` is unchanged. An app that wants its own links ships `apps/web/site/errors/<status>.html`, as before |
| 58 | `FormSchema`, `FormValidationResult`, `FormValidationIssue`, `formatFieldPath`, `IssuePathSegment` from `@ultimat3/ui` | you import one: take `StandardSchemaV1`, `StandardResult<unknown>`, `StandardIssue`, `formatPath`, `StandardPathSegment` from `@ultimat3/schema`. A hand-written schema handed to `formBinding({ schema })` adds `version: 1` and a `vendor` beside `validate`. `parseFieldPath`, `fieldSelector` and `MAX_FIELD_INDEX` are unchanged |
| 59 | `x verify`'s `budgets` step | your stats were stale: it builds first (`x build --target static --no-preflight --out .x/verify-static`) and weighs fresh bytes, so a route stale stats hid is red. Shrink it or raise its budget by the measured figure; `x verify` can be one static build slower after an edit (8.2 s on `dummy/social-media-clone`) |
| 60 | `bin/check.ts`, `bun run check` | a script or CI step expects a new scaffold to have them: run `bun run verify`. An existing app keeps its own; it still works |
| 61 | `JSX_NODE` from `@ultimat3/render` | you import it: `import { JSX_NODE } from '@ultimat3/core/render-node'`, the same global symbol. `HOLE_NODE` and `ISLAND_NODE` live there too |
| 62 | a hand-written `JobHandle`, `JobsFacade` or `JobDriver` | you wrote one: each gains a required member. `JobHandle.enqueueMany(inputs, options?)` (a fake built from `job()` has it already). `JobsFacade.enqueueMany(handle, inputs, options?)`, or build the facade with `outboxJobsFacade()`. `JobDriver.enqueueMany(requests)`: answer each request as `enqueue` would, in order, in one round trip, opening with `assertEnqueueMany('<driver>', requests)`; a driver with no batch primitive loops its own `enqueue`. It refuses a request carrying `onConflict: 'error'` (`X_INVARIANT`): send that one alone through `driver.enqueue`. `OutboxStore.stageMany` / `markPublishedMany` stay optional |
| 63 | an `enqueue` spy on a spread `memoryJobDriver()` | a test wraps the driver as `{ ...driver, enqueue: spy }` and drives `handle.enqueueMany` or the outbox relay: both go through `driver.enqueueMany`, so the spy is never called. Wrap that member too |
| 64 | `outboxRelay()`'s default `batchSize` | it claims 500 rows a pass, not 100. Pass `batchSize: 100` to keep the old figure; the one reason to is a relay running the row loop (a driver without `enqueueMany`, such as `redisJobDriver`) against very slow publishes, where 500 rows must still publish inside the 30 s claim lease |
| 65 | `ERROR_SOURCES`, `ErrorSource` | an exhaustive `switch` on `ErrorSource` with no `default`, or a `Record<ErrorSource, …>`: add a `browser` arm. It is what `/_client-errors` reports with, once `clientErrors.enabled` is set |
| 66 | `AppConfig` | you build one by hand (a test fake, `{ … } as AppConfig` aside): it gains the required `assets` and `clientErrors` (TS2741). Spread `defineConfig({ name })`'s answer, or add `assets: { retain: undefined }` and `clientErrors: { enabled: false, anonymous: false, perMinute: 30 }` |
| 67 | `retentionPlan`, `RetentionPlan`, `Deploy` from `@ultimat3/pwa` | you import them (TS2305): delete the call, which retained nothing unless your own code acted on its answer. To keep earlier builds' hashed files served across deploys, declare `assets: { retain: { disk: '<a disk every web pod shares>', builds: <n> } }` in `app.config.ts`, with that disk in `defineStorage({ disks })` |
| 68 | `ServiceWorkerConfig.retainBuildIds` passed to `generateServiceWorker` | you pass it (TS2353, excess property): delete the key. An activating worker keeps only its own build's caches; a tab open across a deploy gets earlier builds' chunks from the server, through `assets: { retain: { disk, builds } }` |
| 69 | `DefineEvalInput` | you read `input.prompt` off one (TS2339): it is `PromptEvalInput \| AgentEvalInput`; narrow with `'prompt' in input`. A `defineEval({ prompt, … })` call is unchanged, and a custom `Scorer` typed against `{ output; expected? }` still compiles. An eval of a prompt an `agent()` runs may move to the agent arm, `defineEval({ name, agent, cases: [{ name, input }], tools: { <tool>: (input) => <answer> }, scorers, baseline, tolerance })`, with its baseline re-recorded by `ULTIMATE_EVAL_RECORD=1 x test eval` |
| 70 | `MCP_PROTOCOL_VERSION` from `@ultimat3/mcp` | you import it (TS2305; it was `'2025-06-18'`): `MCP_MODERN_VERSIONS[0]` for the modern revision (`'2026-07-28'`), `MCP_LEGACY_VERSIONS` for the handshake ones. `grep -rn MCP_PROTOCOL_VERSION apps packages` finds every caller |
| 71 | the MCP revision a server answers | a client or test pins `initialize`'s answer: it echoes an offered `MCP_LEGACY_VERSIONS` revision, else `2025-11-25` (was always `2025-06-18`). A request whose `_meta` names `2026-07-28` is served statelessly, with no `initialize` or `ping`; an unserved version in `_meta` or `MCP-Protocol-Version` is `400` `-32022`. Legacy clients (`initialize`) keep working unchanged |
| 72 | `POST /mcp` with an `Origin` | it is not this app and not in `http.cors.origins`: `403` `X_MCP_ORIGIN_REFUSED`, before `resolveToken`. Call from the app's own origin, list a sibling page's origin in `http.cors.origins`, or call from a client that sends no `Origin` |
| 73 | `McpAuthRefusal` | an exhaustive `switch` over it, or a `Record<McpAuthRefusal, …>`: add an `'origin-refused'` arm (`mcp.auth.origin-refused`, `onAudit`'s `auth-refused` with status 403) |
| 74 | `x mcp serve --transport http` | you reach it through a name that is not `localhost`, `*.localhost` or a loopback address: `421` `X_MCP_HOST_REFUSED`. Use `http://localhost:<port>/mcp` or `http://127.0.0.1:<port>/mcp`. `GET`/`DELETE` on it answer `405` `X_METHOD_NOT_ALLOWED`, not `404` |
| 75 | `setSolidRuntime` | every island that registers the runtime: add `createRoot` to the `solid-js` import and to the call, `setSolidRuntime({ createContext, useContext, createSignal, createMemo, createEffect, onCleanup, createRoot })`. A six-name call no longer compiles (TS2345). A whole `solid-js` namespace still registers |
| 76 | `DataTable`'s `rowKey` | two rows answering one key are `X_UI_INVALID_VALUE`: make the key the row's primary key — unique within `rows`, stable across updates, never an index — `rowKey={(row) => row.id}` |
| 77 | `UI_KEYS`, `UiKey` | a `Record<UiKey, string>`: add `ui.first` |
| 78 | a cursor-mode `Pagination` with no `prevCursor` | it no longer draws a disabled Previous. To give a keyset list a way back, pass `firstHref` (link mode, `listUrl().firstHref`) or `onFirst` (callback mode) |
| 79 | `refresh()` from `@ultimat3/render/client` | code that relied on the old behaviour, [the table below](#entry-79-what-refresh-no-longer-does) |
| 80 | `.contract()` on a non-public action or query | it calls your handler 16 times with generated legal input: pass `{ exercise: { actor } }` or `{ exercise: { off: '<reason>' } }`, [the rules below](#entry-80-the-exercise-assertion) |
| 81 | `sampleInput` | you imported action's `sample-input` module by path; it is gone: `import { sampleInput } from '@ultimat3/schema/sample'` |
| 82 | `ActionDescriptor`, `QueryDescriptor`, `Query` | code that assigned `.method` to a `'POST'`-typed variable widens it to `ActionHttpMethod`. A hand-built descriptor adds `errors: null`; a hand-built `Query` adds `contract` |
| 83 | `WorkloadToken` (`@ultimat3/auth`) | you build one by hand, usually a test fake: add `raw`, the decoded payload (`raw: {}` where nothing reads it) |
| 84 | a deployed role's app modules | a module throws on import or registration: the role now exits `X_APP_MODULE_LOAD_FAILED` instead of serving without it. `x manifest --json` names the module and why |
| 85 | MCP tool descriptions | you snapshot `tools/list`: an undescribed query reads `Read with the "<name>" query.`, an admin action quotes its name. Set `mcp: { description }` to keep your own wording. `exposedPrimitives()` returns opted-in primitives only |
| 86 | `QueryPoll` | you read `.ms`: keep the number you passed to `pollEvery` |
| 87 | `.contract()` / `exerciseAction` / `exerciseQuery` | you ran one with `ULTIMATE_ENV` or `NODE_ENV` set to `staging` or `production`: run it under `bun test` |
| 88 | `e2eTest`, `E2eBrowserPage` (`@ultimat3/testing`) | Every `e2eTest` now audits the browser's accessibility tree. Name what it lists — visible text, `aria-label`, `<label for>`, `alt` (`alt=""` for a decorative image), a `<title>`, `<html lang>`, nothing focusable under `aria-hidden` — or add `{ a11y: { allow: [{ role, selector, because }] } }` to that test. A tab a suite drives itself (`browser.session.newTab()`) is audited too: name what it lists, or call `allowA11y([{ role, selector, because }])` in that test before the navigation. A hand-written `E2eBrowserPage` passed to `installE2eDriver({ page })` must implement or forward `accessibilityTree(request)`. `toAxNode` is imported from `@ultimat3/testing`, not `@ultimat3/cli`'s `cdp-shot-a11y.ts` |
| 89 | `factoriesFor` (`@ultimat3/testing`) | a test reads a value it never set: it is now the column's declared default, an enumerated value or its kind's value, never derived from the name. Set the value in the call (`build({ column: … })`) or a `defineFactory`. A registry whose `$columns` are not `entity()` columns throws `X_SEED_GRAPH_UNSATISFIED`: pass real entities |
| 90 | `memoryTransactor().run` (`@ultimat3/entity`) | a memory-store write inside it that names no `tx` must survive the transaction throwing: move it outside `run`. A write that names no `tx` now rolls back with the transaction, as on Postgres |
| 91 | `x verify` (`@ultimat3/cli`) | a registered action, query or job no generated run reaches as an allowed actor: the new `exercise` step is red with `X_CONTRACT_EXERCISE_UNREACHED`. Paste the entry `x exercise --json` prints into `apps/web/exercise.ts`, or give `{ off: '<why>' }`. A script that pinned the step count reads one more (`VERIFY_STEP_NAMES`) |
| 92 | `entity()` (`@ultimat3/entity`) | an entity with no tenant column references, directly or through others, one that has one: `X_ENTITY_TENANT_UNDECLARED` from the `exercise` step. Add `orgId: uuid().tenant()` and `x db gen '<table> tenant'` (with `x g backfill` on a table holding rows), or declare `shared: '<why>'` |
| 93 | `memoryTransactor().run` (`@ultimat3/entity`) | a test writes after the transaction ended — an unawaited promise — and now gets `X_DB_TRANSACTION_CLOSED`; or it relied on a nested `run`'s `onCommit` effect running at that nested commit: it now runs at the root's, and never after the root rolls back |
| 94 | `x exercise`, the `exercise` step (`@ultimat3/cli`) | a primitive hands tenant B a row of tenant A's, or changes one, when B passes A's ids: `X_TENANT_LEAK` / `X_TENANT_WRITE_CROSSED`. Read and write by input id through the repository scoped to `ctx.actor`; `x exercise <name> --json` reruns it |
| 95 | `SeedWriter` (`@ultimat3/testing`) | you wrote one by hand: add `snapshot: async (rows) => rows.map(() => null)` at least — TS2741 otherwise. A seeded count per tenant moved: an untenanted child of a tenant-owned table now has rows under each tenant |
| 96 | the framework schema (`@ultimat3/auth`) | you commit a schema dump: five `x_oauth_*` tables appear. `x db gen`, then commit `packages/db/schema/` |
| 97 | `VerificationPurpose` (`@ultimat3/auth`) | you `switch` over a purpose or type one as the two-value union: it is `string`. Add a default arm; declare a new purpose with `defineVerificationPurpose` |
| 98 | `publishEvent` on the Postgres bus (`@ultimat3/jobs`) | you commit a schema dump: `x_job_steps_waiting_idx` appears; `x db gen`, commit. A wait that relied on the 30 s poll to notice an event published by other means than `publishEvent`: `eventPoll: '30s'` |
| 99 | `StepStatus`, step names (`@ultimat3/jobs`) | you `switch` over `StepStatus`: handle `started` and `cleared`. A step named `unwinding` or `compensate:<x>`: rename it. A hand-written driver keeps compiling: `resolveStep` is optional |
| 100 | enqueuing a tenant-declared job (`@ultimat3/jobs`) | it names another org than its `tenantId` or the acting user's: `X_JOB_TENANT_MISMATCH`. Enqueue from the job's own org, or inside `crossTenant(reason, fn)` by an actor holding `tenancy:cross` when crossing is meant. A client that read the code as a 500 now gets 403. Rows already queued that way go dead: re-enqueue them, never `x jobs retry` |
| 101 | `ScrapePage`, the wedge watchdog (`@ultimat3/scraping`) | you wrote a `ScrapePage` fake: add `activity(): void` (the shipped fakes carry it). An attached browser now closes when the watchdog fires, not when the run unwinds: a run that waits on slow work the page cannot see calls `page.activity()` |
| 102 | the schema dump (`@ultimat3/db`) | the database holds a policy, hand-written or from `rowSecurity`: it is rendered under `10_policies/` and no longer named in `unrendered.sql`. `x db gen`, commit `packages/db/schema/`. A hand-built `CatalogDescription` adds the two row-security fields |
| 103 | a prompt over the model's window (`@ultimat3/ai`) | you matched `X_AI_PROVIDER_UNAVAILABLE` or the provider's text for it: the code is `X_AI_CONTEXT_OVERFLOW`, terminal, and `agent()` compacts and retries first, ending `X_AGENT_CONTEXT_EXHAUSTED`. A run over dense tool results compacts earlier: raise `compact: { at }` (up to 1) if it summarises too soon |
| 104 | the Redis client (`@ultimat3/cache`, `@ultimat3/jobs`) | you relied on `Bun.redis` queueing commands while Redis was down: a cache read is now a miss at once and `enqueue` is `X_DRIVER_UNAVAILABLE`. Catch it where a queued retry was implied. A `client` you hand in keeps its behaviour and gets the deadline |
| 105 | the framework schema (`@ultimat3/db`, `@ultimat3/admin`) | you commit a schema dump: `x_migrations.destructive` and `x_error_groups` appear. `x db gen`, commit `packages/db/schema/` |
| 106 | a rollback (`@ultimat3/db`) | you roll back across a migration a newer build recorded as destructive: `X_MIGRATION_ROLLBACK_DESTRUCTIVE`. Check the older build on the newer schema, then `ROLLBACK_ALLOW_DESTRUCTIVE=<id>[,<id>]` on the migrate role |
| 107 | `ClientErrorsConfig` (`@ultimat3/core`) | you build one by hand: TS2741 on `vitals`. `{ ...CLIENT_ERRORS_DEFAULTS, enabled: true }` |
| 108 | a statement inside a transaction (`@ultimat3/db`) | one runs past the role's `statement_timeout` + 5 s on purpose: it now fails `X_DB_UNAVAILABLE`, where it hung. Raise the bound in that transaction: `setLocal(tx, { statement_timeout: '<ms>' })` |
| 109 | `x exercise`, the `exercise` step (`@ultimat3/cli`) | a job makes an effect outside `step.run` that a retry repeats: `X_JOB_CRASH_DIVERGED`. Wrap the effect the finding names in `step.run('<name>', …)`; there is no pin |
| 110 | the chart (`docker/helm`) | your cluster serves the Prometheus operator and you did not want the ServiceMonitor or the alert rules: set `serviceMonitor.enabled: false` and `alerts.enabled: false`. A dashboard grouping on the scrape's labels sees `app_kubernetes_io_instance` |
| 111 | `AppConfig` (`@ultimat3/core`) | you build one by hand (a test fake, `{ … } as AppConfig` aside): TS2741 on `failureCapture` and `time`. Spread `defineConfig({ name })`'s answer, or add `failureCapture: FAILURE_CAPTURE_DEFAULTS` and `time: TIME_DEFAULTS` |
| 112 | the framework schema (`@ultimat3/db`) | you commit a schema dump: `x_failures` appears and `drift` is `X_SCHEMA_DUMP_DRIFT`. `x db gen`, commit `packages/db/schema/` |
| 113 | `/llms.txt`, the `seo` step (`@ultimat3/seo`) | a public, static `site/` page has a `title` and no `description`: `X_SEO_LLMS_DESCRIPTION_MISSING`. Add `description` to its `defineRoute({ meta })`, or set `seo: { llms: false }` in `app.config.ts`. |
| 114 | `TimeZoneConfig` (`@ultimat3/http`) | you build a full `TimeZoneConfig` or `HttpConfig` literal: TS2741 on `detectedCookie`. Spread `DEFAULT_TZ_CONFIG`; `configureHttp({ tz })` is unaffected |
| 115 | `McpResource.read`, `ResourceRegistry.read` (`@ultimat3/mcp`) | you call either yourself: TS2554. Pass the admitted caller: `resource.read(caller)`, `registry.read(uri, caller)`. A resource whose `read()` takes no argument is unchanged |
| 116 | `DevCapabilities` (`@ultimat3/mcp`) | you wrote a `DevHost`: TS2741 on `queryGraph`. Implement `queryGraph(question)` — `x impact <node> --json`'s data for `{ kind: 'impact' }`, `x graph --from --to --json`'s for `{ kind: 'path' }` — or throw for a host with no graph |
| 117 | `DevSources` (`@ultimat3/admin/dev`) | you build one by hand: TS2741 on `mailTemplates`. Add `mailTemplates: async () => []`, or build it with `defaultDevSources()` |
| 118 | `x exercise`, the `exercise` step (`@ultimat3/cli`) | a statement no index serves, a limited read that sorts, or a loop over rows: `X_QUERY_UNINDEXED`, `X_QUERY_SORT_UNSERVED`, `X_QUERY_SCALES_WITH_ROWS`. `x exercise --json`; paste the printed `indexes:` line into that `entity()` and run its `x db gen`, or declare the reason: `entity({ scan: '<why>' })`, `expectedSeqScan('<why>', fn)`, `expectedQueryLoop('<why>', fn)`; a loop is usually a missing `preload()` |
| 119 | `OpenGraph` (`@ultimat3/seo`) | you extend it, or spread one while setting `image`: TS2312, TS2375. Write `type MyOg = OpenGraph & { … }`, and `{ ...og, image, capture: undefined }` |
| 120 | `postgresAuthAdapter()` (`@ultimat3/auth`) | you build it with no client and rely on it keeping that client past a later `setDbClient`, or outside a `withTransaction` it is now called in: pass the client, `postgresAuthAdapter(client)` |
| 121 | `QueryAccessor.refetch` (`@ultimat3/realtime`) | you hand-build an accessor (a test fake): TS2322 on `refetch`. Write `refetch: async () => {}`. Where `noFloatingPromises` is on, `void x.refetch();` or `await x.refetch();` |
| 122 | `UI_KEYS`, `UiKey` (`@ultimat3/ui`) | a `Record<UiKey, string>`: add the 48 keys entry 122 lists, or build it with `uiCatalog(t)`. Code that read `Object.keys(UI_KEYS)` in declaration order reads it alphabetical |
| 123 | `ChartPoint`, `barRects`, `BarChartProps`, `chartTable` (`@ultimat3/ui`) | you read `point.value` or a `barRects()` rect as always there, extend `BarChartProps`, or call `chartTable()` yourself: TS18047, TS2312, TS2345. Check `value === null` and skip a `null` rect; `type X = BarChartProps & { … }`; pass `none: t(UI_KEYS.chartNone)` |
| 124 | `BarChart`, `Sparkline`, `LineChart`, `AreaChart`, `deltaOf` (`@ultimat3/ui`) | you pass a negative value you expected drawn at zero, a series whose `values` do not match `keys`, or test an empty gap cell or `deltaOf(x, 0) === undefined`: clamp it yourself (`Math.max(0, v)`); give each series one value per key, `null` for a gap (`X_UI_INVALID_VALUE`); assert `ui.chart.none`'s text and the signed difference |
| 125 | `TabsProps` (`@ultimat3/ui`) | you extend it or index `TabsProps['onChange']`: TS2312, and `onChange` is `\| undefined`. Name the mode you mean: `TabsButtonProps`, or `TabsLinkProps` for link tabs |
| 126 | `CommandPalette`, `filterItems` (`@ultimat3/ui`) | a test pins the filtered order, or that an accented label stays out: assert the ranked order, label prefix first |
| 127 | `CONTRAST_PAIRS`, `defineTheme()` (`@ultimat3/ui`) | your brand sets `chart-1` or `chart-5` under 4.5:1 against `bg-soft`, or `fg-strong`, `accent` or a status role under 3:1 on a surface: `X_UI_CONTRAST_INSUFFICIENT` names the pair and its ratio. Move that role until it passes |
| 128 | the test preload (`@ultimat3/testing`) | a test passed by reading a module another file mocked: mock it in the file that needs it. `X_TEST_MODULE_MOCK_UNRESTORABLE` on a file's first test: the file before it mocked a module that does not load. Hand that dependency to the code under test as a parameter, or run `x test unit --isolate` |
| 129 | `x build --target static`, `responsiveImage()` (`@ultimat3/cli`) | a static page names an image the export does not hold, or asks for `avif`: `X_IMAGE_SOURCE_UNREADABLE`, `X_IMAGE_UNSUPPORTED`. Put the file under `apps/web/site/assets/` and pass `asset('assets/<file>')`, or an absolute URL; drop `avif` from that route's `formats`. A CDN rule keyed on `?w=` now sees `/_img/` paths |
| 130 | `/assets/*` (`@ultimat3/cli`) | a served `responsiveImage()` over `asset()` passes its own `widths` or `formats`, or a link adds `?q=`: 404 `X_ASSET_VARIANT_UNDECLARED`. Drop the override, or store the image and serve it from `/media`, which answers any width |
| 131 | `.island.tsx` `mount` (`@ultimat3/cli`) | `x verify --only boundaries` is red with `X_ISLAND_DISPOSER_MISSING`: end `mount` with `return render(() => <Island {...props} />, el)` and type it `(): () => void`; a plain-DOM island returns a function that removes what it added (an `AbortController`'s `abort`) |
| 132 | `AuthAdapter` (`@ultimat3/auth`) | an adapter you wrote is TS2420 until it implements `PasskeyStore`'s nine methods; `passkey-postgres.ts` is the reference. The built-in adapters need nothing |
| 133 | `ActionDescriptor.rateLimit` (`@ultimat3/action`) | a reader that met a function `limit` there reads `rateLimitPerCall`; `rateLimit` is now the fixed limit or `null` |
| 134 | `McpConfirmationStore`, `McpConfirmation` (`@ultimat3/mcp`) | a custom store's `decide` takes the signature as its fifth argument and records it; a hand-built confirmation carries `quorum`, `approvers` and `approvals`. The Postgres store's three new columns are added at boot |
| 135 | `x.manifest.json`, the contract diff (`@ultimat3/manifest`) | `x manifest` adds `actions[].meter`: commit it. Introducing or tightening a meter is now `breaking` in `x verify --only contract-diff` |
| 136 | a websocket upgrade (`@ultimat3/cli`) | `x verify --only boundaries` is red with `X_SOCKET_HAND_ROLLED` on `server.upgrade` or `Bun.serve({ websocket })`: declare `deviceSocket({ path, resolveToken, onFrame })` instead |
| 137 | `PushDeviceStore`, `TokenCache` (`@ultimat3/notify`) | a store you wrote is TS2420 until it implements `noteBadToken` and `clearBadToken`; a caller of `invalidate()` passes the token the provider refused. The built-in stores need nothing; `x_push_devices.bad_tokens` is added at boot |
| 138 | `history: true` images (`@ultimat3/db`) | `x db gen "history exact images"` reinstalls the trigger; a reader that did arithmetic on a `bigint` or `decimal` value in `before`/`after` parses the string; timestamps are UTC `…Z`. Rows already written keep the old spelling |
| 139 | `signPart`, `completeUpload` (`@ultimat3/storage`) | pass the policy the grant was made under — the app's own, never a value from the request; without it both throw `X_INVARIANT`. The browser's `MultipartUploadClient` is unchanged |
| 140 | `LiveHandle` (`@ultimat3/realtime`) | a handle you wrote is TS2741 until it implements `retry(): boolean`: re-send the subscribe of a refused registration and answer whether it did |
| 141 | `CsrfCheckInput`, the `csrf` stage (`@ultimat3/http`) | a caller of `checkCsrf` passes `authorization: request.header('authorization')`; a write a browser sends with only cached Basic, Digest or Negotiate credentials is now judged like a cookie session — send `Authorization: Bearer`, or list the origin in `cors.origins` |
| 142 | `enqueueInTx` (`@ultimat3/jobs`) | TS2305 on the import: call `handle.enqueue(input)` (or `jobsFacade().enqueue`) inside the transaction — it joins it and runs the refusals `enqueueInTx` skipped |
| 143 | `EventLookup.find` (`@ultimat3/jobs`) | a custom event bus returns each event's `id` and seeks strictly past `(afterMs, afterId)` when `afterId` is given. A job live across the deploy was queued with no tenant, so a plain enqueue of its key afterwards is a new job |

### Entry 25: a hand-written `DeliveryLedger`

A compile error until it does:

| Was | Now |
|---|---|
| `claim(claim, at): Promise<boolean>` | `claim(claim, at): Promise<DeliveryClaimAnswer>`: `{ outcome: 'owned', claimId }`, `{ outcome: 'sent' }` or `{ outcome: 'held', retryAfterMs }` |
| `settle(claim, status, at): Promise<void>` | `settle(claim, status, at, claimId): Promise<boolean>`: `false` when `claimId` no longer owns a `sending` row, and nothing changes |

A `sending` row is no longer re-claimable at once: it is held for `reclaimAfterMs`, default `DEFAULT_DELIVERY_RECLAIM_MS` (5 minutes). A retry landing inside it raises `X_NOTIFY_DELIVERY_HELD` and is rescheduled past the window with the attempt uncounted.

### Entry 79: what `refresh()` no longer does

Only an app that relied on the old behaviour has anything to do:

| Relied on | Now | Do |
|---|---|---|
| focus landing on `<main>` after `refresh()` | focus stays on the control the visitor was in; `<main>` only when it is gone | call `document.querySelector('main')?.focus()` after `await refresh()` where that is wanted |
| the title read out after `refresh()` | nothing is announced | announce in the app's own live region |
| `refresh()` cancelling a navigation in flight | it resolves and does nothing; that navigation lands | `await navigate(location.href, { replace: true })` to replace it |
| `refresh()` re-fetching the page under an open modal | nothing is fetched | `closeModal()` first, then `refresh()` |

### Entry 80: the exercise assertion

`.contract()`'s fourth assertion calls the handler 16 times with input `input:` accepts, inside a rolled-back transaction.

| Case | Answer | Do |
|---|---|---|
| no `exercise`, on a non-public action or query | `X_CONTRACT_EXERCISE_UNDECLARED` | `{ exercise: { actor } }`, or `{ exercise: { off: '<reason>' } }`. A public one (its policy admits an anonymous caller) runs as the signed-out caller with nothing declared |
| an input a generated uuid cannot satisfy: a row id, an org the policy compares | every run stops before the handler | a binder: `{ exercise: { actor, input: (drawn) => ({ ...drawn, postId, orgId }) } }` |
| the handler throws uncoded, fails its output schema, or answers a coded 5xx on a legal input | `X_CONTRACT_DEFECT`; an `errors:`-undeclared code counts as a 5xx | fix the handler. The `fix:` is the `bun test <file> -t '<name>: handler survives generated input'` that replays the run |
| no run reaches the handler: every run denied, or a sealed network | `X_CONTRACT_EXERCISE_UNREACHED` | a grant the actor holds, or a binder |
| the memory store (`storeMode` `memory`) | each run's writes roll back, as on Postgres (since entry 90; before it, what a run wrote stayed until the next `seed()`) | nothing |

### Not breaking, but you will see it

| What moved | Do |
|---|---|
| with `DATABASE_REPLICA_URL` set, a request that wrote sets `__Host-x-pos` and `x-ultimate-position` | nothing. A proxy with a cookie allowlist passes `__Host-x-pos`; stripped, the next read may land on a standby that has not replayed the write |
| a job attempt that completed and was acked leaves `ctx.signal` un-aborted (`As of 2026-11-07`); before, its timers' writes and enqueues were refused `X_ABORTED`, "was cancelled" | nothing. A body that read an aborted signal as "the run ended" reads the settle instead (`onSettled`); work that must outlive a FAILED attempt too runs in `runToCompletion(() => work)` from `@ultimat3/core` |
| `onSettled`'s `ctx.signal` is live | a hook that treated an aborted signal as "the run ended" reads `settled.outcome` instead. It is aborted at `onSettledTimeout`, 30 s across every try by default: raise it on a hook that takes longer |
| `publishEvent` inside a transaction commits and rolls back with it | nothing, unless a rolled-back event must still resume a run: publish it in `outsideTransaction(fn)` |
| the `drift` step's `rollingSince` rail is off until set | optional: set `rollingSince` in `x.verify.json` to your newest applied migration id. `x doctor` reminds you |
| any other `HeadTag` attribute drops as `attributePair` does: `on*`, `srcdoc`, a non-http(s) `href` or `src` on `base` or `script` | nothing to migrate: these never belonged in a document |
| `disk.delete` still leaves a variant's bytes, now unreadable once its source is gone | an erasure of one object also calls `deleteVariants({ disk, key })` |
| a websocket upgrade from an unlisted origin is refused whatever `sec-fetch-site` it carries | add a sibling host to `allowedOrigins` |
| stored `isr` entries re-render once, for the new key | nothing |
| an `isr` route's `isr.query.undeclared` log line is gone | remove an alert keyed on it |
| a public text answer of 1 KiB to 256 KiB with a declared length leaves as gzip with a weak `etag`; a signed-in one only when `immutable` | a handler that must not be coded sets `no-transform` |
| `html`, `textResponse` and `jsonResponse` set `content-length` | a handler that rewrites such a body drops the header |
| a mount declaring `rateLimit` lifts the token's per-route cap of 120 a minute, and a mount's 422s and pipeline 403s spend the allowance | check the mount's number is the ceiling you meant |
| `Link: rel="successor-version"` on a mount names the mounted path, or is absent when this token cannot follow it | nothing |
| `rpc` or `queryClient` pointed at a mount's prefix never reached it | call a mount from its own OpenAPI document |
| `DEFAULT_CORS` allows and exposes more headers | an app with its own `allowHeaders` or `exposeHeaders` adds the new names itself |
| `RemoteActionError.meta` and a `problemError`'s `meta` gain `requestId` | a `toEqual` on a whole `meta` gains the key |
| a rendered `fix:` prints `bunx x` where the app's own bin exists | an agent parsing the string for `x ` adjusts |
| a positional `x deploy <word>` is refused | `x deploy --image <ref>`, or `x deploy verify` |
| `x deploy verify` with no `DATABASE_URL` passes with `graded: false` and reads no queue | give the step the database credentials to grade the workers |
| `x new`'s `docker/Dockerfile.dockerignore` drops `**/*.test.ts`, `**/*.test.tsx` and `docs` | optional: append the three lines to your own; `!docs/<path>` re-includes a served file |
| `x jobs show` / `inspectJob(driver, id).input` is `'[redacted]'` for a job name this process has not registered with `job()` | to read such a payload, register the job again (a stub `job({ name, input: t.json(), … })`) in the process that inspects it |
| a field marked `.sensitive()` (last in the chain) is masked in the audit record and `x jobs show`; one reachable from an action `output` or a query `rows` fails boot with `X_SCHEMA_SENSITIVE_OUTPUT` | optional: mark what must never be written down. An app's own `logger.info('x', input)` is not covered: keep `redactKeys` or a `Secret` there |
| every builtin schema refusal carries `rule` (`SCHEMA_RULES`) and `params`, on the wire and on `FormIssue` | key a form's `messageFor` on `issue.rule`, not the English `issue.message`; keep a fallback, since a third-party schema library sends no `rule` |
| app codes declare with `defineError({ code, title, status \| offSocket, cause, fix })` from `@ultimat3/core`, which registers the status in the same call | optional: replace `class X extends UltimateError` with `defineError`; `X.is(error)` replaces `instanceof`, and a base class becomes a code prefix. `x g guard error-declared` adds the guard that keeps it so; `x g guard bare-error --force` refreshes the bare-error guard's fix. An app code with no status still answers 500 |
| the Sentry envelope carries `fingerprint: [errorFingerprint(report)]` | nothing: each existing issue reopens once under its new group. A reporter of your own groups the same way with `errorFingerprint(report)` |
| OTLP export retries 429, 502, 503, 504 and a transport failure, inside the exporter's `timeoutMs` (default 10 s) | lower `timeoutMs` if a shorter hold on the in-flight batch matters more than delivery. A collector that accepted a batch then answered 503 receives it twice; span ids dedupe it |
| `X_DEV_ALREADY_RUNNING`'s fix is `x stop --json` | a script that killed `x dev` by pid calls `x stop --json`, which returns once the next `x dev` can boot |
| the AI gateway skips a provider for 30 s once it fails five passes in a row | to keep re-spending every attempt on every call: `providerGateway({ …, breaker: 'off' })`; your own per provider: `breaker: (provider) => breaker({ … })` |
| after the replica breaker's cooldown, one read probes the replica and the rest take the primary until it answers | nothing |
| `x doctor` lists `raw-fetch` as a shipped guard your app lacks | `bunx x g guard raw-fetch`, then give each server `fetch(` a `signal`, or call `outbound()` from `@ultimat3/core/outbound` |
| `DeadlineTimer` and `platformDeadlineTimer` left `@ultimat3/scraping`'s `clock.ts` (never on its barrel) | import them from `@ultimat3/core/outbound` |
| `breaker()`'s `admit()` returns a ticket (or `undefined`) that `succeeded`, `failed` and `released` take; `defaultBreakerClassify` is not exported | pass the ticket back; write your own `classify` |
| `registerErrorStatus`'s table is core's: the writers are on the internal subpath `@ultimat3/core/internal/error-status`, never a barrel | nothing: call `registerErrorStatus` from `@ultimat3/http`, or declare an app code with `defineError` |
| `x new` writes `.mcp.json` and mounts a refusing `POST /mcp` | optional: copy `.mcp.json` (`{ "mcpServers": { "<app>": { "command": "bunx", "args": ["@ultimat3/cli", "mcp", "serve", "--transport", "stdio"] } } }`) and `apps/web/mcp.ts` (`defineAppMcp({ include: 'exposed', resolveToken: apiKeyResolver(() => appAuth().adapter) })`) from a fresh `bunx create-ultimate`, or set `ai: { mcp: { expose: false } }` to silence `X_MCP_APP_UNMOUNTED`. Spell commands `bunx x …` in `AGENTS.md`, `CLAUDE.md` and `.claude/` |
| `x docs` reads the wiki and every installed `@ultimat3/*` package; its hits move | nothing: the `--json` shape is unchanged |
| an app test file through `@ultimat3/testing/preload` starts on the installed frozen instant: the clock is reset at every file boundary | a file that relied on an EARLIER file's `advanceClock` calls `setFrozenClock()` itself |
| `@ultimat3/core/render-nodes` ships the node-tree walker for tests | optional: a test that copied a walker (a `Symbol.for('ultimate.render.jsx')` check over `type`/`props`) imports `renderNodes`, `nodesOf`, `byTag`, `byComponent`, `withAttr`, `one`, `attachRef` and `fire` from it; `examples/dummy/apps/web/app/settings/page.test.ts` is the worked example |
| `x new` writes `AGENTS.md` as a symlink to `CLAUDE.md`; `x upgrade` links an `AGENTS.md` byte-identical to `CLAUDE.md` and reports a different one as `data.agentsMd: "differs"` | optional: merge what only `AGENTS.md` says into `CLAUDE.md`, then `ln -sf CLAUDE.md AGENTS.md`. The `manifest` step then holds `CLAUDE.md` to the 12 kB `AGENTS.md` budget: raise `agentsMdMaxBytes` in `x.verify.json` if it needs more. A Windows clone without `core.symlinks` checks the link out as a file reading `CLAUDE.md` |
| `x env init --production` writes `.env.production`; `x deploy --method compose` refuses without one | add `.env.production` to `.gitignore` if yours does not list it (`x env init` does this itself) |
| `x dev` and the `statements` fixture report a loop of `await job.enqueue(input)` as `X_N_PLUS_ONE_WRITE`, naming `handle.enqueueMany` | replace the loop with `await job.enqueueMany(inputs)`: one round trip per 1,000 inputs |
| a duplicate job enqueue with a different payload, or one still in flight, never threw `X_IDEMPOTENCY_CONFLICT`, whatever the wiki said: it answers `{ deduped: true, id }` of the live row, and the first payload wins | a `catch` for `X_IDEMPOTENCY_CONFLICT` around `<job>.enqueue()` never runs: read `result.deduped`. A job re-enqueuing itself from its own body arms nothing, because its own `running` row holds the key: re-arm from outside the run, for example a `task` on a cron |
| `webhookReceiver()` from `@ultimat3/jobs` is the one supported door for an inbound webhook; a pinned action (`http: { path }`) or a hand-mounted `runtime.ts` route keeps working | optional: replace it with `webhookReceiver({ name, path, tenant, verify, handle })`. In `verify(inbound)` run the signature check (`verifyBodyHmac(...)`, answering `{ text }`, or `verifyWebhookSignature(inbound.request(), { secret })`) and return `{ eventId, payload }` read **from the verified data**, never an unsigned header; move the work into `handle`. List it in `defineApi({ jobs })`, delete the `runtime.ts` line, and point the vendor at the new path, which must not sit under `/api` or `/_x` (`X_WEBHOOK_RECEIVER_INVALID`). A `verify` that parses the body catches the parser's error and throws its own code: Bun's `JSON.parse` message quotes the token it stopped at. Past the queue's live window `handle` runs at least once per `eventId`; the stored payload stays in `x_jobs` until `jobs.doneRetentionMs` / `jobs.deadRetentionMs` |
| `importRows()` from `@ultimat3/jobs` reads a CSV or ndjson object page by page into the app's `handle` | optional: replace a hand-rolled import with `importRows({ name, input, tenant, source: () => disk('…'), key, format, columns, schema, handle, errors: { sink, prefix } })`, writing with `upsertAll` in `handle`, since a page can be handled more than once. A cell `exportRows()` prefixed with `'` comes back with the `'` |
| browser reporting, `clientErrors` in `app.config.ts`, is off by default | optional: `clientErrors: { enabled: true }` mounts `/x-client-errors.js` and `POST /_client-errors` and reports each `app/` document's uncaught failures as `X_CLIENT_ERROR`. Signed-in members only, 30 reports a minute each; `anonymous: true` opens it to visitors with no session, limited per address |
| `assets: { retain: { disk, builds } }` keeps earlier builds' hashed files reachable across a deploy; off unless declared | optional: name a disk every web pod can read (an S3-compatible bucket, or a volume shared on one box) and size the window, `assets: { retain: { disk: 'deploys', builds: 3 } }`. Each app and environment gets its own prefix (`x-build-assets/<config.name>/<environment>/`). A `disk` the app did not declare refuses the web boot with `X_CONFIG_INVALID`; a file over the disk's `maxPutBytes` is skipped and named in one `assets.retain_skipped` line |
| `/admin/audit` refuses a parameter it does not read with a 400 (`X_ADMIN_FILTER_INVALID`), not the unfiltered trail | drop the parameter. An app that overrode `admin.audit.empty` for the row's history card moves that text to `admin.audit.empty-row`; `admin.audit.empty` is the trail's "nothing matches". An app's own `AuditLog` honours `outcome`, `since` and `until` in `entries()` with `auditWindow(subject, query)` and `auditEntryMatches(entry, query)`, or the screen drops the entries that do not match and pages draw short |
| `x ci` gains `tested`; `--branch`, `--run`, `--tail`, `--full` and `--event` belong to `runs` | nothing: `x ci` with no subcommand is `x ci runs`, as before |
| `X_DB_LOCK_TIMEOUT`'s `fix:` reads `x db diagnostics --json \| jq '.data.blockers.rows[] \| {pid, blockedBy, blockerState}'` | a script that matched the old `psql "$DATABASE_URL" -c "select pid, state, query from pg_stat_activity …"` text matches on the code instead. `--limit` on `x db` is accepted by `diagnostics` as well as `backfill` |
| `x new` writes `storeMode(env) === 'memory' ? testDriver() : postgresDriver()` in `packages/db/src/client.ts` | optional, one line: `import { type Driver, database, postgresDriver, testDriver } from '@ultimat3/entity'`, then change the memory arm of `selectDriver` to `testDriver()`. Without it nothing changes; with it a test file that runs the [Testing](/testing#database-state-and-what-a-run-leaves-behind) recipe reads and writes its migrated database through `db`, and a file that installs no client behaves as before. A web workspace that runs the recipe lists `@ultimat3/db` (and `@ultimat3/cli`) among its dependencies |
| hand-rolled polling in an island (`setInterval(…)` beside a `visibilitychange` listener) | optional: `visibleInterval(run, ms)` from `@ultimat3/core/page`, or `useQuery(ref, input, { poll: pollEvery(ms) })` for a read; `x g guard raw-interval` adds the guard that refuses the bare call. A channel event that re-renders the page is `scheduleRefresh()` from `@ultimat3/render/client`, not `refresh()` |
| `errors:` on `action()`, `mutator()` and `query()` is optional | an operation that does not declare it keeps its behaviour and its OpenAPI bytes. To close a contract, list each 4xx code the handler throws, importing the `defineError` class and listing its `.code` (the import registers the status before `action()` checks the list): `errors: [PostNotFound.code, 'X_FORBIDDEN']`. Then `x verify --only contract`: a 4xx the list does not name is `X_ERROR_UNDECLARED`, whose `fix:` names the code to add |
| `http.path` takes `:param` templates, and `http.method` is new | nothing for an app that pins no parameterised path: every static route, its OpenAPI bytes and its manifest fact are unchanged. To serve a resource-shaped URL pin it on the primitive and write no route file: `http: { path: '/orders/:id/refund', method: 'POST' }`, each `:param` a required `t.string` key of `input:`. Call it with `action.client()` or `read.client()`; `derivePath('refundOrder')` throws, so use `actionHttpPath('refundOrder', { id })` |
| a body or search string naming a `:param`'s key | it carries the URL's value or leaves the key out; a different value is 400 `X_HTTP_PATH_INPUT_CONFLICT` |
| every action pinned before 30.0.0 gains `http: { path, method: 'POST' }` in `x.manifest.json` | `additive` in `contract-diff`, not breaking: regenerate with `x manifest` |
| an app with no `persist: true` entity: a `useQuery` / `useRecord` island is no longer held | nothing: it paints earlier |
