@ultimat3/action 23.0.0 → 25.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
@@ -1,11 +1,13 @@
1
1
  # @ultimat3/action
2
2
 
3
- Owns the `action` + `mutator` primitives and their six projections. Tier 3.
3
+ Owns the `action` + `mutator` primitives and five projections. Tier 3. The MCP tool is
4
+ `@ultimat3/mcp`'s one projection (O-tool, 25.0.0).
4
5
 
5
6
  ## Boundary
6
7
 
7
- - May import: `core`, `schema` (t0), `cache`, `i18n`, `time` (t1), `entity`, `policy`, `http` (t2).
8
+ - May import: `core`, `schema` (t0), `cache`, `db`, `i18n`, `time` (t1), `entity`, `policy`, `http` (t2).
8
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
11
  - Never import: `query`, `jobs`, `realtime` (sideways), or any tier 4-5 package.
10
12
  - Never re-implement authz, validation or caching — call `policy`, `schema`, `cache`.
11
13
 
@@ -26,55 +28,59 @@ Owns the `action` + `mutator` primitives and their six projections. Tier 3.
26
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
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 |
28
30
  | `api-declaration.ts` | `defineApi({ http, openapi })` held process-wide for the boot and `x manifest` |
29
- | `errors-http.ts` | the three HTTP-declaration refusals (codes registered in `errors.ts`) |
31
+ | `errors-http.ts` | the three HTTP-declaration refusals |
32
+ | `error-titles.ts` | every code's title, the one `registerErrorCodes` |
30
33
  | `client.ts` | typed RPC client (browser-safe: no server imports) — dispatches through core's `clientTransport` |
31
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 |
32
35
  | `wire-issues.ts` | the ONE reader of a problem document's `issues` member — an untrusted array back into `@ultimat3/schema`'s `ValidationIssue` shape |
33
36
  | `transition.ts` | `transition()`: a MUTATOR factory over one entity column's state machine. Declares no error code — entity's three propagate |
34
- | — | opt-in flight control is **`@ultimat3/core`**'s `client-flight.ts` + `client-wire.ts`, re-exported from `src/index.ts`. There is no local copy and must not be one |
35
- | `wire-headers.ts` | `BUILD_ID_HEADER` + `IDEMPOTENCY_HEADER`, and nothing else. Their own module so `client.ts` can name them without importing `http.ts` |
36
- | `mcp-tool.ts` | MCP descriptor, same `invoke` |
37
+ | — | 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 |
37
38
  | `job-handle.ts` | the `.job()` projection: an action as a queueable payload. **Not** consumed by `@ultimat3/jobs` — see Invariants |
38
39
  | `contract-test.ts` | assertions `x g action` emits |
39
40
  | `sample-input.ts` | a value `input:` accepts, from its own IR — what makes the policy assertion reach a policy |
40
41
  | `idempotency.ts` | the store SEAM: types, the installed-store slot, the scope declaration + `assertIdempotencyScope`, and `withIdempotency` — the replay-or-run gate |
41
42
  | `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 |
42
43
  | `idempotency-memory.ts` | the process default: bounded, swept, `scope: 'process'` |
44
+ | `idempotency-redact.ts` | `restingAnswer` — the copy of an answer a store may keep: core's `isRedactedKey` + `isSecret`, the SAME reference when nothing is redacted |
43
45
  | `idempotency-postgres.ts` | the SHARED store — one table, one `insert … on conflict` |
44
- | `deprecation.ts` | `Deprecation` + the RFC 9745/8594 render + the `deprecated_calls_total` counter |
45
46
  | `policy-gate.ts` | **the only** runtime edge to `@ultimat3/policy` (`errors.ts` takes `SurfaceDenial` as a type, which erases) |
46
- | `cache-gate.ts` | the post-commit bust — **the only** file that calls `invalidateTags` |
47
- | `audit.ts` | the audit seam: `AuditRecord`, `AuditSink`, the installed-sink store |
47
+ | `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 |
50
+ | `request-deadline.ts` | `requestDeadlineMs` — the app's `requestTimeoutMs`, resolved by http's own `defineHttpConfig` |
51
+ | `audit.ts` | the audit seam's TYPES, re-exported from `@ultimat3/core` (shared with `query`); the slot (`setAuditSink`) is core's alone |
48
52
  | `audit-memory.ts` | the process default: a bounded ring that DROPS, and counts what it dropped |
49
53
  | `audit-postgres.ts` | the DURABLE sink — one append-only `x_audit` table, one insert per record |
