@ultimat3/action 30.9.0 → 31.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.
@@ -4,7 +4,7 @@
4
4
  * `registerErrorCodes` call, because a second registration is how two modules end up deciding a
5
5
  * title by load order. Imported bare, so constructing one of these registers its title first.
6
6
  */
7
- import { renderCauseValue, UltimateError } from '@ultimat3/core';
7
+ import { renderCauseValue, UltimateError } from '@ultimat3/core/page';
8
8
  import './error-titles';
9
9
  // Type-only: `idempotency.ts` imports the classes below, and a runtime edge here would close the
10
10
  // cycle. `verbatimModuleSyntax` is what makes that guarantee mechanical.
@@ -78,7 +78,7 @@ export class IdempotencyNotSharedError extends UltimateError {
78
78
  // positional form is `unsafe`), so that line compiled and would have thrown on the first
79
79
  // reservation. The framework's own boot already installs this store; a host booting the
80
80
  // framework itself wraps the client it opened.
81
- fix: "the framework boot installs a shared store — reach this only from a host that boots it itself: setIdempotencyStore(postgresIdempotencyStore({ executor: { query: (text, values) => client.query({ text, values }) }, origin: () => client, reclaimAfterMs: requestDeadlineMs })) from '@ultimat3/action', or drop the declaration to configureIdempotency({ scope: 'process' })",
81
+ fix: "postgresIdempotencyStore({ executor: { query: (text, values) => client.query({ text, values }) }, origin: () => client, reclaimAfterMs: requestDeadlineMs }) # idempotencyStore: in apps/<app>/runtime.ts, or drop that field and the boot builds a shared one over DATABASE_URL — or declare configureIdempotency({ scope: 'process' })",
82
82
  meta: { storeScope: storeScope ?? null },
83
83
  });
84
84
  }
@@ -3,7 +3,7 @@
3
3
  * declaration or a call-time limit that cannot be counted against (`X_METER_INVALID`, the app's).
4
4
  * Codes and titles stay in `error-titles.ts`'s one registration; imported bare for that reason.
5
5
  */
6
- import { renderCauseValue, renderFixShellArg, UltimateError } from '@ultimat3/core';
6
+ import { renderCauseValue, renderFixShellArg, UltimateError } from '@ultimat3/core/page';
7
7
  import './error-titles';
8
8
 
9
9
  export interface MeterExceededInput {
@@ -1,7 +1,7 @@
1
1
  // The streaming action's one refusal, `X_ACTION_STREAM_INVALID`: a declaration that says it streams
2
2
  // and a handler that does not, or a chunk its own `stream` schema refuses.
3
3
 
4
- import { UltimateError } from '@ultimat3/core';
4
+ import { UltimateError } from '@ultimat3/core/page';
5
5
 
6
6
  const FIX =
7
7
  'async *handle({ input, ctx }) { yield chunk; return output; } — yield only what stream: declares';
package/src/errors.ts CHANGED
@@ -5,7 +5,6 @@
5
5
  * and are re-exported here, so `./errors` stays the one import path for all of them.
6
6
  */
7
7
  import {
8
- assertNever,
9
8
  deniedCallerFix,
10
9
  ERROR_DOCS_URL,
11
10
  hasErrorCode,
@@ -14,8 +13,8 @@ import {
14
13
  UltimateError,
15
14
  withRequestId,
16
15
  withStatedDelay,
17
- } from '@ultimat3/core';
18
- import type { SurfaceDenial } from '@ultimat3/policy';
16
+ } from '@ultimat3/core/page';
17
+ import type { PrimitiveDenial, SurfaceDenial } from '@ultimat3/policy';
19
18
  import type { ValidationIssue } from '@ultimat3/schema';
20
19
  // The titles are `error-titles.ts`, imported bare: this module is then side-effect-free and the
21
20
  // array does not list it, so a chunk keeps only the classes it constructs, and a chunk that
@@ -45,7 +44,7 @@ export class ActionUnregisteredError extends UltimateError {
45
44
  super({
46
45
  code: 'X_ACTION_UNREGISTERED',
47
46
  cause: 'an action was projected before it was registered, so it has no name',
48
- fix: "call registerActions(await import('./actions')) at boot, before mounting routes",
47
+ fix: "list the action's module in defineApi({ actions }) in apps/<app>/api/index.ts — the boot names it before mounting routes",
49
48
  });
50
49
  }
51
50
  }
@@ -66,34 +65,6 @@ export class ActionForeignError extends UltimateError {
66
65
  }
67
66
  }
