@ultimat3/cli 30.6.0 → 30.7.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.
@@ -1,12 +1,14 @@
1
1
  // Everything `x g` edits OUTSIDE the files it creates, in the one order they have to happen: a
2
2
  // declared permission granted to a role, a primitive listed in `defineApi`, an entity added to the
3
- // typed handle, an `--admin` override wired, each new workspace edge declared, and every catalog
4
- // made selectable. One function over one disk seam, so a real run and a dry run are the same code
3
+ // typed handle, an `--admin` override wired, a contract test's exercise entry declared, each new
4
+ // workspace edge declared, and every catalog made selectable. One function over one disk seam, so a real run and a dry run are the same code
5
5
  // — and the dry run's list of touched files is what the run touches, not a second guess at it.
6
6
 
7
7
  import { registerAdminResources } from './admin-registration';
8
8
  import { registerGeneratedPrimitives } from './api-registration';
9
+ import type { ExerciseEntry } from './exercise-entry-edit';
9
10
  import type { GenerateDisk } from './generate-disk';
11
+ import { declareGeneratedExercises } from './generate-exercise';
10
12
  import { ungrantedByGenerator } from './generate-grant-findings';
11
13
  import { grantGeneratedPermissions } from './generate-grants';
12
14
  import { declareGeneratedImports } from './generated-imports';
@@ -33,11 +35,15 @@ export async function followUpEdits(
33
35
  written: readonly string[],
34
36
  dbModule: string | undefined,
35
37
  disk: GenerateDisk,
38
+ /** The written contract tests' entries, declared in `apps/web/exercise.ts` (`generate-exercise.ts`). */
39
+ exercises: readonly ExerciseEntry[] = [],
36
40
  ): Promise<FollowUps> {
37
41
  const handle = await registerGeneratedEntities(root, written, dbModule, disk);
38
42
  const adminWiring = await registerAdminResources(root, written, disk);
39
43
  const granted = await grantGeneratedPermissions(root, written, disk);
40
44
  const listed = await registerGeneratedPrimitives(root, written, disk);
45
+ // Before the manifests: the entry may bring an import (`userActor`) into the exercise file.
46
+ const exercise = await declareGeneratedExercises(exercises, disk);
41
47
  // Last of the manifest writers, because the others add imports too: every sibling workspace a
42
48
  // written file imports, declared in the manifest it landed under.
43
49
  const imports = await declareGeneratedImports(root, written, disk);
@@ -53,12 +59,19 @@ export async function followUpEdits(
53
59
  ...new Set([
54
60
  ...granted,
55
61
  ...listed,
62
+ ...exercise.edited,
56
63
  ...handle.edited,
57
64
  ...adminWiring.edited,
58
65
  ...imports,
59
66
  ...(indexSync.edited ?? []),
60
67
  ]),
61
68
  ].filter((path) => !written.includes(path)),
62
- findings: [...handle.findings, ...ungranted, ...adminWiring.findings, ...indexSync.findings],
69
+ findings: [
70
+ ...handle.findings,
71
+ ...ungranted,
72
+ ...adminWiring.findings,
73
+ ...exercise.findings,
74
+ ...indexSync.findings,
75
+ ],
63
76
  };
64
77
  }
package/src/mcp-errors.ts CHANGED
@@ -392,6 +392,8 @@ const CLI_FIXES: Readonly<Record<CliErrorCode, string>> = {
392
392
  'x verify --only boundaries --json # names the file; end its mount() with return render(() => <Island {...props} />, el)',
393
393
  X_SOCKET_HAND_ROLLED:
394
394
  'x verify --only boundaries --json — names the file and line; delete the upgrade and declare the socket with deviceSocket({ path, resolveToken, onFrame }) from @ultimat3/realtime/server',
395
+ X_EXERCISE_FILE_UNEDITABLE:
396
+ 'edit apps/web/exercise.ts — paste the entry the finding names into export const exercise = defineExercise({ … })',
395
397
  };
396
398
 
397
399
  const isCliCode = (code: string): code is CliErrorCode =>
