@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 +75 -37
- package/README.md +147 -32
- package/package.json +8 -8
- package/src/audit-errors.ts +43 -0
- package/src/audit-gate.ts +177 -0
- package/src/cache.ts +4 -4
- package/src/client.ts +19 -6
- package/src/column-kinds.ts +28 -0
- package/src/cursor-value.ts +3 -3
- package/src/errors.ts +6 -3
- package/src/facade.ts +6 -7
- package/src/http.ts +43 -33
- package/src/index.ts +37 -61
- package/src/input-shape.ts +21 -1
- package/src/live.ts +19 -3
- package/src/matcher.ts +14 -7
- package/src/openapi.ts +24 -17
- package/src/page-controls.ts +5 -9
- package/src/page-keys.ts +1 -1
- package/src/pagination.ts +88 -46
- package/src/policy-gate.ts +17 -49
- package/src/query.ts +22 -56
- package/src/rate-limit-gate.ts +71 -0
- package/src/read.ts +97 -14
- package/src/record-answer.ts +9 -5
- package/src/registry.ts +1 -1
- package/src/search.ts +53 -35
- package/src/shape.ts +52 -85
- package/src/single-answer.ts +22 -0
- package/src/source.ts +26 -11
- package/src/sql.ts +3 -3
- package/src/stable.ts +0 -3
- package/src/subscribes.ts +3 -3
- package/src/type-pins.ts +62 -0
- package/src/deprecation.ts +0 -76
- package/src/mcp-tool.ts +0 -81
- package/src/naming.ts +0 -22
package/src/source.ts
CHANGED
|
@@ -4,9 +4,13 @@
|
|
|
4
4
|
* app's `sql:` returns `from()` over an `@ultimat3/entity` repo, and a source only has to
|
|
5
5
|
* answer these four questions.
|
|
6
6
|
*/
|
|
7
|
+
|
|
8
|
+
import { compareByKind } from '@ultimat3/entity';
|
|
9
|
+
import type { KindOf } from './column-kinds';
|
|
10
|
+
import { kindsOf } from './column-kinds';
|
|
7
11
|
import { QueryColumnUnselectedError } from './errors';
|
|
8
12
|
import type { Filter, FilterOp, OrderKey, QueryShape, SeekKey } from './shape';
|
|
9
|
-
import { compareRows,
|
|
13
|
+
import { compareRows, isNull, matchesFilters, totalOrder } from './shape';
|
|
10
14
|
import { columnOf } from './stable';
|
|
11
15
|
|
|
12
16
|
/** Nothing matches. `in ()` is a syntax error in Postgres, so an empty set needs a constant. */
|
|
@@ -48,7 +52,7 @@ export type RowProvider<TRow> =
|
|
|
48
52
|
|
|
49
53
|
/**
|
|
50
54
|
* In-memory reference source. `from<Post>('posts', rows).where({ orgId }).orderBy('createdAt')`
|
|
51
|
-
* generates real SQL text for `
|
|
55
|
+
* generates real SQL text for `explainQuery()` while executing against the provided rows.
|
|
52
56
|
*/
|
|
53
57
|
export function from<TRow extends object>(entity: string, rows: RowProvider<TRow>): Builder<TRow> {
|
|
54
58
|
return new Builder<TRow>(entity, rows, [], [], null, null, []);
|
|
@@ -70,7 +74,7 @@ export class Builder<TRow extends object> implements SqlSource<TRow> {
|
|
|
70
74
|
/**
|
|
71
75
|
* One equality filter per key, and the clauses come out in **lexical key order** — not the order
|
|
72
76
|
* the object literal was typed in. The generated text is something callers compare across runs:
|
|
73
|
-
* `
|
|
77
|
+
* `explainQuery()` prints it, `LiveQuery.sqlText` caches it, and tests pin it. Reordering two keys in
|
|
74
78
|
* a call site must not rewrite the statement.
|
|
75
79
|
*/
|
|
76
80
|
where(equals: Readonly<Record<string, unknown>>): Builder<TRow> {
|
|
@@ -97,8 +101,13 @@ export class Builder<TRow extends object> implements SqlSource<TRow> {
|
|
|
97
101
|
return this.derive({ unsupported: [...this.unsupported, feature] });
|
|
98
102
|
}
|
|
99
103
|
|
|
104
|
+
/**
|
|
105
|
+
* The window is the SMALLER of the two limits. It used to replace a declared `.limit()`, so a
|
|
106
|
+
* page wider than a "top N" served rows the read itself never would.
|
|
107
|
+
*/
|
|
100
108
|
seek(after: SeekKey | null, limit: number): Builder<TRow> {
|
|
101
|
-
|
|
109
|
+
const rowLimit = this.rowLimit === null ? limit : Math.min(this.rowLimit, limit);
|
|
110
|
+
return this.derive({ after, rowLimit, totalized: true });
|
|
102
111
|
}
|
|
103
112
|
|
|
104
113
|
/**
|
|
@@ -134,14 +143,15 @@ export class Builder<TRow extends object> implements SqlSource<TRow> {
|
|
|
134
143
|
async execute(): Promise<readonly TRow[]> {
|
|
135
144
|
const source = typeof this.rows === 'function' ? await this.rows() : this.rows;
|
|
136
145
|
this.assertCarried(source);
|
|
137
|
-
|
|
146
|
+
const kindOf = kindsOf(this.entity);
|
|
147
|
+
let result = source.filter((row) => matchesFilters(row, this.filters, kindOf));
|
|
138
148
|
const keys = this.servedOrder();
|
|
139
149
|
if (keys.length > 0) {
|
|
140
|
-
result = [...result].sort((a, b) => compareRows(a, b, keys));
|
|
150
|
+
result = [...result].sort((a, b) => compareRows(a, b, keys, kindOf));
|
|
141
151
|
}
|
|
142
152
|
if (this.after !== null) {
|
|
143
153
|
const cut = this.after;
|
|
144
|
-
result = result.filter((row) => isAfterKey(row, cut, this.order));
|
|
154
|
+
result = result.filter((row) => isAfterKey(row, cut, this.order, kindOf));
|
|
145
155
|
}
|
|
146
156
|
return this.rowLimit === null ? result : result.slice(0, this.rowLimit);
|
|
147
157
|
}
|
|
@@ -264,7 +274,7 @@ function filterClause(filter: Filter, slot: Slot): string {
|
|
|
264
274
|
|
|
265
275
|
/**
|
|
266
276
|
* NULL's place in the ordering, written down rather than inherited. Postgres already defaults
|
|
267
|
-
* to `nulls last` under `asc` and `nulls first` under `desc` — that is the rule `
|
|
277
|
+
* to `nulls last` under `asc` and `nulls first` under `desc` — that is the rule `compareRows`
|
|
268
278
|
* implements, so the in-memory sort, the live matcher and `seekClause` below can only agree
|
|
269
279
|
* with it. Saying it out loud is what keeps a driver whose default differs from re-opening the
|
|
270
280
|
* divergence, and it is what an agent reads when a nullable sort key surprises it.
|
|
@@ -312,11 +322,16 @@ interface BuilderState {
|
|
|
312
322
|
* pagination needs the same answer when a source cannot push the seek down — a second definition
|
|
313
323
|
* of "after" is how one path skips a row the other returns.
|
|
314
324
|
*/
|
|
315
|
-
export function isAfterKey(
|
|
325
|
+
export function isAfterKey(
|
|
326
|
+
row: object,
|
|
327
|
+
cursor: SeekKey,
|
|
328
|
+
order: readonly OrderKey[],
|
|
329
|
+
kindOf: KindOf,
|
|
330
|
+
): boolean {
|
|
316
331
|
for (const [index, key] of order.entries()) {
|
|
317
|
-
const result =
|
|
332
|
+
const result = compareByKind(kindOf(key.column), columnOf(row, key.column), cursor.key[index]);
|
|
318
333
|
const signed = key.direction === 'asc' ? result : -result;
|
|
319
334
|
if (signed !== 0) return signed > 0;
|
|
320
335
|
}
|
|
321
|
-
return
|
|
336
|
+
return compareByKind(kindOf('id'), columnOf(row, 'id'), cursor.id) > 0;
|
|
322
337
|
}
|
package/src/sql.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* SQL transparency. An agent that can read the SQL a query generates can fix a
|
|
3
3
|
* slow or wrong read by itself; a query builder that hides its output forces it
|
|
4
|
-
* to guess. `
|
|
4
|
+
* to guess. `explainQuery` is the read path for `/_x` and for `x db explain`.
|
|
5
5
|
*/
|
|
6
6
|
import type { Ctx } from '@ultimat3/core';
|
|
7
7
|
import type { StandardSchemaV1 } from '@ultimat3/schema';
|
|
@@ -18,10 +18,10 @@ export interface ExplainResult extends SqlText {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
|
-
* Policy is deliberately NOT enforced here: `
|
|
21
|
+
* Policy is deliberately NOT enforced here: `explainQuery` never returns rows, and the
|
|
22
22
|
* surfaces that expose it (`/_x`, the CLI) are admin-gated in their own right.
|
|
23
23
|
*/
|
|
24
|
-
export async function
|
|
24
|
+
export async function explainQuery<TInput extends StandardSchemaV1, TRow extends object>(
|
|
25
25
|
target: Query<TInput, TRow, boolean>,
|
|
26
26
|
input: unknown,
|
|
27
27
|
ctx?: Ctx,
|
package/src/stable.ts
CHANGED
|
@@ -11,9 +11,6 @@
|
|
|
11
11
|
|
|
12
12
|
import { isJsonObject } from '@ultimat3/core';
|
|
13
13
|
|
|
14
|
-
/** Re-exported, not re-declared: `./stable` stays this package's one import path for it. */
|
|
15
|
-
export { isJsonObject };
|
|
16
|
-
|
|
17
14
|
/** Column read that works for interfaces without an index signature. */
|
|
18
15
|
export function columnOf(row: object, column: string): unknown {
|
|
19
16
|
const record: unknown = row;
|
package/src/subscribes.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The relations a live read is patched from: the one thing about a query that cannot be derived,
|
|
3
|
-
* and the two assertions that keep the declaration honest
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
* and the two assertions that keep the declaration honest — a stale name is a declaration its
|
|
4
|
+
* author reads as true. Since #518 `x db gen` grants these relations no `REPLICA IDENTITY FULL`:
|
|
5
|
+
* every entity is keyed, and a keyed table is correct on DEFAULT (`x db gen` validates the names).
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { QuerySubscribesDriftError, QuerySubscribesInvalidError } from './errors';
|
package/src/type-pins.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// Compile-time pins for this package's public surface. Source, not a `.test.ts`, on purpose:
|
|
2
|
+
// `tsconfig.json` excludes `src/**/*.test.ts`, so `tsc -b` never reads a test file and a
|
|
3
|
+
// type-level claim written in one can never fail. This module emits nothing and exports nothing
|
|
4
|
+
// anybody imports — a regression here is a build error, the only enforcement that counts.
|
|
5
|
+
|
|
6
|
+
import type { McpExposureDeclaration } from '@ultimat3/core';
|
|
7
|
+
import type { StandardSchemaV1 } from '@ultimat3/schema';
|
|
8
|
+
import type { AnyQuery, Query, QueryDef } from './query';
|
|
9
|
+
|
|
10
|
+
/** Fails to compile when `T` is anything but `true`. The whole mechanism. */
|
|
11
|
+
type Assert<T extends true> = T;
|
|
12
|
+
|
|
13
|
+
type Equals<A, B> = [A] extends [B] ? ([B] extends [A] ? true : false) : false;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* IDENTICAL, not merely mutually assignable: an object type without an optional key is assignable
|
|
17
|
+
* to one with it and back, so `Equals` cannot see an optional field added on one side only — and
|
|
18
|
+
* every field of the `mcp` block is optional but `expose`.
|
|
19
|
+
*/
|
|
20
|
+
type Identical<A, B> =
|
|
21
|
+
(<T>() => T extends A ? 1 : 2) extends <T>() => T extends B ? 1 : 2 ? true : false;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* O-tool, 25.0.0: a query projects no MCP tool of its own — `@ultimat3/mcp`'s `toolFrom` is
|
|
25
|
+
* the one projection, and a tier-3 `.tool()` could only ever be a second one. A `tool` member
|
|
26
|
+
* coming back on either view flips these to `false`.
|
|
27
|
+
*/
|
|
28
|
+
export type _AQueryHasNoToolTwin = Assert<Equals<Extract<keyof AnyQuery, 'tool'>, never>>;
|
|
29
|
+
export type _ATypedQueryHasNoToolTwin = Assert<Equals<Extract<keyof Query, 'tool'>, never>>;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 25.0.0 (plan 101, M5): the deprecation TYPES are `@ultimat3/core`'s alone, as the helpers are.
|
|
33
|
+
* A runtime test cannot see a type re-export, so each is an expected error: a re-export coming
|
|
34
|
+
* back makes the directive unused, which `tsc` refuses.
|
|
35
|
+
*/
|
|
36
|
+
// @ts-expect-error — `Deprecation` is imported from `@ultimat3/core`, never from this barrel.
|
|
37
|
+
export type _NoDeprecationReexport = import('./index').Deprecation;
|
|
38
|
+
// @ts-expect-error — `DeprecationField` is imported from `@ultimat3/core`.
|
|
39
|
+
export type _NoDeprecationFieldReexport = import('./index').DeprecationField;
|
|
40
|
+
// @ts-expect-error — `DeprecationRender` is imported from `@ultimat3/core`.
|
|
41
|
+
export type _NoDeprecationRenderReexport = import('./index').DeprecationRender;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 25.0.0: a query's `mcp` block is core's ONE `McpExposureDeclaration`, on the declaration and on
|
|
45
|
+
* both views. `QueryMcp`, `QueryMcpAnnotations` and `QueryListParams` restated it; a twin coming
|
|
46
|
+
* back on the barrel makes its directive unused, and a field drifting flips the pins to `false`.
|
|
47
|
+
*/
|
|
48
|
+
export type _QueryDeclaresCoresBlock = Assert<
|
|
49
|
+
Identical<NonNullable<QueryDef<StandardSchemaV1, object>['mcp']>, McpExposureDeclaration>
|
|
50
|
+
>;
|
|
51
|
+
export type _AnyQueryCarriesCoresBlock = Assert<
|
|
52
|
+
Identical<NonNullable<AnyQuery['mcp']>, McpExposureDeclaration>
|
|
53
|
+
>;
|
|
54
|
+
export type _TypedQueryCarriesCoresBlock = Assert<
|
|
55
|
+
Identical<NonNullable<Query['mcp']>, McpExposureDeclaration>
|
|
56
|
+
>;
|
|
57
|
+
// @ts-expect-error — `QueryMcp` is gone: the block is `@ultimat3/core`'s `McpExposureDeclaration`.
|
|
58
|
+
export type _NoQueryMcpTwin = import('./index').QueryMcp;
|
|
59
|
+
// @ts-expect-error — `QueryMcpAnnotations` is core's `McpAnnotationHints`.
|
|
60
|
+
export type _NoQueryMcpAnnotationsTwin = import('./index').QueryMcpAnnotations;
|
|
61
|
+
// @ts-expect-error — `QueryListParams` is core's `McpListParams`.
|
|
62
|
+
export type _NoQueryListParamsTwin = import('./index').QueryListParams;
|
package/src/deprecation.ts
DELETED
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* A declared retirement, rendered as the two headers the standards already define — RFC 9745
|
|
3
|
-
* `Deprecation` and RFC 8594 `Sunset` — plus the successor link. Pure string and date maths, and
|
|
4
|
-
* deliberately throw-free: each package raises its own `X_*` for a date it cannot render.
|
|
5
|
-
*
|
|
6
|
-
* `@ultimat3/action` carries the twin of this file. Both are tier 3, so neither may import the
|
|
7
|
-
* other, and the shared home is `@ultimat3/http` (tier 2) once that package grows one — the same
|
|
8
|
-
* compromise `naming.ts` is ported under.
|
|
9
|
-
*/
|
|
10
|
-
import { counter } from '@ultimat3/core';
|
|
11
|
-
|
|
12
|
-
export interface Deprecation {
|
|
13
|
-
/** When it was deprecated. ISO-8601, e.g. `'2026-08-01T00:00:00Z'`. */
|
|
14
|
-
readonly since: string;
|
|
15
|
-
/** When it stops answering. ISO-8601 — the date `Sunset` publishes and clients plan against. */
|
|
16
|
-
readonly sunset: string;
|
|
17
|
-
/** The export name of the replacement, projected to a `rel="successor-version"` link. */
|
|
18
|
-
readonly replacedBy?: string;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export type DeprecationField = 'since' | 'sunset';
|
|
22
|
-
|
|
23
|
-
export type DeprecationRender =
|
|
24
|
-
| {
|
|
25
|
-
readonly ok: true;
|
|
26
|
-
readonly headers: Readonly<Record<string, string>>;
|
|
27
|
-
/** The same facts as data, for the descriptor and the manifest. */
|
|
28
|
-
readonly meta: Readonly<Record<string, string>>;
|
|
29
|
-
}
|
|
30
|
-
| { readonly ok: false; readonly field: DeprecationField; readonly value: string };
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* How many calls a deprecated declaration is still taking — the number "can we remove it yet?"
|
|
34
|
-
* needs. Attributes are the primitive and the declared NAME, both bounded by the size of the
|
|
35
|
-
* codebase; a caller id here would be an unbounded series.
|
|
36
|
-
*/
|
|
37
|
-
const deprecatedCalls = counter('deprecated_calls_total', {
|
|
38
|
-
unit: '{call}',
|
|
39
|
-
description: 'Calls served by a declaration that has been deprecated, by primitive and name',
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
export function recordDeprecatedCall(primitive: 'action' | 'query', name: string): void {
|
|
43
|
-
deprecatedCalls.add(1, { primitive, name });
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* `Deprecation` is a structured-field Date (`@` + unix seconds, RFC 9745); `Sunset` is an
|
|
48
|
-
* HTTP-date (IMF-fixdate, RFC 8594). Two spellings of one instant because two RFCs chose
|
|
49
|
-
* differently — never render one in the other's format, and never emit `Invalid Date`.
|
|
50
|
-
*/
|
|
51
|
-
export function renderDeprecation(
|
|
52
|
-
deprecation: Deprecation,
|
|
53
|
-
successorPath: string | undefined,
|
|
54
|
-
): DeprecationRender {
|
|
55
|
-
const since = Date.parse(deprecation.since);
|
|
56
|
-
if (Number.isNaN(since)) return { ok: false, field: 'since', value: deprecation.since };
|
|
57
|
-
const sunset = Date.parse(deprecation.sunset);
|
|
58
|
-
if (Number.isNaN(sunset)) return { ok: false, field: 'sunset', value: deprecation.sunset };
|
|
59
|
-
|
|
60
|
-
const headers: Record<string, string> = {
|
|
61
|
-
deprecation: `@${Math.floor(since / 1000)}`,
|
|
62
|
-
sunset: new Date(sunset).toUTCString(),
|
|
63
|
-
};
|
|
64
|
-
// The successor's URL, derived by the caller from the same `naming.ts` the client uses — a
|
|
65
|
-
// link this file built from the export name would be the second URL derivation in the package.
|
|
66
|
-
if (successorPath !== undefined) {
|
|
67
|
-
headers['link'] = `<${successorPath}>; rel="successor-version"`;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const meta: Record<string, string> = {
|
|
71
|
-
since: new Date(since).toISOString(),
|
|
72
|
-
sunset: new Date(sunset).toISOString(),
|
|
73
|
-
...(deprecation.replacedBy === undefined ? {} : { replacedBy: deprecation.replacedBy }),
|
|
74
|
-
};
|
|
75
|
-
return { ok: true, headers, meta };
|
|
76
|
-
}
|
package/src/mcp-tool.ts
DELETED
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* A query as an MCP read tool. Its `read` goes through `sourceFor` — the same
|
|
3
|
-
* authorized front half the HTTP read and the live subscription use — so the tool
|
|
4
|
-
* cannot drift from the endpoint and cannot acquire a second authz path.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import type { Actor, Ctx } from '@ultimat3/core';
|
|
8
|
-
import { isMcpExposed } from '@ultimat3/core';
|
|
9
|
-
import type { JsonSchema } from '@ultimat3/schema';
|
|
10
|
-
import { toMcpInputSchema } from '@ultimat3/schema';
|
|
11
|
-
import type { QueryPolicy } from './policy-gate';
|
|
12
|
-
import type { AnyQuery } from './query';
|
|
13
|
-
import { queryName, sourceFor } from './read';
|
|
14
|
-
import { listQueries } from './registry';
|
|
15
|
-
|
|
16
|
-
export interface QueryToolReadOptions {
|
|
17
|
-
readonly ctx?: Ctx;
|
|
18
|
-
/** The agent behind the call. `null` is the signed-out caller. */
|
|
19
|
-
readonly actor?: Actor | null;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export interface QueryToolDescriptor {
|
|
23
|
-
/**
|
|
24
|
-
* The export name VERBATIM, and identical to `query` below. `@ultimat3/mcp` serves a read
|
|
25
|
-
* under `queryName(target)` and answers `tools/call` for nothing else, so a snake_cased
|
|
26
|
-
* descriptor named a tool the server had never heard of — which it did until 2026-08.
|
|
27
|
-
*/
|
|
28
|
-
readonly name: string;
|
|
29
|
-
/** The query's `mcp.description`, or its name when the author gave none. */
|
|
30
|
-
readonly description: string;
|
|
31
|
-
readonly query: string;
|
|
32
|
-
/**
|
|
33
|
-
* The query's own policy object, not a copy — `tool().policy === query.policy`
|
|
34
|
-
* is what makes "an MCP call cannot reach a different authz path" checkable.
|
|
35
|
-
*/
|
|
36
|
-
readonly policy: QueryPolicy;
|
|
37
|
-
readonly inputSchema: JsonSchema;
|
|
38
|
-
/** Always false: a query reads. Drives the rate-limit bucket in @ultimat3/mcp. */
|
|
39
|
-
readonly mutates: false;
|
|
40
|
-
read(input: unknown, options?: QueryToolReadOptions): Promise<readonly object[]>;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export function toQueryTool(target: AnyQuery): QueryToolDescriptor {
|
|
44
|
-
const name = queryName(target);
|
|
45
|
-
return {
|
|
46
|
-
name,
|
|
47
|
-
description: target.mcp?.description ?? name,
|
|
48
|
-
query: name,
|
|
49
|
-
policy: target.policy,
|
|
50
|
-
inputSchema: toMcpInputSchema(target.input),
|
|
51
|
-
mutates: false,
|
|
52
|
-
read: async (input, options = {}) => {
|
|
53
|
-
// Executed without the cache tiers on purpose: an agent diffing two tool
|
|
54
|
-
// calls must be reading the rows, not a TTL.
|
|
55
|
-
const source = await sourceFor(target, input, {
|
|
56
|
-
...(options.ctx === undefined ? {} : { ctx: options.ctx }),
|
|
57
|
-
...(options.actor === undefined ? {} : { actor: options.actor }),
|
|
58
|
-
});
|
|
59
|
-
return source.execute();
|
|
60
|
-
},
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Opt-in: a read hands rows to an agent, so silence exposes nothing. `mcp: { expose: true }` is
|
|
66
|
-
* the whole opt-in — `isMcpExposed` from `@ultimat3/core` is the framework's one answer, and an
|
|
67
|
-
* action's tool is now decided by the same call rather than by a second, looser rule.
|
|
68
|
-
*/
|
|
69
|
-
export function isExposed(target: AnyQuery): boolean {
|
|
70
|
-
return isMcpExposed(target.mcp);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/** Deterministic order — the tool list is part of the agent-visible contract. */
|
|
74
|
-
export function toQueryTools(
|
|
75
|
-
queries: readonly AnyQuery[] = listQueries(),
|
|
76
|
-
): readonly QueryToolDescriptor[] {
|
|
77
|
-
return queries
|
|
78
|
-
.filter(isExposed)
|
|
79
|
-
.map(toQueryTool)
|
|
80
|
-
.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
81
|
-
}
|
package/src/naming.ts
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The naming rule for reads, as this package has always exported it: a query's export name
|
|
3
|
-
* derives its HTTP path. The rule itself is `@ultimat3/core`'s `client-paths.ts` — one tier-0 file
|
|
4
|
-
* that `action`, `query` and `realtime` all read, so the path cannot be derived two ways. The MCP
|
|
5
|
-
* tool name is NOT derived — it is the export name verbatim, so there is one name to call.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { queryPath, splitWords } from '@ultimat3/core/page';
|
|
9
|
-
|
|
10
|
-
export { splitWords };
|
|
11
|
-
|
|
12
|
-
/** `liveFeed` -> `live-feed`. */
|
|
13
|
-
export function toKebabCase(name: string): string {
|
|
14
|
-
return splitWords(name).join('-');
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* `liveFeed` -> `/_x/query/live-feed`, read with `GET …?orgId=…`. A read is a GET under its own
|
|
19
|
-
* prefix so a CDN, a browser cache and a log line can all tell it apart from an action's
|
|
20
|
-
* `POST /api/...` without parsing a body.
|
|
21
|
-
*/
|
|
22
|
-
export const derivePath: (name: string) => string = queryPath;
|