68
67
 
69
- export function denialCode(denial: SurfaceDenial): string {
70
- switch (denial.surface) {
71
- case 'http':
72
- return denial.problem.code;
73
- case 'live':
74
- case 'job':
75
- return denial.code;
76
- case 'mcp':
77
- return denial.content[0]?.text.split(':')[0] ?? 'X_FORBIDDEN';
78
- default:
79
- return assertNever(denial);
80
- }
81
- }
82
-
83
- export function denialReason(denial: SurfaceDenial): string {
84
- switch (denial.surface) {
85
- case 'http':
86
- return denial.problem.detail;
87
- case 'live':
88
- case 'job':
89
- return denial.reason;
90
- case 'mcp':
91
- return denial.content[0]?.text ?? 'denied';
92
- default:
93
- return assertNever(denial);
94
- }
95
- }
96
-
97
68
  /**
98
69
  * An authz denial, thrown by `guardAction()`. The code and reason come from the policy
99
70
  * decision — this package never invents an authz code — and the surface-shaped
@@ -102,15 +73,18 @@ export function denialReason(denial: SurfaceDenial): string {
102
73
  export class ActionDeniedError extends UltimateError {
103
74
  readonly denial: SurfaceDenial;
104
75
 
105
- constructor(action: string, denial: SurfaceDenial) {
106
- const code = denialCode(denial);
76
+ /**
77
+ * Built from `guardPrimitive`'s answer, which already read the code and the reason out of the
78
+ * surface shape — a type-only import, so the browser-reachable error module pulls in no policy.
79
+ */
80
+ constructor(refused: PrimitiveDenial<'action'>) {
107
81
  super({
108
- code,
109
- cause: `${action} denied: ${denialReason(denial)}`,
110
- fix: `x policy explain ${action} --json # shows which clause decided and why`,
82
+ code: refused.code,
83
+ cause: `${refused.name} denied: ${refused.reason}`,
84
+ fix: `x policy explain ${refused.name} --json # shows which clause decided and why`,
111
85
  callerFix: deniedCallerFix(),
112
86
  });
113
- this.denial = denial;
87
+ this.denial = refused.denial;
114
88
  }
115
89
  }
116
90
 
@@ -167,6 +141,11 @@ export class InputInvalidError extends UltimateError {
167
141
  * structured channel beside it.
168
142
  */
169
143
  readonly issues: readonly ValidationIssue[] | undefined;
144
+ /**
145
+ * `true` when validation stopped at its issue cap (`MAX_ISSUES`, `@ultimat3/schema`): `issues`
146
+ * is a prefix, and correcting all of it may surface more. Also on `meta` for an HTTP surface.
147
+ */
148
+ readonly issuesTruncated: boolean;
170
149
 
171
150
  /**
172
151
  * `detail` is the rendered form of `issues` and must stay so — `formatIssues(issues).join('; ')`,
@@ -186,6 +165,7 @@ export class InputInvalidError extends UltimateError {
186
165
  * action's.
187
166
  */
188
167
  subject: 'action' | 'tool' = 'action',
168
+ issuesTruncated = false,
189
169
  ) {
190
170
  super({
191
171
  code: 'X_INPUT_INVALID',
@@ -195,9 +175,12 @@ export class InputInvalidError extends UltimateError {
195
175
  // body — and cannot run `x actions show`.
196
176
  callerFix:
197
177
  'correct the fields named in cause to match the published input schema (the tool inputSchema, or the OpenAPI request body) and send again — the same input is refused the same way',
198
- ...(issues === undefined ? {} : { meta: { issues } }),
178
+ ...(issues === undefined
179
+ ? {}
180
+ : { meta: issuesTruncated ? { issues, issuesTruncated: true } : { issues } }),
199
181
  });
200
182
  this.issues = issues;
183
+ this.issuesTruncated = issuesTruncated;
201
184
  }
202
185
  }
203
186
 