package/src/messages.ts CHANGED
@@ -103,6 +103,8 @@ const CATALOG = {
103
103
  'cli.exercise.none': 'this app registers no action, query or job: nothing to exercise',
104
104
  'cli.exercise.summary':
105
105
  '{total} primitive(s): {reached} reached, {refused} refused, {off} off, {unreached} unreached — {file}',
106
+ 'cli.exercise.phases': 'took {total} ms — {phases} — slowest: {slowest}',
107
+ 'cli.exercise.slowest': 'slowest: {primitive} · {proof} {ms} ms',
106
108
  // One rendering of "this file was written", for every command that writes files — `x g`, its
107
109
  // own `--dry-run`, and `x new`. Three copies of the same two characters is how a fourth writer
108
110
  // arrives with a fifth marker; `--json` carries the paths themselves in `data.files`.
@@ -0,0 +1,71 @@
1
+ // A generated action's ONE exercise declaration: its entry in `apps/web/exercise.ts`, which `x g`
2
+ // inserts (`exercise-entry-edit.ts`) and both runners read — the contract test hands that file's
3
+ // export to `.contract({ exercise })`, and `x exercise` re-homes the actor into each tenant it seeds.
4
+
5
+ import type { ExerciseEntry } from '../exercise-entry-edit';
6
+
7
+ const ORG = '00000000-0000-4000-8000-000000000002';
8
+
9
+ /**
10
+ * `../../../exercise` from a directory under the app root (`apps/web/app/post/actions`). Counted
11
+ * off the shared `apps/web/` prefix, which every generated surface sits under.
12
+ */
13
+ export function exerciseImportFrom(dir: string): string {
14
+ const prefix = 'apps/web/';
15
+ const depth = dir.startsWith(prefix) ? dir.slice(prefix.length).split('/').length : 0;
16
+ return depth === 0 ? './exercise' : `${'../'.repeat(depth)}exercise`;
17
+ }
18
+
19
+ /** The import line a contract test reads the declarations with. */
20
+ export const exerciseImportLine = (dir: string): string =>
21
+ `import { exercise } from '${exerciseImportFrom(dir)}';`;
22
+
23
+ /**
24
+ * Who the generated-input runs act as: a member holding the slice's grant, in the org every draw
25
+ * is bound to when the policy compares orgs. A grant that cannot be read is the author's to name,
26
+ * so the entry turns the exercise off and says what to write instead. Printed expanded at every
27
+ * level, the shape Biome keeps for an object written over several lines.
28
+ */
29
+ export function actionExerciseEntry(
30
+ name: string,
31
+ grant: string | undefined,
32
+ orgRule: boolean,
33
+ feature: string,
34
+ ): ExerciseEntry {
35
+ if (grant === undefined) {
36
+ return {
37
+ name,
38
+ value: `{\n off: '${feature}/policy.ts names no one grant: give { actor } holding what it checks',\n}`,
39
+ };
40
+ }
41
+ return memberEntry(name, grant, orgRule ? { org: ORG, bindsInput: true } : {});
42
+ }
43
+
44
+ /**
45
+ * A member holding `grant`; in `org` when given, and with every draw's `orgId` bound to it when
46
+ * `bindsInput` — an action whose input names no org (a create that writes under the actor's) gets
47
+ * the actor's org alone.
48
+ */
49
+ export function memberEntry(
50
+ name: string,
51
+ grant: string,
52
+ where: { readonly org?: string; readonly bindsInput?: boolean },
53
+ ): ExerciseEntry {
54
+ const { org } = where;
55
+ const actor = [
56
+ 'actor: userActor({',
57
+ " id: 'member',",
58
+ ...(org === undefined ? [] : [` orgId: '${org}',`]),
59
+ ` permissions: ['${grant}'],`,
60
+ '}),',
61
+ ];
62
+ const input =
63
+ org === undefined || where.bindsInput !== true
64
+ ? []
65
+ : [`input: (drawn) => ({ ...drawn, orgId: '${org}' }),`];
66
+ return {
67
+ name,
68
+ value: `{\n${[...actor, ...input].map((line) => ` ${line}`).join('\n')}\n}`,
69
+ imports: [{ name: 'userActor', from: '@ultimat3/core' }],
70
+ };
71
+ }
@@ -44,10 +44,12 @@ const preamble = (
44
44
  isMutator: boolean,
45
45
  wrappers: string,
46
46
  outsider: boolean,
47
- /** `testActor` is imported for the outsider, or for a contract suite's exercise actor. */
47
+ /** `testActor` is imported for the outsider. */
48
48
  importsActor = outsider,
49
+ /** A contract suite's import of `apps/web/exercise.ts`, the one home of its exercise. */
50
+ exerciseImport?: string,
49
51
  ): string => `${importsActor ? "import { testActor } from '@ultimat3/policy';\n" : ''}import { ${wrappers} } from '@ultimat3/testing';
50
- import { ${name.camel} } from './${name.kebab}';
52
+ ${exerciseImport === undefined ? '' : `${exerciseImport}\n`}import { ${name.camel} } from './${name.kebab}';
51
53
  ${
52
54
  outsider
53
55
  ? `
@@ -233,33 +235,6 @@ unitTest('${name.camel} rejects input that is not a uuid', async () => {
233
235
  ${shape.isMutator ? localTwinTests() : ''}${handlerTests(feature, shape)}`;
234
236
  };
235
237
 
236
- /**
237
- * Who the generated-input runs act as: an actor holding the slice's grant — `<feature>:write` for
238
- * the policy this generator wrote, the one literal `can('<perm>')` of a handwritten one — in the
239
- * input's org when the rule compares orgs. A grant that cannot be read is the author's to name,
240
- * so those runs are skipped out loud instead.
241
- */
242
- const exerciseActor = (grant: string | undefined, orgRule: boolean): string =>
243
- grant === undefined
244
- ? ''
245
- : `
246
- // Holds the grant${orgRule ? ', in the org every generated input is bound to' : ''}: the actor the handler runs as when \`.contract()\`
247
- // calls it with generated legal input.
248
- const member = testActor('member', {${orgRule ? '\n orgId,' : ''}
249
- permissions: ['${grant}'],
250
- }).actor;
251
- `;
252
-
253
- /** The `.contract()` argument: the actor and, under an org rule, the org every draw is bound to. */
254
- const exerciseOption = (grant: string | undefined, orgRule: boolean): string => {
255
- if (grant === undefined) {
256
- return "{\n exercise: { off: '../policy.ts names no one grant: pass { actor } holding what it checks' },\n }";
257
- }
258
- return orgRule
259
- ? '{\n exercise: { actor: member, input: (drawn) => ({ ...(drawn as object), orgId }) },\n }'
260
- : '{ exercise: { actor: member } }';
261
- };
262
-
263
238
  export const actionContractTest = (
264
239
  name: NameSet,
265
240
  feature: NameSet,
@@ -268,16 +243,17 @@ export const actionContractTest = (
268
243
  scaffoldPolicy = true,
269
244
  /** That policy holds an org rule: false for a slice whose entity names no tenant. */
270
245
  orgRule = scaffoldPolicy,
271
- /** The permission the exercise actor holds; `undefined` when the policy's grant cannot be read. */
272
- grant: string | undefined = scaffoldPolicy ? `${feature.kebab}:write` : undefined,
246
+ /** The line importing `apps/web/exercise.ts`'s export, relative to this test (`action-exercise.ts`). */
247
+ exerciseImport = "import { exercise } from '../../../exercise';",
273
248
  ): string => `// ${name.camel}: the contract every action owes, and the foreign-org actor it denies before the
274
249
  // handler runs. One declaration, every surface.
275
- ${preamble(name, feature, isMutator, 'contractTest, expect', orgRule, orgRule || grant !== undefined)}${exerciseActor(grant, orgRule)}
250
+ ${preamble(name, feature, isMutator, 'contractTest, expect', orgRule, orgRule, exerciseImport)}
276
251
  contractTest('${name.camel} passes the action contract', async () => {
277
252
  // Four assertions the framework makes for any action, without knowing what this one does:
278
253
  // garbage input is rejected, an anonymous actor is denied, the operation reaches the OpenAPI
279
- // document, and the handler survives 16 generated legal inputs, each rolled back.
280
- for (const contract of target.contract(${exerciseOption(grant, orgRule)})) {
254
+ // document, and the handler survives 16 generated legal inputs, each rolled back — run as its
255
+ // entry in apps/web/exercise.ts declares, the same entry \`x exercise\` runs.
256
+ for (const contract of target.contract({ exercise })) {
281
257
  await contract.run();
282
258
  }
283
259
  });
@@ -3,6 +3,7 @@
3
3
  // job handle, an MCP tool and these tests; the generator writes the declaration and the tests.
4
4
 
5
5
  import { SlicePolicyIncompleteError } from '../generate-slice-policy';
6
+ import { actionExerciseEntry, exerciseImportLine } from './action-exercise';
6
7
  import { actionContractTest, actionUnitTest } from './action-tests';
7
8
  import type { FeatureTarget } from './entity';
8
9
  import type { GeneratedFile, NameSet } from './naming';
@@ -194,6 +195,8 @@ export function actionFiles(rawName: string, target: ActionOptions): readonly Ge
194
195
  : grantOf(target.slicePolicy, `can${feature.pascal}Write`);
195
196
  const org =
196
197
  !singleTenant || (target.slicePolicy !== undefined && policyReadsOrg(target.slicePolicy));
198
+ // The org rule the contract can rely on: the one this generator's own policy writes.
199
+ const orgRule = target.slicePolicy === undefined && !singleTenant;
197
200
  // Before a file is planned: a generated action that does not compile is refused, never written.
198
201
  assertPolicyExports(target, feature, isMutator);
199
202
  const lookup =
@@ -247,10 +250,12 @@ export function actionFiles(rawName: string, target: ActionOptions): readonly Ge
247
250
  feature,
248
251
  isMutator,
249
252
  target.slicePolicy === undefined,
250
- target.slicePolicy === undefined && !singleTenant,
251
- // A hand-written policy's one literal grant: its exercise runs as an actor holding it.
252
- grant,
253
+ orgRule,
254
+ exerciseImportLine(dir),
253
255
  ),
256
+ // Who its handler runs as, declared once in apps/web/exercise.ts — the file this test reads
257
+ // and `x exercise` reads. A hand-written policy's one literal grant: an actor holding it.
258
+ exercise: actionExerciseEntry(name.camel, grant, orgRule, target.feature),
254
259
  },
255
260
  ];
256
261
  }
