@ultimat3/action 29.3.2 → 30.0.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 CHANGED
@@ -6,8 +6,7 @@ Owns the `action` + `mutator` primitives and five projections. Tier 3. The MCP t
6
6
  ## Boundary
7
7
 
8
8
  - May import: `core`, `schema` (t0), `cache`, `db`, `i18n`, `time` (t1), `entity`, `policy`, `http` (t2).
9
- `entity` is a real edge since 21.0.0 (`record-wire.ts` → `hasEntityRows`/`rowsOf`), downward 3→2.
10
- `db` is one since 2026-10-02, downward 3→1, through **`tx-scope.ts` only** (`currentTx`).
9
+ `entity` through `record-wire.ts`; `db` through **`tx-scope.ts` only**. Full rule: `docs/history/action.md`.
11
10
  - Never import: `query`, `jobs`, `realtime` (sideways), or any tier 4-5 package.
12
11
  - Never re-implement authz, validation or caching — call `policy`, `schema`, `cache`.
13
12
 
@@ -20,12 +19,13 @@ Owns the `action` + `mutator` primitives and five projections. Tier 3. The MCP t
20
19
  | `facade.ts` | the fluent surface — binds each projection to the action, re-implements none |
21
20
  | `mutator.ts` | action + optimistic `.local` twin + authoritative `.server` + `.conflict` |
22
21
  | `registry.ts` | export-name registration, collisions, `describeActions()` |
22
+ | `tool-gate.ts` | a named tool's gate: `mcp` registers, `ai` reads, first wins |
23
23
  | `define-api.ts` | `defineApi({ actions, mutators, queries, llm, jobs, tasks })` — the app's one boot call, and the `Api` type the typed clients are shaped from |
24
24
  | `client-scale-pins.ts` | compile-time pin: `rpc<Api['actions']>` over 300 actions in 100 modules, 100 reads in 50 |
25
25
  | `http.ts` | route projection (`enforcedBy: 'handler'`) + OpenAPI operation |
26
26
  | `openapi.ts` | deterministic OpenAPI 3.1 document |
