@ultimat3/cli 30.3.0 → 30.5.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-findings.ts +27 -11
- 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/Jobs-And-Workflows.md +2 -0
- package/wiki/MCP-And-AI.md +5 -2
- package/wiki/Testing.md +18 -6
- package/wiki/Upgrading.md +3 -2
package/wiki/Testing.md
CHANGED
|
@@ -92,6 +92,7 @@ Any test that can pass twice and fail the third time is worse than no test — i
|
|
|
92
92
|
| **Frozen clock** | time starts at a fixed instant. `clock.advance('3d')` moves it, and it also drives `step.sleep` and cron in tests |
|
|
93
93
|
| **Seeded RNG** | `Math.random`, `crypto.randomUUID`, and Bun's RNG are seeded per test file from its path — reproducible, distinct across files |
|
|
94
94
|
| **Sealed network** | a `fetch`, a `new WebSocket(…)` or a `Bun.connect(…)` to anywhere not explicitly mocked or allowed **fails the test** with `X_TEST_NETWORK_SEALED`, naming the URL and the fix — the three dials share one gate, so the allow-list and `offline()` apply to all of them — `offline()` refuses a loopback dial too (`X_TEST_NETWORK_OFFLINE`). A dial that goes around those three globals (a `node:net` socket, `Bun.udpSocket`) is not sealed |
|
|
95
|
+
| **Sealed names** | `Bun.dns.lookup`, `dns.lookup` and `dns.promises.lookup` ask the same gate: a host a `mockFetch` route names resolves to `MOCKED_HOST_ADDRESS` (`11.11.11.11`, public, so an SSRF screen such as `screenDial` passes it and the dial it pins still meets the mock by its `Host`); this machine, an address literal and an `allowHost` name resolve for real; offline refuses every name (`X_TEST_NETWORK_OFFLINE`); any other name is `X_TEST_NETWORK_SEALED`. A RegExp route names a host it matches at the origin or spells in its source. Not covered: `lookup` bound by a named ESM import (`import { lookup } from 'node:dns'`) before the seal, and `dns.resolve*` |
|
|
95
96
|
| **Fixed timezone + locale** | `UTC` and `en-US` unless a test declares otherwise; a tz-dependent bug fails deterministically |
|
|
96
97
|
| **Ordered concurrency** | job workers in tests run deterministically; `runJobs()` drains the queue synchronously |
|
|
97
98
|
| **Per-test reset** | before EVERY test, not every file, the preload puts the jobs event bus back (`installPerTestReset()`): an answer one test published never resumes the next test's waiting run, with or without `runJobs`. A test that never loaded `@ultimat3/jobs` pays nothing. `As of 2026-10` |
|
|
@@ -594,6 +595,7 @@ unitTest('a row deleted before the run is skipped, not failed', async ({ runJobs
|
|
|
594
595
|
| On the trace or the call | Is |
|
|
595
596
|
|---|---|
|
|
596
597
|
| `trace.executions[n].result` | what the body returned. Absent on a run that failed, suspended or was interrupted |
|
|
598
|
+
| `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
599
|
| `trace.steps[name].executions` | times the step body ran — a replay from storage does not count. Cumulative for the test |
|
|
598
600
|
| `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
601
|
| `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 +675,12 @@ describeIslandState(counterStates, 'idle', island, (mounted) => {
|
|
|
673
675
|
The props are the manifest's, so the picture `x shot --island` takes and the test are of one
|
|
674
676
|
component. `mountIsland` stays the call for props no state declares. `As of 2026-10`.
|
|
675
677
|
|
|
678
|
+
A realtime island's page outlives its mount: the socket, the record store and the outbox sit on
|
|
679
|
+
`globalThis`, shared by every bundle. Between files (or cases that need a fresh page) call
|
|
680
|
+
`resetPageRealtime()` from `@ultimat3/realtime` in `afterAll` — it closes the socket the CHUNK's copy
|
|
681
|
+
of realtime built and drops every page global. Never `Reflect.deleteProperty(globalThis,
|
|
682
|
+
Symbol.for('ultimate.realtime'))`: that leaves the socket open. `As of 2026-11`.
|
|
683
|
+
|
|
676
684
|
## Testing an authenticator
|
|
677
685
|
|
|
678
686
|
```ts
|
|
@@ -846,7 +854,7 @@ A column whose value comes from another factory, built with the **same strategy*
|
|
|
846
854
|
|
|
847
855
|
`ids.uuid()` and `ids.number()` come from the seeded generators; `index` starts at 1 for the first row.
|
|
848
856
|
|
|
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(
|
|
857
|
+
`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
858
|
|
|
851
859
|
### Persisting
|
|
852
860
|
|
|
@@ -889,8 +897,9 @@ await insertSeedGraph(graph, writer);
|
|
|
889
897
|
|---|---|
|
|
890
898
|
| order | parents before children, over the entities' own `$references()` |
|
|
891
899
|
| 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` |
|
|
900
|
+
| 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
901
|
| values | read off the declaration, as `factoriesFor` reads them, and kept only if `$parse` accepts them |
|
|
902
|
+
| 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
903
|
| 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
904
|
| 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
905
|
| soft delete | a soft-deleting table's `deletedAt` is `null` unless a factory builds it: seeded rows are live |
|
|
@@ -957,7 +966,8 @@ A legal input that crashes the handler is red in `contract`, the day the action
|
|
|
957
966
|
| 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
967
|
| Seed | `fnv1a(<action name>)`, never time, so a red run replays: `bun test <file> -t '<name>: handler survives generated input'` |
|
|
959
968
|
| 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.
|
|
969
|
+
| **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 |
|
|
970
|
+
| 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
971
|
| 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
972
|
| Pass | the output parsed, or a coded refusal under 500 |
|
|
963
973
|
| 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` |
|
|
@@ -984,13 +994,15 @@ shape, same unreached code. `As of 2026-11`.
|
|
|
984
994
|
| 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
995
|
| Actor | the first of `x policy explain`'s actors the policy allows, in the primitive's tenant. None allowed is `refused`, by design |
|
|
986
996
|
| 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` |
|
|
987
|
-
| A job's org | the field(s) its `tenant(input)` reads — any name, nested,
|
|
997
|
+
| 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` |
|
|
998
|
+
| 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` |
|
|
988
999
|
| 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 |
|
|
989
1000
|
| 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` |
|
|
990
1001
|
| 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 |
|
|
991
1002
|
| 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` |
|
|
1003
|
+
| 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` |
|
|
992
1004
|
| 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` |
|
|
993
|
-
| 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 |
|
|
1005
|
+
| Report | `.x/exercise.json` and `--json`: `{ seed, counts, records[] }`, each record `{ kind, name, outcome, code?, reason?, defect?, statements, effects, ms, override?, fix?, tenant?, crash? }` — `fix` where no override reaches it; `defect` on a 5xx: `{ cause, statement?, fields? }`, the error's own cause (a value the input held under a credential key or a `.sensitive()` field masked `[redacted]`), the SQL a database refusal names and the paths a validation failure names, all carried into the `X_CONTRACT_EXERCISE_UNREACHED` finding. The fields before `tenant` are A's call. The human table's `tenant` and `crash` columns are the verdicts, `-` where none ran |
|
|
994
1006
|
| 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 |
|
|
995
1007
|
|
|
996
1008
|
```bash
|
|
@@ -1052,7 +1064,7 @@ actor, after the tenant proof. Source: `packages/testing/src/exercise-crash.ts`,
|
|
|
1052
1064
|
|
|
1053
1065
|
| `crash.verdict` | When | Red |
|
|
1054
1066
|
|---|---|---|
|
|
1055
|
-
| `diverged` | an effect made more often by a killed run: `findings[]` of `{ boundary, mode, effect, clean, killed, after? }` | yes: `X_JOB_CRASH_DIVERGED`, one per effect and the step it
|
|
1067
|
+
| `diverged` | an effect made more often by a killed run: `findings[]` of `{ boundary, mode, effect, clean, killed, after?, inside? }` — `inside` the step whose body made it (`@ultimat3/jobs`' `currentStep()`), `after` the last step completed before it. An effect inside a step that did not complete, made FEWER times by a kill that spent one of its attempts, is not a divergence | yes: `X_JOB_CRASH_DIVERGED`, one per effect and place, every boundary named. Inside a step the fix keys its call on `${jobId}:<step>`; outside every step it wraps it in `step.run` |
|
|
1056
1068
|
| `held` | otherwise; `boundaries` and `reruns` (lost answers — at-least-once, never red) counted | no |
|
|
1057
1069
|
| `unproven` | the proof itself threw: `reason` | no |
|
|
1058
1070
|
|
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
|
|