@@ -1,6 +1,8 @@
1
1
  // Name derivation for generators. One place, because `x g resource post` has to agree with itself
2
2
  // across eight emitted files — a second casing helper is how `Post`/`post`/`posts` drift apart.
3
3
 
4
+ import type { ExerciseEntry } from '../exercise-entry-edit';
5
+
4
6
  /**
5
7
  * A catalog is one file per locale that many generators contribute keys to; a whole-file write
6
8
  * would delete every key already in it, so a `'json'` file merges instead of overwriting. Always
@@ -30,6 +32,11 @@ export interface GeneratedSourceFile {
30
32
  * then the target's own text: the copy `write-generated.ts` writes where the OS refuses a link.
31
33
  */
32
34
  readonly link?: string;
35
+ /**
36
+ * The primitive's entry in `apps/web/exercise.ts`, for the contract test that reads it: the run
37
+ * that writes this file inserts it there (`generate-exercise.ts`), never the file itself.
38
+ */
39
+ readonly exercise?: ExerciseEntry;
33
40
  }
34
41
 
35
42
  /**
@@ -3,6 +3,7 @@
3
3
  // input was `{ id, orgId }` while the form posted `{ title }`. The input is the entity's own view of
4
4
  // the columns a caller supplies; the org comes from the actor, never from the body.
5
5
 
6
+ import { exerciseImportLine, memberEntry } from './action-exercise';
6
7
  import { sortedImports } from './imports';
7
8
  import type { GeneratedFile, NameSet } from './naming';
8
9
  import { names } from './naming';
@@ -119,10 +120,13 @@ unitTest('orgOf answers the org of the actor, and refuses an actor with none', (
119
120
 
120
121
  const createContractTest = (
121
122
  feature: NameSet,
123
+ /** The line importing `apps/web/exercise.ts`'s export, relative to this test. */
124
+ exerciseImport: string,
122
125
  ): string => `// create${feature.pascal}: the contract every action owes, and the orgless actor it denies before
123
126
  // the handler runs — the org a row is written under is the actor's, so an actor with none is refused.
124
127
  import { testActor } from '@ultimat3/policy';
125
128
  import { contractTest, expect } from '@ultimat3/testing';
129
+ ${exerciseImport}
126
130
  import { create${feature.pascal} } from './create-${feature.kebab}';
127
131
 
128
132
  const target = create${feature.pascal}.named('create${feature.pascal}');
@@ -137,9 +141,9 @@ const writer = testActor('writer', {
137
141
  }).actor;
138
142
 
139
143
  contractTest('create${feature.pascal} passes the action contract', async () => {
140
- // The fourth assertion runs the handler as the writer on 16 generated legal inputs, each
141
- // inside a transaction that rolls back, so nothing it creates outlives the test.
142
- for (const contract of target.contract({ exercise: { actor: writer } })) await contract.run();
144
+ // The fourth assertion runs the handler on 16 generated legal inputs, each inside a transaction
145
+ // that rolls back, as its entry in apps/web/exercise.ts declares — the one \`x exercise\` runs.
146
+ for (const contract of target.contract({ exercise })) await contract.run();
143
147
  });
144
148
 
145
149
  contractTest('create${feature.pascal} needs an actor with an org', async () => {
@@ -167,7 +171,9 @@ export function resourceCreateFiles(
167
171
  { path: `${dir}/create-${feature.kebab}.test.ts`, contents: createUnitTest(feature, dbModule) },
168
172
  {
169
173
  path: `${dir}/create-${feature.kebab}.contract.test.ts`,
170
- contents: createContractTest(feature),
174
+ contents: createContractTest(feature, exerciseImportLine(dir)),
175
+ // A writer in one org: the create writes under the actor's org, so no input names one.
176
+ exercise: memberEntry(`create${feature.pascal}`, `${feature.kebab}:write`, { org: ORG }),
171
177
  },
172
178
  ];
173
179
  }
@@ -27,6 +27,7 @@ const devActor = (
27
27
  //
28
28
  // Delete it the day sign-in exists, and the fallback line in \`authenticator.ts\` with it.
29
29
  import { type Actor, logger, readCookie } from '@ultimat3/core';
30
+ import { seedId } from '@ultimat3/entity';
30
31
  import { DEMO_ORG_ID } from '../../shared/demo-org';
31
32
  import { roles } from '../../shared/roles';
32
33
 
@@ -56,13 +57,20 @@ export const devRoleFrom = (cookieHeader: string | null): string | undefined =>
56
57
  return Object.hasOwn(roles, named) ? named : undefined;
57
58
  };
58
59
 
60
+ /**
61
+ * The viewer's user id for \`role\`: a uuid, as \`ctx.actor.id\` is on every real request (an
62
+ * \`x_users\` key) — a name here was \`22P02 invalid input syntax for type uuid\` the first time a
63
+ * handler looked its caller up. \`seedId\`, so a seed's id('user:dev:<role>') is this very user.
64
+ */
65
+ export const devActorId = (role: string): string => seedId(\`user:dev:\${role}\`);
66
+
59
67
  /**
60
68
  * \`roles\`, never a permission list: \`can()\` expands the role map at decision time, so a grant
61
69
  * moved between roles reaches this actor without an edit here.
62
70
  */
63
71
  export const devActorFor = (role: string): Actor => ({
64
72
  kind: 'user',
65
- id: 'dev-actor',
73
+ id: devActorId(role),
66
74
  // The seed's org, never an invented string: a generated tenant policy compares it with a uuid.
67
75
  orgId: DEMO_ORG_ID,
68
76
  roles: [role],
@@ -230,6 +238,14 @@ unitTest('a cookie naming an undeclared role is never the default viewer', () =>
230
238
  }
231
239
  });
232
240
 
241
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-5[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
242
+
243
+ unitTest('the viewer is a user: a uuid id, one per role', () => {
244
+ // An app reads \`ctx.actor.id\` as an \`x_users\` key; a name in it fails the uuid cast.
245
+ for (const role of declaredDevRoles()) expect(devActorFor(role).id).toMatch(UUID);
246
+ expect(devActorFor('admin').id).not.toBe(devActorFor('member').id);
247
+ });
248
+
233
249
  unitTest('the actor it mints holds what the role map grants it, and nothing else', () => {
234
250
  // \`actorHas\` and not \`holds\`: this is the function \`can()\` itself calls, so the assertion is
235
251
  // the pipeline's own decision rather than a second implementation of it. The map is passed
@@ -7,7 +7,8 @@ const exercise =
7
7
  (): string => `// What \`x exercise\` cannot guess about this app: an input a primitive needs bound to a real