50
54
  | `audit-input.ts` | what may be written DOWN: an `input` redacted through core's table and made JSON-representable on every path |
51
55
  | `audit-gate.ts` | **the only** file that calls a sink, and where the two failure policies live |
52
56
  | `type-pins.ts` | compile-time assertions `tsc` checks — what the erased view projects, and why `client()` is not part of it |
53
- | `naming.ts`, `validate.ts`, `json-schema.ts`, `stable.ts` | pure helpers. `stable.ts` is the DOCUMENT serializer plus a re-export of core's `isJsonObject` — the hash form is `@ultimat3/core`'s `canonicalJson`/`fingerprint` |
57
+ | `naming.ts`, `validate.ts`, `json-schema.ts`, `stable.ts` | pure helpers. `stable.ts` is the DOCUMENT serializer (core's `isJsonObject` is imported from core, never re-exported) — the hash form is `@ultimat3/core`'s `canonicalJson`/`fingerprint` |
54
58
 
55
59
  ## Invariants — execution and authz
56
60
 
57
- - Every surface goes through `invoke`: the policy's actor half (`guardBeforeInput`, 403 before the
61
+ - Every surface goes through `invoke`: the policy's actor half (`guardActionBeforeInput`, 403 before the
58
62
  parse), parse input, evaluate policy, handle, parse output. A second
59
63
  execution path is the one unforgivable change here.
60
64
  - **An explicit `ctx` is INSTALLED, never merely passed** — the ambient context (which
61
- `@ultimat3/entity`'s tenant guard reads) must be the identity `guard()` decided about.
65
+ `@ultimat3/entity`'s tenant guard reads) must be the identity `guardAction()` decided about.
62
66
  `invoke-context.test.ts` asserts ambient, `options.actor` and `options.ctx` are one caller.
63
67
  - The declaration never leaves `invoke.ts`: `defOf`/`stashDef` are never re-exported from
64
68
  `src/index.ts` (`index.test.ts`). An action has no `.def`; outside, read `.input`/`.output`/
65
69
  `.policy`/`.mcp` or `describe()`. App code reaches a projection through the action
66
- (`publishPost.tool()`); new methods are bound in `facade.ts`.
70
+ (`publishPost.openapi()`); new methods are bound in `facade.ts`. **No `.tool()`**: it was a second
71
+ MCP projection beside mcp's `toolFrom` (`index.test.ts`, `type-pins.ts`).
67
72
  - **`toRoute` sets `enforcedBy: 'handler'`** — `invoke` is the one evaluation and the only one holding
68
73
  the row `def.row` loaded; `meta.policy` stays set. `http.test.ts` counts exactly one evaluation.
69
- - **`meta.auth` comes from `@ultimat3/policy`'s `admitsAnonymous`** (a walk of the tree, via
70
- `policy-gate.ts`), never the root combinator — never a copy here (`@ultimat3/query` needs the same
74
+ - **`meta.auth` comes from `@ultimat3/policy`'s `admitsAnonymous`** (a walk of the tree, imported
75
+ from `@ultimat3/policy` by `http.ts`; 25.0.0 dropped this package's re-export, `index.test.ts`
76
+ pins the absence), never the root combinator — never a copy here (`@ultimat3/query` needs the same
71
77
  answer).
72
78
  - Authz goes through `enforce(surface, policy, { input, actor, ctx })`; a denial becomes
73
79
  `ActionDeniedError`, keeping the policy's code. `policy-gate.ts` is the only RUNTIME edge to
74
80
  `@ultimat3/policy` (`errors.ts`'s `import type { SurfaceDenial }` erases).
75
81
  - No policy at registration → `X_ACTION_POLICY_MISSING`. No exceptions, no flag.
76
82
  - **`registerAction` guards the derived PATH as well as the name** (`X_ACTION_PATH_DUPLICATE`; a
77
- second index cleared by `resetRegistry`).
83
+ second index cleared by `resetActions`).
78
84
  - **A path is the app's `pathStyle` or the action's pin, never a third answer** (`http-path.ts`).
79
85
  `'resource'` stays the default (changing it moves every URL). `defineApi` sets the style FIRST and
