@ultimat3/action 30.9.0 → 31.1.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
@@ -47,17 +47,20 @@ Owns the `action` + `mutator` primitives and five projections. Tier 3. The MCP t
47
47
  | `declared-errors-contract.ts` | `.contract()`'s source scan |
48
48
  | `openapi-errors.ts` | core's builder bound to http's status and `problemTypeFor` |
49
49
  | `idempotency.ts` | the store SEAM: types, the installed-store slot, the scope declaration + `assertIdempotencyScope`, and `withIdempotency` — the replay-or-run gate |
50
+ | `loader.ts` | `@ultimat3/action/loader`: `registerAction`/`registerActions` for `x`'s loader and framework fixtures. Never the barrel — `defineApi` is an app's one path (`loader.test.ts`) |
51
+ | `adapter.ts` | `@ultimat3/action/adapter`: both stores' installers (each returns a release removing that install alone) + resets, port types, store errors, and `overrideHandler`. Never the barrel |
52
+ | `handler-override.ts` | the slot `overrideHandler` writes: a test double where `handle` is, keyed by the stored declaration and read per call by `invoke` (`run`) and, for a stream, by the drain (`action-stream.ts`) — so policy, parse, meter, idempotency and output parse still run on every surface. `@ultimat3/testing`'s `stub()` is its one caller (`handler-override.test.ts`) |
50
53
  | `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 |
51
54
  | `idempotency-memory.ts` | the process default: bounded, swept, `scope: 'process'` |
52
55
  | `idempotency-redact.ts` | `restingAnswer` — the copy of an answer a store may keep: core's `isRedactedKey` + `isSecret`, the SAME reference when nothing is redacted |
53
56
  | `idempotency-postgres.ts` | the SHARED store — one table, one `insert … on conflict` |
54
- | `policy-gate.ts` | **the only** runtime edge to `@ultimat3/policy` (`errors.ts` takes `SurfaceDenial` as a type, which erases) |
57
+ | `policy-gate.ts` | **the only** runtime edge to `@ultimat3/policy` (`errors.ts` takes `PrimitiveDenial` and `SurfaceDenial` as types, which erases) |
55
58
  | `cache-gate.ts` | the post-COMMIT bust — **the only** file that calls `invalidateTags` |
56
59
  | `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 |
57
60
  | `rate-limit-gate.ts` | **the one place** a declared `rateLimit:` is spent (a `limit` resolved per call from `ctx`, through `toBucket`) |
58
61
  | `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 |
59
62
  | `request-deadline.ts` | `requestDeadlineMs` — the app's `requestTimeoutMs`, resolved by http's own `defineHttpConfig` |
60
- | `audit.ts` | the audit seam's TYPES, re-exported from `@ultimat3/core` (shared with `query`); the slot (`setAuditSink`) is core's alone |
63
+ | `audit.ts` | the audit seam's TYPES, re-exported from `@ultimat3/core` (shared with `query`); the slot (`installAuditSink`, `@ultimat3/core/adapter`) is core's alone |
61
64
  | `audit-memory.ts` | the process default: a bounded ring that DROPS, and counts what it dropped |
62
65
  | `audit-postgres.ts` | the DURABLE sink — one append-only `x_audit` table, one insert per record |
63
66
  | `audit-input.ts` | what may be written DOWN: an `input` redacted through core's table and made JSON-representable on every path |
@@ -157,8 +160,8 @@ Owns the `action` + `mutator` primitives and five projections. Tier 3. The MCP t
157
160
  - **Both clients inject `traceparent`** before the caller's headers; an incomplete span sends nothing.
158
161
  - `serializeOpenApi` output is byte-stable: sorted keys, sorted registry, no clock. `client.ts` stays
159
162
  free of server imports.
160
- - **`stable.ts` holds the DOCUMENT form only** (`stableStringify` → `openapi.json`, re-read by
161
- `json-schema.ts`). The HASH form is core's `canonicalJson`/`fingerprint` (injective: tags
163
+ - **`stable.ts` holds the DOCUMENT form only** (`stableStringify`, exported: `openapi.json`,
164
+ `json-schema.ts`, mcp's `tools.lock.json`, testing's cassettes — no other serializer). The HASH form is core's `canonicalJson`/`fingerprint` (injective: tags
162
165
  `NaN`/`±Infinity`/`-0`, `Date`, `Map`, `Set`); `stable.test.ts` pins byte-equality for ordinary
163
166
  payloads.
164
167
  - **`tagKeys` is `@ultimat3/cache`'s and `toBucket` is `@ultimat3/http`'s** (imported, never
@@ -184,21 +187,17 @@ Owns the `action` + `mutator` primitives and five projections. Tier 3. The MCP t
184
187
  - **Flight control is `@ultimat3/core`'s, and NOT re-exported** — its values are imported from
185
188
  core (`X_HELPER_COPY` refuses a re-export); only the types `ClientFlight`/`ClientRetry`. Fix the
186
189
  pipeline in `packages/core/src/client-flight.ts`. No new code, curve, fence or retry loop here (`bun run flight-copies`).
187
- - **`isTransientFailure` INVERTS `retryDecision`'s unclassified default** (a caller's `AbortError`
188
- is terminal). Must survive.
189
190
  - **`ClientFlight` is a TYPE inside `client.ts`, never a value.** Measured,
190
191
  `bun build --target=browser --minify`, one entry importing from `@ultimat3/action` — **the ONE
191
192
  table for these figures** (`packages/core/CLAUDE.md` points here):
192
193
 
193
- | Entry | `As of 2026-11-06`, Bun 1.4.4, one entry over the working tree |
194
+ | Entry | `As of 2026-11-09`, Bun 1.4.4, one entry over the working tree |
194
195
  |---|---|
195
- | `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) |
196
- | `rpc` + core's `clientFlight` | 38,054 B |
196
+ | `rpc` | 24,473 B, pinned ≤ 25,000 (`client-bundle.test.ts`); 29,972 B while it read core's barrel |
197
+ | `rpc` + `clientFlight` from `@ultimat3/core/page` | 32,929 B (38,416 B from the barrel) |
197
198
 