8
8
  // row, an actor its policy permits, or a primitive that must not run in the pass and why. Empty
9
9
  // runs every action, query and job as generated. \`x exercise --json\` prints the entry to paste
10
- // for each one it could not reach; each value is the \`exercise\` option \`.contract()\` takes.
10
+ // for each one it could not reach. A contract test reads the same entry:
11
+ // \`target.contract({ exercise })\`, handed this file's export.
11
12
  import { defineExercise } from '@ultimat3/testing';
12
13
 
13
14
  export const exercise = defineExercise({});
package/wiki/Actions.md CHANGED
@@ -144,7 +144,7 @@ Every projection needs the name `registerActions()` stamps on. It names the expo
144
144
  | 3 | **Typed client function** | `input` + `output` | one map-wide client, `export const client = rpc<Api['actions']>({ baseUrl })`, then `await client.publishPost({ postId })` in `app/`; or `publishPost.client({ baseUrl })` for a single method. `rpc` is the only name for the map-wide client — no `createClient` alias, no fetch, no codegen step to remember. `x new` writes it as `apps/web/shared/browser-client.ts` (`browserClient`, beside `browserQueries`); it typechecks at any app size (pinned at 300 actions in 100 modules). **`rpc` and `queryClient` do not reach a mount**: under `baseUrl: '…/v1'` they derive `/v1/api/…` and a valid token answers 404. A machine caller uses the mount's OpenAPI document |
145
145
  | 4 | **Job handle** | the whole declaration | `publishPost.job()` — a namespaced name, an `idempotencyKey` from the payload, and an `invoke` that runs the same handler durably. Register it with the queue; `.enqueue()` belongs to a declared `job` |
146
146
  | 5 | **MCP tool** | `mcp` + `input` + `policy` | `toolFrom(publishPost)` from `@ultimat3/mcp` — one tool named `publishPost` per exposed action, exactly the `tools/list` entry, JSON Schema from `input`, every call through this action's `invoke` (authz unchanged) |
147
- | 6 | **Test** | `input` + `policy` + `handle` | `publishPost.contract({ exercise: { actor } })` — schema round-trip, a denial test per policy branch, and the handler on generated legal input, generated green, not as a `TODO` |
147
+ | 6 | **Test** | `input` + `policy` + `handle` | `publishPost.contract({ exercise })`, handed `apps/web/exercise.ts`'s export, where `x g` declares its entry — schema round-trip, a denial test per policy branch, and the handler on generated legal input, generated green, not as a `TODO` |
148
148
 