80
86
  `configureActionPathStyle` re-derives every seated action (the module scan may have seated them
@@ -98,6 +104,9 @@ Owns the `action` + `mutator` primitives and their six projections. Tier 3.
98
104
  unchanged** — an upgrade must not make a committed contract stale by itself.
99
105
  - Registration names the action the app exported, in place. Naming an already-named action is the
100
106
  only case that twins.
107
+ - **A mutator declares `idempotent: true` or does not exist**: required by `MutatorDef`, and
108
+ `X_MUTATOR_NOT_IDEMPOTENT` at `mutator()` for anything else. The client replays a mutator's write
109
+ under one key; `transition()` and `x g mutator` declare it. Never a default.
101
110
  - A mutator projects `.local`, `.server`, `.conflict` plus every action member — no aliases.
102
111
  `mutator.server()` calls the action's own callable (lands in `invoke`); `.local()` never leaves the
103
112
  client.
@@ -110,7 +119,8 @@ Owns the `action` + `mutator` primitives and their six projections. Tier 3.
110
119
  - `src/index.ts` re-exports `t` from `@ultimat3/schema` **verbatim** (`index.test.ts` asserts identity).
111
120
  - **`transition()` is a mutator factory that decides nothing about the machine**: entity's three
112
121
  codes propagate; `from` is REQUIRED (it is the UPDATE's predicate); `conflict: 'server-wins'` fixed;
113
- `audit` off unless declared.
122
+ `audit` off unless declared; `id` is the key `output.id` declares (`keySchemaOf`), never a fixed
123
+ `t.uuid`.
114
124
  - **A lookup table is read with `Object.hasOwn`** (`IRREGULAR` in `naming.ts`, `BY_FORMAT` in
115
125
  `sample-input.ts`) — caller- or provider-supplied keys.
116
126
 
@@ -145,11 +155,21 @@ Owns the `action` + `mutator` primitives and their six projections. Tier 3.
145
155
  `json-schema.ts`). The HASH form is core's `canonicalJson`/`fingerprint` (injective: tags
146
156
  `NaN`/`±Infinity`/`-0`, `Date`, `Map`, `Set`); `stable.test.ts` pins byte-equality for ordinary
147
157
  payloads.
148
- - **`tagKeys` is `@ultimat3/cache`'s and `toBucket` is `@ultimat3/http`'s** (re-exported; raises
149
- `X_RATE_LIMIT_INVALID`). Never restore a local copy; never use `@ultimat3/render`'s `tagKeys`.
150
- - **`rateLimit:` reaches the limiter**: `toRoute` sets `meta.rateLimit` AND `meta.rateLimitBucket`
151
- (`toBucket`), which `@ultimat3/http`'s `withRouteBuckets` registers. The computed rate must be
152
- finite and `limit` at least one token.
158
+ - **`tagKeys` is `@ultimat3/cache`'s and `toBucket` is `@ultimat3/http`'s** (imported, never
159
+ re-exported; `toBucket` raises `X_RATE_LIMIT_INVALID`). Never restore a local copy.
160
+ - **`rateLimit:` is spent ONCE, inside `invoke`** (`rate-limit-gate.ts`), so HTTP, MCP, the agent
161
+ tool and `.job()` share one bucket; `surface: 'server'` spends nothing. `toRoute` sets
162
+ `rateLimitedBy: 'handler'` and NO `meta.rateLimit`/`rateLimitBucket` — the stage spends neither
163
+ the action's bucket (the old HTTP-only point) nor `default`. Key `action:<name>|<subject>`.
164
+ HTTP gets `RateLimit-*` via `onRateLimit` → http's `publishRateLimit`. The store is
165
+ `@ultimat3/http`'s installed one, adopted by the boot on every role. Spent after
166
+ `guardActionBeforeInput`, BEFORE the parse and `def.row()` (an input/row-denied flood is refused). A key
167
+ the idempotency store holds spends nothing (replay); a reservation `withIdempotency` CREATES that
168
+ the peek did not pay for spends in `beforeRun`, released on refusal. A job run is charged to its
169
+ org, else `job:unattributed`. A surface's `clientAddress` is inherited by nested `invoke`s
170
+ (`withCallerAddress`); a `'job'` invoke opens a fresh frame (`withJobFrame`) so no visitor's
171
+ address follows work into the worker, and calls nested in it are the job's.
172
+ `rate-limit-surfaces.test.ts`. The computed rate must be finite and `limit` at least one token.
153
173
  - **`deprecated:` is a compat WINDOW** — headers on every response including failures, a
154
174
  `rel="successor-version"` link via `derivePath`, `deprecated: true` in OpenAPI,
155
175
  `deprecated_calls_total`. Rendered ONCE at projection (`X_ACTION_DEPRECATION_INVALID` at mount).