@@ -254,6 +237,8 @@ export interface RemoteFailure {
254
237
  * refuses to read, and in both cases `cause` still holds every rejection.
255
238
  */
256
239
  readonly issues?: readonly ValidationIssue[] | undefined;
240
+ /** The server cut `issues` at its cap (`MAX_ISSUES`): the list is a prefix. Only ever `true`. */
241
+ readonly issuesTruncated?: true | undefined;
257
242
  /**
258
243
  * The document's `meta` member — the keys the server's `registerProblemMeta` declared for the
259
244
  * code, already read as a plain JSON object by `client.ts`. Carried onto the error's own `meta`
@@ -340,6 +325,7 @@ export class RemoteActionError extends UltimateError {
340
325
  // Absent rather than `undefined`: `meta` is rendered into `--json` and the error
341
326
  // reporter, and a null member reads as "the server sent an empty list".
342
327
  ...(failure.issues === undefined ? {} : { issues: failure.issues }),
328
+ ...(failure.issuesTruncated === true ? { issuesTruncated: true } : {}),
343
329
  },
344
330
  failure.requestId,
345
331
  ),
@@ -361,7 +347,7 @@ export class RpcFailedError extends UltimateError {
361
347
  super({
362
348
  code: 'X_RPC_FAILED',
363
349
  cause: `${name} returned HTTP ${status} without a problem+json body`,
364
- fix: `check the gateway in front of the app, then: x actions show ${renderFixShellArg(name, '<action-name>')} --json`,
350
+ fix: "match X_CLIENT_TRANSPORT_FAILED instead: isUltimateError(e) && e.code === 'X_CLIENT_TRANSPORT_FAILED', with isUltimateError from @ultimat3/core",
365
351
  retry: retryForStatus('X_RPC_FAILED', status),
366
352
  });
367
353
  }
@@ -379,7 +365,7 @@ export class AuditSinkMissingError extends UltimateError {
379
365
  super({
380
366
  code: 'X_AUDIT_SINK_MISSING',
381
367
  cause: `action "${action}" declares \`audit: true\` and no audit sink is installed`,
382
- fix: "call setAuditSink(yourSink) from '@ultimat3/core' at boot, before registerActions()",
368
+ fix: 'auditSink: postgresAuditSink({ executor }) # in export const runtime = { … }, apps/<app>/runtime.ts',
383
369
  });
384
370
  }
385
371
  }
@@ -410,8 +396,8 @@ export class AuditSinkFailedError extends UltimateError {
410
396
  code: 'X_AUDIT_SINK_FAILED',
411
397
  cause: `"${action}" ran and its audit sink refused the record, so the change is unrecorded — the handler had already committed`,
412
398
  fix: replayable
413
- ? `fix the sink installed by setAuditSink, then retry with the same Idempotency-Key — the replay re-records without re-running the handler`
414
- : `fix the sink installed by setAuditSink, then reconcile this one change by hand — do NOT retry: ${action} ran with no Idempotency-Key, so a second call runs the committed handler again. Add \`idempotent: true\` and send the header to make retries safe`,
399
+ ? `fix the sink runtime.ts installs as auditSink (apps/<app>/runtime.ts), then retry with the same Idempotency-Key — the replay re-records without re-running the handler`
400
+ : `fix the sink runtime.ts installs as auditSink (apps/<app>/runtime.ts), then reconcile this one change by hand — do NOT retry: ${action} ran with no Idempotency-Key, so a second call runs the committed handler again. Add \`idempotent: true\` and send the header to make retries safe`,
415
401
  // Read by `--json` and the error reporter: whether a retry is safe is the one decision an
416
402
  // operator makes here, so it is a field and not only a sentence.
417
403
  meta: { action, replayable },
package/src/exercise.ts CHANGED
@@ -14,7 +14,8 @@ import {
14
14
  tryUseContext,
15
15
  } from '@ultimat3/core';
16
16
  import type { ExerciseRequestOption } from '@ultimat3/http';
17
- import { statusFor, withRequestAs, withScratchRateLimitStore } from '@ultimat3/http';
17
+ import { statusFor, withRequestAs } from '@ultimat3/http';
18
+ import { withScratchRateLimitStore } from '@ultimat3/http/adapter';
18
19
  import { admitsAnonymous } from '@ultimat3/policy';
19
20
  import { arbitrary } from '@ultimat3/schema/sample';
20
21
  import type { AnyAction } from './action';
@@ -0,0 +1,18 @@
1
+ // The handler-override slot: a stored declaration's `handle`, replaced for a test. Keyed by the
2
+ // declaration `invoke` runs, read at call time — by `invoke` for a plain action and by the drain
3
+ // for a stream-declaring one — so every surface reaches the double through the one execution path.
4
+
5
+ import type { Ctx } from '@ultimat3/core';
6
+
7
+ /** What stands where `handle` is: the declaration's own argument shape, any answer. */
8
+ export type HandlerOverride = (args: { readonly input: unknown; readonly ctx: Ctx }) => unknown;
9
+
10
+ const OVERRIDES = new WeakMap<object, HandlerOverride>();
11
+
12
+ /** `undefined` puts the declared handler back. */
13
+ export function setHandlerOverride(def: object, handle: HandlerOverride | undefined): void {
14
+ if (handle === undefined) OVERRIDES.delete(def);
15
+ else OVERRIDES.set(def, handle);
16
+ }
17
+
18
+ export const handlerOverrideOf = (def: object): HandlerOverride | undefined => OVERRIDES.get(def);
@@ -3,7 +3,7 @@
3
3
  * and for tests, and refused at registration under a `shared` declaration — its `scope` says so.
4
4
  * Shaped after `@ultimat3/http`'s `memoryRateLimitStore`, including the deliberate eviction order.
5
5
  */
6
- import { finiteCount, uuidV7 } from '@ultimat3/core';
6
+ import { finiteCount, liveClock, logger, uuidV7 } from '@ultimat3/core';
7
7
  import { IdempotencyReservationLostError } from './errors-idempotency';
8
8
  import type {
9
9
  IdempotencyFailure,
@@ -88,7 +88,7 @@ export class MemoryIdempotencyStore implements IdempotencyStore {
88
88
  );
89
89
  // Batched down to 90% so the eviction sort is paid once per 10% of the cap, not per write.
90
90
  this.#evictTo = Math.max(1, Math.floor(this.#maxKeys * 0.9));
91
- this.#now = options.now ?? ((): number => Date.now());
91
+ this.#now = options.now ?? ((): number => liveClock.now().getTime());
92
92
  this.#reclaimAfterMs = options.reclaimAfterMs ?? requestDeadlineMs;
93
93
  }
94
94
 
@@ -161,6 +161,7 @@ export class MemoryIdempotencyStore implements IdempotencyStore {
161
161
  const tx = liveTransaction();
162
162
  if (tx === undefined) {
163
163
  if (existing !== undefined) this.#records.set(key, settled(existing));
164
+ else fenced(key, reservationId, 'settle');
164
165
  return Promise.resolve();
165
166
  }
166
167
  // Inside a transaction the record is settled BY the commit, as the Postgres store's is: a
@@ -188,6 +189,8 @@ export class MemoryIdempotencyStore implements IdempotencyStore {
188
189
  const existing = this.#owned(key, reservationId);
189
190
  if (existing !== undefined) {
190
191
  this.#records.set(key, { ...existing, status: 'failed', value: undefined, failure });
192
+ } else {
193
+ fenced(key, reservationId, 'fail');
191
194
  }
192
195
  return Promise.resolve();
193
196
  }
@@ -245,6 +248,15 @@ export class MemoryIdempotencyStore implements IdempotencyStore {
245
248
  }
246
249
  }
247
250
 
251
+ /**
252
+ * Logged, never thrown — the Postgres store's line, word for word (`idempotency-postgres.ts`), so
253
+ * an operator's alert does not depend on which store is installed. A fenced settlement means the
254
+ * value this attempt produced is stored nowhere; throwing would turn a done write into an error.
255
+ */
256
+ function fenced(key: string, reservationId: string, statement: 'settle' | 'fail'): void {
257
+ logger.warn('action.idempotency.settlement-fenced', { key, reservationId, statement });
258
+ }
259
+
248
260
  /**
249
261
  * The one way to build the memory store — the twin of `postgresIdempotencyStore()`. The class
250
262
  * stays a type in the barrel (`X_FACTORY_NAME_SPELLING`), so `new` is never a second spelling.
@@ -5,7 +5,7 @@
5
5
  * Statements are spelled out so an agent can run the exact one it saw in a log.
6
6
  */
7
7
  import type { PgExecutor } from '@ultimat3/core';
8
- import { finiteCount, logger, uuidV7 } from '@ultimat3/core';
8
+ import { finiteCount, liveClock, logger, uuidV7 } from '@ultimat3/core';
9
9
  import { IdempotencyReservationLostError, IdempotencyStatusUnknownError } from './errors';
10
10
  import type {
11
11
  IdempotencyFailure,
@@ -172,7 +172,7 @@ export interface PostgresIdempotencyStore extends IdempotencyStore {
172
172
  /**
173
173
  * **The boot installs this for you — an app declares the scope and nothing else.**
174
174
  * `@ultimat3/cli`'s `startServices` builds a `PgExecutor` from the client it already resolved and
175
- * calls `setIdempotencyStore(postgresIdempotencyStore({ executor, origin, reclaimAfterMs }))` before `loadApp`, so the
175
+ * installs `postgresIdempotencyStore({ executor, origin, reclaimAfterMs })` before `loadApp`, so the
176
176
  * store is in place by the time `registerAction` evaluates a declaration against it. All an app
177
177
  * owes is the one line `x new` scaffolds into `apps/web/server.ts`:
178
178
  *
@@ -180,19 +180,19 @@ export interface PostgresIdempotencyStore extends IdempotencyStore {
180
180
  * configureIdempotency({ scope: 'shared' });
181
181
  * ```
182
182
  *
183
- * Installing one by hand is for a host that boots the framework itself, and it needs a real
183
+ * Handing one over is `idempotencyStore:` in `apps/<app>/runtime.ts`, and it needs a real
184
184
  * `PgExecutor` — never `Bun.sql`, which has no `.query`. Wrap the client this process already
185
185
  * opened, so a second pool is not opened against a URL the boot resolved once:
186
186
  *
187
187
  * ```ts
188
188
  * const client = db();
189
- * setIdempotencyStore(
190
- * postgresIdempotencyStore({
189
+ * export const runtime = {
190
+ * idempotencyStore: postgresIdempotencyStore({
191
191
  * executor: { query: (text, values) => client.query({ text, values }) },
192
192
  * origin: () => client,
193
193
  * reclaimAfterMs: requestDeadlineMs, // the app's `requestTimeoutMs`, from '@ultimat3/action'
194
194
  * }),
195
- * );
195
+ * };
196
196
  * ```
197
197
  */
198
198
  export function postgresIdempotencyStore(
@@ -219,7 +219,7 @@ export function postgresIdempotencyStore(
219
219
  const tx = liveTransaction();
220
220
  return tx !== undefined && tx.origin === options.origin() ? tx.executor : undefined;
221
221
  };
222
- const now = options.now ?? ((): number => Date.now());
222
+ const now = options.now ?? ((): number => liveClock.now().getTime());
223
223
 
224
224
  const fetch = async (key: string): Promise<IdempotencyRecord | undefined> => {
225
225
  const rows = await exec.query<IdempotencyRow>(SQL_IDEMPOTENCY_GET, [key, windowSecs]);
@@ -4,6 +4,7 @@
4
4
  * a value or a failure — and a concurrent duplicate is refused rather than run twice, because a
5
5
  * double charge is worse than a 409.
6
6
  */
7
+
7
8
  import {
8
9
  compareFingerprint,
9
10
  finiteCount,
@@ -15,6 +16,7 @@ import {
15
16
  runToCompletion,
16
17
  tryUseContext,
17
18
  } from '@ultimat3/core';
19
+ import { adapterSlot } from '@ultimat3/core/adapter';
18
20
  import {
19
21
  IdempotencyConflictError,
20
22
  IdempotencyNotSharedError,
@@ -178,7 +180,9 @@ export interface IdempotencyConfig {
178
180
  export const DEFAULT_IDEMPOTENCY_CONFIG: IdempotencyConfig = Object.freeze({ scope: 'process' });
179
181
 
180
182
  let config: IdempotencyConfig = DEFAULT_IDEMPOTENCY_CONFIG;
181
- let defaultStore: IdempotencyStore = memoryIdempotencyStore();
183
+ let baseStore: IdempotencyStore = memoryIdempotencyStore();
184
+ /** The installed store: a stack over the process default, released in any order. */
185
+ const storeSlot = adapterSlot<IdempotencyStore>(() => baseStore);
182
186
 
183
187
  /** Declared at boot, before `registerActions()`. Nothing infers a replica count. */
184
188
  export function configureIdempotency(next: IdempotencyConfig): void {
@@ -208,18 +212,24 @@ export function idempotencyCeilingMs(): number {
208
212
  return deadline > 0 ? Math.ceil(2 * deadline) : CEILING_WITHOUT_DEADLINE_MS;
209
213
  }
210
214
 
211
- export function setIdempotencyStore(store: IdempotencyStore): void {
212
- defaultStore = store;
215
+ /**
216
+ * The boot's installer (its release removes THIS install only, in any order — `adapterSlot`), on `@ultimat3/action/adapter` and never the `.` barrel: the boot installs
217
+ * `postgresIdempotencyStore` over its pool, and an app swaps it with `idempotencyStore:` in
218
+ * `apps/<app>/runtime.ts`.
219
+ */
220
+ export function installIdempotencyStore(store: IdempotencyStore): () => void {
221
+ return storeSlot.install(store);
213
222
  }
214
223
 
215
224
  export function getIdempotencyStore(): IdempotencyStore {
216
- return defaultStore;
225
+ return storeSlot.current();
217
226
  }
218
227
 
219
228
  /** Test-only. A process configures its idempotency once at boot and never reconfigures it. */
220
229
  export function resetIdempotency(): void {
221
230
  config = DEFAULT_IDEMPOTENCY_CONFIG;
222
- defaultStore = memoryIdempotencyStore();
231
+ storeSlot.reset();
232
+ baseStore = memoryIdempotencyStore();
223
233
  }
224
234
 
225
235
  /**
@@ -400,12 +410,12 @@ function runWithinCeiling<T>(key: string, ceilingMs: number, run: () => Promise<
400
410
  }
401
411
 
402
412
  /**
403
- * A stored fingerprint that is not this request's exact string. A legacy unkeyed one (a row
404
- * written before keying) is still checked exactly. One keyed under a secret this process does not
405
- * hold — the signing secret rotated inside the window — cannot be checked either way, and is
406
- * answered by its STATUS alone: replay, never re-run. Refusing it would 409 every honest retry
407
- * across a rotation; running it would be the double charge. What is lost for that window only is
408
- * the "same key, different body" diagnosis, and a warning says so.
413
+ * A stored fingerprint that is not this request's exact string. One keyed under a secret this
414
+ * process does not hold — the signing secret rotated inside the window — or in no form this build
415
+ * reads (a bare pre-v24 hash) cannot be checked either way, and is answered by its STATUS alone:
416
+ * replay, never re-run. Refusing it would 409 every honest retry across a rotation; running it
417
+ * would be the double charge. What is lost for that window only is the "same key, different body"
418
+ * diagnosis, and a warning says so.
409
419
  */
410
420
  function assertSamePayload(key: string, stored: string, input: unknown): void {
411
421
  const verdict = compareFingerprint(stored, input, IDEMPOTENCY_FINGERPRINT_PURPOSE);
package/src/index.ts CHANGED
@@ -57,10 +57,10 @@ export { apiDeclaration } from './api-declaration';
57
57
  * The audit seam. `AuditSink` is the whole extension point: the framework supplies the record
58
58
  * and never the row. `audit-gate.ts` stays unexported — the sink has one caller, and that
59
59
  * absence is what keeps it one. The record and the sink are `@ultimat3/core`'s, re-exported as
60
- * TYPES only because this barrel's sinks name them; the slot (`setAuditSink`) is core's alone.
60
+ * TYPES only because this barrel's sinks name them; the slot (`installAuditSink`) is core's alone.
61
61
  */
62
62
  export type { AuditRecord, AuditSink } from './audit';
63
- export { AUDIT_INPUT_MAX_DEPTH, auditableInput, UNREPRESENTABLE } from './audit-input';
63
+ export { auditableInput } from './audit-input';
64
64
  export type { MemoryAuditSink, MemoryAuditSinkOptions } from './audit-memory';
65
65
  export { DEFAULT_MAX_AUDIT_RECORDS, memoryAuditSink } from './audit-memory';
66
66
  /**
@@ -76,7 +76,7 @@ export type {
76
76
  AuditSubjectTarget,
77
77
  AuditSubjectTargetOptions,
78
78
  } from './audit-subject-target';
79
- export { AUDIT_SUBJECT_COLUMNS, auditSubjectTarget } from './audit-subject-target';
79
+ export { auditSubjectTarget } from './audit-subject-target';
80
80
  export type {
81
81
  ActionLike,
82
82
  ActionMap,
@@ -89,29 +89,20 @@ export type {
89
89
  } from './client';
90
90
  export { rpc } from './client';
91
91
  export type { ContractTest, ContractTestOptions } from './contract-test';
92
- export { anonymousCtx, contractTestsFor, policyTestStubFor } from './contract-test';
92
+ export { anonymousCtx, contractTestsFor } from './contract-test';
93
93
  export type { Api, ApiDef, ApiModule, ApiModules } from './define-api';
94
94
  export { defineApi } from './define-api';
95
95
  export type { IdempotencyConflictReason, IdempotencyKeyProblem, RemoteFailure } from './errors';
96
96
  export {
97
97
  ActionDeniedError,
98
- ActionDeprecationInvalidError,
99
- ActionDuplicateError,
100
- ActionForeignError,
101
- ActionPathDuplicateError,
102
- ActionPolicyMissingError,
103
- ActionUnregisteredError,
104
98
  AuditSinkFailedError,
105
- AuditSinkMissingError,
106
99
  ContractDriftError,
107
100
  IdempotencyConflictError,
108
101
  IdempotencyKeyInvalidError,
109
- IdempotencyNotSharedError,
110
102
  IdempotencyOutputUnserializableError,
111
103
  IdempotencyReplayedFailureError,
112
104
  IdempotencyReservationLostError,
113
105
  IdempotencyRunCeilingError,
114
- IdempotencyStatusUnknownError,
115
106
  IdempotentReplayRedactedError,
116
107
  InputInvalidError,
117
108
  MutatorNotIdempotentError,
@@ -119,14 +110,9 @@ export {
119
110
  RemoteActionError,
120
111
  RpcFailedError,
121
112
  } from './errors';
122
- export {
123
- ActionDeprecationSuccessorInvalidError,
124
- ActionHttpPathInvalidError,
125
- ActionPathStyleInvalidError,
126
- OpenApiConfigInvalidError,
127
- } from './errors-http';
128
- export type { MeterExceededInput, MeterField } from './errors-meter';
129
- export { MeterExceededError, MeterInvalidError } from './errors-meter';
113
+ export { ActionDeprecationSuccessorInvalidError } from './errors-http';
114
+ export type { MeterExceededInput } from './errors-meter';
115
+ export { MeterExceededError } from './errors-meter';
130
116
  export { ActionStreamInvalidError } from './errors-stream';
131
117
  export type {
132
118
  ExerciseActorOptions,
@@ -137,12 +123,7 @@ export type {
137
123
  export { exerciseAction } from './exercise';
138
124
  export type { ContractExercise, ExerciseDeclarations } from './exercise-declarations';
139
125
  export type { OpenApiOperation } from './http';
140
- export {
141
- operationTagOf,
142
- toOpenApiOperation,
143
- toPostBinding,
144
- toRoute,
145
- } from './http';
126
+ export { toOpenApiOperation, toPostBinding, toRoute } from './http';
146
127
  /** `http: { path }` on an action pins its URL; the app's `pathStyle` derives every other one. */
147
128
  export type { ActionHttp, ActionHttpMethod } from './http-path';
148
129
  export {
@@ -171,14 +152,9 @@ export type {
171
152
  export {
172
153
  assertIdempotencyScope,
173
154
  configureIdempotency,
174
- DEFAULT_IDEMPOTENCY_CONFIG,
175
155
  getIdempotencyStore,
176
- IDEMPOTENCY_STATUSES,
177
156
  idempotencyCeilingMs,
178
- idempotencyConfig,
179
157
  isIdempotencyStatus,
180
- resetIdempotency,
181
- setIdempotencyStore,
182
158
  withIdempotency,
183
159
  } from './idempotency';
184
160
  export { idempotencyKeyFor, MAX_IDEMPOTENCY_KEY_LENGTH } from './idempotency-key';
@@ -186,11 +162,7 @@ export type {
186
162
  MemoryIdempotencyStore,
187
163
  MemoryIdempotencyStoreOptions,
188
164
  } from './idempotency-memory';
189
- export {
190
- DEFAULT_IDEMPOTENCY_WINDOW_MS,
191
- DEFAULT_MAX_IDEMPOTENCY_KEYS,
192
- memoryIdempotencyStore,
193
- } from './idempotency-memory';
165
+ export { DEFAULT_MAX_IDEMPOTENCY_KEYS, memoryIdempotencyStore } from './idempotency-memory';
194
166
  export type {
195
167
  PostgresIdempotencyStore,
196
168
  PostgresIdempotencyStoreOptions,
@@ -200,7 +172,6 @@ export { postgresIdempotencyStore, SQL_IDEMPOTENCY_TABLE } from './idempotency-p
200
172
  /** The one execution path. `defOf` stays unexported — that is the enforcement. */
201
173
  export { actionName, invoke } from './invoke';
202
174
  export type { ActionJobHandle } from './job-handle';
203
- export { toJobHandle } from './job-handle';
204
175
  export type { JsonSchemaObject } from './json-schema';
205
176
  export { jsonSchemaOf } from './json-schema';
206
177
  /**
@@ -217,13 +188,13 @@ export type {
217
188
  MeterWarnTarget,
218
189
  ResolvedLimit,
219
190
  } from './meter';
220
- export { METER_PERIODS, MeterWarning } from './meter';
191
+ export { MeterWarning } from './meter';
221
192
  export type { MemoryMeterStoreOptions } from './meter-memory';
222
- export { DEFAULT_MAX_METER_KEYS, memoryMeterStore } from './meter-memory';
193
+ export { memoryMeterStore } from './meter-memory';
223
194
  export type { PostgresMeterStoreOptions } from './meter-postgres';
224
195
  export { postgresMeterStore, SQL_METER_TABLE } from './meter-postgres';
225
196
  export type { MeterDraw, MeterDrawResult, MeterKey, MeterStore } from './meter-store';
226
- export { getMeterStore, resetMeterStore, setMeterStore } from './meter-store';
197
+ export { getMeterStore } from './meter-store';
227
198
  export type {
228
199
  LocalTable,
229
200
  LocalTableName,
@@ -235,15 +206,10 @@ export type {
235
206
  } from './mutator';
236
207
  export { custom, isMutator, mutator } from './mutator';
237
208
  export type { ActionPath } from './naming';
238
- export { derivePath, inputSchemaName, outputSchemaName } from './naming';
209
+ export { derivePath, inputSchemaName } from './naming';
239
210
  export type { BuildOpenApiOptions, OpenApiDocument, OpenApiInfo } from './openapi';
240
211
  export { buildOpenApi, serializeOpenApi } from './openapi';
241
- export {
242
- BEARER_SCHEME,
243
- COOKIE_SCHEME,
244
- completeOpenApi,
245
- mountOpenApi,
246
- } from './openapi-complete';
212
+ export { completeOpenApi, mountOpenApi } from './openapi-complete';
247
213
  /**
248
214
  * `@ultimat3/http`'s `hooks.explainMiss` for the action surface: a POST to an action's path under
249
215
  * a `pathStyle` this app does not serve answers `X_CONTRACT_DRIFT` naming the style it does.
@@ -257,17 +223,18 @@ export type { ActionPolicy, PolicySubject, Surface } from './policy-gate';
257
223
  * `@ultimat3/core`'s — imported from there, so `query` reads the same walk through one import path.
258
224
  */
259
225
  export { guardAction, guardActionBeforeInput } from './policy-gate';
226
+ // `registerAction`/`registerActions` are deliberately absent: `defineApi` is an app's one
227
+ // registration path, and `x`'s loader reaches them through `@ultimat3/action/loader`.
260
228
  export {
261
229
  actionHttpPath,
262
230
  configureActionPathStyle,
263
231
  describeActions,
264
232
  getAction,
265
233
  listActions,
266
- registerAction,
267
- registerActions,
268
234
  resetActions,
269
235
  } from './registry';
270
236
  export { requestDeadlineMs } from './request-deadline';
237
+ export { stableStringify } from './stable';
271
238
  /**
272
239
  * What a named tool passes on its way to `invoke`, on either tool surface. `@ultimat3/mcp`'s
273
240
  * `mcpConfirmations` registers one and `@ultimat3/ai`'s adapter reads it: one tier down from both.
@@ -288,12 +255,7 @@ export {
288
255
  * handle and its manifest row. The machine itself is `@ultimat3/entity`'s — this package owns the
289
256
  * projection, not the legality rule.
290
257
  */
291
- export type {
292
- TransitionDef,
293
- TransitionInput,
294
- TransitionTarget,
295
- TransitionValues,
296
- } from './transition';
258
+ export type { TransitionDef, TransitionInput, TransitionTarget } from './transition';
297
259
  export { transition } from './transition';
298
260
  /**
299
261
  * `invoke`'s input parse and its `X_INPUT_INVALID` refusal, alone. Exported so a caller that must
@@ -307,4 +269,4 @@ export { parseActionInput } from './validate';
307
269
  * `x g resource` emits, to keep this package out of its chunk — holds the parsed body itself and
308
270
  * would otherwise write a second, unvalidated reader.
309
271
  */
310
- export { issuesFromWire, MAX_WIRE_ISSUES } from './wire-issues';
272
+ export { issuesFromWire } from './wire-issues';