149
149
  **A client for code that cannot import the app.** `rpc` needs the app's action types, so a customer's TypeScript never had one. `x client customer --out <dir>` compiles the committed `openapi.json` (or a bearer mount's own document, `--mount /v1`) into `index.ts` + `types.ts` with no import beyond the platform: `createClient({ baseUrl, headers })`, one method per `operationId`, a non-2xx as `ProblemError`, and `refusalOf(error, 'publishPost')` narrowing to the codes that operation declares under `errors:`. The calling direction is `x client vendor <name>` — `outbound()` and `t` schemas from a third party's document. One compiler for both, never a second way: [CLI reference](CLI-Reference#x-client). `As of 2026-11`.
150
150
 
@@ -356,7 +356,7 @@ is a word (`a-z`, `0-9`, `-`, `_`) or `[param]` / `[...rest]`; `..`, `.` and emp
356
356
  | `--force` | boolean | `false` | overwrite existing files |
357
357
  | `--dry-run` | boolean | `false` | the write plan, nothing written: **every file the run would touch** — the ones it creates (a slice module already on disk is skipped, not listed) and, `As of 27.6.2`, the ones it EDITS outside the slice (role map, typed handle, API index, manifests, catalogs, the i18n index), in the order the run lists them. `--json`: `data.files` is that list, `data.edits` the subset that already exists, `data.refreshes` the committed contracts (`x.manifest.json`, `openapi.json`) a real run re-projects. `ok: false` with every finding the real run would report — an existing file, an API-index binding already held, a grant or a handle entry it cannot place |
358
358
 
359
- `resource` emits the whole slice — `entity`, `repo`, `policy`, `errors`, `service`, `actions`, `live`, `jobs`, `ui`, the form island, the plural route and a test beside each declaration — **37 files** on a dry run in an app scaffolded with its example slice, 39 on the real run after `bun run setup` (`x.manifest.json` and `openapi.json` re-projected; 40 dry with `--admin`) — and `x g resource` in a `--no-example` app, which also gains two `package.json` edits, writes 39 and 41 files (42 and 44 with `--admin`; `--live` is refused — a resource already ships a live query, and the flag belongs to `x g query`: `X_CLI_BAD_FLAG`, fix `x g query <name>-feed --feature <name> --live`), and **no migration**: `x db gen` is the only writer of `packages/db/migrations`, so a new slice is `x g resource <name>`, then `x db gen "create <table>"`, then `x db migrate`, then `x build --target static && x verify --only budgets` — the four commands the run prints, in that order (`data.next` under `--json`). **The plural route is reachable as written**, `As of 2026-10-01`: its `load` reads the list through the slice's query as the request's actor (no org in the URL), `AsyncRegion` renders ready, empty and refused, the form island is mounted, and the route declares `policy: { permission: '<name>:read' }`. Its `budget.js` is MEASURED on `x new`'s app (58,522 B, Bun 1.4.0) and carries the `measured:` / `why:` comment `bun run budget-raises` reads; an app that charges more to every document — `navigation: { client: [...] }` adds its router — is over it on the printed build, and the raise is that edit. The page sits in `apps/web/shared/shell.tsx` when the app has one. `backfill` emits a `backfill()` declaration with its `source()` and `handle()` to fill in — see [Migrations and backfills](Migrations-And-Backfills). **`x g query` and `x g backfill` read a table and never create one**, `As of 2026-11-02`: both name a slice holding an `entity.ts`, by `--feature <slice>` or by a name that is one, and neither finding it is `X_FEATURE_REQUIRED`, zero files written, its fix the closest slice's `--feature` line or `x g resource <name>`. `x g resource` and `x g entity` are what create a table. Every generator produces code that passes `x verify` unmodified, and writes it through the app's own Biome when the app has one: `node_modules/.bin/biome` over the TypeScript files that run created, best effort, never a failed `x g`. A slice whose `entity.ts` declares another table under its name gets no foundation from `x g action`/`x g mutator`, and `x g query` there is `X_QUERY_SLICE_ENTITY`. Errors: `X_GENERATE_CONFLICT`, `X_DB_HANDLE_UNREGISTERED`, `X_FEATURE_UNKNOWN`, `X_FEATURE_REQUIRED`, `X_QUERY_SLICE_ENTITY`.
359
+ `resource` emits the whole slice — `entity`, `repo`, `policy`, `errors`, `service`, `actions`, `live`, `jobs`, `ui`, the form island, the plural route and a test beside each declaration — **38 files** on a dry run in an app scaffolded with its example slice, 40 on the real run after `bun run setup` (`x.manifest.json` and `openapi.json` re-projected; 41 dry with `--admin`) — and `x g resource` in a `--no-example` app, which also gains two `package.json` edits, writes 40 and 42 files (43 and 45 with `--admin`; `--live` is refused — a resource already ships a live query, and the flag belongs to `x g query`: `X_CLI_BAD_FLAG`, fix `x g query <name>-feed --feature <name> --live`), and **no migration**: `x db gen` is the only writer of `packages/db/migrations`, so a new slice is `x g resource <name>`, then `x db gen "create <table>"`, then `x db migrate`, then `x build --target static && x verify --only budgets` — the four commands the run prints, in that order (`data.next` under `--json`). **The plural route is reachable as written**, `As of 2026-10-01`: its `load` reads the list through the slice's query as the request's actor (no org in the URL), `AsyncRegion` renders ready, empty and refused, the form island is mounted, and the route declares `policy: { permission: '<name>:read' }`. Its `budget.js` is MEASURED on `x new`'s app (58,522 B, Bun 1.4.0) and carries the `measured:` / `why:` comment `bun run budget-raises` reads; an app that charges more to every document — `navigation: { client: [...] }` adds its router — is over it on the printed build, and the raise is that edit. The page sits in `apps/web/shared/shell.tsx` when the app has one. `backfill` emits a `backfill()` declaration with its `source()` and `handle()` to fill in — see [Migrations and backfills](Migrations-And-Backfills). **`x g query` and `x g backfill` read a table and never create one**, `As of 2026-11-02`: both name a slice holding an `entity.ts`, by `--feature <slice>` or by a name that is one, and neither finding it is `X_FEATURE_REQUIRED`, zero files written, its fix the closest slice's `--feature` line or `x g resource <name>`. `x g resource` and `x g entity` are what create a table. Every generator produces code that passes `x verify` unmodified, and writes it through the app's own Biome when the app has one: `node_modules/.bin/biome` over the TypeScript files that run created, best effort, never a failed `x g`. A slice whose `entity.ts` declares another table under its name gets no foundation from `x g action`/`x g mutator`, and `x g query` there is `X_QUERY_SLICE_ENTITY`. A written contract test reads its exercise from `apps/web/exercise.ts` (`target.contract({ exercise })`), and the same run inserts its entry there — once, the file created from `x new`'s when absent, `As of 2026-11-08`; a file that is not one `export const exercise = defineExercise({ … })` is `X_EXERCISE_FILE_UNEDITABLE`, the entry to paste in its `fix:`. Errors: `X_GENERATE_CONFLICT`, `X_DB_HANDLE_UNREGISTERED`, `X_FEATURE_UNKNOWN`, `X_FEATURE_REQUIRED`, `X_QUERY_SLICE_ENTITY`, `X_EXERCISE_FILE_UNEDITABLE`.
360
360
 
361
361
  **A name the templates would mangle is `X_CLI_BAD_FLAG` before anything is planned**, each with a `fix:` that runs: a reserved word or a leading digit; a letter outside ASCII (`Über` → `x g entity uber` — it was dropped to `ber`); an already-plural `resource`/`entity` name (`posts` → `x g resource post` — it became `entity('postses')`); and a name whose type spelling an emitted file also uses as a global or local type (`promise`, `omit`, `partial`, `row` → `x g resource promise-resource`).
362
362
 
@@ -168,14 +168,15 @@ gate's `exercise` step, around each primitive's own call: B, then A, then B agai
168
168
 
169
169
  ```
170
170
  X_TENANT_LEAK: another tenant's row reached a call's output or effect
171
- cause: action readAttachment, called by tenant B with tenant A's ids before A ran, handed back A's seeded data at output.url: …
172
- fix: x exercise readAttachment --json — then read the row behind output.url through the repository scoped to ctx.actor, never by the input id alone
171
+ cause: action readAttachment, called by tenant B with A's own input before A ran, handed back A's seeded data at output.url: …
172
+ fix: x exercise readAttachment --json — then read the row behind output.url through the repository scoped to ctx.actor, never by the input id alone, nor by an unscoped statement
173
173
  ```
174
174
 
175
175
  | Not a failure | Why |
176
176
  |---|---|
177
177
  | `public` | the policy admits an anonymous caller: what it returns, anyone may read. The read half is skipped; a crossed **write** is still red |
178
178
  | `public`, as a declared sweep | a job that reads every org on purpose says so with `crossTenant(reason, fn)`, and its input names no org's rows. The proof sees the scope and quotes the reason; the read half is skipped, a crossed **write** is still red. The same scope in an action or a query, or in a job handed A's id, is still `X_TENANT_LEAK` |
179
+ | `public`, as a tenantless job | a job declaring `tenant: 'none'` whose input names none of A's rows — a nightly cleanup across every org, input `{ occurrenceMs }`. No org and no id of B's choosing reaches its run, so B's run is A's run: what it writes it writes for every enqueuer. The **write** half is skipped, the read half kept. Handed one of A's ids it is crossed as ever; an action or a query handed no id still crosses, since B's org runs it |
179
180
  | `unproven` | A's own call was not reached (that record is already red as `X_CONTRACT_EXERCISE_UNREACHED`), or a query answered A none of A's rows — B's empty answer then proves nothing |
180
181
 
181
182
  B holds the role A held, minus `tenancy:cross`. **Not checked**: a `route` `load` and a `task`
package/wiki/Testing.md CHANGED
@@ -609,7 +609,7 @@ Enforced through it, keyed or not: `concurrency` and `whenBusy` — each pass is
609
609
 
610
610
  | Call | Runs | Answers |
611
611
  |---|---|---|
612
- | `crashProof(job, input)` | one clean run, then one per boundary it crossed (each step write landing, each settle), the worker killed there and the next one reclaiming; then each boundary again with the write's answer lost | `{ boundaries, divergences, reruns }`. A divergence carries `error`, an `X_JOB_CRASH_DIVERGED` naming the effect and the step it follows — `throw report.divergences[0].error`. A rerun is at-least-once, reported, never red |
612
+ | `crashProof(job, input)` | one clean run, then one per distinct boundary it crossed (each step write landing, each settle), the worker killed there and the next one reclaiming; then each again with the write's answer lost. A loop's steps named by row id (`fanout:${row.id}`, `page-3` — a uuid, a digit run, an 8+ character token with a digit) are one point, killed at its first and last row: the cost is the job's steps, never the rows seeded. Over `CRASH_POINT_BOUND` (32) points per mode it throws `X_JOB_CRASH_UNBOUNDED` after the clean run, never a pass | `{ boundaries, crossed, divergences, reruns }` — `boundaries` the kills, `crossed` every boundary they stand for. A divergence carries `error`, an `X_JOB_CRASH_DIVERGED` naming the effect and the step it follows — `throw report.divergences[0].error`. A rerun is at-least-once, reported, never red |
613
613
  | `simulateJobs({ jobs: [{ handle, input }], workers, faults, invariant })` | `seeds` (20) seeds from `seed` (1), N workers, every driver call interleaved and faulted by a seeded picker: `kill`, `answer-lost`, `lease-lapse`, `duplicate-claim`, `clock-jump` (`ALL_FAULTS`) | throws `X_JOB_SIM_INVARIANT` with the seed and its faults when `invariant(state)` throws or answers `false`; `X_JOB_SIM_NONDETERMINISTIC` when one seed traces two ways. `x test job --seed <n>` replays one |
614
614
 
615
615
  A `compensates: true` job is also forced to fail for good and killed while it unwinds: each `compensate:*` once, last in first out. A kill inside `step.run(…, { once: true })` ends `X_STEP_OUTCOME_UNKNOWN` and is green. Postgres is not simulated. The reference: `examples/dummy/apps/web/app/posts/jobs.job.test.ts`.
@@ -896,10 +896,10 @@ await insertSeedGraph(graph, writer);
896
896
  | Rule | What it does |
897
897
  |---|---|
898
898
  | order | parents before children, over the entities' own `$references()` |
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 |
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. A `shared` (or tenantless) table a tenant-owned one references through a key unique across the table — the unique holds the reference and not the tenant column — seeds `perTable` rows **per tenant**, still `tenant: null`, so every tenant's unique children find a parent row no other tenant took (`As of 2026-11-08`) |
900
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` |
901
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 |
902
+ | patterns and schemas | a `matches(/…/)` pattern adds strings drawn from the pattern itself — `patternSamples` (`@ultimat3/schema/sample`), the one generator the input sampler uses too: 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 |
903
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 |
904
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`) |
905
905
  | soft delete | a soft-deleting table's `deletedAt` is `null` unless a factory builds it: seeded rows are live |
@@ -960,10 +960,12 @@ A legal input that crashes the handler is red in `contract`, the day the action
960
960
 
961
961
  | Rule | |
962
962
  |---|---|
963
- | Who acts | `action.contract({ exercise: { actor } })`. Required unless the policy admits an anonymous caller. There is no bypass: a policy that denies `actor` makes an unreached run |
964
- | A row | `{ exercise: { actor, input: (drawn) => ({ ...drawn, postId }) } }` binds what a generated uuid cannot resolve |
965
- | Opting out | `{ exercise: { off: '<reason>' } }`. The reason is in the test's name. A blank one is `X_CONTRACT_EXERCISE_UNDECLARED` |
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 |
963
+ | **One declaration** | in an app, an action's exercise lives in `apps/web/exercise.ts` and nowhere else: `import { exercise } from '<…>/exercise'` then `action.contract({ exercise })` — the whole `defineExercise()` value, of which the contract reads the entry named for the action. `x exercise` reads the same entry, so a test and the pass cannot disagree on who acts. An entry declared only inline in a test is one `x exercise` cannot see (it reports the primitive `unreached`). `As of 2026-11-08` |
964
+ | Who acts | the entry's `actor`. Required unless the policy admits an anonymous caller: no entry is `X_CONTRACT_EXERCISE_UNDECLARED`, its `fix:` the `defineExercise({ <name>: { actor } })` line to paste. An entry with no `actor` (`{ input }` alone, which typechecks) runs signed out where the policy admits it, else is `X_CONTRACT_EXERCISE_UNDECLARED` too: `x exercise` picks a role, a contract test has none to pick from (`As of 2026-11-08`). There is no bypass: a policy that denies `actor` makes an unreached run |
965
+ | Inline, for a test's own world | `action.contract({ exercise: { actor, input } })` — one entry, not the app's — only when the test binds rows it seeded itself (`seed('dev').pick(…)`, an `actorFor`): those ids exist in that test alone. Kept and not deprecated; the who-acts of the primitive still belongs in `apps/web/exercise.ts` |
966
+ | A row | `{ actor, input: (drawn) => ({ ...drawn, postId }) }` binds what a generated uuid cannot resolve |
967
+ | Opting out | `{ off: '<reason>' }` as the entry. The reason is in the test's name. A blank one is `X_CONTRACT_EXERCISE_UNDECLARED` (`defineExercise` refuses it as the file loads) |
968
+ | 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. A `t.string.pattern(/…/)` no drawn text meets is walked from the pattern itself (days, months, hex digests, digit ids, base64url tokens), inside its length bounds; the minimal draw keeps `'sample'` when the pattern accepts it (`As of 2026-11-08`) |
967
969
  | Seed | `fnv1a(<action name>)`, never time, so a red run replays: `bun test <file> -t '<name>: handler survives generated input'` |
968
970
  | 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`) |
969
971
  | **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 |
@@ -972,14 +974,14 @@ A legal input that crashes the handler is red in `contract`, the day the action
972
974
  | Pass | the output parsed, or a coded refusal under 500 |
973
975
  | 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` |
974
976
  | Red, `X_CONTRACT_EXERCISE_UNREACHED` | no run reached the handler. `X_TEST_NETWORK_SEALED` counts as unreached, its host in `meta.outcomes` |
975
- | Red, `X_CONTRACT_DRIFT` | the schema refused every draw: a provider refinement the IR does not carry. Bind it with `exercise.input` |
977
+ | Red, `X_CONTRACT_DRIFT` | the schema refused every draw: a provider refinement the IR does not carry, or a `pattern` past the generator's subset (lookaround, backreferences, `\b`, `\p{…}`), named in the cause as `field (must match …)`. Bind it with `exercise.input` |
976
978
 
977
- A query has the same assertion: `orgFeed.contract({ exercise: { actor } })`, named
979
+ A query has the same assertion, read off the same file: `orgFeed.contract({ exercise })`, named
978
980
  `<name>: read survives generated input`. "Reached" is `sql()` building a source, and a read
979
981
  writes nothing, so no transaction wraps it; it refuses a deploy the same way. Both are one loop,
980
982
  `@ultimat3/core`'s `runExercise`: the verdicts above are the same words for a read and a write.
981
- The generators emit the call with an actor (`x g action`, `x g resource`), holding the slice's grant: a hand-written `policy.ts`'s one literal
982
- `can('<perm>')`, or `{ off }` when its grant cannot be read.
983
+ The generators declare an actor (`x g action`, `x g resource`), holding the slice's grant: a hand-written `policy.ts`'s one literal
984
+ `can('<perm>')`, or `{ off }` when its grant cannot be read. As of 2026-11-08 the call is `target.contract({ exercise })` and the actor is the ENTRY the generator inserts into `apps/web/exercise.ts` (`userActor({ id: 'member', orgId, permissions: [grant] })`, its draw's `orgId` bound under a tenancy rule; `x new` writes its example slice's the same way). The file is created from `x new`'s when absent; a file the generator cannot edit — no single `export const exercise = defineExercise({ … })` — is `X_EXERCISE_FILE_UNEDITABLE`, the entry to paste in its `fix:`. `x g query` emits no `.contract()` call: `x exercise` picks a reader by role.
983
985
  `@ultimat3/schema/sample`'s `arbitrary(schema, { seed, run })` is the draw, off the barrel.
984
986
 
985
987
  ## The exercise pass
@@ -992,7 +994,7 @@ shape, same unreached code. `As of 2026-11`.
992
994
  |---|---|
993
995
  | 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 |
994
996
  | 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)) |