@@ -157,11 +177,11 @@ Owns the `action` + `mutator` primitives and their six projections. Tier 3.
157
177
  - **The span wraps `execute` whole** (including `def.row()`), with bounded attributes plus the
158
178
  namespaced idempotency key (never a metric label). `telemetry.test.ts`.
159
179
  - **`policyCapability` is a display label; `policyPermissions` (`ActionDescriptor.permissions`) is
160
- what a report matches on** (a `not()` clause contributes its permissions).
161
- - **MCP exposure reads core's `isMcpExposed`, in all three places** (`toMcpTools`, `describeAction`,
162
- `x-ultimate.mcpTool`). **A tool's NAME is the export name verbatim everywhere** — `toToolName` is
163
- deleted; `mcp-tool.test.ts` asserts one verbatim name across surfaces. `ActionFact.mcp` in the app
164
- manifest carries only `{ expose, description? }`.
180
+ what a report matches on** (a `not()` clause contributes its permissions). Both are
181
+ `@ultimat3/policy`'s, imported from there.
182
+ - **MCP exposure is core's `isMcpExposed`** (`describeAction`, `x-ultimate.mcpTool`); **a tool's
183
+ NAME is the export name verbatim** (`mcp-surface.test.ts`). `ActionFact.mcp` in the app manifest
184
+ carries only `{ expose, description? }`.
165
185
  - **`ActionJobHandle` is not consumed by `@ultimat3/jobs`** (`isJobHandle` needs `job()`'s private
166
186
  map). `.job()` gives `action:<name>` as a queue key, a payload-derived idempotency key and an
167
187
  `invoke` under `surface: 'job'`. The bridge is `agentJob()` in `@ultimat3/ai`, or an app's own
@@ -169,24 +189,25 @@ Owns the `action` + `mutator` primitives and their six projections. Tier 3.
169
189
 
170
190
  ## Invariants — flight control
171
191
 
172
- - **Flight control is `@ultimat3/core`'s, re-exported** — the same objects `@ultimat3/query`
173
- exports. Fix the pipeline in `packages/core/src/client-flight.ts`. No new code, curve, fence or
174
- retry loop here (`bun run flight-copies`).
192
+ - **Flight control is `@ultimat3/core`'s, and NOT re-exported** — its values are imported from
193
+ core (`X_HELPER_COPY` refuses a re-export); only the types `ClientFlight`/`ClientRetry`. Fix the
194
+ pipeline in `packages/core/src/client-flight.ts`. No new code, curve, fence or retry loop here (`bun run flight-copies`).
175
195
  - **`isTransientFailure` INVERTS `retryDecision`'s unclassified default** (a caller's `AbortError`
176
196
  is terminal). Must survive.
177
197
  - **`ClientFlight` is a TYPE inside `client.ts`, never a value.** Measured,
178
198
  `bun build --target=browser --minify`, one entry importing from `@ultimat3/action` — **the ONE
179
199
  table for these figures** (`packages/core/CLAUDE.md` points here):
180
200
 