27
- | `openapi-complete.ts` | the COMPLETE document (`defineApi({ openapi })`: info, servers, security schemes, 401/429 by the ROUTE's `meta.auth`) and a bearer mount's own cut |
28
- | `http-path.ts` | where an action is served: the app's `pathStyle` + a per-action `http.path` pin, read lazily by every projection |
27
+ | `openapi-complete.ts` | the COMPLETE document (`defineApi({ openapi })`: info, servers, security schemes, 401/429 by the ROUTE's `meta.auth`, a declared `rateLimit` or `meter`) and a bearer mount's own cut |
28
+ | `http-path.ts` | where an action is served: the app's `pathStyle` + a per-action `http.path` pin, read lazily by every projection; the pin's `:param` template + `method` (`ACTION_HTTP_METHODS`), `assertPinnedHttp`, `filledRoute`, `pathParametersOf`, `actionHttpPinOf` |
29
29
  | `path-style-miss.ts` | `explainActionPathMiss` — `@ultimat3/http`'s `hooks.explainMiss`: a POST under the style this app does not serve is `X_CONTRACT_DRIFT` naming the one it does |
30
30
  | `api-declaration.ts` | `defineApi({ http, openapi })` held process-wide for the boot and `x manifest` |
31
31
  | `errors-http.ts` | the three HTTP-declaration refusals |
@@ -33,11 +33,18 @@ Owns the `action` + `mutator` primitives and five projections. Tier 3. The MCP t
33
33
  | `client.ts` | typed RPC client (browser-safe: no server imports) — dispatches through core's `clientTransport` |
34
34
  | `record-wire.ts` | the record envelope on the HTTP projection: `carriesRecords` (from the output schema), the enveloped 200, and its OpenAPI shape. Server-only — `client.ts` never imports it |
35
35
  | `wire-issues.ts` | the ONE reader of a problem document's `issues` member — an untrusted array back into `@ultimat3/schema`'s `ValidationIssue` shape |
36
+ | `action-stream.ts` | `streamingDef`: a `stream`-declaring `handle` (an async generator) DRAINED, each chunk parsed by `stream` (`X_ACTION_STREAM_INVALID`, `errors-stream.ts`); `chunksRead()` |
37
+ | `http-stream.ts` | `accept: text/event-stream` → `event: chunk`… then ONE `done` or `error`; a refusal before the handler is the ordinary problem response |
38
+ | `audit-subject-target.ts` | `auditSubjectTarget()`: `x_audit` as a subject-erase target — overwrites who acted and the input, never deletes the row |
36
39
  | `transition.ts` | `transition()`: a MUTATOR factory over one entity column's state machine. Declares no error code — entity's three propagate |
37
40
  | — | opt-in flight control is **`@ultimat3/core`**'s `client-flight.ts` + `client-wire.ts`, imported from core — `src/index.ts` re-exports only the `ClientFlight`/`ClientRetry` types. There is no local copy and must not be one |
38
41
  | `job-handle.ts` | the `.job()` projection: an action as a queueable payload. **Not** consumed by `@ultimat3/jobs` — see Invariants |
39
42
  | `contract-test.ts` | assertions `x g action` emits |
40
- | `sample-input.ts` | a value `input:` accepts, from its own IR — what makes the policy assertion reach a policy |
43
+ | `exercise.ts` | the fourth contract assertion: run loop, verdict, `exerciseContract` |
44
+ | `exercise-errors.ts` | the three `X_CONTRACT_*` classes, off-socket |
45
+ | `declared-errors.ts` | the wrap `invoke` puts around `handle` and `row`, the ONLY judged throws. `errors:` is checked, sorted and frozen at `action()` |
46
+ | `declared-errors-contract.ts` | `.contract()`'s source scan |
47
+ | `openapi-errors.ts` | core's builder bound to http's status and `problemTypeFor` |
41
48
  | `idempotency.ts` | the store SEAM: types, the installed-store slot, the scope declaration + `assertIdempotencyScope`, and `withIdempotency` — the replay-or-run gate |
42
49
  | `idempotency-key.ts` | the namespaced key — action + actor + the caller's key, as one JSON tuple — and the refusal of one that names no request |
43
50
  | `idempotency-memory.ts` | the process default: bounded, swept, `scope: 'process'` |
@@ -45,8 +52,9 @@ Owns the `action` + `mutator` primitives and five projections. Tier 3. The MCP t
45
52
  | `idempotency-postgres.ts` | the SHARED store — one table, one `insert … on conflict` |
46
53
  | `policy-gate.ts` | **the only** runtime edge to `@ultimat3/policy` (`errors.ts` takes `SurfaceDenial` as a type, which erases) |
47
54
  | `cache-gate.ts` | the post-COMMIT bust — **the only** file that calls `invalidateTags` |
48
- | `tx-scope.ts` | the open transaction — **the only** file that imports `@ultimat3/db` (`tx-scope.test.ts`): `openCommitScope` (the bust) and `liveTransaction` (the settle) |
49
- | `rate-limit-gate.ts` | **the one place** a declared `rateLimit:` is spent, and the installed-store slot |
55
+ | `tx-scope.ts` | the open transaction — **the only** file that imports `@ultimat3/db` (`tx-scope.test.ts`): `openCommitScope` (the bust), `liveTransaction` (the settle), `completeTransaction`, and `inRolledBackScope`, the exercise's always-rolled-back scope and the memory transaction it opens always |
56
+ | `rate-limit-gate.ts` | **the one place** a declared `rateLimit:` is spent (a `limit` resolved per call from `ctx`, through `toBucket`) |
57
+ | `meter*.ts`, `errors-meter.ts` | `meter:` — declaration (`meter.ts`), UTC window, store seam + memory + Postgres (`x_meter`), and `meter-gate.ts`, **the one place** it is drawn |
50
58
  | `request-deadline.ts` | `requestDeadlineMs` — the app's `requestTimeoutMs`, resolved by http's own `defineHttpConfig` |
51
59
  | `audit.ts` | the audit seam's TYPES, re-exported from `@ultimat3/core` (shared with `query`); the slot (`setAuditSink`) is core's alone |
52
60
  | `audit-memory.ts` | the process default: a bounded ring that DROPS, and counts what it dropped |
@@ -60,7 +68,8 @@ Owns the `action` + `mutator` primitives and five projections. Tier 3. The MCP t
60
68
 
61
69
  - Every surface goes through `invoke`: the policy's actor half (`guardActionBeforeInput`, 403 before the
62
70
  parse), parse input, evaluate policy, handle, parse output. A second
63
- execution path is the one unforgivable change here.
71
+ execution path is the one unforgivable change here. A 5xx keeps a bundle only under core's
72
+ `withFailureCapture` (`execute`); off, it runs bare.
64
73
  - **An explicit `ctx` is INSTALLED, never merely passed** — the ambient context (which
65
74
  `@ultimat3/entity`'s tenant guard reads) must be the identity `guardAction()` decided about.
66
75
  `invoke-context.test.ts` asserts ambient, `options.actor` and `options.ctx` are one caller.
@@ -81,25 +90,11 @@ Owns the `action` + `mutator` primitives and five projections. Tier 3. The MCP t
81
90
  - No policy at registration → `X_ACTION_POLICY_MISSING`. No exceptions, no flag.
82
91
  - **`registerAction` guards the derived PATH as well as the name** (`X_ACTION_PATH_DUPLICATE`; a
83
92
  second index cleared by `resetActions`).
84
- - **A path is the app's `pathStyle` or the action's pin, never a third answer** (`http-path.ts`).
85
- `'resource'` stays the default (changing it moves every URL). `defineApi` sets the style FIRST and
86
- `configureActionPathStyle` re-derives every seated action (the module scan may have seated them
87
- under the default). `derivePath(name)` is pin-aware through `installPinLookup` — handed down by
88
- `registry.ts`, never imported up (cycle through `action.ts`). `rpc()` cannot see a pin;
89
- `action.client()` holds it.
90
- - **The style is stated ONCE, on the server.** A browser never restates it: the document carries
91
- `<meta name="ultimate-path-style">` (render's `clientPathStyleTags`, written only for a
92
- non-default style) and core's `actionPath(name)` reads it for every caller naming none — `rpc`,
93
- realtime's `useMutation` and the outbox replay. `client.ts` passes `options.pathStyle` through
94
- and adds NO default of its own. `ClientOptions.pathStyle` is for a caller with no document; a
95
- wrong one is answered `X_CONTRACT_DRIFT` (404) by `explainActionPathMiss`, never
96
- `X_ROUTE_NOT_FOUND`. A pinned action and a path under the SERVED style are never explained.
97
- - **`Merge` in `define-api.ts` never recurses over the module list.** `Head & Merge<Rest>` is not
98
- a tail call: it cost one instantiation level per MODULE and answered TS2589 at the 48th entry of
99
- one list, whatever each exported (#534). The list is read as a union and intersected in one
100
- step. `client-scale-pins.ts` is the build error; `client-scale.contract.test.ts` compiles real
101
- `action()`/`query()` modules through `defineApi`'s `const` inference; `@ultimat3/query` pins
102
- `QueryClient` itself.
93
+ - **A path is the app's `pathStyle` or the action's pin, never a third answer** (`http-path.ts`). A pin may be a template: `derivePath(name)` refuses one, `actionHttpPath(name, params)` fills it.
94
+ `'resource'` stays the default. Full rule: `docs/history/action.md`.
95
+ - **The style is stated ONCE, on the server**; a browser reads `<meta name="ultimate-path-style">` through core's `actionPath(name)`. Full rule: `docs/history/action.md`.
96
+ - **`Merge` in `define-api.ts` never recurses over the module list.** `client-scale-pins.ts` is the
97
+ build error. Full rule: `docs/history/action.md`.
103
98
  - **`defineApi({ openapi })` opts into the complete document; absent, `openapi.json`'s bytes are
104
99
  unchanged** — an upgrade must not make a committed contract stale by itself.
105
100
  - Registration names the action the app exported, in place. Naming an already-named action is the
@@ -118,15 +113,29 @@ Owns the `action` + `mutator` primitives and five projections. Tier 3. The MCP t
118
113
  built from the registrars' results, never the modules' exports.
119
114
  - `src/index.ts` re-exports `t` from `@ultimat3/schema` **verbatim** (`index.test.ts` asserts identity).
120
115
  - **`transition()` is a mutator factory that decides nothing about the machine**: entity's three
121
- codes propagate; `from` is REQUIRED (it is the UPDATE's predicate); `conflict: 'server-wins'` fixed;
122
- `audit` off unless declared; `id` is the key `output.id` declares (`keySchemaOf`), never a fixed
123
- `t.uuid`; `row` goes to `mutator()` as declared, never loaded here (#687); **the rule's reads
124
- are pinned in the move** (#702, `transition-observe.ts`: `def.row`, `def.handle` share `input`).
125
- - **A lookup table is read with `Object.hasOwn`** (`IRREGULAR` in `naming.ts`, `BY_FORMAT` in
126
- `sample-input.ts`) — caller- or provider-supplied keys.
116
+ codes propagate; `from` is REQUIRED (it is the UPDATE's predicate); **the rule's reads are pinned in
117
+ the move** (`transition-observe.ts`). Full rule: `docs/history/action.md`.
118
+ - **A lookup table is read with `Object.hasOwn`** (`IRREGULAR` in `naming.ts`).
119
+ - **`handlerReached(target)` in `invoke.ts` is a private per-declaration counter, never on the
120
+ barrel.** `exercise.ts` reads it to tell a run that reached `handle` from one stopped before it.
121
+ - **The exercise has no bypass.** A coded 5xx, `X_OUTPUT_INVALID` or an uncoded
122
+ throw is `X_CONTRACT_DEFECT`. Full rule: `docs/history/action.md`.
127
123
 
128
124
  ## Invariants — the wire
129
125
 
126
+ - **A streaming action is still ONE action through `invoke`** (`StreamAction`, `action({ stream })`):
127
+ `invoke` stores the declaration draining, so MCP, `.job()`, `.as()` and JSON get the RETURN value.
128
+ Only the first draining handler of THE declaration a stream was opened for gets the sink;
129
+ `chunksRead()` is `true` there alone, so a costly chunk is produced only when read.
130
+ - **`.stream(input)` is on every typed-client method; the TYPE offers it only where the action
131
+ streams** (`StreamClientMethod`; the `rpc` proxy cannot see a declaration). `POST` through core's
132
+ `clientStream`, never `EventSource`; never retried (`retry` is not an option); a JSON answer is
133
+ the output.
134
+
135
+ - **A `:param` pin's values come from the router's decoded params, merged into the input before
136
+ `invoke`** (`toRoute` → core's `mergePathParams`). A body naming the key differently is
137
+ `X_HTTP_PATH_INPUT_CONFLICT`, never overridden. `registerAction`'s duplicate index keys on method
138
+ + path. `http-path-params.test.ts`.
130
139
  - **Every browser call goes through core's `clientTransport`**: `client.ts` hands it the method, the
131
140
  URL (core's `actionPath`; `naming.ts` re-exports core's path helpers), body, headers, signal, key,
132
141
  flight and `retry`. Only action-specific hooks ride in: `onResponse` (build-id check,
@@ -141,14 +150,9 @@ Owns the `action` + `mutator` primitives and five projections. Tier 3. The MCP t
141
150
  - **`conflict` is core's `ConflictPolicy`, over ROWS**; `custom(merge)` builds
142
151
  `{ kind: 'custom', merge(localRow, serverRow) }`; core's `resolveConflict` is the one resolver.
143
152
  - **`X_INPUT_INVALID` carries the rejections twice**: the line in `cause`
144
- (`formatIssues(issues).join('; ')`, pinned by `validate.test.ts`) and `meta.issues` structured.
145
- The rendering stays out of `InputInvalidError` (browser-reachable). **`toValidationIssues`** keeps
146
- four members, never a library's raw issue (it may carry the value). `issuesFromWire` rebuilds
147
- member by member, all-or-nothing, bounded by `MAX_WIRE_ISSUES`. **`X_OUTPUT_INVALID` keeps the line alone.**
153
+ (`formatIssues(issues).join('; ')`, pinned by `validate.test.ts`) and `meta.issues` structured. Full rule: `docs/history/action.md`. **`X_OUTPUT_INVALID` keeps the line alone.**
148
154
  - **The client keeps the server's code and marks it remote** (`RemoteActionError`, `meta.origin:
149
- 'remote'`), only for an `X_SCREAMING_SNAKE` code. It never synthesizes a docs URL: the server's
150
- `http(s)` `docs`/`type` (as an ordered pair), else this build's registered link, else
151
- `ERROR_DOCS_URL`.
155
+ 'remote'`), only for an `X_SCREAMING_SNAKE` code. It never synthesizes a docs URL. Full rule: `docs/history/action.md`.
152
156
  - **Both clients inject `traceparent`** before the caller's headers; an incomplete span sends nothing.
153
157
  - `serializeOpenApi` output is byte-stable: sorted keys, sorted registry, no clock. `client.ts` stays
154
158
  free of server imports.
@@ -158,28 +162,14 @@ Owns the `action` + `mutator` primitives and five projections. Tier 3. The MCP t
158
162
  payloads.
159
163
  - **`tagKeys` is `@ultimat3/cache`'s and `toBucket` is `@ultimat3/http`'s** (imported, never
160
164
  re-exported; `toBucket` raises `X_RATE_LIMIT_INVALID`). Never restore a local copy.
161
- - **`rateLimit:` is spent ONCE, inside `invoke`** (`rate-limit-gate.ts`), so HTTP, MCP, the agent
162
- tool and `.job()` share one bucket; `surface: 'server'` spends nothing. `toRoute` sets
163
- `rateLimitedBy: 'handler'` and NO `meta.rateLimit`/`rateLimitBucket` — the stage spends neither
164
- the action's bucket (the old HTTP-only point) nor `default`. Key `action:<name>|<subject>`.
165
- HTTP gets `RateLimit-*` via `onRateLimit` → http's `publishRateLimit`. The store is
166
- `@ultimat3/http`'s installed one, adopted by the boot on every role. Spent after
167
- `guardActionBeforeInput`, BEFORE the parse and `def.row()` (an input/row-denied flood is refused). A key
168
- the idempotency store holds spends nothing (replay); a reservation `withIdempotency` CREATES that
169
- the peek did not pay for spends in `beforeRun`, released on refusal. A job run is charged to its
170
- org, else `job:unattributed`. A surface's `clientAddress` is inherited by nested `invoke`s
171
- (`withCallerAddress`); a `'job'` invoke opens a fresh frame (`withJobFrame`) so no visitor's
172
- address follows work into the worker, and calls nested in it are the job's.
173
- `rate-limit-surfaces.test.ts`. The computed rate must be finite and `limit` at least one token.
174
- - **`deprecated:` is a compat WINDOW** — headers on every response including failures, a
175
- `rel="successor-version"` link via `derivePath`, `deprecated: true` in OpenAPI,
176
- `deprecated_calls_total`. Rendered ONCE at projection (`X_ACTION_DEPRECATION_INVALID` at mount).
177
- Twinned in `@ultimat3/query`.
165
+ - **`rateLimit:` is spent ONCE, inside `invoke`** (`rate-limit-gate.ts`), so HTTP, MCP, the agent tool and `.job()` share one bucket; `surface: 'server'` spends nothing. Full rule: `docs/history/action.md`.
166
+ - **`meter:` is drawn ONCE, inside `invoke`, on every surface (`server` too)**, after the policy, before `handle`; under a key only in `beforeRun`, so a replay draws nothing. One conditional increment, never read-then-write. Periods are UTC. `meter-surfaces.test.ts`. Subject: org, else actor, else `job:unattributed` inside a job frame (`inJobFrame()`, `rate-limit-gate.ts` — the limiter's split, one answer), else `anonymous`. `meter-memory.ts` evicts a counter whose period ENDED before a live one, then oldest start.
167
+ - **`deprecated:` is a compat WINDOW**, rendered ONCE at projection
168
+ (`X_ACTION_DEPRECATION_INVALID`); twinned in `@ultimat3/query`. Full rule: `docs/history/action.md`.
178
169
  - **The span wraps `execute` whole** (including `def.row()`), with bounded attributes plus the
179
170
  namespaced idempotency key (never a metric label). `telemetry.test.ts`.
180
171
  - **`policyCapability` is a display label; `policyPermissions` (`ActionDescriptor.permissions`) is
181
- what a report matches on** (a `not()` clause contributes its permissions). Both are
182
- `@ultimat3/policy`'s, imported from there.
172
+ what a report matches on**; both `@ultimat3/policy`'s.
183
173
  - **MCP exposure is core's `isMcpExposed`** (`describeAction`, `x-ultimate.mcpTool`); **a tool's
184
174
  NAME is the export name verbatim** (`mcp-surface.test.ts`). `ActionFact.mcp` in the app manifest
185
175
  carries only `{ expose, description? }`.
@@ -199,10 +189,10 @@ Owns the `action` + `mutator` primitives and five projections. Tier 3. The MCP t
199
189
  `bun build --target=browser --minify`, one entry importing from `@ultimat3/action` — **the ONE
200
190
  table for these figures** (`packages/core/CLAUDE.md` points here):
201
191
 
202
- | Entry | `As of 2026-10-01` |
192
+ | Entry | `As of 2026-11-06`, Bun 1.4.4, one entry over the working tree |
203
193
  |---|---|
204
- | `rpc` | 19,671 B (+181 B for the document's path-style stamp) |
205
- | `rpc` + core's `clientFlight` | 25,954 B |
194
+ | `rpc` | 29,291 B: `.stream` (core's `clientStream`, through `dispatchStream`) +1,746 B over the 27,545 B before it; pinned paths (wave 5b) `fillWire` +758 B, `renderedActionPin` +619 B (each stubbed out) |
195
+ | `rpc` + core's `clientFlight` | 38,054 B |
206
196
 
207
197
  - **`sideEffects` is `["./src/error-titles.ts"]`**, never `false` (drops its bare imports): the
208
198
  titles ride into every barrel chunk (`problemError` decodes by code), `errors.ts`'s classes do
@@ -214,6 +204,7 @@ Owns the `action` + `mutator` primitives and five projections. Tier 3. The MCP t
214
204
  - **Everything `withIdempotency`'s `run` throws is treated as possibly-committed**: the reservation
215
205
  is SETTLED as a failure and a retry replays it; `release()` is only for a pre-handler failure. A
216
206
  `settle` that refuses leaves the record in flight. `idempotency-failure.test.ts`.
207
+ - **A keyed run is owed its end**: `completeTransaction()`, then `runToCompletion(run)` under `idempotencyCeilingMs()` (`X_IDEMPOTENCY_RUN_CEILING`); `invoke` reads the handler's `ctx` inside it. `idempotency-orphan.test.ts`.
217
208
  - **A record belongs to ONE CALLER**: the key is
218
209
  `JSON.stringify([action, actor.kind, actor.id, actor.orgId ?? null, key])` — a JSON tuple, never a
219
210
  joined string. A blank header is `X_IDEMPOTENCY_KEY_INVALID` (4xx, before the handler), never "no
@@ -225,14 +216,9 @@ Owns the `action` + `mutator` primitives and five projections. Tier 3. The MCP t
225
216
  `settle(key, value, reservationId, redacted)` / `fail(key, failure, reservationId)` /
226
217
  `release(key, reservationId)`. A fenced no-op is never thrown.
227
218
  - **An answer rests REDACTED** (#591): `withIdempotency` passes the store `restingAnswer(value)`
228
- (core's `isRedactedKey` + `isSecret` — never a second rule) and `redacted` as `settle`'s
229
- REQUIRED 4th argument; both stores keep it (`x_idempotency.redacted`, memory's record field).
230
- A flagged replay is `X_IDEMPOTENT_REPLAY_REDACTED`, decided by the FLAG, never by scanning for
231
- `[redacted]`. Plain data is the same reference, replayed as before; anything else that
232
- `JSON.stringify` would run app code on (`toJSON`, `Map`/`Set`, class instances, `Date`
233
- subclasses) is redacted FAIL-CLOSED, and a getter is read once. `keepsRedaction: true` is a
234
- required store declaration (`type-pins.ts`: a pre-25 store is a build error — a required
235
- parameter alone is not). A reclaim and a `fail` reset the flag.
219
+ (core's `isRedactedKey` + `isSecret`) and `redacted` as `settle`'s REQUIRED 4th argument. A
220
+ flagged replay is `X_IDEMPOTENT_REPLAY_REDACTED`, by the FLAG, never by scanning. Full rule: `docs/history/action.md`.
221
+ - **A finished record skips the row half; a stale peek judges it in `beforeRun`** (`idempotency-row.test.ts`). **A `bigint` answer settles failed** (`X_IDEMPOTENCY_OUTPUT_UNSERIALIZABLE`).
236
222
  - **A stored status is NARROWED** (`isIdempotencyStatus`; unknown is
237
223
  `X_IDEMPOTENCY_STATUS_UNKNOWN`), never cast.
238
224
  - **Where records live is DECLARED and refused at registration**: `IdempotencyStore.scope` vs
@@ -241,56 +227,37 @@ Owns the `action` + `mutator` primitives and five projections. Tier 3. The MCP t
241
227
  - **The memory store is bounded; `in-flight` records are the last evicted.** Never an LRU.
242
228
  - **`postgresIdempotencyStore` is the shared store** over `@ultimat3/core`'s structural `PgExecutor`; the
243
229
  reservation is ONE `insert … on conflict` statement. The CLI boot installs it.
244
- - **Inside a transaction the SETTLE commits with the write, on BOTH stores** (`liveTransaction()`):
245
- Postgres on the handler's connection, memory at `onCommit`. The reservation and `fail` stay
246
- immediate. A tx-bound settle that matches no record THROWS `X_IDEMPOTENCY_RESERVATION_LOST` (the
247
- rollback of an attempt a retry replaced); the autocommit one only logs. Only a tx-bound
248
- `in-flight` record is reclaimed past the deadline — never an autocommit one.
249
- `idempotency-parity.test.ts` (memory + PGlite), `idempotency-postgres.live.test.ts`.
230
+ - **Inside a transaction the SETTLE commits with the write, on BOTH stores** (`liveTransaction()`). `idempotency-parity.test.ts`. Full rule: `docs/history/action.md`.
250
231
  - **The deadline is `reclaimAfterMs: () => number`, REQUIRED on the Postgres store** and read per
251
- reservation — the app's `requestTimeoutMs` through `requestDeadlineMs` (`request-deadline.ts`,
252
- http's own default, never a number here). **`origin: () => object` is required too**: a
253
- transaction on another database settles on the pool. A FINISHED scope's handle binds nothing.
232
+ reservation (`requestDeadlineMs`, never a number here); `origin: () => object` is required too. Full rule: `docs/history/action.md`.
254
233
 
255
234
  ## Invariants — cache and audit
256
235
 
257
236
  - **The post-commit bust never fails the write** — `cache-gate.ts` (the only `invalidateTags` caller)
258
237
  absorbs a refusing fan-out and logs through core's `logger`, never rendering the tags. A replay
259
- skips the bust.
238
+ skips the bust. **A started handler busts, thrown or not.**
260
239
  - **The bust waits for the ROOT commit** when a transaction is open (`openCommitScope().onCommit`):
261
- dropped on rollback, never fired before the rows are durable. A straggler past its scope gets
262
- `DbTx.onCommit`'s own answer, as entity's row observer does. `cache-gate-tx.test.ts` (PGlite).
240
+ dropped on rollback, never fired before the rows are durable. Full rule: `docs/history/action.md`.
263
241
  - **The OpenAPI contract assertion reads the registry-wide document** and compares `operationId`
264
242
  — one built from the action alone could not fail. **`Problem` is `toProblem`'s members**, no
265
- `code` pattern (`openapi-problem.test.ts`).
243
+ `code` pattern (`openapi-problem.test.ts`), checked both ways; `retry` is an enum of core's `ERROR_RETRY_KINDS`.
266
244
  - **The policy contract test asserts `ActionDeniedError` and sends valid input** (`sampleInput` from
267
- the input IR). Only `X_INPUT_INVALID` (and `X_AUDIT_SINK_MISSING`, the one refusal before the parse)
268
- becomes `X_CONTRACT_DRIFT`; everything else keeps its own code; a non-`UltimateError` is rethrown.
269
- `contract-test.contract.test.ts`.
245
+ the input IR). Full rule: `docs/history/action.md`.
270
246
  - **The audit seam ships the mechanism and none of the row**: `audit: true` wraps `execute`, so a
271
- DENIED attempt is recorded. No audit entity, retention, hash chain or "who" convention. The
272
- vocabulary matches `@ultimat3/admin`'s by name (tier 5, no edge); admin's `AuditSink` is a known
273
- duplicate that unifying would need core for.
274
- - **Records carry `name` + `primitive`, required** (`type-pins.ts`); no `action` alias since 25.0.0.
275
- The `x_audit.action` column holds `name`.
276
- - **The memory sink DROPS** — a ring at `DEFAULT_MAX_AUDIT_RECORDS`, oldest first, counting `dropped`;
277
- no spelling of "unbounded".
278
- - **A durable sink writes what `audit-input.ts` allows**: input redacted through core's
279
- `isRedactedKey` (the table `defineEnv({ secret: true })` extends) plus `isSecret` by value, and
280
- always JSON-representable (named markers, cycle detection; `toJSON` never called).
247
+ DENIED attempt is recorded. No audit entity, retention, hash chain or "who" convention. Full rule: `docs/history/action.md`.
248
+ - **`invoke` masks `trace.input` with `maskSensitive(def.input, …, REDACTED)` only when `audit: true`**;
249
+ `registerAction` calls `assertNoSensitiveOutput` on `output`. `wire-issues.ts` and
250
+ `PROBLEM_SCHEMA` carry `rule` and `params` (the four members stay `required`).
251
+ - **A durable sink writes what `audit-input.ts` allows**. Full rule: `docs/history/action.md`.
281
252
  - **The `Ctx` is never walked**: `postgresAuditSink` reads an allow-list (`requestId`, `traceId`,
282
253
  `locale`, `tz`, `buildId`, `role`, actor `id`/`kind`/`orgId`/`onBehalfOf`) and keeps
283
254
  `failure.code`, never the error.
284
255
  - **`x_audit` ships no purge**, deliberately. **`SQL_AUDIT_INSERT` is positional**, pinned by
285
256
  `audit-parity.test.ts` with a distinct value per field. Only `*_TABLE` DDL is exported
286
257
  (`X_SQL_EXPORT_UNREAD`).
287
- - **The two failure policies are opposites**: `X_AUDIT_SINK_MISSING` before the input parse (no
288
- logger-backed default sink); a sink refusing an ALLOWED record is `X_AUDIT_SINK_FAILED`, whose `fix:`
289
- branches on `record.idempotencyKey !== null` (`meta.replayable`); a sink refusing a denied or failed
290
- record is logged (`audit.sink.failed`) and the original error still reaches the caller.
258
+ - **The two failure policies are opposites**: `X_AUDIT_SINK_MISSING` before the input parse; a sink refusing an ALLOWED record is `X_AUDIT_SINK_FAILED`; refusing a denied or failed one is logged. Full rule: `docs/history/action.md`.
291
259
  - **`auditSettled` sits outside the `catch`**, and **`auditOutcomeFor` is TOTAL** (fails closed to
292
260
  `failed`); `audit.test.ts` asserts the caller's throwable by identity.
293
- - **`json-schema.ts`'s refusal names `introspect()`** (`normalizeJsonSchema`, test-only export).
294
261
 
295
262
  ## Commands
296
263
 
package/README.md CHANGED
@@ -3,6 +3,8 @@
3
3
  One declaration → six artifacts. Five are projected here; the MCP tool is `@ultimat3/mcp`'s
4
4
  (tier 4), from the same declaration.
5
5
 
6
+ `bun add @ultimat3/action` · guide: [Actions](https://github.com/developerz-ai/ultimate/wiki/Actions)
7
+
6
8
  | # | Artifact | Reach it with | Guarantees |
7
9
  |---|---|---|---|
8
10
  | 1 | HTTP route `POST /api/<resource>/<verb>` | `toRoute(publishPost)` — the server mounts it | policy + validation + idempotency + invalidation, non-optional |
@@ -10,7 +12,7 @@ One declaration → six artifacts. Five are projected here; the MCP tool is `@ul
10
12
  | 3 | Typed RPC client | `publishPost.client({ baseUrl })` / `rpc<Api['actions']>()` | server typo = compile error in Solid |
11
13
  | 4 | MCP tool | `toolFrom(publishPost)` from `@ultimat3/mcp` — what `tools/list` serves | *identical* policy evaluation to the route: it calls this package's `invoke` with `surface: 'mcp'` |
12
14
  | 5 | Job handle | `publishPost.job()` | enqueue durable work, no rewrite |
13
- | 6 | Contract tests | `publishPost.contract()` | garbage rejected, anonymous denied, spec present |
15
+ | 6 | Contract tests | `publishPost.contract({ exercise: { actor } })` | garbage rejected, anonymous denied, spec present, the handler survives generated legal input |
14
16
 
15
17
  ## The fluent surface
16
18
 
@@ -26,7 +28,7 @@ await publishPost.as(actor, { postId }) // run as someone, one execution
26
28
  publishPost.openapi() // OpenAPI operation
27
29
  publishPost.client({ baseUrl }) // typed RPC method
28
30
  publishPost.job() // durable-work handle
29
- publishPost.contract() // the three generated assertions
31
+ publishPost.contract({ exercise: { actor } }) // the four generated assertions
30
32
  ```
31
33
 
32
34
  There is no `.tool()` (`As of 25.0.0`): it was a second MCP projection built a tier below the
@@ -167,6 +169,25 @@ Importing `rpc` alone from this package is **19,671 B** minified for the browser
167
169
  what the delta is). `ClientFlight` is a TYPE inside `client.ts` and never a value, which is what
168
170
  keeps the second number off the first caller's bill.
169
171
 
172
+ ## Streaming — `stream:`
173
+
174
+ An action may answer in pieces before it answers. `stream: <chunk schema>` beside an
175
+ `async *handle` whose yields are chunks and whose **return** is the output. One route, chosen by
176
+ `accept`; no channel, no `EventSource`, no second primitive.
177
+
178
+ | Caller | Gets |
179
+ |---|---|
180
+ | `accept: text/event-stream` on its route | `event: chunk` per yield (each parsed by `stream` first), then one `done` (the output) or `error` (the problem document); `x-accel-buffering: no`, never compressed |
181
+ | JSON, MCP, `.job()`, `.as()`, a direct call | the generator drained: the output alone, one contract |
182
+ | the typed client | `api.sayWords.stream(input)` — an `AsyncGenerator` of chunks whose return is the output, through core's `clientStream` (POST, the page's credentials, the same problem decode). Typed on a stream-declaring method only (`StreamClientMethod`); never retried |
183
+ | the handler itself | `chunksRead()` — `true` only while a caller reads the chunks; `llm()` streams its one-attempt turn then, and is the repairing call otherwise |
184
+
185
+ A refusal before `handle` starts (policy, input, rate limit) is the ordinary problem response, not
186
+ a stream. A chunk its schema refuses, or `stream:` beside a handle that is not a generator, is
187
+ `X_ACTION_STREAM_INVALID`: one `error` frame, then close. A client that leaves cancels the body and
188
+ aborts `ctx.signal`. Every typed-client method carries `.stream`, because the name-only `rpc` proxy
189
+ cannot see a declaration: +2,064 B per island that uses `rpc` (measured 2026-11-06).
190
+
170
191
  ## Path derivation
171
192
 
172
193
  One rule per app, declared once — `defineApi({ http: { pathStyle } })` — plus a per-action pin.
@@ -183,9 +204,16 @@ One rule per app, declared once — `defineApi({ http: { pathStyle } })` — plu
183
204
  follows for reads. The OpenAPI tag becomes the resource the POLICY names (`can('cases:create')`
184
205
  → `cases`), never a noun parsed from `In`.
185
206
  - **`http: { path }` pins one action's URL** whatever the style — for a webhook a vendor holds:
186
- `action({ …, http: { path: '/api/webhooks/wompi' } })`. Static, lowercase, no params, never
187
- `/_x` (`X_ACTION_HTTP_PATH_INVALID`). A pin colliding with another path is
207
+ `action({ …, http: { path: '/api/webhooks/wompi' } })`. Lowercase, never `/_x`
208
+ (`X_ACTION_HTTP_PATH_INVALID`). A pin colliding with another path on the same method is
188
209
  `X_ACTION_PATH_DUPLICATE` at boot.
210
+ - **A pin may carry `:param`s and a method** — `http: { path: '/orders/:id/refund', method:
211
+ 'PATCH' }` (`ACTION_HTTP_METHODS`: `POST` default, `PUT`, `PATCH`, `DELETE`). Each `:param` is a
212
+ required `t.string` key of `input:`; the URL's value joins the body before `invoke`, and a body
213
+ naming it differently is 400 `X_HTTP_PATH_INPUT_CONFLICT` (core's). `actionHttpPath(name, { id })`
214
+ fills one, `derivePath(name)` refuses it, `action.client()` fills it from the input, and
215
+ `actionHttpPinOf(action)` is the manifest's `{ path, method }`. OpenAPI: `{id}` `in: path`, the
216
+ key out of the body component, the operation under its verb.
189
217
  - Every server-side reader agrees: `toRoute`, `toOpenApiOperation`, `describeAction`,
190
218
  `derivePath(name)` (pin-aware) and `actionHttpPath(action | name)`. A pinned action is called
191
219
  through `action.client()`, which holds the pin.
@@ -231,9 +259,9 @@ export const api = defineApi({
231
259
  // (cookie; bearer when a mount exists), 401 + 429 (Retry-After) on every authenticated
232
260
  // operation, RateLimit-* on rate-limited ones. Absent, the bytes are what they were.
233
261
  openapi: {
234
- title: 'Notificado API',
262
+ title: 'Acme API',
235
263
  version: '1.0.0',
236
- servers: [{ url: 'https://www.notificado.co' }],
264
+ servers: [{ url: 'https://www.example.com' }],
237
265
  sessionCookie: 'session',
238
266
  },
239
267
  });
@@ -297,7 +325,9 @@ path) and `.as()`/jobs. An audited action's denied attempt still records the par
297
325
  `cache: { invalidates }` fans out **after** the handler commits, so it never fails it: a
298
326
  fan-out that refuses — an undeclared tag, `X_CACHE_TAG_UNKNOWN` — is one
299
327
  `action.invalidate.failed` log line and the entries expire by TTL. A replayed idempotent
300
- call busts nothing; the first call already did.
328
+ call busts nothing; the first call already did. A handler that **threw** busts too: it may have
329
+ committed, which is why `idempotent` records the same throw. A refused input or policy ran no handler
330
+ and busts nothing.
301
331
 
302
332
  **After the COMMIT, not after the handler** (`As of 2026-10-02`). An action invoked inside a
303
333
  `withTransaction` — an app's own unit of work, or another action's — hands its bust to the ROOT
@@ -446,6 +476,7 @@ call (`surface: 'server'` — a service, a seed, a test) is app code, not a call
446
476
  | an idempotent replay | spends nothing: the stored answer is served, never a 429 |
447
477
  | who a job run is | its actor, else its tenant (`org:<orgId>`); a run with neither shares one `job:unattributed` bucket, never the anonymous callers' |
448
478
  | OpenAPI | `x-ultimate.rateLimit`, and with `defineApi({ openapi })` the `RateLimit-*` headers and `429` |
479
+ | a per-call limit | `limit: ({ ctx }) => planOf(ctx).perMinute` — resolved before any bucket is touched, through `toBucket`'s screen (`NaN`/`0`/`Infinity` is `X_RATE_LIMIT_INVALID`); published as `rateLimitPerCall: { windowMs }` and `x-ultimate.rateLimit.limit: 'per-call'` |
449
480
 
450
481
  ```ts
451
482
  import type { PgExecutor } from '@ultimat3/core';
@@ -470,6 +501,25 @@ which would cap an action declaring more than it — and only the tenant allowan
470
501
  `http.rateLimit.buckets.<actionName>` entry is a different table: it neither conflicts with nor
471
502
  loosens what the action declared.
472
503
 
504
+ ## `meter:` — a durable per-period allowance
505
+
506
+ ```ts
507
+ import type { ActionMeter } from '@ultimat3/action';
508
+
509
+ const quotaWarning = { enqueue: async () => undefined }; // in an app: a job({ input: MeterWarning, … })
510
+
511
+ // 500 calls per tenant per UTC day; one warning per tenant per day at 80%.
512
+ const meter: ActionMeter = { name: 'calls', period: 'day', limit: 500, warnAt: 0.8, onWarn: quotaWarning };
513
+ ```
514
+
515
+ Drawn once per call inside `invoke` (`meter-gate.ts`) on every surface, `server` included, after
516
+ the policy and before `handle`; spent, `X_METER_EXCEEDED` (429, `meta: { meter, limit, resetsAt,
517
+ retryAfterSeconds }`). Counter per `org:<id>` → `actor:<id>` → `anonymous`; periods are UTC and the
518
+ stored key says so (`day@UTC`). An idempotent replay draws nothing. `limit` may be a function of
519
+ `{ ctx }`. Store: `memoryMeterStore()` by default, `postgresMeterStore({ executor })` over `x_meter`
520
+ installed by the CLI boot (one row per counter, rolled over in place, no purge). Full table:
521
+ [Actions → Usage meters](https://github.com/developerz-ai/ultimate/wiki/Actions#usage-meters-meter).
522
+
473
523
  ## `idempotent:` — and where its records live
474
524
 
475
525
  `idempotent: true` + an `Idempotency-Key` header replays the first **outcome**
@@ -492,6 +542,17 @@ post-commit: a handler that took the money and then failed its own `output:` sch
492
542
  The reservation is settled as a FAILURE and the retry re-throws it under the first attempt's own
493
543
  code. Releasing it there is what made idempotency the cause of a double charge.
494
544
 
545
+ **A finished record answers its own retry before any row rule is asked.** A retried delete's row is
546
+ gone, so a rule asked again judges `null` and refuses a write that committed. With a settled or
547
+ failed record under this key, `row:` and the policy's input and row half are skipped; the actor's
548
+ half (`can()` with a permission revoked since) is decided on every call, and the key is this
549
+ actor's alone. Another payload is `X_IDEMPOTENCY_CONFLICT`. In flight or absent, the rule decides.
550
+
551
+ **An answer no retry could replay is refused, on both stores.** A `bigint` has no JSON form:
552
+ `X_IDEMPOTENCY_OUTPUT_UNSERIALIZABLE` (500), settled as a failure and replayed, the handler having
553
+ run. Return `String(id)`. A cycle stays `X_IDEMPOTENT_REPLAY_REDACTED`. `undefined` rests as `null`:
554
+ JSON has no `undefined`, and the action's `output` schema answers for the shape of a replay.
555
+
495
556
  **A credential in the answer is never kept at rest.** The record lives a day, and a mutator is
496
557
  always idempotent — so a "create an API key" mutator would otherwise leave the plaintext key in
497
558
  `x_idempotency.value`. On settle, every value under a key `@ultimat3/core`'s `isRedactedKey` names
@@ -571,6 +632,8 @@ setIdempotencyStore(
571
632
  configureIdempotency({ scope: 'shared' });
572
633
  ```
573
634
 
635
+ A keyed handler runs past the request deadline on a fresh signal, so its replay answers success, up to a hard ceiling: twice `requestTimeoutMs` by default (`idempotencyCeilingMs()`, 60 s), or `configureIdempotency({ scope, ceilingMs })`. Past it the handler is aborted and the key replays `X_IDEMPOTENCY_RUN_CEILING`.
636
+
574
637
  `configureIdempotency({ scope: 'shared' })` over a per-process store — or over a store that
575
638
  declares no scope at all — is `X_IDEMPOTENCY_NOT_SHARED` at `registerAction`, before the socket
576
639
  opens. The table is `SQL_IDEMPOTENCY_TABLE`, applied the way `SQL_JOBS_TABLE` is: `x db up` in
@@ -633,6 +696,53 @@ its `onCommit` (memory).
633
696
 
634
697
  A `query` has none and never will: a read has nothing to be idempotent about.
635
698
 
699
+ ## `registerToolGate` — what a named tool passes first
700
+
701
+ `registerToolGate(names, gate)`, `toolGateFor(name)`, `gatedToolNames()`, `noteToolNames(names)`,
702
+ `knownToolNames()`, `admitActionCaller(action, name)`, `asCallerContext(actor, fn)`, types `ToolGate`,
703
+ `ToolGateCall`. A mechanism for the two tool surfaces, one tier above: `@ultimat3/mcp`'s
704
+ `mcpConfirmations` registers and `@ultimat3/ai`'s adapter reads. Keyed by name because a tool is
705
+ named across both. First registered keeps the name, a later one is a no-op. `resetActions` clears
706
+ the table, so a test that calls it after each case must re-declare a module-level gate, or its later
707
+ cases run ungated. Declared outside what a worker loads is `X_ROLE_LOAD_INCOMPLETE`.
708
+
709
+ ## `errors:` — the refusals a caller may branch on, closed
710
+
711
+ ```ts
712
+ import { action } from '@ultimat3/action';
713
+ import { defineError } from '@ultimat3/core';
714
+ import { allow } from '@ultimat3/policy';
715
+ import { t } from '@ultimat3/schema';
716
+
717
+ const RefundTooLate = defineError({
718
+ code: 'X_REFUND_TOO_LATE',
719
+ title: 'the refund window has closed',
720
+ status: 409,
721
+ cause: 'the order is past its refund window',
722
+ fix: 'read the order first, and offer a refund only inside its window',
723
+ });
724
+
725
+ export const refundOrder = action({
726
+ input: t.object({ late: t.boolean }),
727
+ output: t.object({ refunded: t.boolean }),
728
+ policy: allow(),
729
+ errors: [RefundTooLate.code], // import the defineError class: the import registers its status
730
+ handle: ({ input }) => {
731
+ if (input.late) throw new RefundTooLate();
732
+ return { refunded: true };
733
+ },
734
+ });
735
+ ```
736
+
737
+ Each entry a registered 4xx code, listed once (`X_ACTION_ERRORS_INVALID` at `action()`). Judged
738
+ around `handle` and `row` only (`declared-errors.ts`): a 4xx code outside the list is
739
+ `X_ERROR_UNDECLARED` in a test or dev run and the original, warned and counted
740
+ (`undeclared_errors_total`), in production. `.contract()` gains one assertion that scans the
741
+ handler's own source (`declared-errors-contract.ts`); the OpenAPI operation names each code at its
742
+ status (`openapi-errors.ts`); `describe().errors` publishes the sorted list, `null` when absent.
743
+ The rule is `@ultimat3/core`'s, shared with `@ultimat3/query`. Wiki:
744
+ [Declared refusals](../../wiki/Actions.md#declared-refusals-errors).
745
+
636
746
  ## `deprecated:` — a compat window, not a version
637
747
 
638
748
  ```ts
@@ -803,15 +913,25 @@ setAuditSink({
803
913
  });
804
914
  ```
805
915
 
916
+ ## The audit trail, for a subject export or erase
917
+
918
+ `auditSubjectTarget({ executor, actor?, placeholder? })` is `x_audit` as a `defineSubject({ targets
919
+ })` entry (`@ultimat3/jobs`). The export pages the rows the subject acted in, or was acted for, by
920
+ id (`AUDIT_SUBJECT_COLUMNS`); the erase never deletes a trail row — it overwrites `actor_id` and
921
+ `on_behalf_of_id` with `placeholder` (`'erased'`) and `input` with `null`, a bounded batch per
922
+ statement. `actor` defaults to the subject's key. Keeping the person too is the app's call:
923
+ `{ target, retain: '<why>' }`.
924
+
806
925
  ## Contract tests
807
926
 
808
- `publishPost.contract()` returns three assertions. Run them; they throw `X_CONTRACT_DRIFT`.
927
+ `publishPost.contract()` returns four assertions. Run them; the first three throw `X_CONTRACT_DRIFT`.
809
928
 
810
929
  | Assertion | Holds when |
811
930
  |---|---|
812
931
  | input schema rejects garbage | the invocation fails `X_INPUT_INVALID` — that code, not any failure |
813
932
  | policy denies an anonymous actor | the invocation fails with an `ActionDeniedError` |
814
933
  | OpenAPI document contains its operation | the action's path, in the document built from the WHOLE registry (plus this action when a test drives an unregistered `.named()` twin), is this action's operation — a second registered action deriving the same route fails it |
934
+ | handler survives generated input | 16 runs (`exerciseAction`, `exercise.ts`) of input `arbitrary()` draws from `input:`, seeded by the name, each in a rolled-back transaction (`tx-scope.ts`): every answer is the output or a coded refusal under 500, and at least one run reached the handler |
815
935
 
816
936
  The denial assertion sends an input synthesized from `input:`'s own schema — required keys
817
937
  only, formats included — because a payload the schema rejects never reaches a policy. It
@@ -823,9 +943,15 @@ publishPost.contract({
823
943
  garbage: 42, // what the input schema must reject
824
944
  input: { postId, orgId }, // when the synthesized one cannot fit
825
945
  ctx: myCtx, // default: an anonymous context
946
+ exercise: { actor, input: (drawn) => ({ ...drawn, postId }) }, // or { off: '<reason>' }
826
947
  })
827
948
  ```
828
949
 
950
+ `exercise` is required unless the policy admits an anonymous caller
951
+ (`X_CONTRACT_EXERCISE_UNDECLARED`). A defect is `X_CONTRACT_DEFECT` (`meta.run`, `meta.input`,
952
+ `meta.failure`, and a `bun test <file> -t '<name>'` that replays it); no run reaching the handler
953
+ is `X_CONTRACT_EXERCISE_UNREACHED`. Full rules: [Testing](https://github.com/developerz-ai/ultimate/wiki/Testing#the-exercise-assertion).
954
+
829
955
  Pass `input:` when the schema carries a constraint the IR cannot invert (a bare `pattern`) or
830
956
  when `row:` needs an id that resolves. Anything thrown *before* the policy decides is drift,
831
957
  never a pass — the assertion says which code got in the way and names `input:` as the fix.
@@ -838,6 +964,8 @@ never a pass — the assertion says which code got in the way and names `input:`
838
964
  | `X_ACTION_PATH_DUPLICATE` | two actions derive one HTTP path (`archiveOrder` / `archiveOrders`) | rename one export |
839
965
  | `X_ACTION_POLICY_MISSING` | registration without `policy:` | add `policy: can('…')` |
840
966
  | `X_RATE_LIMIT_INVALID` | `rateLimit:` with a non-positive or non-finite half — `windowMs: 0` refills infinitely. Owned by `@ultimat3/http`, which owns the conversion | make both positive, or delete the block |
967
+ | `X_METER_EXCEEDED` | a `meter:`'s allowance for the current UTC period is spent; `meta.resetsAt` and `Retry-After` say when it resets | wait, or raise what `meter.limit` resolves to for the tenant |
968
+ | `X_METER_INVALID` | a `meter:` with a bad name, period or `warnAt`, half a `warnAt`/`onWarn` pair, or a limit resolving to anything but a whole number ≥ 0 | fix the declaration the cause names |
841
969
  | `X_ACTION_DEPRECATION_INVALID` | `deprecated:` with a `since`/`sunset` that is not a date | use an ISO-8601 instant |
842
970
  | `X_INPUT_INVALID` | input failed the Standard Schema. Carries the rejections **twice**: the flattened line in `cause`, and the structured list in `meta.issues` — one value rendered two ways, `As of 2026-08-24` | `x actions show <name> --json` |
843
971
  | `X_IDEMPOTENCY_CONFLICT` | key reused with a new payload / still in flight | new key, or retry later |
@@ -848,6 +976,9 @@ never a pass — the assertion says which code got in the way and names `input:`
848
976
  | `X_MUTATOR_NOT_IDEMPOTENT` | `mutator()` declared without `idempotent: true` | add `idempotent: true` to the definition |
849
977
  | `X_IDEMPOTENCY_STATUS_UNKNOWN` | `x_idempotency.status` holds a word this build has no branch for — written by a newer deploy | finish the rollout onto the build that writes it, then reconcile those requests — never DELETE the rows, which frees the key to run an already-committed action a second time |
850
978
  | `X_CONTRACT_DRIFT` | client/server build skew, missing spec entry | reload / `x verify --only contract` |
979
+ | `X_CONTRACT_DEFECT` | a generated legal input made the handler throw uncoded, fail its output, or answer a coded 5xx | the replay command in `fix:` |
980
+ | `X_CONTRACT_EXERCISE_UNREACHED` | no generated run reached the handler | `.contract({ exercise: { actor } })` with a permitted actor, or bind a row with `exercise.input` |
981
+ | `X_CONTRACT_EXERCISE_UNDECLARED` | `.contract()` on a guarded action with no `exercise`, or a blank `off` | `.contract({ exercise: { actor } })` or `{ exercise: { off: '<reason>' } }` |
851
982
  | `X_RPC_FAILED` | registered, thrown by nothing since 21.0.0 — a non-`problem+json` failure is core's `X_CLIENT_TRANSPORT_FAILED` now, as for a query | match `X_CLIENT_TRANSPORT_FAILED` instead |
852
983
  | `X_ACTION_UNREGISTERED` | projected before `registerActions()` ran | register at boot |
853
984
  | `X_AUDIT_SINK_MISSING` | `audit: true` and no sink installed — raised before the input parse | `setAuditSink(yourSink)` at boot |
@@ -861,7 +992,8 @@ The client does the same with the server's: a `problem+json` failure comes back
861
992
  the browser bundle may never have registered it, and linked only to a page that exists — the
862
993
  server's own `docs`/`type` when it sent an `http(s)` one, this build's registered link when it
863
994
  knows the code, otherwise the error index. A per-code URL is never synthesized for a code
864
- nothing here declares.
995
+ nothing here declares. The document's root `requestId` lands on `meta.requestId` (core's
996
+ `withRequestId`), so a report names the request support can find.
865
997
 
866
998
  A document carrying an `issues` member arrives parsed as well: `meta.issues`, read by
867
999
  `issuesFromWire` — a wire value, so the list is rebuilt member by member and a list this build
@@ -902,3 +1034,13 @@ a job boundary the class is gone and the `code` is what survives — match on th
902
1034
 
903
1035
  Tier 3. Imports `@ultimat3/core`, `schema`, `cache`, `db`, `entity`, `policy`, `http`. Never imports
904
1036
  `query`, `jobs`, `realtime` (same tier) or anything above it — those import *this*.
1037
+
1038
+ ## Supporting exports
1039
+
1040
+ `As of 2026-11-02`.
1041
+
1042
+ | Name | What |
1043
+ |---|---|
1044
+ | `IdempotencyOutputUnserializableError` | the class behind `X_IDEMPOTENCY_OUTPUT_UNSERIALIZABLE` |
1045
+ | `IdempotencyRunCeilingError` | the class behind `X_IDEMPOTENCY_RUN_CEILING` |
1046
+ | `parseActionInput(schema, raw, name)` | `invoke`'s input parse, alone: the parsed value, or the same `X_INPUT_INVALID` (line and `issues`) `invoke` throws. `@ultimat3/ai` holds an agent eval's tool double to it |