995
- | Actor | the first of `x policy explain`'s actors the policy allows, in the primitive's tenant. None allowed is `refused`, by design |
997
+ | Actor | the entry's `actor` when it declares one, seated in the primitive's tenant — and a top-level value of its bound input equal to the org the entry declared becomes that tenant too, so `input: (drawn) => ({ ...drawn, orgId: '<the actor's org>' })` holds in every tenant the pass seeds; otherwise the first of `x policy explain`'s actors the policy allows, in the primitive's tenant (an `{ input }`-only entry keeps this pick and still binds its input). None allowed is `refused`, by design. Its id has a user's shape: a uuid per role × tenant, the same on every run (`exerciseActorId`), never the role's name. Where the database has `x_users` (the framework schema every pass database is built with) each actor the pass acts as gets its row first — `<id>@exercise.invalid`, its tenant, roles, permissions, scopes — so `findUserById(ctx.actor.id)` finds it; a refused insert is `X_EXERCISE_ACTOR_USER_UNSEATED`. `defineExercise(…, { actor: members })` makes the id a seeded row's key instead (`As of 2026-11-08`) |
996
998
  | 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` |
997
999
  | 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
1000
  | A job's run | its own execution alone: enqueued on a queue holding nothing else, run by one worker pass. What it enqueues is left queued, as an action's is — an `enqueue` effect on its record, searched by the tenant proof, never run in its room. Its `statements`, effects, verdict and scale and plan sweeps are its own body's: a job that queues one child per row is flat, a body that reads each row is still `X_QUERY_SCALES_WITH_ROWS`. A child is judged as its own primitive. The crash proof too: its clean and killed runs claim only the job's own row, a child compared as its `enqueue:` effect |
@@ -1000,9 +1002,10 @@ shape, same unreached code. `As of 2026-11`.
1000
1002
  | 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 |
1001
1003
  | 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` |