181
- | Entry | before (HEAD `98d16d84`) | onto `clientTransport` | trace headers moved to core's outbound slot | As of 2026-09-23 |
182
- |---|---|---|---|---|
183
- | `rpc` | 18,097 B | 23,007 B | 18,119 B | 19,074 B |
184
- | `rpc` + `createClientFlight` | 23,903 B | 28,823 B | not measured | 25,197 B |
201
+ | Entry | `As of 2026-10-01` |
202
+ |---|---|
203
+ | `rpc` | 19,671 B (+181 B for the document's path-style stamp) |
204
+ | `rpc` + core's `clientFlight` | 25,954 B |
185
205
 
186
- Net against the pre-transport figure: +977 B, about the envelope decoder's size. `As of 2026-10-01`
187
- `rpc` is 19,671 B (19,490 B before the document's path-style stamp was read: +181 B, +45 gzipped)
188
- and `rpc` + `createClientFlight` 25,954 B.
189
- - **The `sideEffects` array is load-bearing**: `errors.ts` runs `registerErrorCodes` at import. Never `false`.
206
+ Earlier columns (pre-transport 18,097 B, +977 B net for the envelope decoder) are in git history.
207
+ - **`sideEffects` is `["./src/error-titles.ts"]`**, never `false` (drops its bare imports): the
208
+ titles ride into every barrel chunk (`problemError` decodes by code), `errors.ts`'s classes do
209
+ not. An http refusal needs no http module: core's decoders take its title off the body and its
210
+ `retry-after` off the header (`rpc-refusal.test.ts`).
190
211
 
191
212
  ## Invariants — idempotency
192
213
 
@@ -201,22 +222,47 @@ Owns the `action` + `mutator` primitives and their six projections. Tier 3.
201
222
  inside `withWriteOrigin(writeDigest(key))` so the page recognises its own `records` echo. A label,
202
223
  never a gate.
203
224
  - **Both stores FENCE a settlement on the reservation `id` AND `in-flight`**:
204
- `settle(key, value, reservationId)` / `fail(key, failure, reservationId)`. A fenced no-op is logged,
225
+ `settle(key, value, reservationId, redacted)` / `fail(key, failure, reservationId)`. A fenced no-op is logged,
205
226
  never thrown.
227
+ - **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.
206
236
  - **A stored status is NARROWED** (`isIdempotencyStatus`; unknown is
207
237
  `X_IDEMPOTENCY_STATUS_UNKNOWN`), never cast.
208
238
  - **Where records live is DECLARED and refused at registration**: `IdempotencyStore.scope` vs
209
239
  `configureIdempotency({ scope })`, compared by `assertIdempotencyScope` in `registerAction`
210
240
  (`X_IDEMPOTENCY_NOT_SHARED`). Default `'process'`.
211
241
  - **The memory store is bounded; `in-flight` records are the last evicted.** Never an LRU.
212
- - **`postgresIdempotencyStore` is the shared store** over a structural `PgExecutor` (no `action -> db`
213
- edge); the reservation is ONE `insert … on conflict` statement. The CLI boot installs it.
242
+ - **`postgresIdempotencyStore` is the shared store** over `@ultimat3/core`'s structural `PgExecutor`; the
243
+ 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`.
250
+ - **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.
214
254
 
215
255
  ## Invariants — cache and audit
216
256
 
217
257
  - **The post-commit bust never fails the write** — `cache-gate.ts` (the only `invalidateTags` caller)
218
258
  absorbs a refusing fan-out and logs through core's `logger`, never rendering the tags. A replay
219
259
  skips the bust.
260
+ - **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).
263
+ - **The OpenAPI contract assertion reads the registry-wide document** and compares `operationId`
264
+ — one built from the action alone could not fail. **`Problem` is `toProblem`'s members**, no
265
+ `code` pattern (`openapi-problem.test.ts`).
220
266
  - **The policy contract test asserts `ActionDeniedError` and sends valid input** (`sampleInput` from
221
267
  the input IR). Only `X_INPUT_INVALID` (and `X_AUDIT_SINK_MISSING`, the one refusal before the parse)
222
268
  becomes `X_CONTRACT_DRIFT`; everything else keeps its own code; a non-`UltimateError` is rethrown.
@@ -225,6 +271,8 @@ Owns the `action` + `mutator` primitives and their six projections. Tier 3.
225
271
  DENIED attempt is recorded. No audit entity, retention, hash chain or "who" convention. The
226
272
  vocabulary matches `@ultimat3/admin`'s by name (tier 5, no edge); admin's `AuditSink` is a known
227
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`.
228
276
  - **The memory sink DROPS** — a ring at `DEFAULT_MAX_AUDIT_RECORDS`, oldest first, counting `dropped`;
229
277
  no spelling of "unbounded".
230
278
  - **A durable sink writes what `audit-input.ts` allows**: input redacted through core's
@@ -234,7 +282,8 @@ Owns the `action` + `mutator` primitives and their six projections. Tier 3.
234
282
  `locale`, `tz`, `buildId`, `role`, actor `id`/`kind`/`orgId`/`onBehalfOf`) and keeps
235
283
  `failure.code`, never the error.
236
284
  - **`x_audit` ships no purge**, deliberately. **`SQL_AUDIT_INSERT` is positional**, pinned by
237
- `audit-parity.test.ts` with a distinct value per field.
285
+ `audit-parity.test.ts` with a distinct value per field. Only `*_TABLE` DDL is exported
286
+ (`X_SQL_EXPORT_UNREAD`).
238
287
  - **The two failure policies are opposites**: `X_AUDIT_SINK_MISSING` before the input parse (no
239
288
  logger-backed default sink); a sink refusing an ALLOWED record is `X_AUDIT_SINK_FAILED`, whose `fix:`
240
289
  branches on `record.idempotencyKey !== null` (`meta.replayable`); a sink refusing a denied or failed