@ultimat3/query 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
@@ -16,74 +16,88 @@ Owns the `query` primitive: reads, live reads, cursors, the incremental matcher.
16
16
  | `read.ts` | **the one read path** (`runQuery`, `sourceFor`) + the private declaration store `sql` lives in |
17
17
  | `facade.ts` | the fluent surface — binds each projection to the query, re-implements none |
18
18
  | `http.ts` | route projection (`GET /_x/query/<kebab>`, `enforcedBy: 'handler'`) |
19
- | `mcp-tool.ts` | MCP read descriptor, same `sourceFor` |
20
19
  | `client.ts` | typed read client (browser-safe); dispatches through `@ultimat3/core`'s `clientTransport`, never `fetch` |
21
20
  | `client-scale-pins.ts` | compile-time pin: `queryClient<Api['queries']>` over 100 reads in 50 modules — list, `.page` and `single: true` |
22
21
  | `record-answer.ts` | a read's HTTP answer: bare rows, or the record envelope when `rows:` is an entity's branded row schema |
23
22
  | — | flight control is **`@ultimat3/core`**'s `client-flight.ts` + `client-wire.ts`, re-exported. No local copy |
24
- | `naming.ts` | export name → `/_x/query/<kebab>` — `derivePath` IS core's `queryPath`. **Paths only** |
25
23
  | `registry.ts` | export-name registration, `describeQueries()`, the `registerPrimitiveRegistrar('query', …)` announcement |