198
- - **`sideEffects` is `["./src/error-titles.ts"]`**, never `false` (drops its bare imports): the
199
- titles ride into every barrel chunk (`problemError` decodes by code), `errors.ts`'s classes do
200
- not. An http refusal needs no http module: core's decoders take its title off the body and its
201
- `retry-after` off the header (`rpc-refusal.test.ts`).
199
+ - `isTransientFailure`'s inversion, the client graph's import rule, `sideEffects`: still rules, in
200
+ `docs/history/action-client-graph.md` (moved 2026-11-09).
202
201
 
203
202
  ## Invariants — idempotency
204
203
 
@@ -206,29 +205,15 @@ Owns the `action` + `mutator` primitives and five projections. Tier 3. The MCP t
206
205
  is SETTLED as a failure and a retry replays it; `release()` is only for a pre-handler failure. A
207
206
  `settle` that refuses leaves the record in flight. `idempotency-failure.test.ts`.
208
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`.
209
- - **A record belongs to ONE CALLER**: the key is
210
- `JSON.stringify([action, actor.kind, actor.id, actor.orgId ?? null, key])` — a JSON tuple, never a
211
- joined string. A blank header is `X_IDEMPOTENCY_KEY_INVALID` (4xx, before the handler), never "no
212
- key". Anonymous callers still share one key space.
213
- - **The `idempotency-key` header also NAMES the write, on every action**: `http.ts` runs `invoke`
214
- inside `withWriteOrigin(writeDigest(key))` so the page recognises its own `records` echo. A label,
215
- never a gate.
216
208
  - **Both stores FENCE a settlement on the reservation `id` AND `in-flight`**:
217
209
  `settle(key, value, reservationId, redacted)` / `fail(key, failure, reservationId)` /
218
- `release(key, reservationId)`. A fenced no-op is never thrown.
210
+ `release(key, reservationId)`. A fenced no-op is never thrown; an autocommit `settle`/`fail` logs `action.idempotency.settlement-fenced` on BOTH stores (`idempotency-memory-fenced.test.ts`).
219
211
  - **An answer rests REDACTED** (#591): `withIdempotency` passes the store `restingAnswer(value)`
220
212
  (core's `isRedactedKey` + `isSecret`) and `redacted` as `settle`'s REQUIRED 4th argument. A
221
213
  flagged replay is `X_IDEMPOTENT_REPLAY_REDACTED`, by the FLAG, never by scanning. Full rule: `docs/history/action.md`.
222
214
  - **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`).
223
- - **A stored status is NARROWED** (`isIdempotencyStatus`; unknown is
224
- `X_IDEMPOTENCY_STATUS_UNKNOWN`), never cast.
225
- - **Where records live is DECLARED and refused at registration**: `IdempotencyStore.scope` vs
226
- `configureIdempotency({ scope })`, compared by `assertIdempotencyScope` in `registerAction`
227
- (`X_IDEMPOTENCY_NOT_SHARED`). Default `'process'`.
228
- - **The memory store is bounded; `in-flight` records are the last evicted.** Never an LRU.
229
- - **`postgresIdempotencyStore` is the shared store** over `@ultimat3/core`'s structural `PgExecutor`; the
230
- reservation is ONE `insert … on conflict` statement. The CLI boot installs it.
231
- - **Inside a transaction the SETTLE commits with the write, on BOTH stores** (`liveTransaction()`). `idempotency-parity.test.ts`. Full rule: `docs/history/action.md`.
215
+ - **Caller keying, the write-origin label, status narrowing, declared scope, eviction order, the shared store**: `docs/history/action.md#idempotency-invariants-moved-from-claudemd`.
216
+ - **Inside a transaction the SETTLE commits with the write, on BOTH stores** (`liveTransaction()`). `idempotencyStoreConformance` (`@ultimat3/testing/conformance`). Full rule: `docs/history/action.md`.
232
217
  - **The deadline is `reclaimAfterMs: () => number`, REQUIRED on the Postgres store** and read per
233
218
  reservation (`requestDeadlineMs`, never a number here); `origin: () => object` is required too. Full rule: `docs/history/action.md`.
234
219
 
@@ -254,7 +239,7 @@ Owns the `action` + `mutator` primitives and five projections. Tier 3. The MCP t
254
239
  `locale`, `tz`, `buildId`, `role`, actor `id`/`kind`/`orgId`/`onBehalfOf`) and keeps
255
240
  `failure.code`, never the error.
256
241
  - **`x_audit` ships no purge**, deliberately. **`SQL_AUDIT_INSERT` is positional**, pinned by
