24.x → 25.0.0, entry by entry
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.
The upgrade, top to bottom
| # | 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 | — |
Entry by entry
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).
Before → after
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'Not breaking, but you will see it
| 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 |
Where the sites are
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.