26
- | `live.ts` | `LiveQuery` descriptor + cursor arithmetic |
24
+ | `live.ts` | `LiveQuery` descriptor + cursor arithmetic + `spendQueryLimit` (a live subscribe's spend) |
25
+ | `rate-limit-gate.ts` | **the one place** a read's declared `rateLimit:` is spent |
27
26
  | `subscribes.ts` | the relations a live read declares, and the two assertions that keep them true |
28
27
  | `sealed-shape.ts` | a live read keyed on a `.sealed()` column is refused at subscribe (`X_MATCHER_UNSUPPORTED`) |
29
28
  | `matcher.ts` | change event → minimal patch, `refill` when the window cannot place the row, or `X_MATCHER_UNSUPPORTED` |
30
29
  | `pagination.ts` | `paginate()` over core's cursor codec — no offset, ever |
31
30
  | `cursor-value.ts` | what a sort value becomes inside a cursor, and back |
32
31
  | `input-shape.ts` | what a read's `input:` may be, given that its route is a query STRING |
33
- | `sql.ts` | `explain()` / `describeSql()` |
32
+ | `sql.ts` | `explainQuery()` / `describeSql()` |
34
33
  | `cache.ts` | the read path: the request memo, and the fill through `@ultimat3/cache`'s registered tiers |
35
34
  | `search.ts` | `search()` — the query FACTORY over an entity's `.searchable()` columns |
36
35
  | `source.ts` | `SqlSource` contract + `from()` in-memory reference |
37
36
  | `shape.ts` | shared read vocabulary (filters, ordering, seek keys) |
37
+ | `column-kinds.ts` | `kindsOf(relation)`: a column's DECLARED kind, from `QueryShape.entity` |
38
38
  | `policy-gate.ts` | **the only** file that touches `@ultimat3/policy` |
39
- | `deprecation.ts` | `Deprecation` + RFC 9745/8594 render + `deprecated_calls_total` — TWINNED with `@ultimat3/action`'s |
39
+ | `audit-gate.ts` | `audit: true` — **the only** file that calls an `AuditSink` (core's): one record per call, an audited source per `execute()` |
40
+ | `audit-errors.ts` | `X_QUERY_AUDIT_SINK_MISSING` / `X_QUERY_AUDIT_SINK_FAILED` (titles in `errors.ts`) |
40
41
 
41
42
  ## Invariants — the read path
42
43
 
43
- - Every surface goes through `sourceFor`: the policy's actor half (`guardBeforeInput`), parse input,
44
+ - Every surface goes through `sourceFor`: the policy's actor half (`guardQueryBeforeInput`), parse input,
44
45
  evaluate policy, build the source. A second
45
46
  read path is the one unforgivable change here.
46
47
  - **An explicit `ctx` is INSTALLED, never merely passed** — the ambient context (which
47
- `@ultimat3/entity`'s tenant guard reads) must be the identity `guard()` decided about.
48
+ `@ultimat3/entity`'s tenant guard reads) must be the identity `guardQuery()` decided about.
48
49
  `read-context.test.ts` asserts ambient, `options.actor` and `options.ctx` are one caller.
49
50
  - **Skipping a read's policy costs a WRITTEN REASON** (`unenforced?: string`; blank refused).
50
51
  `ToLiveOptions.enforce` stays a boolean, translated into `SHARED_WINDOW_REASON`.
51
52
  - The declaration never leaves `read.ts`. `defOf`/`stashDef`/`hasDef` are never re-exported from
52
53
  `src/index.ts` — that omission is the enforcement. A query has no `.def`; outside the package read
53
54
  `.input`/`.policy`/`.cache`/`.mcp`/`.isLive` or `describe()`. A projection is reached through the
54
- query (`liveFeed.tool()`); new methods are bound in `facade.ts`.
55
+ query (`liveFeed.live()`); new methods are bound in `facade.ts`. **No MCP tool is projected
56
+ here** (O-tool, 25.0.0): `@ultimat3/mcp`'s `toolFrom` is the one projection, reading through
57
+ `sourceFor(…, { surface: 'mcp' })`. `.tool()`/`toQueryTool` coming back is red in `index.test.ts`,
58
+ `dsl.test.ts` and `type-pins.ts`.
55
59
  - `src/index.ts` re-exports `t` from `@ultimat3/schema` **verbatim** (`index.test.ts` asserts identity).
56
60
  - **`LiveQuery` describes the read *and* runs it** (`execute()`, never memoised).
57
61
  `@ultimat3/realtime`'s shared window reads through it.
58
62
  - `isLive` is the declared boolean, `live()` the subscription. `QueryDescriptor.live` keeps its name
59
63
  (`manifest` and `admin` read it).
60
- - `mcp` is opt-in; `isExposed` delegates to core's `isMcpExposed`.
64
+ - `mcp` is opt-in; `openapi.ts` asks core's `isMcpExposed` directly — the `isExposed` wrapper is gone.
61
65
  - **A read has ONE tool name, the export name verbatim.** `toToolName` is deleted.
62
- `mcp-tool.test.ts` asserts `toQueryTool(q).name === queryName(q)`; `index.test.ts` asserts the
63
- barrel exports nothing matching `/tool_?name/i`.
66
+ `index.test.ts` asserts the barrel exports nothing matching `/tool_?name/i`.
64
67
  - **`queryClient` is the map-wide read client (mirror of `rpc`)**; both spellings run
65
- `queryClientMethodFor`. **`toQueryRoute` and `client()` derive the same URL from `naming.ts`.**
68
+ `queryClientMethodFor`. **`toQueryRoute` and `client()` derive the same URL from core's `queryPath`.**
66
69
  - **`QueryClient` typechecks at any app size, and a `.test.ts` cannot say so** (`tsconfig.json`
67
70
  excludes them): `client-scale-pins.ts` is source, so a regression is a `typecheck` error. The map
68
71
  it pins is the intersection `@ultimat3/action`'s `Merge` hands over; that package pins `Merge`.
69
72
  - **The route coerces (`coerceQuery`), `runQuery` validates.** No `request.query(schema)` and no
70
73
  `meta.input` on the route.
71
- - **`rateLimit:` is declarable; `toQueryRoute` sets both `meta.rateLimit` and
72
- `meta.rateLimitBucket`**, via `@ultimat3/http`'s `toBucket` — never a local copy.
74
+ - **The client and the route are two halves of one encoding.** `searchOf` sends a `Date` as its ISO
75
+ instant and nothing for `[]`; `absentArraysOf` (`input-shape.ts`) reads a REQUIRED array nobody
76
+ sent as `[]`. Never for an optional or defaulted one — absence there is the schema's answer.
77
+ `http-round-trip.test.ts` pins both ends against each other.
78
+ - **A `single: true` read answers the row or `X_NOT_FOUND` by ONE rule** (`single-answer.ts`:
79
+ `oneRowOf`, `readAnswer`) — the route and `@ultimat3/mcp`'s served tool both call it.
80
+ - **`rateLimit:` is spent ONCE, in `buildSource`** (`rate-limit-gate.ts`'s `spendReadLimit`), for
81
+ every enforced build — route, `page()`, the MCP tool (`surface: 'mcp'`, judged as `'server'`).
82
+ `'server'` and an `unenforced` build spend nothing. A live subscribe spends via `live.ts`'s
83
+ `spendQueryLimit`, called by realtime per subscriber. `toQueryRoute` sets `rateLimitedBy:
84
+ 'handler'` and no stage bucket. Counted in `@ultimat3/http`'s installed store; key
85
+ `query:<name>|<subject>`. `toBucket` is http's — never a local copy. `rate-limit-surfaces.test.ts`.
73
86
  - **`deprecated:` is a compat WINDOW**: `Deprecation` / `Sunset` / `rel="successor-version"` on every
74
87
  answer, rendered ONCE at projection (`X_QUERY_DEPRECATION_INVALID` at mount). Versioning is two
75
88
  deployments behind one ingress.
76
89
  - **The span wraps the whole read** (`readRows` holds it, `readRowsIn` is the body); bounded
77
90
  attributes only — never the input or an actor id. `telemetry.test.ts` asserts the extent through
78
91
  `currentSpan()`.
79
- - **`policyCapability` is a display label; `policyPermissions` (`QueryDescriptor.permissions`) is what
80
- a report matches on.**
92
+ - **`policyCapability` is a display label; `policyPermissions` (`QueryDescriptor.permissions`) is
93
+ what a report matches on.** Both are `@ultimat3/policy`'s, imported from there.
81
94
  - **`client.ts` injects `traceparent`** before the caller's headers; an incomplete span context sends
82
95
  nothing.
83
96
  - **A read is `no-store`, and its policy is `enforcedBy: 'handler'`** — `runQuery` is the one
84
97
  evaluation (`http.test.ts` counts exactly one).
85
- - **`meta.auth` comes from `@ultimat3/policy`'s `admitsAnonymous`** (a walk of the tree, via
86
- `policy-gate.ts`), never the root combinator.
98
+ - **`meta.auth` comes from `@ultimat3/policy`'s `admitsAnonymous`** (a walk of the tree, imported
99
+ from `@ultimat3/policy` by `http.ts`), never the root combinator. 25.0.0 dropped this package's
100
+ re-export; `index.test.ts` pins the absence.
87
101
  - `registry.ts` announces `registerQueries` in core's registrar table at import; `defineApi` in
88
102
  `@ultimat3/action` depends on it (`X_REGISTRAR_MISSING` otherwise).
89
103
  - Authz goes through `enforce(surface, policy, { input, actor, ctx })`; a live denial keeps its 4403
@@ -99,11 +113,25 @@ Owns the `query` primitive: reads, live reads, cursors, the incremental matcher.
99
113
  `toLiveQuery`): a change row carries none, so the window would be read once and never patched.
100
114
  The refusal names the column, never the filter's value.
101
115
 
116
+ ## Invariants — audit
117
+
118
+ - **`audit: true` observes the read path, never forks it**: `buildSource` fills the trace (`input`,
119
+ parsed-or-nothing on a pre-parse denial); `readRowsIn` sets `replayed`. The record shape and the
120
+ sink are `@ultimat3/core`'s (`AuditRecord`, `setAuditSink`) — shared with `@ultimat3/action`, and
121
+ `AUDIT_RECORD_FIELDS` is the parity pin both packages' tests assert (`audit.test.ts`).
122
+ - **A memo or cache hit is recorded** (`replayed: true`): an audit of a read is who saw what.
123
+ - **One record per CALL**: `runQuery` and `readCall` (`.page()`) record the whole call;
124
+ `sourceFor` hands out an audited source (one record per `execute()`, `@ultimat3/mcp`'s served
125
+ tool included) and records a refused build at once. An `unenforced` build records nothing.
126
+ - **Never the rows.** The sink is resolved before the parse (`X_QUERY_AUDIT_SINK_MISSING`); a
127
+ refused allowed record withholds the rows (`X_QUERY_AUDIT_SINK_FAILED`); a refused denied/failed
128
+ record is logged and the original error wins. `audit-surfaces.test.ts`.
129
+
102
130
  ## Invariants — the client
103
131
 
104
- - **Flight control is `@ultimat3/core`'s, re-exported** (`createClientFlight`,
105
- `DEFAULT_CLIENT_RETRY`, `isTransientFailure`, `isSuperseded`, the types) — the same objects
106
- `@ultimat3/action` exports. Fix the pipeline in `packages/core/src/client-flight.ts`. No new error
132
+ - **Flight control is `@ultimat3/core`'s, and NOT re-exported** (`clientFlight`,
133
+ `DEFAULT_CLIENT_RETRY`, `isTransientFailure`, `isSuperseded` — `X_HELPER_COPY` refuses a value
134
+ re-export; only the types `ClientFlight`/`ClientRetry` the options name). Fix the pipeline in `packages/core/src/client-flight.ts`. No new error
107
135
  code, curve, fence or retry loop here; `bun run flight-copies` says so.
