@ultimat3/cli 30.2.0 → 30.3.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/package.json +28 -28
- package/src/exercise-database.ts +15 -1
- package/src/exercise-findings.ts +3 -1
- package/wiki/Entities-And-Migrations.md +16 -0
- package/wiki/Testing.md +5 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ultimat3/cli",
|
|
3
|
-
"version": "30.
|
|
3
|
+
"version": "30.3.0",
|
|
4
4
|
"description": "The `x` binary: new, dev, build, verify, generate, db, mcp, doctor, deploy",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ultimate",
|
|
@@ -53,33 +53,33 @@
|
|
|
53
53
|
},
|
|
54
54
|
"dependencies": {
|
|
55
55
|
"@babel/core": "^8.0.7",
|
|
56
|
-
"@ultimat3/action": "30.
|
|
57
|
-
"@ultimat3/admin": "30.
|
|
58
|
-
"@ultimat3/ai": "30.
|
|
59
|
-
"@ultimat3/auth": "30.
|
|
60
|
-
"@ultimat3/cache": "30.
|
|
61
|
-
"@ultimat3/core": "30.
|
|
62
|
-
"@ultimat3/db": "30.
|
|
63
|
-
"@ultimat3/entity": "30.
|
|
64
|
-
"@ultimat3/flags": "30.
|
|
65
|
-
"@ultimat3/http": "30.
|
|
66
|
-
"@ultimat3/i18n": "30.
|
|
67
|
-
"@ultimat3/jobs": "30.
|
|
68
|
-
"@ultimat3/mail": "30.
|
|
69
|
-
"@ultimat3/manifest": "30.
|
|
70
|
-
"@ultimat3/mcp": "30.
|
|
71
|
-
"@ultimat3/money": "30.
|
|
72
|
-
"@ultimat3/notify": "30.
|
|
73
|
-
"@ultimat3/policy": "30.
|
|
74
|
-
"@ultimat3/pwa": "30.
|
|
75
|
-
"@ultimat3/query": "30.
|
|
76
|
-
"@ultimat3/realtime": "30.
|
|
77
|
-
"@ultimat3/render": "30.
|
|
78
|
-
"@ultimat3/schema": "30.
|
|
79
|
-
"@ultimat3/seo": "30.
|
|
80
|
-
"@ultimat3/storage": "30.
|
|
81
|
-
"@ultimat3/testing": "30.
|
|
82
|
-
"@ultimat3/time": "30.
|
|
56
|
+
"@ultimat3/action": "30.3.0",
|
|
57
|
+
"@ultimat3/admin": "30.3.0",
|
|
58
|
+
"@ultimat3/ai": "30.3.0",
|
|
59
|
+
"@ultimat3/auth": "30.3.0",
|
|
60
|
+
"@ultimat3/cache": "30.3.0",
|
|
61
|
+
"@ultimat3/core": "30.3.0",
|
|
62
|
+
"@ultimat3/db": "30.3.0",
|
|
63
|
+
"@ultimat3/entity": "30.3.0",
|
|
64
|
+
"@ultimat3/flags": "30.3.0",
|
|
65
|
+
"@ultimat3/http": "30.3.0",
|
|
66
|
+
"@ultimat3/i18n": "30.3.0",
|
|
67
|
+
"@ultimat3/jobs": "30.3.0",
|
|
68
|
+
"@ultimat3/mail": "30.3.0",
|
|
69
|
+
"@ultimat3/manifest": "30.3.0",
|
|
70
|
+
"@ultimat3/mcp": "30.3.0",
|
|
71
|
+
"@ultimat3/money": "30.3.0",
|
|
72
|
+
"@ultimat3/notify": "30.3.0",
|
|
73
|
+
"@ultimat3/policy": "30.3.0",
|
|
74
|
+
"@ultimat3/pwa": "30.3.0",
|
|
75
|
+
"@ultimat3/query": "30.3.0",
|
|
76
|
+
"@ultimat3/realtime": "30.3.0",
|
|
77
|
+
"@ultimat3/render": "30.3.0",
|
|
78
|
+
"@ultimat3/schema": "30.3.0",
|
|
79
|
+
"@ultimat3/seo": "30.3.0",
|
|
80
|
+
"@ultimat3/storage": "30.3.0",
|
|
81
|
+
"@ultimat3/testing": "30.3.0",
|
|
82
|
+
"@ultimat3/time": "30.3.0",
|
|
83
83
|
"babel-preset-solid": "^1.9.16"
|
|
84
84
|
}
|
|
85
85
|
}
|
package/src/exercise-database.ts
CHANGED
|
@@ -7,7 +7,12 @@ import type { Environment } from '@ultimat3/core';
|
|
|
7
7
|
import { ENVIRONMENT_KEY, resolveEnvironment, UltimateError } from '@ultimat3/core';
|
|
8
8
|
import type { DbClient } from '@ultimat3/db';
|
|
9
9
|
import { migrate, raw, setDbClient } from '@ultimat3/db';
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
captureDeterminism,
|
|
12
|
+
installDeterminism,
|
|
13
|
+
restoreCapturedDeterminism,
|
|
14
|
+
testSealKeyEnv,
|
|
15
|
+
} from '@ultimat3/testing';
|
|
11
16
|
import { chooseReplayEngine, withScratchDatabase } from './db-replay-engine';
|
|
12
17
|
import { applyFrameworkSchema } from './framework-schema';
|
|
13
18
|
import { readMigrations } from './migrations';
|
|
@@ -50,6 +55,12 @@ const PINNED = [ENVIRONMENT_KEY, 'NODE_ENV'] as const;
|
|
|
50
55
|
* module reads it, and the throwaway seal key `bun test` gets when the app supplied none (CI names
|
|
51
56
|
* no `ULTIMATE_SECRETS_KEY`, and every row sealed here dies with the scratch database). Every
|
|
52
57
|
* variable is put back as found: a command that exits must not leave the next one a test process.
|
|
58
|
+
*
|
|
59
|
+
* The frozen clock too, and FIRST — as `bun test`'s preload freezes it before any module loads. The
|
|
60
|
+
* scratch database is PGlite, whose Postgres arms its timers off `Date.now()`: booted on the real
|
|
61
|
+
* clock and frozen under later (the pass's room), its 10 s idle-stats timer was re-armed at
|
|
62
|
+
* real-now minus frozen-now, ~2^34 ms, clamped to 1 ms — a `TimeoutOverflowWarning` busy loop for
|
|
63
|
+
* the whole pass. One clock for the engine's whole life: the deadline and the delay agree.
|
|
53
64
|
*/
|
|
54
65
|
export async function withExerciseProcess<T>(
|
|
55
66
|
root: string,
|
|
@@ -62,10 +73,13 @@ export async function withExerciseProcess<T>(
|
|
|
62
73
|
);
|
|
63
74
|
const added = { ...pinned, ...testSealKeyEnv({ root, env: { ...env, ...pinned } }) };
|
|
64
75
|
const previous = Object.keys(added).map((key) => [key, process.env[key]] as const);
|
|
76
|
+
const clock = captureDeterminism();
|
|
77
|
+
if (!clock.installed) installDeterminism();
|
|
65
78
|
Object.assign(process.env, added);
|
|
66
79
|
try {
|
|
67
80
|
return await work();
|
|
68
81
|
} finally {
|
|
82
|
+
restoreCapturedDeterminism(clock);
|
|
69
83
|
for (const [key, value] of previous) {
|
|
70
84
|
if (value === undefined) delete process.env[key];
|
|
71
85
|
else process.env[key] = value;
|
package/src/exercise-findings.ts
CHANGED
|
@@ -57,7 +57,9 @@ const unreachedFinding = (record: ExerciseRecord): Finding => {
|
|
|
57
57
|
return {
|
|
58
58
|
code: 'X_CONTRACT_EXERCISE_UNREACHED',
|
|
59
59
|
cause: `${record.kind} ${renderCauseValue(record.name)} never reached its handler as an allowed actor in the exercise pass${stopped}${why}`,
|
|
60
|
-
fix:
|
|
60
|
+
fix:
|
|
61
|
+
record.fix ??
|
|
62
|
+
`defineExercise({ ${override} }) # paste the entry into ${EXERCISE_FILE}, or { off: '<why>' }; then: x exercise ${name} --json`,
|
|
61
63
|
docs: ERROR_DOCS_URL,
|
|
62
64
|
at: EXERCISE_FILE,
|
|
63
65
|
};
|
|
@@ -968,6 +968,22 @@ export const postTotals = entity('post_totals', {
|
|
|
968
968
|
| Admin, MCP | listed read-only: `list`, `detail`, `search`, and no create/edit/delete route, button or admin MCP tool. An `operations` override naming a write is `X_ADMIN_VIEW_WRITE` ([Admin dashboard](Admin-Dashboard)) |
|
|
969
969
|
| Manifest | the entity's fact carries `view: { materialized }`; a table becoming a view is a **breaking** change, a view becoming a table additive |
|
|
970
970
|
|
|
971
|
+
## Read-only tables
|
|
972
|
+
|
|
973
|
+
`entity(name, { readOnly: '<why>', columns })` declares a table the app reads and never writes —
|
|
974
|
+
legacy history a trigger closed to INSERTs, a table another system owns. The reason is required. As
|
|
975
|
+
of 2026-11:
|
|
976
|
+
|
|
977
|
+
| Rule | |
|
|
978
|
+
|---|---|
|
|
979
|
+
| Writes | `insert`, `insertAll`, `upsertAll`, `update`, `delete`, `updateWhere`, `deleteWhere`, `lock`: `X_ENTITY_READ_ONLY`, in both drivers, before a statement exists, the reason in the cause |
|
|
980
|
+
| Refused at declaration | a blank reason, `view` (already read-only), `appendOnly` (it admits the inserts this refuses): `X_INVARIANT_VIOLATED` |
|
|
981
|
+
| Seeds | `x db seed --generate` and `x exercise` seed no row of it. A nullable reference into it is seeded null; a NOT NULL one is `X_SEED_GRAPH_UNSATISFIED` naming the table, its reason, and the fix: `readOnly` on the referencing table too, or `.nullable()` then `x db gen` |
|
|
982
|
+
| Subjects | `defineSubject`'s `'delete'` or `anonymize` on it is `X_SUBJECT_TREATMENT_INVALID`; `retain` is the one treatment that holds |
|
|
983
|
+
| Admin, MCP | listed read-only, as a view; an `operations` override naming a write is `X_ADMIN_READ_ONLY_WRITE` |
|
|
984
|
+
| Projection, manifest | `$describe().readOnly` and the manifest's entity fact carry the reason, absent when undeclared; becoming readOnly is **breaking**, ceasing to be additive |
|
|
985
|
+
| DDL | none: `x db gen` emits nothing for it. The trigger, grant or ownership that closes the table is yours |
|
|
986
|
+
|
|
971
987
|
## Migration workflow
|
|
972
988
|
|
|
973
989
|
| Step | Command | Behavior |
|
package/wiki/Testing.md
CHANGED
|
@@ -981,14 +981,16 @@ shape, same unreached code. `As of 2026-11`.
|
|
|
981
981
|
| Rule | |
|
|
982
982
|
|---|---|
|
|
983
983
|
| 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) |
|
|
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). 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
985
|
| Actor | the first of `x policy explain`'s actors the policy allows, in the primitive's tenant. None allowed is `refused`, by design |
|
|
986
986
|
| 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, or left out of the draw — 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` |
|
|
987
988
|
| 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` |
|
|
989
|
+
| 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
|
+
| 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
991
|
| 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` |
|
|
990
992
|
| 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? }
|
|
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 |
|
|
992
994
|
| 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
995
|
|
|
994
996
|
```bash
|