1002
1004
  | 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 |
1003
- | 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` |
1005
+ | Overrides | `apps/web/exercise.ts`: `export const exercise = defineExercise({ publishPost: { actor, input }, sendInvoice: { off: '<why>' } })`. The same value is what every contract test hands `.contract({ exercise })` ([one declaration](#the-exercise-assertion)). An unknown name is `X_DECLARATION_UNKNOWN`; a blank `off` is `X_CONTRACT_EXERCISE_UNDECLARED` |
1004
1006
  | 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` |
1005
1007
  | 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` |
1008
+ | Drift | a schema no draw satisfies is `unreached`, `X_CONTRACT_DRIFT`; its `override` binds each field the sampler names as a gap with what it needs (`digest: "<must match ^[0-9a-f]{64}$>"`), the contract step's `fix:` the same `input:` (`As of 2026-11-08`) |
1006
1009
  | 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 |
1007
1010
  | 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 |
1008
1011
 
@@ -1038,16 +1041,20 @@ only what came back and what changed.
1038
1041
  | `unproven` | A's own call was not `reached` | no — the record is already `unreached`, which is |
1039
1042
  | `public` | the policy admits an anonymous caller with an empty input: read half skipped, write half kept | no |
1040
1043
  | `public` | a **job** whose B leg opened `crossTenant(reason, fn)` and whose input carried none of A's marks — a declared sweep, `sendDigest` fanning out one delivery per org. The reason is quoted in the verdict; read half skipped, write half kept | no |
1044
+ | `public` | a **job** declaring `tenant: 'none'` whose input carried none of A's marks — a nightly cleanup across every org, input `{ occurrenceMs }`. No org and no id B chose reaches its run, so B's run is A's run: write half skipped (no snapshots taken), read half kept. Handed one of A's ids, crossed as ever. Both this and a declared sweep: both reasons, `; `-joined | no |
1041
1045
  | `unproven` | a query whose A answer held none of A's marks: B's empty answer proves nothing | no |
1042
1046
  | `proven` | otherwise | no |
1043
1047
 
1044
1048
  `off` and `refused` records carry no `tenant`. A job has no policy, so it is `public` only as a
1045
- declared sweep. `crossTenant()` is the one declaration the proof reads (`@ultimat3/entity` tells a
1046
- witness each scope opened, after its capability is proven); it excuses nothing on an action or a
1047
- query, which answer B directly, nor on a job B handed one of A's ids — that is B choosing A's row,
1048
- crossed as ever. A finding prints paths, tables and seeded ids, never a bound value. The fix on each:
1049
- `x exercise <name> --json`, then read or write the row through the repository scoped to
1050
- `ctx.actor`. What this does not walk — a `route` `load`, a `task` — is
1049
+ declared sweep or a `tenant: 'none'` job handed nothing of A's. `crossTenant()` is the one
1050
+ declaration the proof reads (`@ultimat3/entity` tells a witness each scope opened, after its
1051
+ capability is proven); it excuses nothing on an action or a query, which answer B directly, nor on a
1052
+ job B handed one of A's ids — that is B choosing A's row, crossed as ever. `tenant: 'none'` excuses
1053
+ only the write half, and only with no id of A's handed: an action or a query runs as B's actor, so a
1054
+ write to A's row with no id in the input is a statement no tenant predicate scopes, crossed. A
1055
+ finding prints paths, tables and seeded ids, never a bound value. The fix on each: `x exercise
1056
+ <name> --json`, then read or write the row through the repository scoped to `ctx.actor`. What this
1057
+ does not walk — a `route` `load`, a `task` — is
1051
1058
  [Policies and authz](Policies-And-Authz#the-dynamic-tenant-proof).
1052
1059
 
1053
1060
  ### The crash proof
@@ -1062,12 +1069,20 @@ actor, after the tenant proof. Source: `packages/testing/src/exercise-crash.ts`,
1062
1069
  | Runs | committed on the pass's scratch database, after the control. A job body runs outside every transaction, so no rollback scope can hold its writes, and an effect is counted when it commits |
1063
1070
  | Oracle | the effect multiset — row writes through a repository, mail, calls out, enqueues — of each killed run against the clean one. Raw SQL is no effect, and reads are none |
1064
1071
  | A job with no step | one boundary, before the ack: an effect in its body runs again on the retry |
1072
+ | A sweep | one step per row is one kill point when the step is named by the row's id: a job over 1,500 seeded rows costs the runs one over 3 does. Steps named otherwise (a slug, an email) past 32 points are `unproven`, `X_JOB_CRASH_UNBOUNDED` |
1065
1073
 
1066
1074
  | `crash.verdict` | When | Red |
1067
1075
  |---|---|---|
1068
1076
  | `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` |
1069
- | `held` | otherwise; `boundaries` and `reruns` (lost answers — at-least-once, never red) counted | no |
1070
- | `unproven` | the proof itself threw: `reason` | no |
1077
+ | `held` | otherwise; `boundaries` (kills), `crossed` (boundaries they stand for) and `reruns` (lost answers — at-least-once, never red) counted | no |
1078
+ | `unproven` | the proof itself threw, or could not finish in its bound: `reason`, `X_JOB_CRASH_UNBOUNDED: …` for the second | no |
1079
+
1080
+ **Where the time went.** Every record carries `proofMs: { tenant, crash, scale }` beside `ms` (the
1081
+ control's own call); the report carries `phases: { seed, primitives, scale, plans, subject }`, in
1082
+ milliseconds. No per-record plan time: one statement is planned once for every primitive that sent
1083
+ it. The table's `tenant ms` / `crash ms` / `scale ms` columns, then
1084
+ `took 44704 ms — seed 818 · primitives 41099 · … — slowest: primitives` and the five slowest
1085
+ proofs by record, `slowest: job billing.plan-catalog · crash 35692 ms`. `As of 2026-11`.
1071
1086
 
1072
1087
  The human output prints one line per killed boundary,
1073
1088
  `job chargeOrder · killed at mail · X_JOB_CRASH_DIVERGED row:payments:insert 2× (clean 1×)`. The fix
@@ -1085,7 +1100,7 @@ Source: `packages/testing/src/scale-sweep*.ts`, `plan-oracle.ts`, `plan-sweep*.t
1085
1100
  | Sweep | What | Red |
