@ultimat3/cli 30.2.0 → 30.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +6 -6
- package/package.json +28 -28
- package/src/app-mcp.ts +82 -47
- package/src/cmd-upgrade-spec.ts +1 -1
- package/src/exercise-database.ts +15 -1
- package/src/exercise-findings.ts +3 -1
- package/src/island-bundle.ts +3 -1
- package/src/island-realtime.ts +66 -3
- package/src/role-start.ts +23 -7
- package/src/runtime-jobs-unserved.ts +46 -0
- package/src/runtime-jobs.ts +0 -17
- package/src/templates/entity-module.ts +7 -2
- package/src/templates/scaffold-db-client.ts +22 -2
- package/src/templates/scaffold-mcp-admin.ts +99 -0
- package/src/templates/scaffold-mcp-package.ts +5 -2
- package/src/upgrade-pins.ts +6 -5
- package/src/upgrade-plan.ts +44 -17
- package/src/upgrade-report.ts +4 -1
- package/src/verify-checks.ts +3 -0
- package/src/verify-mcp-mount.ts +30 -0
- package/wiki/Admin-Dashboard.md +31 -0
- package/wiki/CLI-Reference.md +1 -1
- package/wiki/Configuration.md +1 -1
- package/wiki/Deployment.md +1 -1
- package/wiki/Entities-And-Migrations.md +16 -0
- package/wiki/Jobs-And-Workflows.md +2 -0
- package/wiki/MCP-And-AI.md +5 -2
- package/wiki/Testing.md +19 -6
- package/wiki/Upgrading.md +3 -2
package/wiki/Testing.md
CHANGED
|
@@ -594,6 +594,7 @@ unitTest('a row deleted before the run is skipped, not failed', async ({ runJobs
|
|
|
594
594
|
| On the trace or the call | Is |
|
|
595
595
|
|---|---|
|
|
596
596
|
| `trace.executions[n].result` | what the body returned. Absent on a run that failed, suspended or was interrupted |
|
|
597
|
+
| `trace.executions[n].code` | the stable `X_*` code a failed run's error carried, beside its `error` message. Absent on an uncoded throw and on a run that did not fail |
|
|
597
598
|
| `trace.steps[name].executions` | times the step body ran — a replay from storage does not count. Cumulative for the test |
|
|
598
599
|
| `runJobs(job, input, { actor })` · `drain({ actor })` | the identity the **worker** runs as (`WorkerOptions.context()` in a real one). Its org is still the job's declared `tenant`; `tenant: 'none'` strips it. Absent is the anonymous actor |
|
|
599
600
|
| `runJobs(job, input, { tenantId })` · `enqueue(…, { tenantId })` | the **enqueuer's** tenant on the row: the limiter's bucket and the dedupe namespace, as `handle.as(actor, input)` files it. One key in two tenants is two jobs |
|
|
@@ -673,6 +674,12 @@ describeIslandState(counterStates, 'idle', island, (mounted) => {
|
|
|
673
674
|
The props are the manifest's, so the picture `x shot --island` takes and the test are of one
|
|
674
675
|
component. `mountIsland` stays the call for props no state declares. `As of 2026-10`.
|
|
675
676
|
|
|
677
|
+
A realtime island's page outlives its mount: the socket, the record store and the outbox sit on
|
|
678
|
+
`globalThis`, shared by every bundle. Between files (or cases that need a fresh page) call
|
|
679
|
+
`resetPageRealtime()` from `@ultimat3/realtime` in `afterAll` — it closes the socket the CHUNK's copy
|
|
680
|
+
of realtime built and drops every page global. Never `Reflect.deleteProperty(globalThis,
|
|
681
|
+
Symbol.for('ultimate.realtime'))`: that leaves the socket open. `As of 2026-11`.
|
|
682
|
+
|
|
676
683
|
## Testing an authenticator
|
|
677
684
|
|
|
678
685
|
```ts
|
|
@@ -846,7 +853,7 @@ A column whose value comes from another factory, built with the **same strategy*
|
|
|
846
853
|
|
|
847
854
|
`ids.uuid()` and `ids.number()` come from the seeded generators; `index` starts at 1 for the first row.
|
|
848
855
|
|
|
849
|
-
`factoriesFor(registry, seed?)` derives a factory per entity, each value read off the column's **declaration**, never its name (`As of 2026-11`, 30.0.0 entry 89): the declared default first, else an enumerated value, else what the kind offers (uuid, text, integer, boolean, timestamp, money as `{ minor, currency }`, …), and `null` only where the column is nullable. Every candidate goes through the column's own `$parse`, so a built row is one the table stores. A registry entry whose `$columns` are not `entity()` columns, or a column that refuses every generated value (a `json(
|
|
856
|
+
`factoriesFor(registry, seed?)` derives a factory per entity, each value read off the column's **declaration**, never its name (`As of 2026-11`, 30.0.0 entry 89): the declared default first, else an enumerated value, else what the kind offers (uuid, text, integer, boolean, timestamp, money as `{ minor, currency }`, …), and `null` only where the column is nullable. A text column held to a `matches(/…/)` invariant is also offered strings built from that pattern, and those come first; a `json(schema)` column is offered values its schema accepts (`As of 2026-11-07`). Every candidate goes through the column's own `$parse`, so a built row is one the table stores. A registry entry whose `$columns` are not `entity()` columns, or a column that refuses every generated value (a `json()` whose schema carries a `pattern` its sampler cannot meet), throws `X_SEED_GRAPH_UNSATISFIED` at `build()`, naming the `defineFactory` to write. Source: `packages/testing/src/factory-registry.ts`, `seed-graph-values.ts`.
|
|
850
857
|
|
|
851
858
|
### Persisting
|
|
852
859
|
|
|
@@ -889,8 +896,9 @@ await insertSeedGraph(graph, writer);
|
|
|
889
896
|
|---|---|
|
|
890
897
|
| order | parents before children, over the entities' own `$references()` |
|
|
891
898
|
| tenancy | each child points at a parent of its **own** tenant; the entity a tenant column references holds one row per tenant, keyed by the tenant. A table with no tenant column whose references reach a tenant-owned one, and no `shared` reason, is seeded per tenant too, under its own tenant's parent — `row.tenant` is that parent's tenant |
|
|
892
|
-
| a cycle | broken at a nullable key: inserted `null`, set by an entry of `graph.updates` once its target exists. A cycle with no nullable key is `X_SEED_GRAPH_UNSATISFIED` |
|
|
899
|
+
| a cycle | broken at a nullable key: inserted `null`, set by an entry of `graph.updates` once its target exists. The break prefers a table that takes updates — never an `appendOnly` one, whose key stays `null` when it is the only break. In `x exercise`, a deferred update the database refuses (a trigger no declaration carries) leaves the key `null`; an environment failure still fails. A cycle with no nullable key is `X_SEED_GRAPH_UNSATISFIED` |
|
|
893
900
|
| values | read off the declaration, as `factoriesFor` reads them, and kept only if `$parse` accepts them |
|
|
901
|
+
| patterns and schemas | a `matches(/…/)` pattern adds strings drawn from the pattern itself: literals, classes, `.`, groups, `\|`, quantifiers, anchors. Lookaround, backreferences, `\b` and `\p{…}` add nothing, so the column's other candidates stand. A `json(schema)` column draws from its schema, as the contract step's `arbitrary` does. Both are seeded by the row, so the same seed gives the same bytes |
|
|
894
902
|
| rules | every invariant, the primary key, unique columns and unique indexes are kept; a partial unique is treated as total. A unique column that runs out of values ends its table early — but a table-wide unique that runs out before every tenant holds a row is `X_SEED_GRAPH_UNSATISFIED` (`… runs out of values before tenant 2 of 2 holds a row`): that tenant's children would have no parent |
|
|
895
903
|
| an unseeded target | a key — the tenant column included — whose target entity is not in the set is `X_SEED_GRAPH_UNSATISFIED` (`… which is not in the set seeded — pass it too`) |
|
|
896
904
|
| soft delete | a soft-deleting table's `deletedAt` is `null` unless a factory builds it: seeded rows are live |
|
|
@@ -957,7 +965,8 @@ A legal input that crashes the handler is red in `contract`, the day the action
|
|
|
957
965
|
| Runs | 16 (`EXERCISE_RUNS` from `@ultimat3/core`), `runs` from 1 to 64. Runs 0-3 sit on the IR's bounds (`minLength`, `maxLength`, `minimum`, `maximum`, `minItems`). The rest draw multibyte text, every enum and union member, optional keys on and off, `null` where nullable. A format value (`t.uuid`, `t.email`, …) is never varied |
|
|
958
966
|
| Seed | `fnv1a(<action name>)`, never time, so a red run replays: `bun test <file> -t '<name>: handler survives generated input'` |
|
|
959
967
|
| Rolled back | each run inside a memory transaction, always, and in it `withTransaction` when SQL can hold the writes: a SAVEPOINT in a test's open transaction, a root transaction once `setDbClient` installed a client. Both always roll back. The handler's SQL writes, its memory-store writes (`storeMode` `memory`, `tx` passed or not, since `memoryTransactor().run` is ambient), the outbox stage and the post-commit bust drop with it (`packages/action/src/tx-scope.ts` `inRolledBackScope`) |
|
|
960
|
-
| **Not rolled back** | a write on a second client or `outsideTransaction`. A job enqueued with `outbox: false`. Storage puts. Mail and outbound fetch: the test preload seals both, so a real one is only sent where a test opted out.
|
|
968
|
+
| **Not rolled back** | a write on a second client or `outsideTransaction`. A job enqueued with `outbox: false`. Storage puts. Mail and outbound fetch: the test preload seals both, so a real one is only sent where a test opted out. An audit record a non-database sink accepted |
|
|
969
|
+
| Rate-limit counters | spent in a scratch store of the exercise's own (`withScratchRateLimitStore`, `@ultimat3/http`), answering for the installed store's scope: the same file's next call has its whole declared limit |
|
|
961
970
|
| Where it runs | `development` and `test` only. `staging` or `production` (`ULTIMATE_ENV`, else `NODE_ENV`) is `X_CONTRACT_EXERCISE_ENVIRONMENT` before the first run: generated input never reaches a deploy. `bun test` sets `NODE_ENV=test` |
|
|
962
971
|
| Pass | the output parsed, or a coded refusal under 500 |
|
|
963
972
|
| Defect, `X_CONTRACT_DEFECT` | an uncoded throw, `X_OUTPUT_INVALID`, or a coded 5xx (an undeclared `errors:` code is one). `meta.run`, `meta.input`, `meta.failure` |
|
|
@@ -981,14 +990,18 @@ shape, same unreached code. `As of 2026-11`.
|
|
|
981
990
|
| Rule | |
|
|
982
991
|
|---|---|
|
|
983
992
|
| Database | a scratch one with the framework tables and every migration applied; never the developer's. The pass commits, so the tenant proof can read rows after the call |
|
|
984
|
-
| Rows | one seed graph for the pass, a fresh pair of tenants per primitive in it — the first the actor's (A), the second the tenant proof's intruder (B); every table seeded off its declaration, parents first, same-tenant keys (`x db seed --generate`'s graph) |
|
|
993
|
+
| Rows | one seed graph for the pass, a fresh pair of tenants per primitive in it — the first the actor's (A), the second the tenant proof's intruder (B); every table seeded off its declaration, parents first, same-tenant keys (`x db seed --generate`'s graph). A view and a `readOnly: '<why>'` table get no row; a NOT NULL key into a `readOnly` one is `X_SEED_GRAPH_UNSATISFIED` naming it ([Read-only tables](Entities-And-Migrations#read-only-tables)) |
|
|
985
994
|
| Actor | the first of `x policy explain`'s actors the policy allows, in the primitive's tenant. None allowed is `refused`, by design |
|
|
986
995
|
| Input | the override, then an id field whose stem names a seeded entity (`postId` → `posts`), then the schema's minimal draw. A wrong guess shows as `unreached` |
|
|
996
|
+
| A job's org | the field(s) its `tenant(input)` reads — any name, nested, left out of the draw, or under a parent the draw left out (a scoped backfill's `payload.input.orgId`) — are set to the tenant it is enqueued in, found by reading them: A's for the control, B's for the tenant proof (B's own job, carrying A's ids). A `tenant` no input field moves (one fixed org) is `unreached`, `X_JOB_TENANT_MISMATCH`, its `fix:` the declaration: `tenant: (input) => input.orgId` |
|
|
997
|
+
| A job's failure | judged by the action rule: a coded refusal under 500 (an app's 409) is `unreached` under its own code, never a defect; an uncoded throw or a coded 5xx is `X_CONTRACT_DEFECT` |
|
|
987
998
|
| Async input | an override's `input` may be `async`: it is awaited inside the call's request, as the call's actor, so it can mint what the action checks (a one-time token, a passkey assertion). `x drill`'s load skips an `async` input and says why |
|
|
988
|
-
| Room | network sealed, mail captured, jobs in memory, clock frozen. A call out is refused and counted, so the primitive is `unreached` |
|
|
999
|
+
| Room | network sealed, mail captured, jobs in memory, clock frozen — from before the app loads and the scratch database boots, as `bun test`'s preload does, so the embedded Postgres never sees its clock jump. A call out is refused and counted, so the primitive is `unreached` |
|
|
1000
|
+
| Cost | rows are primitives × tables; statements are not: the graph is written one `insertAll` per table, each tenant-proof snapshot is one statement, and a primitive reads only its own pair's rows of the graph |
|
|
989
1001
|
| Overrides | `apps/web/exercise.ts`: `export const exercise = defineExercise({ publishPost: { actor, input }, sendInvoice: { off: '<why>' } })`. Each value is `.contract({ exercise })`'s option. An unknown name is `X_DECLARATION_UNKNOWN`; a blank `off` is `X_CONTRACT_EXERCISE_UNDECLARED` |
|
|
1002
|
+
| A handler that calls a vendor | `defineExercise({ … }, { setup: () => { mockFetch('https://api.vendor.test/*', handler); } })` — or `setup: () => cassette(import.meta.dir, 'exercise')`. Runs once in the exercise process, after the seal and before the first row; what it returns is disposed after the pass. A call a setup route answers is reached, not refused. `allowHost` in it is `X_EXERCISE_SETUP_UNSEALED` |
|
|
990
1003
|
| Rows a generated value cannot satisfy | a cross-column `c.satisfies(…)` reads `X_SEED_GRAPH_UNSATISFIED`: `defineExercise({ … }, { factories: { bosses: defineFactory(bosses, { defaults: (i) => ({ … }) }) } })`, keyed by entity name, the graph's keys kept. A column that follows from a key or reference the graph picked: `derive: (row) => ({ scopeKey: … })` beside `build`/`reset`, reading `row.ownerOrgId`. A partial unique whose columns the factory builds is the database's to judge. An unknown name is `X_DECLARATION_UNKNOWN` |
|
|
991
|
-
| Report | `.x/exercise.json` and `--json`: `{ seed, counts, records[] }`, each record `{ kind, name, outcome, code?, reason?, statements, effects, ms, override?, tenant?, crash? }
|
|
1004
|
+
| Report | `.x/exercise.json` and `--json`: `{ seed, counts, records[] }`, each record `{ kind, name, outcome, code?, reason?, statements, effects, ms, override?, fix?, tenant?, crash? }` — `fix` where no override reaches it. The fields before `tenant` are A's call. The human table's `tenant` and `crash` columns are the verdicts, `-` where none ran |
|
|
992
1005
|
| Gate | the `exercise` step, after `job`, runs `x exercise --json` as a child and is red on its findings: `X_CONTRACT_EXERCISE_UNREACHED`, `X_TENANT_LEAK`, `X_TENANT_WRITE_CROSSED`, `X_JOB_CRASH_DIVERGED`, and — on a whole pass, not `x exercise <name>` — `X_ENTITY_TENANT_UNDECLARED`. `unreached` is never sampled or skipped: it is a finding whose `fix:` is the entry to paste |
|
|
993
1006
|
|
|
994
1007
|
```bash
|
package/wiki/Upgrading.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Upgrading
|
|
2
2
|
|
|
3
|
-
**`As of 2026-08`. Semver applies from here.** A breaking change to a documented API needs a major. Every `@ultimat3/*` package moves in lockstep — never mix versions.
|
|
3
|
+
**`As of 2026-08`. Semver applies from here.** A breaking change to a documented API needs a major. Every `@ultimat3/*` package moves in lockstep — never mix versions. `x new` declares them at `^<version>`, so `bun update` takes a same-major fix before the first hop; `x upgrade` rewrites every pin it moves exact (`20.1.0`, no caret), on purpose — a caret is how `^19.0.0` beside `^19.4.0` came to be mixed, and a range is a silent upgrade. `bun.lock` pins the exact build either way.
|
|
4
4
|
|
|
5
5
|
> **From v30, one major per 90 days.** The window, what semver covers, the support window and the security exception: [Stability](Stability). A removal now ships its `@deprecated` alias for at least one minor before a major takes it, so `x upgrade` across one major names no edit a minor did not already warn about. The 29 sections below stay as they are.
|
|
6
6
|
|
|
@@ -299,6 +299,7 @@ Only an app that relied on the old behaviour has anything to do:
|
|
|
299
299
|
| What moved | Do |
|
|
300
300
|
|---|---|
|
|
301
301
|
| 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 |
|
|
302
|
+
| 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` |
|
|
302
303
|
| `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 |
|
|
303
304
|
| `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)` |
|
|
304
305
|
| 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 |
|
|
@@ -3566,7 +3567,7 @@ bunx @ultimat3/cli@latest upgrade --json # an app below 30.0.0, whose own CLI
|
|
|
3566
3567
|
|
|
3567
3568
|
A repository with pins and no `app.config.ts` hops too: no `x verify` can run, so it runs the `typecheck` and `test` scripts it declares, and `data.proof` (`verify`, `scripts`, `none`) says what `done` rests on.
|
|
3568
3569
|
|
|
3569
|
-
Refusals name their own fix: `X_UPGRADE_MIXED_PINS` (`x upgrade --to <the
|
|
3570
|
+
Refusals name their own fix: `X_UPGRADE_MIXED_PINS` (`x upgrade --to <the lowest pins' next major> --json`), `X_UPGRADE_HOP_TOO_WIDE`, `X_UPGRADE_PIN_UNREADABLE`, `X_UPGRADE_REGISTRY_UNREACHABLE`, `X_UPGRADE_VERSION_UNPUBLISHED`, `X_UPGRADE_INSTALL_FAILED`. `data.bun.manual[]` lists what only you can move: a digest-pinned image, an expression. `data.guards.differs[]` offers `x g guard <name> --force`, and your guard is never touched. A hop below 30.0.0 reports `X_BUDGET_UNMEASURED` until `x build --target static --json` has run. Field by field: [CLI reference](CLI-Reference#x-upgrade).
|
|
3570
3571
|
|
|
3571
3572
|
## Breaking-change classes and how each is detected
|
|
3572
3573
|
|