108
136
  - **`isTransientFailure` INVERTS `retryDecision`'s unclassified default** — a caller's own
109
137
  `AbortError` is terminal. Must survive.
@@ -112,11 +140,12 @@ Owns the `query` primitive: reads, live reads, cursors, the incremental matcher.
112
140
  transport owns `Accept`, trace/budget headers, the principal fence, the error decode and the record
113
141
  envelope; the flight handed to it dedups concurrent identical reads. `bun run browser-transport`
114
142
  is the guard.
115
- - **`@ultimat3/query/client` is the read client without the barrel** — `client.ts` and `naming.ts`
116
- import core from `@ultimat3/core/page`, the page keys live in the leaf `page-keys.ts`, and
143
+ - **`@ultimat3/query/client` is the read client without the barrel** — `client.ts`
144
+ imports core from `@ultimat3/core/page`, the page keys live in the leaf `page-keys.ts`, and
117
145
  `client.ts` never imports `page-controls.ts` or `stable.ts`. `client-bundle.test.ts` fails on any
118
- titles table in the graph or past 11 kB; `client-subpath.test.ts` pins the specifier. Byte figures
119
- (`As of 2026-10-01`, browser, minified): barrel `queryClient` 23,997 B; `./client` 10,899 B.
146
+ titles table in the graph or past 12 kB; `client-subpath.test.ts` pins the specifier. Byte figures
147
+ (browser, minified): barrel `queryClient` 25,237 B (`As of 2026-10-02`); `./client` 11,983 B
148
+ (`As of 2026-10-05`, +971 B: core reads a refusal's `Retry-After` and body title).
120
149
  - **The record envelope is derived from `rows:`** — `answersRecords(rows)` (`hasEntityRows`,
121
150
  `@ultimat3/entity`), decided once at projection for `http.ts` and `openapi.ts`, sent on every
122
151
  answer. No `rows:` is byte-identical on the wire.
@@ -131,7 +160,9 @@ Owns the `query` primitive: reads, live reads, cursors, the incremental matcher.
131
160
  `SearchChain`). No `PRIMITIVE_FACTORIES` row (it returns a query from the query package). **It serves
132
161
  ONE page**: `windowOf.execute` refuses only when rows would be CUT; `onePage.seek` narrows with
133
162
  `Builder.limit()`, never `Builder.seek()`, so the chain's own ranking survives; no `total()`.
134
- `search.test.ts`'s fixture must stay NOT id-ascending.
163
+ Its three refusals (blank term, a cursor, a window that cuts rows) are caller-reachable, so each
164
+ is `X_INPUT_INVALID` — never core's `assert`. `search.test.ts`'s fixture must stay NOT
165
+ id-ascending.
135
166
  - **`subscribes:` is DECLARED (nothing can derive it) and CROSS-CHECKED**: `toLiveQuery` asserts
136
167
  `shape.entity` is among the declared names at first subscribe (`X_QUERY_SUBSCRIBES_DRIFT`) —
137
168
  membership, never equality. An empty list, or one on a non-live read, is refused at `query()`
@@ -145,7 +176,11 @@ Owns the `query` primitive: reads, live reads, cursors, the incremental matcher.
145
176
  - The matcher patches from `QueryShape`, never from SQL text.
146
177
  - `paginate` has no `offset` and never grows one; it is reachable only as
147
178
  `query.page(input, { first, after })`. **A page is bounded**: `first` is 1…`MAX_PAGE_SIZE`