257
- `audit-parity.test.ts` with a distinct value per field. Only `*_TABLE` DDL is exported
242
+ `audit-postgres-columns.test.ts` (a distinct value per field) and `auditSinkConformance` live. Only `*_TABLE` DDL is exported
258
243
  (`X_SQL_EXPORT_UNREAD`).
259
244
  - **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`.
260
245
  - **`auditSettled` sits outside the `catch`**, and **`auditOutcomeFor` is TOTAL** (fails closed to
@@ -263,7 +248,7 @@ Owns the `action` + `mutator` primitives and five projections. Tier 3. The MCP t
263
248
  ## Commands
264
249
 
265
250
  ```
266
- bun test packages/action
251
+ bun test ./packages/action/
267
252
  bun run typecheck
268
253
  ```
269
254
 
package/README.md CHANGED
@@ -105,7 +105,8 @@ names deriving one route collide with `X_ACTION_PATH_DUPLICATE` — `pluralize`
105
105
 
106
106
  `registerActions` is what `defineApi` composes for the three action-shaped keys; the other three
107
107
  go through core's `primitiveRegistrar(kind)`, because this package may not import `@ultimat3/query`
108
- or `@ultimat3/jobs` sideways. An app calling either directly is a second path.
108
+ or `@ultimat3/jobs` sideways. It is not on the `.` barrel: `defineApi` is the one path an app has,
109
+ and `x`'s module loader imports `registerAction`/`registerActions` from `@ultimat3/action/loader`.
109
110
 
110
111
  ## Call it — `rpc`
111
112
 
@@ -470,7 +471,7 @@ call (`surface: 'server'` — a service, a seed, a test) is app code, not a call
470
471
  | Fact | Answer |
471
472
  |---|---|
472
473
  | whose bucket | the caller's: actor → org → connection address (`@ultimat3/http`'s `rateLimitSpends`). An anonymous caller with no address (MCP, a job) shares one bucket |
473
- | where it is counted | `@ultimat3/http`'s installed store (`installRateLimitStore()`) — the slot `@ultimat3/query` spends from too; the boot fills it with the SAME instance it gives `httpServer({ rateLimitStore })`, on every role. Default: one process' memory |
474
+ | where it is counted | `@ultimat3/http`'s installed store — the slot `@ultimat3/query` spends from too; the boot fills it from `rateLimitStore` in `apps/<app>/runtime.ts` (else Postgres over its pool), on every role |
474
475
  | what HTTP answers | `RateLimit-*` from the bucket closest to refusing, and on a refusal `429` + `Retry-After` — the pipeline's own shape |
475
476
  | a retry | an admitted attempt spends one token; a refused one spends nothing. A job refused `X_RATE_LIMITED` is rescheduled for its `Retry-After` WITHOUT counting an attempt, so a backlog is delayed, never dead-lettered |
476
477
  | an idempotent replay | spends nothing: the stored answer is served, never a 429 |
@@ -480,15 +481,13 @@ call (`surface: 'server'` — a service, a seed, a test) is app code, not a call
480
481
 
481
482
  ```ts
482
483
  import type { PgExecutor } from '@ultimat3/core';
483
- import { installRateLimitStore, postgresRateLimitStore } from '@ultimat3/http';
484
+ import { postgresRateLimitStore } from '@ultimat3/http';
484
485
 
485
- declare const executor: PgExecutor; // the pool this boot already opened
486
+ declare const executor: PgExecutor; // the pool this process already opened
486
487
 
487
- // Boot, before the first call — the one store every surface and every replica counts in.
488
- // `x dev` and a container boot do this themselves, and `httpServer({ rateLimitStore })` adopts
489
- // the store it is handed; a process with no server (a worker of your own) installs it here.
490
- const rateLimitStore = postgresRateLimitStore({ executor });
491
- installRateLimitStore(rateLimitStore);
488
+ // apps/<app>/runtime.ts — the one store every surface and every replica counts in. `x dev` and a
489
+ // container boot install Postgres over their own pool without it; this line replaces that store.
490
+ export const runtime = { rateLimitStore: postgresRateLimitStore({ executor }) };
492
491
  ```
493
492
 
494
493
  `toBucket` is the only conversion — `capacity: limit`, `refillPerSecond: limit / (windowMs / 1000)`
@@ -604,13 +603,8 @@ must say so and bring a store that can keep it, or the retry that lands on anoth
604
603
  no record and runs the handler again:
605
604
 
606
605
  ```ts
607
- // boot, before registerActions()
608
- import {
609
- configureIdempotency,
610
- postgresIdempotencyStore,
611
- requestDeadlineMs,
612
- setIdempotencyStore,
613
- } from '@ultimat3/action';
606
+ // apps/<app>/runtime.ts — the boot installs the store before defineApi() registers
607
+ import { configureIdempotency, postgresIdempotencyStore, requestDeadlineMs } from '@ultimat3/action';
614
608
  import { db } from '@ultimat3/db';
615
609
 
616
610
  const client = db();
@@ -618,17 +612,17 @@ const client = db();
618
612
  // NOT `executor: Bun.sql` — `Bun.sql.query` is `undefined` `As of 2026-08` (it is a tagged
619
613
  // template whose positional form is `unsafe`), so that line compiles and throws on the first
620
614
  // reservation.