1086
1101
  |---|---|---|
1087
1102
  | scale | every reached primitive once more in a tenant at 2 rows a table, once in one at 8; per statement shape, the counts compared | a shape sent more often at 8: `X_QUERY_SCALES_WITH_ROWS`, fix `preload()` or `expectedQueryLoop('<why>', fn)` |
1088
- | plans | every distinct statement whose first relation is an app table, `PREPARE`d and explained as a generic plan with `enable_seqscan`, `enable_bitmapscan`, `enable_sort` off, in a read-only transaction | `Seq Scan`, or an index scan with no `Index Cond` no `Limit` stops: `X_QUERY_UNINDEXED`. `Sort` under `Limit`: `X_QUERY_SORT_UNSERVED`. Each prints `indexes: [{ on: [...] }]` |
1103
+ | plans | every distinct statement whose first relation is an app table, `PREPARE`d and explained as a generic plan with `enable_seqscan`, `enable_bitmapscan`, `enable_sort` off, in a read-only transaction | `Seq Scan`, or an index scan with no `Index Cond` no `Limit` stops: `X_QUERY_UNINDEXED`. `Sort` under `Limit`: `X_QUERY_SORT_UNSERVED`, on the table its key names. Each prints an `indexes:` entry, one per arm for an `or`; a sort no declarable index serves (an expression, an aggregate, a derived column, keys across tables, mixed directions) or an index the entity already declares prints a rewrite or `expectedSeqScan` instead |
1089
1104
 
1090
1105
  With the planner's scans and sorts disabled it picks one only when no index can answer, so the
1091
1106
  verdict reads the schema on empty tables — no volume, no statistics. Never red: `scale.verdict`