148
- (10,000, a twin of `@ultimat3/entity`'s).
179
+ (10,000 — `@ultimat3/entity`'s, imported; this package never re-publishes it).
180
+ - **A declared `.limit()` bounds the LISTING, on every page**: the window is
181
+ `min(rowsLeft, first + 1)`, and a limited read's cursor carries the rows served so far at the
182
+ tail of its key (after the typed id). A cursor without it is `X_CURSOR_INVALID` on a limited
183
+ read. `Builder.seek()` takes the smaller limit too. `pagination-limit.test.ts`.
149
184
  - **A `RowProvider` may be a list, a sync function or an async one** (`source.test.ts`).
150
185
  - **A cursor is a position, not a row**: `isAfterKey` (`source.ts`) is the one definition of "after";
151
186
  never reintroduce a row lookup. The seek predicate is spelled out per key
@@ -163,22 +198,25 @@ Owns the `query` primitive: reads, live reads, cursors, the incremental matcher.
163
198
  - **A sort value carries its TYPE through the cursor** (`cursor-value.ts`: `{ $x: 'date' | 'bigint',
164
199
  v }`; `undefined` → `null`). Untaggable values are `X_CURSOR_VALUE_UNSUPPORTED` where the cursor is
165
200
  minted.
166
- - **`compareValues` orders numbers and bigints in one order**; equality treats `number` and `bigint`
167
- as one family (`shape.ts` `same`). `shape-order.test.ts` reads `@ultimat3/entity`'s `COLUMN_KINDS`.
168
- - **`numeric` and the TEXT form of `bigint` are a DECLARED gap** (`DECLARED_GAP` in
169
- `shape-order.test.ts`): closing it needs an `OrderKey` carrying a kind, never a guessing comparator.
201
+ - **This package has NO comparator.** How two values compare is `@ultimat3/entity`'s
202
+ `compareByKind` / `sameValueOfKind`, asked under the column's declared kind (`kindsOf(shape.entity)`,
203
+ `column-kinds.ts`). `compareRows`, `matchesFilter(s)` and `isAfterKey` REQUIRE the `KindOf`; never
204
+ add a `typeof` branch here — fix the rule in entity. `equalOrBothNull` adds only this package's
205
+ NULL rule. `compare-parity-fixture.ts` is the one table: `compare-parity.test.ts` (entity, matcher,
206
+ `from()`) and `compare-parity.live.test.ts` (Postgres). `grep compareValues src` stays empty.
207
+ - **A relation no entity declares has no kinds** — digits there are text, as in a `text` column.
208
+ `shape-order.test.ts` reads `@ultimat3/entity`'s `COLUMN_KINDS`, one case per kind.
170
209
  - **A refill is owed by a FULL window only** (`held >= shape.limit`). **A move OUT of a full window is
171
210
  a `refill`, never an `add`.**
172
211
  - **A position is decided against the rows the WINDOW holds**: `unprojectedOrderKey` — a held row
173
212
  missing an ordering column (`Object.hasOwn`, never a value check) gets a `refill` rather than a
174
213
  guess. A delete is addressed by index and still patches.
175
- - **`tagKeys` is `@ultimat3/cache`'s.** `@ultimat3/render` exports a different `tagKeys`; never import
176
- that one here.
214
+ - **`tagKeys` is `@ultimat3/cache`'s** — the only public one; never restore a local copy.
177
215
  - **A fingerprint is `@ultimat3/core`'s** (`canonicalJson` + SHA-256/16 `fingerprint`), tagging `Date`,
178
216
  `Map`, `Set`. `query-hash.test.ts` pins it at `queryHash` and `cacheKeyFor`. `stable.ts` keeps
179
- `columnOf` and re-exports core's `isJsonObject`.
217
+ `columnOf`; core's `isJsonObject` is imported from core (`X_HELPER_COPY` refuses a re-export).
180
218
  - The cursor codec is `@ultimat3/core`'s; this package supplies only the scope (`queryHash(name,
181
- input)`). An unverified cursor is `X_CURSOR_INVALID` (`CursorInvalidError`, re-exported).
219
+ input)`). An unverified cursor is `X_CURSOR_INVALID` (`CursorInvalidError`, core's — imported from `@ultimat3/core`, never re-exported).
182
220
 
183
221
  ## Invariants — caching
184
222
 
@@ -192,10 +230,10 @@ Owns the `query` primitive: reads, live reads, cursors, the incremental matcher.
192
230
  - **`cache.ttlMs` is judged at `query()`** (`X_QUERY_CACHE_TTL_INVALID`). A `cache:` read always
193
231
  expires: `def.cache.ttlMs ?? DEFAULT_READ_CACHE_TTL_MS` (60 s); an unset TTL is OMITTED so the tier
194
232
  defaults.
195
- - **A fill is FENCED, by `@ultimat3/cache`'s fence inside `createCacheStack.read`** — never a second
233
+ - **A fill is FENCED, by `@ultimat3/cache`'s fence inside `cacheStack.read`** — never a second
196
234
  one here (`cache-fence.test.ts`).
197
235
  - **This package owns NO cache store**: a `cache:` read fills
198
- `createCacheStack(registeredTiers(), { clock })`. Never reintroduce a store, never call
236
+ `cacheStack(registeredTiers(), { clock })`. Never reintroduce a store, never call
199
237
  `tier.invalidateTags()` here. A tier refusal degrades the cache, never the read.
200
238
  - **The read path reads NO clock** — it hands the stack a relative `ttlMs` (`read-tier.test.ts`).
201
239
  - **A process that registered no tier reads uncached** — correct, and slower.
package/README.md CHANGED
@@ -29,7 +29,7 @@ Every projection is a method on the query itself. A query has no `.def`.
29
29
  | `liveFeed.as(actor, { orgId })` | the same read as another actor — the surrounding context is untouched, `null` is signed out |
30
30
  | `liveFeed.page({ orgId }, { first: 20, after })` | one bounded page plus the signed cursor that continues it. There is no `offset` |
31
31
  | `liveFeed.live({ orgId })` | the `LiveQuery` `@ultimat3/realtime` subscribes to, carrying the same policy object |
32
- | `liveFeed.tool()` | the MCP read tool, named `liveFeed`. `tool().policy === liveFeed.policy`, and it reads fresh |
32
+ | `toolFrom(liveFeed)` | the MCP read tool, from `@ultimat3/mcp` — named `liveFeed`, exactly what `tools/list` serves, reading fresh through `sourceFor` with the same policy object. There is no `liveFeed.tool()` (`As of 25.0.0`): it was a second projection a tier below the served one |
33
33
  | `liveFeed.client({ baseUrl })` | `GET /_x/query/live-feed?orgId=…`, typed both ways. `.page(input, { first, after })` on the same method reads one page over the wire |
34
34
  | `liveFeed.describe()` | the manifest row |
35
35
 
@@ -68,8 +68,11 @@ A detail read declares `single: true`. The route answers the first row as the bo
68
68
  `X_NOT_FOUND`** when `sql` returns none (a list read answers `200 []`), refuses `_first`/`_after`
69
69
  (400), and `openapi.json` documents one object with a `404` and no page controls. `.client()` and
70
70
  `queryClient` type it `Promise<TRow>` with no `.page`. In process nothing moves: `postById({ id })`,
71
- `.as()`, `.page()`, `.live()` and the MCP tool still answer `readonly TRow[]`, so opting in breaks no
72
- `[0]` already written. `As of 2026-09-29`.
71
+ `.as()`, `.page()` and `.live()` still answer `readonly TRow[]`, so opting in breaks no `[0]`
72
+ already written. `As of 2026-09-29`. **`tool().read()` answers as the route does**
73
+ (`As of 2026-10`): the row itself, and `X_NOT_FOUND` when `sql` returns none — an agent given `[]`
74
+ for a missing id read "found, and empty" where HTTP said 404. The tool `@ultimat3/mcp` serves over
75
+ `tools/call` answers the same two ways.
73
76
 
74
77
  ```ts
75
78
  import { can } from '@ultimat3/policy';
@@ -119,20 +122,21 @@ or with a schema no `entity()` branded (a `t.object` look-alike, a `.pick()`ed p
119
122
  is the bare rows, byte-identical to before. `rows:` exists because a read's `sql:` names its table
120
123
  as a STRING, so nothing else carries a schema the envelope could be derived from.
121
124
 
122
- ### Flight control — `createClientFlight`, opt-in
125
+ ### Flight control — `clientFlight`, opt-in
123
126
 
124
127
  A typed read is one dispatch and nothing else until a `flight` is installed. With one, N concurrent
125
128
  identical reads become ONE dispatch, a fence can retire everything issued before now, and a failure
126
129
  worth sending again is sent again on `@ultimat3/core`'s one backoff curve.
127
130
 
128
131
  ```ts
129
- import { createClientFlight, isSuperseded, queryClient } from '@ultimat3/query';
132
+ import { clientFlight, isSuperseded } from '@ultimat3/core';
133
+ import { queryClient } from '@ultimat3/query';
130
134
 
131
135
  declare const session: () => { userId: string };
132
136
  declare const baseUrl: string;
133
137
  type Api = { queries: Record<string, never> };
134
138
 
135
- const flight = createClientFlight({
139
+ const flight = clientFlight({
136
140
  principal: () => session().userId, // dedup is OFF without this — see below
137
141
  retry: { attempts: 3 }, // default is `attempts: 1`, i.e. no retry
138
142
  deadlineMs: 10_000,
@@ -156,10 +160,11 @@ flight.bump();
156
160
  | an **unclassified** throw is not retried | core's `retryDecision` retries anything nobody classified — a caller's own `AbortError` included. A client retries a declared `retryable`/`retry-after`, plus a dispatch that produced no response at all |
157
161
  | a deadline is `X_TIMEOUT` | core's code, already classified `retryable`, so a caller's own loop needs no table |
158
162
 
159
- `createClientFlight` is **`@ultimat3/core`'s**, re-exported here: it is the same object
160
- `@ultimat3/action` re-exports, because both packages are tier 3 and neither may import the other.
161
- It shipped as a byte-identical copy in each; the copies are gone and every name is importable from
162
- this package exactly as before.
163
+ `clientFlight` is **`@ultimat3/core`'s**, and imported from there, `isSuperseded` with it.
164
+ It shipped as a byte-identical copy here and in `@ultimat3/action` (both tier 3, neither may import
165
+ the other); the copies are gone, and since 25.0.0 so are the re-exports: one value, one import
166
+ path (`X_HELPER_COPY`, `bun run flight-copies`). The types `ClientFlight` and `ClientRetry` stay
167
+ re-exported, because `queryClient`'s options name them.
163
168
 
164
169
  Bundle cost is why `ClientFlight` is a TYPE inside `client.ts` and never a value. Importing
165
170
  `queryClient` alone through this package's barrel, `bun build --target=browser --minify`,
@@ -176,7 +181,7 @@ merely looks like a query (`kind: 'query'`, no declaration) is `X_QUERY_FOREIGN`
176
181
  ### Skipping the policy costs a written reason
177
182
 
178
183
  Two reads have no subscriber to decide about: developer tooling that returns the statement and no
179
- rows (`explain`, `describeSql`), and the shared, subject-less window a sync node builds once per
184
+ rows (`explainQuery`, `describeSql`), and the shared, subject-less window a sync node builds once per
180
185
  `(query, input)`. Both say so, in words:
181
186
 
182
187
  ```ts
@@ -199,15 +204,17 @@ forgetting the policy, and the reason is what tells the next reader which of the
199
204
  | `mcp-tool.ts` | the MCP read descriptor |
200
205
  | `client.ts` | the typed read client (browser-safe) |
201
206
  | `http.ts` | the route projection — `GET /_x/query/<kebab>`, the URL the client derives |
202
- | `page-controls.ts` | `_first` / `_after` — the two search-string keys that page the route, split out and judged at the wire; `MAX_PAGE_SIZE` |
207
+ | `page-controls.ts` | `_first` / `_after` — the two search-string keys that page the route, split out and judged at the wire against `@ultimat3/entity`'s `MAX_PAGE_SIZE` |
203
208
  | `openapi.ts` | the read half of `openapi.json` — one `GET` path item per registered query |
204
- | `naming.ts` | export name → wire path. The MCP tool name is the export name verbatim |
205
209
  | `live.ts` | the `LiveQuery` descriptor `@ultimat3/realtime` subscribes to |
206
210
  | `matcher.ts` | change event → minimal patch (`add` / `update` / `remove` / `refill`) |
207
211
  | `pagination.ts` | `paginate()` — keyset pages over core's cursor codec |
208
- | `sql.ts` | `explain()` — the generated SQL, verbatim |
212
+ | `sql.ts` | `explainQuery()` — the generated SQL, verbatim |
213
+ | `audit-gate.ts` | `audit: true` — the only file that calls an `AuditSink`; one record per call, an audited source per `execute()` |
214
+ | `audit-errors.ts` | `X_QUERY_AUDIT_SINK_MISSING` / `X_QUERY_AUDIT_SINK_FAILED` |
209
215
  | `cache.ts` | request memo + tag-keyed tier, one invalidation graph |
210
216
  | `source.ts` | the `SqlSource` contract + `from()`, the in-memory reference |
217
+ | `column-kinds.ts` | `kindsOf(relation)` — the declared kind of each column, which every comparison is decided by |
211
218
  | `sealed-shape.ts` | a live read may not filter or order on a `.sealed()` column — refused at subscribe |
212
219
 
213
220
  ## Live queries
@@ -340,8 +347,17 @@ empty box is not an empty result set.
340
347
 
341
348
  **It serves one page.** The rows come from the entity chain, which pages by its own signed cursor;
342
349
  that cursor cannot cross the `SqlSource` seam, and slicing in memory instead would cut inside the
343
- page the provider fetched and report `hasNextPage: false` at its edge. So a second page is
344
- refused, and the `fix:` names the chain — `db.posts.search(term).after(cursor).page()`.
350
+ page the provider fetched and report `hasMore: false` at its edge. So a second page is
351
+ refused, and the cause names the chain — `db.posts.search(term).after(cursor).page()`.
352
+
353
+ | A request `search()` refuses | Answer |
354
+ |---|---|
355
+ | `q` of only whitespace (`?q=%20%20` passes `t.string.min(1)`) | **400** `X_INPUT_INVALID` |
356
+ | a cursor (`?_after=…`) | **400** `X_INPUT_INVALID` |
357
+ | a window that would cut rows (`?_first=1` with two matches) | **400** `X_INPUT_INVALID`, naming both edits: widen `first` to the read's `limit`, or narrow `limit` |
358
+
359
+ All three arrive on the request, so all three are the caller's. `As of 2026-10`; until then they
360
+ were core `assert`s — `X_INVARIANT`, a 500 blaming the server.
345
361
 
346
362
  ## Pagination is cursor-only
347
363
 
@@ -358,17 +374,19 @@ not exported, because a page is the read's own answer rather than an imported he
358
374
 
359
375
  The route reads two controls off the search string BEFORE the schema sees it, and with either
360
376
  present answers the `Page` envelope `.page()` answers a server caller with — `{ rows, nextCursor,
361
- hasMore, endCursor, hasNextPage }`, the same names, so a cursor read off the wire and one read off a
362
- direct call are one string in one field. **`nextCursor` and `hasMore` are the preferred names**
363
- (`As of 22.12`); `endCursor` and `hasNextPage` carry the same two values, always, as aliases — not
364
- deprecated, so a client reading them keeps working. `openapi.json` lists all five as required, the
365
- aliases described as such. Without a control the answer is the bare array it has always been, so a
377
+ hasMore }`, the same names, so a cursor read off the wire and one read off a direct call are one
378
+ string in one field. The `endCursor`/`hasNextPage` alias pair is gone (`As of 25.0.0`): `Page` is
379
+ the framework's one page type — `@ultimat3/core`'s, re-exported here, the same an entity `findMany`
380
+ answers — and `openapi.json` lists exactly those three as required. **`nextCursor` is `null` exactly
381
+ when `hasMore` is false** (`As of 25.0.0`; the last page used to keep a cursor, so a
382
+ `while (page.nextCursor)` loop fetched an empty page past the end): `openapi.json` says so as a
383
+ `oneOf`, and the type is a union that narrows `nextCursor` to `string` under `if (page.hasMore)`. Without a control the answer is the bare array it has always been, so a
366
384
  client written before the controls existed keeps reading rows. `As of 2026-09`; until then the
367
385
  route had no envelope, and a query over a paged external source carried its page marker on a row
368
386
  (an `olderCursor` column on the oldest message), which is a cursor on the wrong side of the shape.
369
387
 
370
388
  ```
371
- GET /_x/query/live-feed?orgId=…&_first=20 → { rows, nextCursor, hasMore, endCursor, hasNextPage }
389
+ GET /_x/query/live-feed?orgId=…&_first=20 → { rows, nextCursor, hasMore }
372
390
  GET /_x/query/live-feed?orgId=…&_first=20&_after=<c> → the next page (<c> = nextCursor)
373
391
  GET /_x/query/live-feed?orgId=… → [ …rows ] (unchanged)
374
392
  ```
@@ -380,19 +398,22 @@ GET /_x/query/live-feed?orgId=… → [ …rows ] (uncha
380
398
  | `_first` outside 1–10,000, not a whole number, or `_after` without `_first` | **400** `X_INPUT_INVALID` with the read's own fix line — judged at the wire (`page-controls.ts`), because `paginate` asserts the same bound as `X_INVARIANT`, which is a 500 blaming the server for a number the caller typed |
381
399
  | a cursor that is not this read's | **400** `X_CURSOR_INVALID` — `decodeCursor` is the one judge of a cursor; the route checks only that `_after` is one non-empty string |
382
400
  | a control sent twice | refused, never resolved to the last one as a declared scalar is: two page sizes is two pages asked for in one request |
401
+ | a declared `.limit()` is the size of the LISTING | `first` is the size of a page and never widens it: the window is `min(what the limit has left, first + 1)`, a limited read's cursor carries how many rows are already served, and the last page inside it answers `hasMore: false` with `nextCursor: null`. `As of 2026-10`; until then `first` REPLACED the limit, so `?_first=10000` walked a "top 3" to the end of the table. A read paged past its limit on purpose drops the `.limit()` |
402
+ | a cursor a limited read minted before that | **400** `X_CURSOR_INVALID` — it does not say how much of the limit is spent, and honouring it would start a second "top N" below the first |
383
403
  | the typed client | `feed.client({ baseUrl }).page(input, { first, after })` and `queries.feed.page(…)` append the two controls after the sorted input, so a paged URL is the plain URL plus a suffix |
384
404
  | `openapi.json` | every read is a `GET` path item with its input as `in: query` parameters, the two controls after them, and a `200` that is `oneOf` the bare rows and the envelope — `queryOpenApiPaths`, merged into `@ultimat3/action`'s document by the CLI |
385
405
 
386
406
  The MCP read tool does not page: `read()` answers the source's rows, bounded by the read's own
387
- `limit`, and an agent that wants more asks a narrower question.
407
+ `limit` — or the one row of a `single: true` read — and an agent that wants more asks a narrower
408
+ question.
388
409
 
389
410
  The codec lives in `@ultimat3/core`, not here: `encodeCursor`, `decodeCursor`,
390
411
  `configureCursorSigning` (set the signing secret once at boot; rotating it invalidates every
391
412
  open cursor) and `usesDevCursorSecret` are all imported from there. `As of 2026-08`, `x doctor`
392
413
  reports `X_CURSOR_SECRET_DEV` when a production process is still signing with the key shipped in
393
414
  the package, and rotating the secret is what invalidates every open cursor. This package supplies
394
- the only thing that is its business — the scope, `queryHash(name, input)` — and re-exports
395
- `CursorInvalidError` so the failure keeps its name on this surface.
415
+ the only thing that is its business — the scope, `queryHash(name, input)`. `CursorInvalidError`
416
+ is core's too, imported from `@ultimat3/core` (25.0.0 dropped this package's re-export).
396
417
 
397
418
  The scope's hash is **SHA-256, first 16 hex** — `fingerprint` from `@ultimat3/core`, the primitive
398
419
  and width `@ultimat3/entity`'s `planScope` already uses, and the same function
@@ -453,13 +474,36 @@ the rows after it were unreachable through a cursor. An ascending key now reache
453
474
  NULL under `nulls last`, and the page continues on the id tiebreak, which is never NULL.
454
475
 
455
476
  `nulls last` / `nulls first` are Postgres' own defaults, written down rather than inherited: it is
456
- the rule `compareValues` implements, so the in-memory sort and the seek predicate can only agree
477
+ the rule `compareRows` implements, so the in-memory sort and the seek predicate can only agree
457
478
  with it, and a driver whose default differs cannot re-open the divergence.
458
479
 
480
+ ## How two values compare
481
+
482
+ **By the column's declared kind, and by `@ultimat3/entity`'s comparator — this package has none.**
483
+ `kindsOf(shape.entity)` resolves each column's `ColumnKind` from the entity declared on that table,
484
+ and `compareRows`, `matchesFilter`, `isAfterKey` and the matcher hand it to entity's
485
+ `compareByKind` / `sameValueOfKind`: the one JS statement of how Postgres compares a column.
486
+ `As of 2026-10`; until then a second comparator here decided by `typeof`.
487
+
488
+ | Column | What its row holds | Decided as |
489
+ |---|---|---|
490
+ | `bigint()`, `decimal()` | decimal TEXT | digits, exactly: `'9'` before `'10'`, `'2.50'` equal to `'2.5'` and to `2.5` |
491
+ | `uuid()` | text | a value: upper and lower case are one id |
492
+ | `timestamp()` | `Date` | an instant: a `Date` equals the ISO text naming it |
493
+ | `text()` | text | characters: `'10'` before `'9'`, as `order by` on a text column answers |
494
+ | a relation no entity declares | whatever the row provider returned | no kind — numbers numerically (a `number` and a `bigint` in one order), `Date`s by instant, the rest as text |
495
+
496
+ `compare-parity-fixture.ts` is the one `(kind, left, right)` table: `compare-parity.test.ts` runs it
497
+ through entity's evaluator, the matcher and `from()`; `compare-parity.live.test.ts` runs it through
498
+ Postgres, and walks a live window ordered on a `bigint()` and a `decimal()` column against the
499
+ database's own `order by`. A custom `SqlSource` calling `compareRows`, `matchesFilter` or
500
+ `isAfterKey` passes `kindsOf(shape.entity)` — the argument is required, because a comparison with
501
+ no kind is the defect.
502
+
459
503
  ## Caching
460
504
 
461
505
  Request memo (same read twice in one render ⇒ one round trip), then the tier ladder
462
- `@ultimat3/cache` has registered — `createCacheStack(registeredTiers())`, read down and promoted
506
+ `@ultimat3/cache` has registered — `cacheStack(registeredTiers())`, read down and promoted
463
507
  up. Keys are `query:<name>:<authority>:<input fingerprint>:<tags>`. An action's `cache.invalidates`
464
508
  and a query's `cache.tags` meet in the one graph owned by `@ultimat3/cache`, because there is one
465
509
  registry and this package holds no store of its own.
@@ -482,7 +526,7 @@ skipped policy. `readAuthority(actor, scope)` is the only producer of the compon
482
526
  required positional argument of `cacheKeyFor`, because an optional one is one a call site forgets.
483
527
 
484
528
  **The fill is fenced, best-effort and single-flighted, and none of that is written here.**
485
- `createCacheStack` samples `@ultimat3/cache`'s fence immediately before it runs the source and
529
+ `cacheStack` samples `@ultimat3/cache`'s fence immediately before it runs the source and
486
530
  re-asks it per rung before each write, so a bust that lands mid-read cannot be republished for the
487
531
  whole TTL — the caller still gets the rows it read, because those are its answer; only publishing is
488
532
  refused. Every tier call goes through `bestEffort`, so a Redis refusal is a miss rather than a
@@ -535,8 +579,21 @@ rateLimit: { limit: 3, windowMs: 600_000 }, // 3 held, one back every three an
535
579
  Symmetric with an action's, and for a reason: without it **every** `GET /_x/query/*` fell to the
536
580
  `default` bucket — 120 burst, 2/s per actor — so one authenticated caller could hold 120
537
581
  cross-tenant aggregates in flight and then 2/s indefinitely, from a single account, with no
538
- declaration able to say otherwise. `toQueryRoute` sets the bucket NAME and the NUMBERS, and
539
- `@ultimat3/http`'s `withRouteBuckets` registers them: a name alone falls through to `default`.
582
+ declaration able to say otherwise.
583
+
584
+ **One bucket, every surface.** The read path's front half (`buildSource`) spends it, so the HTTP
585
+ route, a paged read and the MCP tool (`surface: 'mcp'`) all draw on the same bucket. A live
586
+ SUBSCRIBE is realtime's — the sync node builds one shared window per `(query, input)` with no
587
+ subscriber, which spends nothing — and spends per subscriber through `spendQueryLimit(target,
588
+ { actor })`. An in-process read (`surface: 'server'`, a page's render, a service) spends nothing.
589
+
590
+ | Fact | Answer |
591
+ |---|---|
592
+ | whose bucket | the reader's: actor → org → connection address (`@ultimat3/http`'s `rateLimitSpends`) |
593
+ | where it is counted | `@ultimat3/http`'s installed store (`installRateLimitStore`) — the one `@ultimat3/action` spends from, and the instance the boot hands `httpServer` |
594
+ | what HTTP answers | `RateLimit-*` from the bucket closest to refusing; `429` + `Retry-After` on a refusal |
595
+ | `default` | not spent for this route: it is `rateLimitedBy: 'handler'`, so a read declaring more than `default` is not capped at it. The tenant allowance still applies |
596
+
540
597
  The conversion is `toBucket` from `@ultimat3/http` — the same one the action route uses, because
541
598
  http owns `Bucket` and `action` is the same tier as this package. A pair the limiter cannot run
542
599
  on is `X_RATE_LIMIT_INVALID`, at projection.
@@ -554,19 +611,75 @@ before the read is deleted. A date that cannot be rendered is `X_QUERY_DEPRECATI
554
611
  projection, not on the first read. Versioning is two deployments behind one ingress, never a
555
612
  router feature here.
556
613
 
614
+ `Deprecation`, `renderDeprecation` and `recordDeprecatedCall` are `@ultimat3/core`'s and are
615
+ imported from there — this package does not re-export them (`As of 25.0.0`).
616
+
617
+ ## `audit: true` — who saw what
618
+
619
+ ```ts
620
+ import { can } from '@ultimat3/policy';
621
+ import { from, query, t } from '@ultimat3/query';
622
+
623
+ type Chart = { readonly id: string; readonly patientId: string };
624
+ declare const charts: readonly Chart[];
625
+
626
+ export const patientChart = query({
627
+ input: t.object({ patientId: t.uuid }),
628
+ policy: can('chart:read'),
629
+ audit: true,
630
+ sql: ({ patientId }) => from<Chart>('charts', charts).where({ patientId }),
631
+ });
632
+ ```
633
+
634
+ Every **call** is one record in the installed `AuditSink` — allowed, denied (before or after the
635
+ input parse) and failed. The sink, the record and the installed slot are `@ultimat3/core`'s, the
636
+ same ones an action's `audit: true` writes to: `setAuditSink(sink)` from `@ultimat3/core` (or
637
+ `@ultimat3/action` — the same function) installs one sink for both, and `record.primitive` tells a
638
+ read from a write.
639
+
640
+ **A memo or cache hit is recorded too**, with `replayed: true`. An audit of a read is about who saw
641
+ what, not what was computed: a reader handed rows by the request memo saw them exactly as much as
642
+ the reader whose call executed the SQL.
643
+
644
+ | Field | On a read |
645
+ |---|---|
646
+ | `name` | the export name — required, and the record's only name (the `action` alias left in 25.0.0) |
647
+ | `primitive` / `mutator` | `'query'` / `false` |
648
+ | `surface` | `server` (a direct call, `.as()`, `.page()`), `http` (the route), `mcp` (`@ultimat3/mcp`'s served tool: `sourceFor(…, { surface: 'mcp' })`) |
649
+ | `input` | the **parsed** input, as an action's record carries it; `undefined` when the parse failed. Unredacted in the record — a persisting sink redacts through `auditableInput` (core's credential-key table and `Secret`), as `postgresAuditSink` does |
650
+ | `replayed` | `true` when the request memo or a cache tier answered |
651
+ | `idempotencyKey` | always `null` |
652
+ | `outcome` / `failure` | `allowed` \| `denied` \| `failed`, and the `X_*` code with the thrown value |
653
+
654
+ **The rows are never on the record** — an action's record carries no result for the same reason:
655
+ a copy of every row seen is a second table to protect as carefully as the first.
656
+
657
+ | Path | Records |
658
+ |---|---|
659
+ | `read(input)`, `.as()`, the route, `.page()` | once per call — `.page()` once even when a spent `.limit()` answers without executing |
660
+ | `sourceFor(target, input, …)` then `execute()` | once per `execute()`; a build that is refused (denied, unparsed, rate-limited) once, at the build |
661
+ | `explainQuery()`, `describeSql()`, the shared live window | never — they are built `unenforced`, with no caller to attribute a sighting to |
662
+
663
+ Failure policies are the action seam's: no sink installed is `X_QUERY_AUDIT_SINK_MISSING`, refused
664
+ before the parse; a sink refusing a **denied/failed** record is logged (`audit.sink.failed`) and the
665
+ original error still reaches the caller; a sink refusing an **allowed** record withholds the rows,
666
+ `X_QUERY_AUDIT_SINK_FAILED` — a read commits nothing, so a retry is safe.
667
+
557
668
  ## Errors
558
669
 
559
670
  | Code | When | Fix |
560
671
  |---|---|---|
672
+ | `X_QUERY_AUDIT_SINK_MISSING` | `audit: true` and no audit sink installed — refused before the parse | `setAuditSink(sink)` from `@ultimat3/core` at boot |
673
+ | `X_QUERY_AUDIT_SINK_FAILED` | the sink refused the record of an allowed read; the rows were withheld | fix the sink, then retry the read |
561
674
  | `X_QUERY_DUPLICATE` | two queries under one name | rename one export |
562
675
  | `X_QUERY_DEPRECATION_INVALID` | `deprecated:` with a `since`/`sunset` that is not a date | use an ISO-8601 instant |
563
676
  | `X_QUERY_POLICY_MISSING` | registration without `policy:` | add `policy: can('…')` |
564
677
  | `X_MATCHER_UNSUPPORTED` | live query the matcher cannot patch | reshape it, or `live: false` |
565
678
  | `X_CURSOR_INVALID` | tampered / foreign / malformed cursor | request the first page again |
566
679
  | `X_QUERY_NOT_PAGEABLE` | a paged or live read returned a row with no `id` | select the primary key: `db.<rows>.select({ id: true, … })` |
567
- | `X_INPUT_INVALID` | input failed the Standard Schema | `x queries describe <name> --json` |
680
+ | `X_INPUT_INVALID` | input failed the Standard Schema; a page control the wire refuses; a `search()` handed a blank term, a cursor, or a window that would cut rows | `x queries describe <name> --json` |
568
681
  | `X_QUERY_UNREGISTERED` | used before `registerQueries()` ran | register at boot |
569
- | `X_NOT_FOUND` | a `single: true` read matched no row — the HTTP route's 404 only (entity's code) | `x queries describe <name> --json` prints the SQL |
682
+ | `X_NOT_FOUND` | a `single: true` read matched no row — the HTTP route's 404, and the MCP tool's refusal (entity's code) | `x queries describe <name> --json` prints the SQL |
570
683
  | `X_QUERY_SINGLE_INVALID` | `single:` declared as something other than a boolean | `single: true`, or drop the key |
571
684
  | `X_QUERY_FOREIGN` | a look-alike was projected as a query | declare it with `query({ … })` |
572
685
  | `X_CLIENT_TRANSPORT_FAILED` | `.client()` got no response, or a non-`problem+json` failure (core's code, since 21.0.0; was `X_RPC_FAILED`) | check the gateway in front of the app: `x doctor --json` |
@@ -583,6 +696,8 @@ a job boundary the class is gone and the `code` is what survives — match on th
583
696
  |---|---|---|
584
697
  | `CursorValueUnsupportedError` | `X_CURSOR_VALUE_UNSUPPORTED` | `src/errors.ts` |
585
698
  | `MatcherUnsupportedError` | `X_MATCHER_UNSUPPORTED` | `src/errors.ts` |
699
+ | `QueryAuditSinkFailedError` | `X_QUERY_AUDIT_SINK_FAILED` | `src/audit-errors.ts` |
700
+ | `QueryAuditSinkMissingError` | `X_QUERY_AUDIT_SINK_MISSING` | `src/audit-errors.ts` |
586
701
  | `QueryDeniedError` | the policy denial's own code (`X_FORBIDDEN`, `X_UNAUTHENTICATED`, …), kept on `.denial` | `src/errors.ts` |
587
702
  | `QueryDeprecationInvalidError` | `X_QUERY_DEPRECATION_INVALID` | `src/errors.ts` |
588
703
  | `QueryDuplicateError` | `X_QUERY_DUPLICATE` | `src/errors.ts` |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ultimat3/query",
3
- "version": "23.0.0",
3
+ "version": "25.0.0",
4
4
  "description": "The query primitive: a policy-checked read, optionally live, with cursor pagination and an incremental matcher",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -30,18 +30,18 @@
30
30
  "LICENSE"
31
31
  ],
32
32
  "engines": {
33
- "bun": ">=1.4.0"
33
+ "bun": ">=1.4.2"
34
34
  },
35
35
  "scripts": {
36
36
  "typecheck": "tsc --noEmit -p tsconfig.json",
37
37
  "test": "bun test"
38
38
  },
39
39
  "dependencies": {
40
- "@ultimat3/cache": "23.0.0",
41
- "@ultimat3/core": "23.0.0",
42
- "@ultimat3/entity": "23.0.0",
43
- "@ultimat3/http": "23.0.0",
44
- "@ultimat3/policy": "23.0.0",
45
- "@ultimat3/schema": "23.0.0"
40
+ "@ultimat3/cache": "25.0.0",
41
+ "@ultimat3/core": "25.0.0",
42
+ "@ultimat3/entity": "25.0.0",
43
+ "@ultimat3/http": "25.0.0",
44
+ "@ultimat3/policy": "25.0.0",
45
+ "@ultimat3/schema": "25.0.0"
46
46
  }
47
47
  }