621
- // The framework boot installs this store for you; reach for it by hand only from a host that
622
- // boots the framework itself, and wrap the client that host already opened.
623
- setIdempotencyStore(
624
- postgresIdempotencyStore({
615
+ // The framework boot installs this store for you; hand it one only to replace it, and wrap the
616
+ // client this process already opened.
617
+ export const runtime = {
618
+ idempotencyStore: postgresIdempotencyStore({
625
619
  executor: { query: (text, values) => client.query({ text, values }) },
626
620
  // The client transactions on THIS database open on: a settle rides one of those and no other.
627
621
  origin: () => client,
628
622
  // The app's `requestTimeoutMs` (`configureHttp`), read per reservation — never a number here.
629
623
  reclaimAfterMs: requestDeadlineMs,
630
624
  }),
631
- );
625
+ };
632
626
  configureIdempotency({ scope: 'shared' });
633
627
  ```
634
628
 
@@ -675,7 +669,7 @@ status is `in-flight`** — one that ignores the second argument reopens the dou
675
669
  type-checking cannot catch it: a one-parameter function still satisfies the signature.
676
670
 
677
671
  **Inside a transaction the settlement commits with the write** (`As of 2026-10-02`, BOTH stores —
678
- `idempotency-parity.test.ts` runs one set of cases over memory and Postgres). An action invoked
672
+ `idempotencyStoreConformance` on `@ultimat3/testing/conformance` runs one set of cases over memory and Postgres). An action invoked
679
673
  inside `withTransaction` settles with that transaction: on its own connection (Postgres), or at
680
674
  its `onCommit` (memory).
681
675
 
@@ -779,11 +773,15 @@ price change are not the same event, and the framework is not the thing that kno
779
773
  them your business has to keep.
780
774
 
781
775
  ```ts
782
- import { setAuditSink } from '@ultimat3/core';
776
+ // apps/web/runtime.ts — the boot installs it before the app's modules import
777
+ import type { RuntimeOverrides } from '@ultimat3/cli/serve';
778
+ import type { AuditRecord } from '@ultimat3/core';
783
779
 
784
- setAuditSink({
785
- async write(record) { await record.ctx.db.auditRows.insert(myRow(record)); },
786
- });
780
+ declare function keep(record: AuditRecord): Promise<void>; // your table, log or queue
781
+
782
+ export const runtime = {
783
+ auditSink: { write: (record) => keep(record) },
784
+ } satisfies RuntimeOverrides;
787
785
  ```
788
786
 
789
787
  What the framework supplies is what it genuinely knows:
@@ -800,8 +798,9 @@ What the framework supplies is what it genuinely knows:
800
798
  | `failure` | the `X_*` code and the thrown value, on every outcome but `allowed` |
801
799
 
802
800
  **One contract, one sink, shared with reads.** `AuditRecord`, `AuditSink` and the installed slot
803
- are `@ultimat3/core`'s, and so is the one way to install it: `setAuditSink` from `@ultimat3/core`
804
- (this package re-exports the two types, never the slot — `As of 25.0.0`) installs the sink `query({ audit: true })` writes to as well (`@ultimat3/query`'s README). A sink
801
+ are `@ultimat3/core`'s, and so is the one way to install it: the `auditSink` field of
802
+ `apps/web/runtime.ts`, which the boot installs through core's `installAuditSink`
803
+ (`@ultimat3/core/adapter`, 31.0; this package re-exports the two types, never the slot) — the sink `query({ audit: true })` writes to as well (`@ultimat3/query`'s README). A sink
805
804
  tells the two apart by `record.primitive`.
806
805
 
807
806
  What it does **not** supply: an audit entity, a retention policy, a hash chain, a subject index,
@@ -818,18 +817,17 @@ one would make three of them wrong.
818
817
  The memory sink is bounded because a record pins a whole `Ctx`: at 50 audited writes a second an
819
818
  unbounded array is 4.3M immortal records a day and the pod dies holding the trail it was
820
819
  retaining. The trap it names out loud is that the shortest edit clearing `X_AUDIT_SINK_MISSING`
821
- is `setAuditSink(memoryAuditSink())`, and nothing at that call site says the result is amnesiac.
820
+ is `auditSink: memoryAuditSink()`, and nothing at that line says the result is amnesiac.
822
821
 
823
822
  ```ts
824
- // apps/web/server.ts — the app owns the connection, so the app installs the sink
823
+ // apps/web/runtime.ts — the boot installs the sink, over the pool it opened
825
824
  import { postgresAuditSink } from '@ultimat3/action';
826
- import { setAuditSink } from '@ultimat3/core';
827
- import { db } from '@ultimat3/db';
825
+ import type { RuntimeOverrides } from '@ultimat3/cli/serve';
826
+ import { dbExecutor } from '@ultimat3/db';
828
827
 
829
- const client = db();
830
- setAuditSink(
831
- postgresAuditSink({ executor: { query: (text, values) => client.query({ text, values }) } }),
832
- );
828
+ export const runtime = {
829
+ auditSink: postgresAuditSink({ executor: dbExecutor() }),
830
+ } satisfies RuntimeOverrides;
833
831
  ```
834
832
 
835
833
  **The table is applied by the boot; the sink is not.** `startQueue` runs `SQL_AUDIT_TABLE` on
@@ -898,7 +896,8 @@ The row it produces is the app's, and so is every question the framework refused
898
896
  which fields, whose tenant, chained or not, kept how long:
899
897
 
900
898
  ```ts
901
- setAuditSink({
899
+ // apps/web/runtime.ts — every name below but `runtime` is the app's own
900
+ export const runtime = { auditSink: {
902
901
  async write(record) {
903
902
  const { ctx } = record; // the services a sink needs to write a row
904
903
  const prev = await chainHead(ctx); // hash-chained: the app's choice
@@ -910,7 +909,7 @@ setAuditSink({
910
909
  prevHash: prev, hash: await sha256(prev, record),
911
910
  });
912
911
  },
913
- });
912
+ } } satisfies RuntimeOverrides; // apps/web/runtime.ts
914
913
  ```
915
914
 
916
915
  ## The audit trail, for a subject export or erase
@@ -956,6 +955,24 @@ Pass `input:` when the schema carries a constraint the IR cannot invert (a bare
956
955
  when `row:` needs an id that resolves. Anything thrown *before* the policy decides is drift,
957
956
  never a pass — the assertion says which code got in the way and names `input:` as the fix.
958
957
 
958
+ ## `@ultimat3/action/loader` — where an export is named
959
+
960
+ `registerAction(name, action)` / `registerActions(module)`: what `defineApi` and `x`'s module scan
961
+ call to stamp an export's name onto it, and what a framework fixture calls to name one action.
962
+ Never on the `.` barrel — an app hands its modules to `defineApi` (`loader.test.ts` pins both).
963
+
964
+ ## `@ultimat3/action/adapter` — the stores' installers and the handler override
965
+
966
+ The boot's installers live here, never on the `.` barrel: `installIdempotencyStore(store)` and
967
+ `installMeterStore(store)` each return a release that removes that install alone, in any order, beside
968
+ `resetIdempotency()` / `resetMeterStore()`, the port types and the errors a store throws. An app
969
+ swaps a store with `idempotencyStore:` / `meterStore:` in `apps/<app>/runtime.ts`.
970
+
971
+ `overrideHandler(action, handle | undefined)` puts a test double where `handle` is, on every
972
+ surface — a direct call, `.as()`, HTTP, MCP, `.job()`, `.stream()` (the drain reads it per call).
973
+ The policy, the rate limit, the input parse, the meter, idempotency and the output parse still run
974
+ around it; `undefined` restores the declared handler. `@ultimat3/testing`'s `stub()` drives it.
975
+
959
976
  ## Errors
960
977
 
961
978
  | Code | When | Fix |
@@ -980,8 +997,8 @@ never a pass — the assertion says which code got in the way and names `input:`
980
997
  | `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
998
  | `X_CONTRACT_EXERCISE_UNDECLARED` | `.contract()` on a guarded action with no `exercise`, or a blank `off` | `.contract({ exercise: { actor } })` or `{ exercise: { off: '<reason>' } }` |
982
999
  | `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 |
983
- | `X_ACTION_UNREGISTERED` | projected before `registerActions()` ran | register at boot |
984
- | `X_AUDIT_SINK_MISSING` | `audit: true` and no sink installed — raised before the input parse | `setAuditSink(yourSink)` at boot |
1000
+ | `X_ACTION_UNREGISTERED` | projected before `defineApi()` named it | hand its module to `defineApi` at boot |
1001
+ | `X_AUDIT_SINK_MISSING` | `audit: true` and no sink installed — raised before the input parse | `auditSink: postgresAuditSink({ executor })` in `apps/web/runtime.ts` |
985
1002
  | `X_AUDIT_SINK_FAILED` | the sink refused the record for an attempt that **succeeded** | fix the sink — then retry the same `Idempotency-Key` if this call carried one, else reconcile by hand |
986
1003
 
987
1004
  Denials re-throw the policy layer's own codes (`X_FORBIDDEN`, `X_UNAUTHENTICATED`) —
@@ -1009,21 +1026,12 @@ a job boundary the class is gone and the `code` is what survives — match on th
1009
1026
  | Class | Code | Declared in |
1010
1027
  |---|---|---|
1011
1028
  | `ActionDeniedError` | the policy denial's own code (`X_FORBIDDEN`, `X_UNAUTHENTICATED`, …), kept on `.denial` | `src/errors.ts` |
1012
- | `ActionDeprecationInvalidError` | `X_ACTION_DEPRECATION_INVALID` | `src/errors.ts` |
1013
- | `ActionDuplicateError` | `X_ACTION_DUPLICATE` | `src/errors.ts` |
1014
- | `ActionForeignError` | `X_ACTION_FOREIGN` | `src/errors.ts` |
1015
- | `ActionPathDuplicateError` | `X_ACTION_PATH_DUPLICATE` | `src/errors.ts` |
1016
- | `ActionPolicyMissingError` | `X_ACTION_POLICY_MISSING` | `src/errors.ts` |
1017
- | `ActionUnregisteredError` | `X_ACTION_UNREGISTERED` | `src/errors.ts` |
1018
1029
  | `AuditSinkFailedError` | `X_AUDIT_SINK_FAILED` | `src/errors.ts` |
1019
- | `AuditSinkMissingError` | `X_AUDIT_SINK_MISSING` | `src/errors.ts` |
1020
1030
  | `ContractDriftError` | `X_CONTRACT_DRIFT` | `src/errors.ts` |
1021
1031
  | `IdempotencyConflictError` | `X_IDEMPOTENCY_CONFLICT` | `src/errors-idempotency.ts` |
1022
1032
  | `IdempotencyKeyInvalidError` | `X_IDEMPOTENCY_KEY_INVALID` | `src/errors-idempotency.ts` |
1023
- | `IdempotencyNotSharedError` | `X_IDEMPOTENCY_NOT_SHARED` | `src/errors-idempotency.ts` |
1024
1033
  | `IdempotencyReplayedFailureError` | `X_IDEMPOTENCY_REPLAYED_FAILURE` | `src/errors-idempotency.ts` |
1025
1034
  | `IdempotencyReservationLostError` | `X_IDEMPOTENCY_RESERVATION_LOST` | `src/errors-idempotency.ts` |
1026
- | `IdempotencyStatusUnknownError` | `X_IDEMPOTENCY_STATUS_UNKNOWN` | `src/errors-idempotency.ts` |
1027
1035
  | `InputInvalidError` | `X_INPUT_INVALID` | `src/errors.ts` |
1028
1036
  | `MutatorNotIdempotentError` | `X_MUTATOR_NOT_IDEMPOTENT` | `src/errors-idempotency.ts` |
1029
1037
  | `OutputInvalidError` | `X_OUTPUT_INVALID` | `src/errors.ts` |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ultimat3/action",
3
- "version": "30.9.0",
3
+ "version": "31.1.0",
4
4
  "description": "The action primitive: one declaration projected to route, OpenAPI, client, MCP tool, job handle, tests",
5
5
  "keywords": [
6
6
  "ultimate",
@@ -28,7 +28,9 @@
28
28
  "provenance": true
29
29
  },
30
30
  "exports": {
31
- ".": "./src/index.ts"
31
+ ".": "./src/index.ts",
32
+ "./adapter": "./src/adapter.ts",
33
+ "./loader": "./src/loader.ts"
32
34
  },
33
35
  "files": [
34
36
  "src",
@@ -46,12 +48,12 @@
46
48
  "test": "bun test"
47
49
  },
48
50
  "dependencies": {
49
- "@ultimat3/cache": "30.9.0",
50
- "@ultimat3/core": "30.9.0",
51
- "@ultimat3/db": "30.9.0",
52
- "@ultimat3/entity": "30.9.0",
53
- "@ultimat3/http": "30.9.0",
54
- "@ultimat3/policy": "30.9.0",
55
- "@ultimat3/schema": "30.9.0"
51
+ "@ultimat3/cache": "31.1.0",
52
+ "@ultimat3/core": "31.1.0",
53
+ "@ultimat3/db": "31.1.0",
54
+ "@ultimat3/entity": "31.1.0",
55
+ "@ultimat3/http": "31.1.0",
56
+ "@ultimat3/policy": "31.1.0",
57
+ "@ultimat3/schema": "31.1.0"
56
58
  }
57
59
  }
@@ -9,6 +9,7 @@ import { asyncContext } from '@ultimat3/core';
9
9
  import type { StandardSchemaV1 } from '@ultimat3/schema';
10
10
  import { formatIssues, validateAsync } from '@ultimat3/schema';
11
11
  import { ActionStreamInvalidError } from './errors-stream';
12
+ import { handlerOverrideOf } from './handler-override';
12
13
 
13
14
  /** Where a draining handler hands its chunks: `http-stream.ts` opens one per streamed request. */
14
15
  export interface ChunkSink {
@@ -39,6 +40,18 @@ export function withChunkSink<T>(sink: ChunkSink, fn: () => T): T {
39
40
  return sinks.run({ sink, claimed: false }, fn);
40
41
  }
41
42
 
43
+ /**
44
+ * Each draining copy → the declaration it wraps. Built once per `.named()` twin, so an override is
45
+ * keyed by the declaration (`declarationOf`), never the copy, and reaches every twin at once.
46
+ */
47
+ const DRAINED = new WeakMap<object, object>();
48
+
49
+ /** Whether `def`'s `handle` is the drain: the drain, not `invoke`, resolves its override. */
50
+ export const drainsItsHandler = (def: object): boolean => DRAINED.has(def);
51
+
52
+ /** The declaration an override is keyed by: the wrapped one for a draining copy, else `def`. */
53
+ export const declarationOf = (def: object): object => DRAINED.get(def) ?? def;
54
+
42
55
  interface StreamingDef {
43
56
  readonly stream?: StandardSchemaV1;
44
57
  handle(args: { readonly input: unknown; readonly ctx: Ctx }): unknown;
@@ -53,9 +66,12 @@ export function streamingDef<D extends StreamingDef>(def: D, name: () => string)
53
66
  if (stream === undefined) return def;
54
67
  const wrapped: D = {
55
68
  ...def,
69
+ // The override is resolved per call, never captured here: a test double installed after the
70
+ // declaration (`overrideHandler`) is drained exactly as the declared generator would be.
56
71
  handle: (args: { readonly input: unknown; readonly ctx: Ctx }) =>
57
- drain(wrapped, stream, name(), def.handle(args)),
72
+ drain(wrapped, stream, name(), (handlerOverrideOf(def) ?? def.handle.bind(def))(args)),
58
73
  };
74
+ DRAINED.set(wrapped, def);
59
75
  return wrapped;
60
76
  }
61
77
 
package/src/adapter.ts ADDED
@@ -0,0 +1,22 @@
1
+ // `@ultimat3/action/adapter` — what an idempotency or meter store is built and installed from: the
2
+ // port interfaces, the errors a store throws, and the boot's installers with their resets. Never
3
+ // the `.` barrel: an app swaps a store in `apps/<app>/runtime.ts` (`idempotencyStore:`,
4
+ // `meterStore:`), so the only callers here are the cli boot, `@ultimat3/testing` and a store author.
5
+
6
+ export {
7
+ IdempotencyReservationLostError,
8
+ IdempotencyStatusUnknownError,
9
+ } from './errors';
10
+ export type { HandlerOverride } from './handler-override';
11
+ export type {
12
+ IdempotencyFailure,
13
+ IdempotencyRecord,
14
+ IdempotencyReservation,
15
+ IdempotencyScope,
16
+ IdempotencyStatus,
17
+ IdempotencyStore,
18
+ } from './idempotency';
19
+ export { installIdempotencyStore, resetIdempotency } from './idempotency';
20
+ export { overrideHandler } from './invoke';
21
+ export type { MeterDraw, MeterDrawResult, MeterKey, MeterStore } from './meter-store';
22
+ export { installMeterStore, resetMeterStore } from './meter-store';
package/src/audit-gate.ts CHANGED
@@ -4,7 +4,13 @@
4
4
  * an attempt that already failed do not want the same answer.
5
5
  */
6
6
 
7
- import { getAuditSink, isUltimateError, logger } from '@ultimat3/core';
7
+ import {
8
+ AUDIT_SINK_FAILED_EVENT,
9
+ auditFailureOf,
10
+ auditOutcomeOf,
11
+ getAuditSink,
12
+ logger,
13
+ } from '@ultimat3/core';
8
14
  import type { AuditFailure, AuditOutcome, AuditRecord, AuditSink } from './audit';
9
15
  import { ActionDeniedError, AuditSinkFailedError, AuditSinkMissingError } from './errors';
10
16
 
@@ -31,15 +37,11 @@ export function auditSinkFor(action: string): AuditSink {
31
37
  * examined is not evidence of a policy denial, so it fails closed to `failed`.
32
38
  */
33
39
  export function auditOutcomeFor(error: unknown): AuditOutcome {
34
- try {
35
- return error instanceof ActionDeniedError ? 'denied' : 'failed';
36
- } catch {
37
- return 'failed';
38
- }
40
+ return auditOutcomeOf(error, (thrown) => thrown instanceof ActionDeniedError);
39
41
  }
40
42
 
41
43
  export function auditFailureFor(error: unknown): AuditFailure {
42
- return { code: isUltimateError(error) ? error.code : null, error };
44
+ return auditFailureOf(error);
43
45
  }
44
46
 
45
47
  /**
@@ -82,7 +84,7 @@ export async function auditThrew(sink: AuditSink, record: AuditRecord): Promise<
82
84
  // Core's logger, not `ctx.logger`: an HTTP `Ctx` is a cast request context that carries none,
83
85
  // the same reason `cache-gate.ts` gives. Never the record — rendering an input the sink just
84
86
  // choked on is the second throw this branch exists to prevent.
85
- logger.error('audit.sink.failed', {
87
+ logger.error(AUDIT_SINK_FAILED_EVENT, {
86
88
  action: record.name,
87
89
  outcome: record.outcome,
88
90
  error,
@@ -26,8 +26,8 @@ export interface MemoryAuditSinkOptions {
26
26
  * merely because it is not durable: it DISCARDS.** Past `maxRecords` the oldest record is dropped
27
27
  * on every write, so an audited action can run, succeed, be recorded, and leave nothing behind —
28
28
  * which is exactly what an audit trail must never do. The trap this shape exists to make visible
29
- * is that the shortest edit clearing `X_AUDIT_SINK_MISSING` is `setAuditSink(memoryAuditSink())`,
30
- * and nothing about the call site says the result is amnesiac. A deployment that must keep its
29
+ * is that the shortest edit clearing `X_AUDIT_SINK_MISSING` is `auditSink: memoryAuditSink()`,
30
+ * and nothing about that line says the result is amnesiac. A deployment that must keep its
31
31
  * trail installs `postgresAuditSink({ executor })`, which drops nothing.
32
32
  *
33
33
  * `dropped` is what makes that statement checkable in a running process rather than a sentence
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * The durable audit sink: one append-only Postgres table, one insert per record. Without it the
3
- * shortest edit that clears `X_AUDIT_SINK_MISSING` is `setAuditSink(memoryAuditSink())`, which
4
- * reads durable at the call site and is a ring that DROPS — compliant in dev, amnesiac in
3
+ * shortest edit that clears `X_AUDIT_SINK_MISSING` is `auditSink: memoryAuditSink()`, which
4
+ * reads durable at the line and is a ring that DROPS — compliant in dev, amnesiac in
5
5
  * production. Statements are spelled out so an agent can run the exact one it saw in a log.
6
6
  */
7
7
 
@@ -102,16 +102,13 @@ export interface PostgresAuditSink extends AuditSink {
102
102
  }
103
103
 
104
104
  /**
105
- * **Install it at boot, beside the store that declares the rest of this app's durability.** The
106
- * app owes one line in `apps/web/server.ts`, over the client this process already opened:
105
+ * **Name it in `apps/web/runtime.ts`, beside the rest of this app's durability**, over the pool the
106
+ * boot opened — the boot installs it (`installAuditSink`) before the app's modules import:
107
107
  *
108
108
  * ```ts
109
- * const client = db();
110
- * setAuditSink(
111
- * postgresAuditSink({
112
- * executor: { query: (text, values) => client.query({ text, values }) },
113
- * }),
114
- * );
109
+ * export const runtime = {
110
+ * auditSink: postgresAuditSink({ executor: dbExecutor() }),
111
+ * } satisfies RuntimeOverrides;
115
112
  * ```
116
113
  *
117
114
  * `Bun.sql` does not satisfy `PgExecutor` — `Bun.sql.query` is `undefined`; see that interface.
package/src/audit.ts CHANGED
@@ -3,8 +3,8 @@
3
3
  * The contract — the record, the sink, the one installed-sink slot — moved to `@ultimat3/core`'s
4
4
  * `audit.ts` so `@ultimat3/query` (a sibling this package cannot import) writes the same record
5
5
  * into the same sink. The TYPES are re-exported for this package's own modules; the slot's VALUES
6
- * (`getAuditSink`, `setAuditSink`, `resetAuditSink`) are imported from `@ultimat3/core` and never
7
- * re-exported — a second import path for one value is `X_HELPER_COPY` (`bun run flight-copies`).
6
+ * (`getAuditSink`; the boot's `installAuditSink` is `@ultimat3/core/adapter`'s) are imported from
7
+ * core and never re-exported — a second import path for one value is `X_HELPER_COPY` (`bun run flight-copies`).
8
8
  */
9
9
 
10
10
  export type { AuditFailure, AuditOutcome, AuditRecord, AuditSink } from '@ultimat3/core';
package/src/client.ts CHANGED
@@ -21,7 +21,7 @@ import type {
21
21
  ClientRetry,
22
22
  FetchLike,
23
23
  UltimateError,
24
- } from '@ultimat3/core';
24
+ } from '@ultimat3/core/page';
25
25
  import {
26
26
  actionPath,
27
27
  BUILD_ID_HEADER,
@@ -33,7 +33,7 @@ import {
33
33
  problemOf,
34
34
  remoteTitleOf,
35
35
  renderedActionPin,
36
- } from '@ultimat3/core';
36
+ } from '@ultimat3/core/page';
37
37
  import type { InferInput, InferOutput, StandardSchemaV1 } from '@ultimat3/schema';
38
38
  import type { Action, StreamAction } from './action';
39
39
  import { ContractDriftError, RemoteActionError } from './errors';
@@ -101,7 +101,7 @@ export type StreamClientMethod<
101
101
  };
102
102
 
103
103
  /** Core's one declaration, re-exported by name so `import type { FetchLike }` keeps resolving. */
104
- export type { FetchLike } from '@ultimat3/core';
104
+ export type { FetchLike } from '@ultimat3/core/page';
105
105
 
106
106
  export interface ClientOptions {
107
107
  readonly baseUrl: string;
@@ -311,6 +311,7 @@ function toUltimateError(
311
311
  // Parsed, never taken: `body` is whatever answered the request. A list this build cannot read
312
312
  // is dropped rather than repaired, and `cause` below still carries every rejection in it.
313
313
  issues: issuesFromWire(body['issues']),
314
+ issuesTruncated: body['issuesTruncated'] === true ? true : undefined,
314
315
  meta: metaFromWire(body['meta']),
315
316
  requestId: body['requestId'],
316
317
  cause: stringOr(body['cause'] ?? body['detail'], `${name} failed with ${status}`),
@@ -325,8 +326,8 @@ function toUltimateError(
325
326
 
326
327
  /**
327
328
  * The document's `meta` member, or nothing. Parsed JSON is JSON-safe by construction, so the only
328
- * work is shape: an object, copied own key by own key onto a fresh record. `issues` is skipped
329
- * because it has its own reader and its own bound one line up, and `__proto__` because
329
+ * work is shape: an object, copied own key by own key onto a fresh record. `issues` and its flag
330
+ * `issuesTruncated` are skipped because they have their own reader up top, and `__proto__` because
330
331
  * `JSON.parse` mints it as a real own key — copied by assignment it would replace the prototype
331
332
  * of the error's `meta`, which the reporter and the overlay walk. EMPTY is `undefined`: an
332
333
  * `{}` under `meta` would say the server declared keys and sent none.
@@ -336,7 +337,7 @@ function metaFromWire(value: unknown): Readonly<Record<string, unknown>> | undef
336
337
  const out: Record<string, unknown> = {};
337
338
  let carried = 0;
338
339
  for (const [key, member] of Object.entries(value)) {
339
- if (key === 'issues' || key === '__proto__') continue;
340
+ if (key === 'issues' || key === 'issuesTruncated' || key === '__proto__') continue;
340
341
  Object.defineProperty(out, key, {
341
342
  value: member,
342
343
  writable: true,
@@ -4,7 +4,7 @@
4
4
  // `index.ts` imports this bare, because `problemError` rebuilds a server's refusal by code and reads
5
5
  // its title from the registry; `errors.ts` and its two siblings import it bare as well.
6
6
 
7
- import { registerErrorCodes, registerErrorRetry } from '@ultimat3/core';
7
+ import { registerErrorCodes, registerErrorRetry } from '@ultimat3/core/page';
8
8
 
9
9
  /**
10
10
  * Titles for the framework-wide code table — every one of them owned by this package.
@@ -5,7 +5,7 @@
5
5
  * these registers its title first.
6
6
  */
7
7
 
8
- import { renderCauseValue, UltimateError } from '@ultimat3/core';
8
+ import { renderCauseValue, UltimateError } from '@ultimat3/core/page';
9
9
  import './error-titles';
10
10
 
11
11
  export class ActionHttpPathInvalidError extends UltimateError {