@aotter/mantle 0.2.0 → 0.2.1-alpha.2

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.
Files changed (89) hide show
  1. package/README.md +6 -2
  2. package/dist/build/compiler.d.ts +6 -6
  3. package/dist/build/compiler.js +6 -3
  4. package/dist/build/spec/domain/model/ManifestGrammar.d.ts +39 -1
  5. package/dist/build/spec/domain/model/RuntimePlan.d.ts +16 -3
  6. package/dist/build/spec/domain/model/SqlIr.d.ts +30 -0
  7. package/dist/build/spec/kernel/diagnostic.d.ts +1 -1
  8. package/dist/domain/describe/chunk-CW7X5Q5T.js +1 -0
  9. package/dist/domain/describe/index.d.ts +1 -0
  10. package/dist/domain/describe/index.js +1 -1
  11. package/dist/domain/describe/match.d.ts +19 -0
  12. package/dist/domain/describe/public.d.ts +3 -0
  13. package/dist/domain/describe/public.js +1 -0
  14. package/dist/infrastructure/adminEntries.js +19 -3
  15. package/dist/infrastructure/bundle.js +43 -8
  16. package/dist/infrastructure/cli.js +1 -1
  17. package/dist/infrastructure/generate.js +17 -3
  18. package/dist/kernel/binds.d.ts +18 -3
  19. package/dist/kernel/binds.js +1 -1
  20. package/dist/kernel/contracts.d.ts +7 -0
  21. package/dist/kernel/contracts.js +9 -0
  22. package/dist/kernel/driver/bun-sqlite.js +3 -3
  23. package/dist/kernel/driver/d1.d.ts +1 -1
  24. package/dist/kernel/driver/d1.js +6 -3
  25. package/dist/kernel/driver/postgres.d.ts +2 -1
  26. package/dist/kernel/driver/postgres.js +11 -6
  27. package/dist/kernel/mcp-apps.d.ts +3 -2
  28. package/dist/kernel/mcp-apps.js +7 -2
  29. package/dist/kernel/mcp-handler.js +22 -9
  30. package/dist/kernel/mcp.d.ts +18 -0
  31. package/dist/kernel/mcp.js +1 -0
  32. package/dist/kernel/rest-router.js +3 -5
  33. package/dist/kernel/rest.d.ts +2 -1
  34. package/dist/kernel/rest.js +1 -0
  35. package/dist/kernel/route-pattern.d.ts +26 -0
  36. package/dist/kernel/route-pattern.js +34 -0
  37. package/dist/kernel/store.d.ts +15 -1
  38. package/dist/kernel/store.js +51 -18
  39. package/dist/usecase/adminBindings.js +2 -2
  40. package/dist/usecase/adminClient.js +4 -1
  41. package/dist/usecase/generate.js +7 -5
  42. package/dist/usecase/handlers.js +1 -1
  43. package/dist/usecase/publishing.js +20 -7
  44. package/dist/usecase/rest.js +5 -5
  45. package/dist/usecase/stores.js +8 -3
  46. package/docs/examples/README.md +1 -1
  47. package/docs/examples/app-shell/README.md +58 -10
  48. package/docs/examples/app-shell/package.json +6 -6
  49. package/docs/examples/club-events/manifests/club-events.yaml +2 -0
  50. package/docs/examples/generated-web/seed.sql +5 -4
  51. package/docs/examples/purchasing/manifests/purchasing.yaml +3 -0
  52. package/docs/handbook/cloudflare/media-r2.md +6 -3
  53. package/docs/handbook/cloudflare/service-entry.md +2 -0
  54. package/docs/handbook/concepts/lifecycle-and-locales.md +21 -1
  55. package/docs/handbook/examples/hub.md +1 -1
  56. package/docs/handbook/guides/admin-ui.md +1 -1
  57. package/docs/handbook/guides/agent-setup.md +13 -8
  58. package/docs/handbook/guides/agent-workflow-acceptance.md +129 -19
  59. package/docs/handbook/guides/local-testing.md +50 -5
  60. package/docs/handbook/guides/mcp-apps.md +40 -0
  61. package/docs/handbook/guides/native-admin.md +218 -11
  62. package/docs/handbook/guides/native-execution.md +5 -4
  63. package/docs/handbook/guides/revisions.md +584 -0
  64. package/docs/handbook/guides/size-and-startup.md +140 -0
  65. package/docs/handbook/guides/sql-patterns.md +47 -1
  66. package/docs/handbook/guides/typed-queries.md +8 -3
  67. package/docs/handbook/navigation.json +4 -0
  68. package/docs/handbook/reference/authorization.md +2 -0
  69. package/docs/handbook/reference/describe.md +56 -4
  70. package/docs/handbook/reference/diagnostics.md +12 -3
  71. package/docs/handbook/reference/procedure.md +4 -4
  72. package/docs/handbook/reference/schema.md +71 -7
  73. package/docs/handbook/reference/sql.md +2 -1
  74. package/docs/handbook/reference/surface.md +13 -2
  75. package/docs/handbook/reference/trigger.md +12 -0
  76. package/docs/handbook/releases/index.md +75 -1
  77. package/docs/handbook/start/agents.md +6 -2
  78. package/docs/handbook/start/project-and-cli.md +4 -0
  79. package/docs/handbook/start/quickstart-worker.md +1 -1
  80. package/docs/skills/develop/SKILL.md +15 -3
  81. package/docs/skills/integrate/SKILL.md +3 -1
  82. package/docs/skills/performance/SKILL.md +169 -0
  83. package/docs/skills/update/SKILL.md +9 -5
  84. package/docs/upgrade-0.1-to-0.2.md +151 -39
  85. package/package.json +10 -11
  86. package/skills/README.md +6 -2
  87. package/skills/mantle/SKILL.md +19 -5
  88. package/dist/domain/describe/chunk-SGMRVDGE.js +0 -1
  89. package/dist/domain/describe/target-hash.js +0 -1
@@ -46,6 +46,15 @@ export function deny(caller, allowed, path) {
46
46
  }
47
47
  export const ROW_CONFLICT = "No row matched this change: it does not exist or is not visible to you, it changed since it was read, or its state no longer allows this change. Read it again before deciding whether to retry.";
48
48
  export const conflict = (path, unique, cause) => new ContractError("CONFLICT", unique ? "A unique constraint of the Schema was violated." : ROW_CONFLICT, path, { cause, fields: { kind: unique ? "unique" : "row" } });
49
+ export const lockConflict = (path, probe, rows) => {
50
+ const lock = rows[0]?.lock;
51
+ if (lock === "publishing.required" && probe.required)
52
+ return new ContractError("INPUT_VALIDATION_FAILED", probe.required, path);
53
+ if (typeof lock !== "string" || !Object.hasOwn(probe.locks, lock))
54
+ return conflict(path, false);
55
+ const message = probe.locks[lock];
56
+ return new ContractError("CONFLICT", message, path, { fields: { kind: "lock", schema: probe.schema, verb: probe.verb, lock, message } });
57
+ };
49
58
  export const checkConflict = (path, error) => {
50
59
  const message = error?.message ?? "", marker = /MANTLE_CHECK ([^:\n]+): (.+)/.exec(message);
51
60
  if (!/MANTLE_CHECK /.test(message))
@@ -1,4 +1,4 @@
1
- import { checkConflict, conflict, ContractError } from "../contracts.js";
1
+ import { checkConflict, conflict, ContractError, lockConflict } from "../contracts.js";
2
2
  import { unique } from "../dialect/sqlite.js";
3
3
  export { definitive } from "../dialect/sqlite.js";
4
4
  import { context, guard } from "./transaction-context.js";
@@ -30,8 +30,8 @@ export async function procedure(db, steps, path) {
30
30
  catch (error) {
31
31
  throw unique(error) ? conflict(path, true, error) : checkConflict(path, error) ?? error;
32
32
  }
33
- if (s.row && changes !== 1)
34
- throw conflict(path, false);
33
+ if (s.row && (changes < 1 || changes > 1 && s.row !== "some"))
34
+ throw s.row === true && s.lock ? lockConflict(path, s.lock, handle.query(s.lock.sql).all(...s.lock.binds)) : conflict(path, false);
35
35
  return rows;
36
36
  });
37
37
  return available(db, () => transactions.has(db) || steps.length === 1 ? execute(db) : db.transaction(execute)(db));
@@ -20,5 +20,5 @@ export interface Handle extends Reader {
20
20
  }[]>;
21
21
  }
22
22
  export declare const read: (db: Reader, sql: string, binds: unknown[]) => Promise<any[]>;
23
- /** The assert table's trigger aborts the whole batch with `CONFLICT op=<k>` when statement k did not change exactly one row. */
23
+ /** The assert table's trigger aborts the whole batch with `CONFLICT op=<k>` when statement k did not change exactly one row (at least one for `row: "some"`). */
24
24
  export declare function procedure(db: Handle, steps: readonly Bound[], path: string): Promise<Row[][]>;
@@ -1,4 +1,4 @@
1
- import { checkConflict, conflict } from "../contracts.js";
1
+ import { checkConflict, conflict, lockConflict } from "../contracts.js";
2
2
  import { unique } from "../dialect/sqlite.js";
3
3
  export { definitive } from "../dialect/sqlite.js";
4
4
  export const read = async (db, sql, binds) => (await db.prepare(sql).bind(...binds).all()).results;
@@ -8,14 +8,17 @@ export async function procedure(db, steps, path) {
8
8
  at.push(batch.length);
9
9
  batch.push(db.prepare(s.sql).bind(...s.binds));
10
10
  if (s.row)
11
- batch.push(db.prepare(`INSERT INTO _mantle_assert (op,ok) SELECT ${i}, changes() = 1`));
11
+ batch.push(db.prepare(`INSERT INTO _mantle_assert (op,ok) SELECT ${i}, changes() ${s.row === "some" ? "> 0" : "= 1"}`));
12
12
  });
13
13
  let out;
14
14
  try {
15
15
  out = await db.batch(batch);
16
16
  }
17
17
  catch (error) {
18
- throw unique(error) ? conflict(path, true, error) : /CONFLICT op=\d/.test(String(error?.message)) ? conflict(path, false, error) : checkConflict(path, error) ?? error;
18
+ const op = /CONFLICT op=(\d+)/.exec(String(error?.message)), step = op ? steps[Number(op[1])] : undefined, probe = step?.row === true ? step.lock : undefined;
19
+ if (probe)
20
+ throw lockConflict(path, probe, await read(db, probe.sql, probe.binds));
21
+ throw unique(error) ? conflict(path, true, error) : op ? conflict(path, false, error) : checkConflict(path, error) ?? error;
19
22
  }
20
23
  return at.map(i => out[i].results);
21
24
  }
@@ -16,7 +16,8 @@ export declare function definitive(error: unknown): boolean;
16
16
  */
17
17
  export declare function postgresOutcome(error: unknown, path: string): unknown;
18
18
  /**
19
- * One statement is atomic on its own (a row statement touches at most one row, so a failed count check changes nothing)
19
+ * One statement is atomic on its own (a row statement touches at most one row, and a `"some"` one fails only when it wrote
20
+ * none, so a failed count check changes nothing)
20
21
  * and runs in autocommit. Several run in `begin(serializable)`: a failure before the callback returns was rolled back;
21
22
  * only a failed COMMIT can leave the outcome unknown.
22
23
  */
@@ -1,4 +1,4 @@
1
- import { conflict, ContractError, schemaCheckFailed } from "../contracts.js";
1
+ import { conflict, ContractError, lockConflict, schemaCheckFailed } from "../contracts.js";
2
2
  import { context, guard } from "./transaction-context.js";
3
3
  const transactions = new WeakSet();
4
4
  const ISOLATION = ["serializable", "repeatable read", "read committed", "read uncommitted"];
@@ -15,18 +15,23 @@ export function definitive(error) {
15
15
  const state = sqlstate(error);
16
16
  return typeof state === "string" && /^[0-9A-Z]{5}$/.test(state) && typeof error?.severity === "string" && !/^(08|28|53|57|58|XX)/.test(state);
17
17
  }
18
+ function serialization(error, path) {
19
+ const state = sqlstate(error);
20
+ if ((state === "40001" || state === "40P01") && definitive(error))
21
+ return new ContractError("CONFLICT", "A concurrent transaction prevented this write. Read again before deciding whether to retry.", path, { cause: error, fields: { kind: "serialization" } });
22
+ }
18
23
  export function postgresOutcome(error, path) {
19
24
  if (definitive(error))
20
- return error;
25
+ return serialization(error, path) ?? error;
21
26
  return new ContractError("OUTCOME_UNKNOWN", "The write outcome is unknown; reconcile before retrying.", path, { cause: error });
22
27
  }
23
28
  export async function procedure(db, steps, path) {
24
29
  const execute = async (handle) => {
25
30
  const results = [];
26
31
  for (const s of steps) {
27
- const { rows, changes } = await run(handle, s.sql, s.binds).catch(error => { throw sqlstate(error) === "23505" ? conflict(path, true, error) : sqlstate(error) === "23514" ? check(path, error) : error; });
28
- if (s.row && changes !== 1)
29
- throw conflict(path, false);
32
+ const { rows, changes } = await run(handle, s.sql, s.binds).catch(error => { throw sqlstate(error) === "23505" ? conflict(path, true, error) : sqlstate(error) === "23514" ? check(path, error) : serialization(error, path) ?? error; });
33
+ if (s.row && (changes < 1 || changes > 1 && s.row !== "some"))
34
+ throw s.row === true && s.lock ? lockConflict(path, s.lock, (await run(handle, s.lock.sql, s.lock.binds)).rows) : conflict(path, false);
30
35
  results.push(rows);
31
36
  }
32
37
  return results;
@@ -75,7 +80,7 @@ export async function transaction(db, fn, options = {}, path = "store/transactio
75
80
  });
76
81
  }
77
82
  catch (error) {
78
- throw error instanceof ContractError || !committing ? error : postgresOutcome(error, path);
83
+ throw serialization(error, path) ?? (error instanceof ContractError || !committing ? error : postgresOutcome(error, path));
79
84
  }
80
85
  finally {
81
86
  state.outer = undefined;
@@ -40,7 +40,7 @@ export interface McpAppResource {
40
40
  readonly domain?: string;
41
41
  /** Tool names whose results this resource renders. */
42
42
  readonly renders?: readonly string[];
43
- /** Views only this App may call (`visibility: ["app"]`); a client without MCP Apps does not see them. A Procedure cannot be hidden from the model. */
43
+ /** Views only this App may call (`visibility: ["app"]`); a client without MCP Apps does not see them. To hide a Procedure, give its tool `meta: { ui: { visibility: ["app"] } }`. */
44
44
  readonly appOnly?: readonly string[];
45
45
  }
46
46
  export interface McpApps {
@@ -51,7 +51,7 @@ export type McpAppsSource = McpApps | ((context: Pick<McpAppRender, "env" | "req
51
51
  /** Flatten contributors; `mcp(...)` reads static resources getters once at construction. */
52
52
  export declare const resourcesOf: (apps: McpApps | readonly McpApps[]) => McpApps;
53
53
  /** Static contributors are checked at construction; resolved factories are checked per request. A request-bound tool table defers tool-link checks until its first request. */
54
- export declare function linkApps(apps: McpApps | readonly McpApps[], tools?: ReadonlySet<string>, views?: ReadonlySet<string>): {
54
+ export declare function linkApps(apps: McpApps | readonly McpApps[], tools?: ReadonlyMap<string, Readonly<Record<string, any>> | undefined>, views?: ReadonlySet<string>): {
55
55
  resources: McpAppResource[];
56
56
  renders: Map<string, string>;
57
57
  appOnly: Set<string>;
@@ -60,6 +60,7 @@ export declare function linkApps(apps: McpApps | readonly McpApps[], tools?: Rea
60
60
  export declare function linkSurface(apps: McpApps | readonly McpApps[], selected: readonly {
61
61
  name: string;
62
62
  page?: unknown;
63
+ meta?: Readonly<Record<string, unknown>>;
63
64
  }[], previous?: {
64
65
  key: string;
65
66
  links: ReturnType<typeof linkApps>;
@@ -27,9 +27,14 @@ export function linkApps(apps, tools, views) {
27
27
  appOnly.add(tool);
28
28
  }
29
29
  }
30
+ for (const [tool, uri] of renders) {
31
+ const meta = tools?.get(tool) ?? {}, ui = meta.ui ?? {};
32
+ if ([ui.resourceUri, meta["ui/resourceUri"], meta["openai/outputTemplate"]].some(value => value && value !== uri) || appOnly.has(tool) && ui.visibility && `${ui.visibility}` !== "app")
33
+ refuse(`Tool '${tool}' meta contradicts its MCP App links.`);
34
+ }
30
35
  return { resources, renders, appOnly };
31
36
  }
32
37
  export function linkSurface(apps, selected, previous) {
33
- const key = JSON.stringify(selected.map(tool => [tool.name, !!tool.page]).sort(([a], [b]) => a.localeCompare(b)));
34
- return previous?.key === key ? previous : { key, links: linkApps(apps, new Set(selected.map(tool => tool.name)), new Set(selected.filter(tool => tool.page).map(tool => tool.name))) };
38
+ const key = JSON.stringify(selected.map(tool => [tool.name, !!tool.page, tool.meta]).sort(([a], [b]) => a.localeCompare(b)));
39
+ return previous?.key === key ? previous : { key, links: linkApps(apps, new Map(selected.map(tool => [tool.name, tool.meta])), new Set(selected.filter(tool => tool.page).map(tool => tool.name))) };
35
40
  }
@@ -20,9 +20,12 @@ function observe(listener, event) {
20
20
  }
21
21
  const paging = (schema) => ({ ...schema, properties: { ...schema.properties, limit: { type: "integer", minimum: 1, maximum: 500 }, cursor: { type: "string" } } });
22
22
  const MCP_PAGE_BUDGET = 60_000;
23
+ const MCP_INPUT_ELEMENTS = 10_000;
23
24
  const union = (scopes, extra) => [...new Set([...scopes, ...extra])].join(" ");
24
25
  export function handler(tools, options, apps = {}, reads = []) {
25
26
  const allowed = options.allowedOrigins ?? [];
27
+ if (options.toolsPageSize !== undefined && !(Number.isInteger(options.toolsPageSize) && options.toolsPageSize > 0))
28
+ throw new TypeError("mcp: toolsPageSize must be a positive integer.");
26
29
  return draining(async (request, env, ctx) => {
27
30
  const access = options.gateway ? (await import("./host-access.js")).accessRequest(ctx, options) : undefined;
28
31
  let resolved;
@@ -207,21 +210,29 @@ export function handler(tools, options, apps = {}, reads = []) {
207
210
  const serverInfo = { name: "aotter.mantle", ...options.serverInfo, version: options.serverInfo?.version ?? version };
208
211
  const handler = createMcpHandler(() => {
209
212
  const schema = (value) => fromJsonSchema(value, { getValidator() { return (input) => ({ valid: true, data: input, errorMessage: undefined }); } });
210
- const server = new McpServer(serverInfo, options.instructions ? { instructions: options.instructions } : {});
213
+ const server = new McpServer(serverInfo, { maxToolInputElements: MCP_INPUT_ELEMENTS, ...(options.instructions ? { instructions: options.instructions } : {}) });
211
214
  server.server.registerCapabilities({ tools: { listChanged: false } });
212
215
  if (host)
213
216
  server.server.onclose = () => access?.finish(isRecord(parsedBody) && parsedBody.method === "initialize" ? undefined : new ContractError("INPUT_VALIDATION_FAILED", "MCP request refused.", "mcp", { status: 400 }));
214
- const appMeta = links ? registerApps(server, links, parsedBody, new Set(selected.map(tool => tool.name)), { env, request }) : undefined;
217
+ const enabled = clientUiSupport(parsedBody) !== "unsupported";
218
+ const appMeta = links ? registerApps(server, links, enabled, new Set(selected.map(tool => tool.name)), { env, request }) : undefined;
215
219
  const registered = new Set(), set = server.server.setRequestHandler.bind(server.server);
220
+ const size = options.toolsPageSize;
216
221
  server.server.setRequestHandler = ((method, handler) => set(method, method === "tools/call" && typeof handler === "function" ? (request, context) => {
217
222
  if (!registered.has(request.params.name))
218
223
  throw new ProtocolError(ProtocolErrorCode.InvalidParams, `No tool named ${JSON.stringify(request.params.name)} exists in the current catalogue (catalogue version ${serverInfo.version}).`);
219
224
  return handler(request, context);
225
+ } : method === "tools/list" && size && typeof handler === "function" ? async (request, context) => {
226
+ const { tools } = await handler(request, context), cursor = request.params?.cursor, start = cursor === undefined ? 0 : Number(cursor.split(":")[1]);
227
+ if (cursor !== undefined && !(start > 0 && start % size === 0 && start < tools.length && cursor === `${version}:${start}`))
228
+ throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Unknown tools/list cursor; list again from the first page (catalogue version ${serverInfo.version}).`);
229
+ return { tools: tools.slice(start, start + size), ...(start + size < tools.length ? { nextCursor: `${version}:${start + size}` } : {}) };
220
230
  } : handler));
221
231
  for (const tool of selected) {
222
- const meta = appMeta?.(tool.name);
223
- if (meta === false)
232
+ const linked = appMeta?.(tool.name), own = tool.meta, visibility = own?.ui?.visibility;
233
+ if (linked === false || !enabled && Array.isArray(visibility) && !visibility.includes("model"))
224
234
  continue;
235
+ const meta = linked && own ? { ...linked, ...own, ui: { ...linked.ui, ...own.ui } } : linked ?? own;
225
236
  registered.add(tool.name);
226
237
  const about = (contract) => { const text = [tool.version && `tool version ${tool.version}`, contract && `input schema ${JSON.stringify(tool.page ? paging(tool.inputSchema) : tool.inputSchema)}`].filter(Boolean).join("; "); return text ? [{ type: "text", text }] : []; };
227
238
  server.registerTool(tool.name, { ...(meta ? { _meta: meta } : {}), ...(tool.title ? { title: text(tool.title, locale) } : {}), description: text(tool.description, locale) ?? text(tool.title, locale) ?? tool.name, inputSchema: schema(tool.page ? paging(tool.inputSchema) : tool.inputSchema), ...(tool.outputSchema ? { outputSchema: schema(tool.outputSchema) } : {}), ...(tool.annotations ? { annotations: tool.annotations } : {}) }, async (raw) => {
@@ -235,8 +246,11 @@ export function handler(tools, options, apps = {}, reads = []) {
235
246
  tool.check?.(caller);
236
247
  if (tool.run && (!host || !tool.readOnly))
237
248
  checkOrigin(request, caller);
238
- let db;
239
- const run = (input) => callTool(tool, input, () => db ??= options.db(env), caller, options.handlers, access?.dryRun ? () => ({ dryRun: true, event: access.finish() }) : undefined);
249
+ let db, resultMeta;
250
+ const run = async (input) => {
251
+ const out = await callTool(tool, input, () => db ??= options.db(env), caller, options.handlers, access?.dryRun ? () => ({ dryRun: true, event: access.finish() }) : undefined);
252
+ return isRecord(out) && Symbol.for("net.aotter.mantle/result") in out ? (resultMeta = out._meta, out.data) : out;
253
+ };
240
254
  let data = await run(raw);
241
255
  for (let tries = 0; tool.page && isRecord(data) && Array.isArray(data.rows); tries++) {
242
256
  const rows = data.rows, total = JSON.stringify(data).length;
@@ -260,7 +274,7 @@ export function handler(tools, options, apps = {}, reads = []) {
260
274
  if (host)
261
275
  access?.finish();
262
276
  const text = JSON.stringify(data), notice = tool.page && text.length > MCP_PAGE_BUDGET;
263
- return { ...(meta ? { _meta: { "net.aotter.mantle/tool": tool.name } } : {}), content: [{ type: "text", text }, ...about(false), ...(notice ? [{ type: "text", text: `This page is larger than the ${MCP_PAGE_BUDGET}-character page size budget (a single row over it, or a View without an order, is returned whole), so a host may truncate it. Continue with nextCursor, if any.` }] : [])], ...(data && typeof data === "object" && !Array.isArray(data) ? { structuredContent: data } : {}) };
277
+ return { ...(meta || resultMeta ? { _meta: { ...resultMeta, "net.aotter.mantle/tool": tool.name } } : {}), content: [{ type: "text", text }, ...about(false), ...(notice ? [{ type: "text", text: `This page is larger than the ${MCP_PAGE_BUDGET}-character page size budget (a single row over it, or a View without an order, is returned whole), so a host may truncate it. Continue with nextCursor, if any.` }] : [])], ...(data && typeof data === "object" && !Array.isArray(data) ? { structuredContent: data } : {}) };
264
278
  }
265
279
  catch (error) {
266
280
  if (host)
@@ -321,8 +335,7 @@ function appMeta(r, csp = r.csp) {
321
335
  const ui = { ...(csp ? { csp } : {}), ...(r.permissions ? { permissions: r.permissions } : {}), ...(r.domain !== undefined ? { domain: r.domain } : {}), ...(r.prefersBorder !== undefined ? { prefersBorder: r.prefersBorder } : {}) };
322
336
  return Object.keys(ui).length > 0 ? { ui } : undefined;
323
337
  }
324
- function registerApps(server, links, message, tools, request) {
325
- const enabled = clientUiSupport(message) !== "unsupported";
338
+ function registerApps(server, links, enabled, tools, request) {
326
339
  if (enabled)
327
340
  for (const resource of links.resources) {
328
341
  const meta = appMeta(resource);
@@ -37,6 +37,8 @@ export interface Options<Env, Database> extends GatewayOptions {
37
37
  allowedOrigins?: readonly string[];
38
38
  onObservation?: (event: McpObservation) => void | Promise<void>;
39
39
  onRefusal?: (event: CallerRefusal) => void | Promise<void>;
40
+ /** Tools per `tools/list` page: the result carries `nextCursor` while more remain. Unset, one page lists every tool. */
41
+ toolsPageSize?: number;
40
42
  }
41
43
  export interface CallerRefusal {
42
44
  readonly at: number;
@@ -98,7 +100,23 @@ export interface Tool<Database, Handlers = never> {
98
100
  readonly path?: string;
99
101
  readonly page?: (db: Database, caller: Caller, input: never, paging: Paging) => Promise<unknown>;
100
102
  readonly run?: (db: Database, caller: Caller, input: never, handlers: Handlers) => Promise<unknown>;
103
+ /**
104
+ * Merged into this tool's `_meta` in `tools/list` (top level shallow, `ui` one level deep) with what its App links produce;
105
+ * a contradicting value fails at `mcp(...)`. `ui.visibility: ["app"]` hides the tool from clients without MCP Apps.
106
+ * It is a listing hint and grants or removes no execution authority.
107
+ */
108
+ readonly meta?: Readonly<Record<string, unknown>>;
101
109
  }
110
+ /** What `withResultMeta` returns: `data` is the tool result as usual, `_meta` goes only to the `tools/call` result `_meta`. */
111
+ export interface ResultWithMeta<T> {
112
+ readonly data: T;
113
+ readonly _meta: Readonly<Record<string, unknown>>;
114
+ }
115
+ /**
116
+ * Returned from a tool's `run`: MCP sends `data` exactly as an unwrapped return, and `meta` only in the result `_meta`
117
+ * (beside `net.aotter.mantle/tool`, which it cannot replace). `callTool` returns the wrapper itself; drop `_meta` before forwarding to a model.
118
+ */
119
+ export declare const withResultMeta: <T>(data: T, meta: Readonly<Record<string, unknown>>) => ResultWithMeta<T>;
102
120
  /** Request-bound tool tables keep native environment bindings outside the cached App catalogue. */
103
121
  export type Tools<Env, Database, Handlers = never> = readonly Tool<Database, Handlers>[] | ((request: Request, env: Env) => readonly Tool<Database, Handlers>[] | Promise<readonly Tool<Database, Handlers>[]>);
104
122
  /** The selected MCP routes; all request work loads only when an MCP request arrives. */
@@ -1,6 +1,7 @@
1
1
  import { linkApps, linkSurface, resourcesOf } from "./mcp-apps.js";
2
2
  export const callTool = async (...args) => (await import("./mcp-call.js")).callTool(...args);
3
3
  export const callToolLazy = async (...args) => (await import("./mcp-call.js")).callToolLazy(...args);
4
+ export const withResultMeta = (data, meta) => ({ [Symbol.for("net.aotter.mantle/result")]: true, data, _meta: meta });
4
5
  export function mcp(tools, options, reads = []) {
5
6
  let serve;
6
7
  const linked = {};
@@ -2,6 +2,7 @@ import { forwarded, ContractError } from "./contracts.js";
2
2
  import { checkOrigin, surfaceCaller } from "./surface-failure.js";
3
3
  import { decode, invalid } from "./surface.js";
4
4
  import { draining } from "./drain.js";
5
+ import { canonicalPath } from "./route-pattern.js";
5
6
  const isApi = (path) => path === "/api" || path.startsWith("/api/");
6
7
  const fail = message => invalid(message, "rest");
7
8
  export function router(routes, options, send) {
@@ -14,11 +15,8 @@ export function router(routes, options, send) {
14
15
  if (isForwarded)
15
16
  caller = (await import("./host-caller.js")).gatewayCaller(ctx, options.gateway, options.fields);
16
17
  const original = new URL(request.url).pathname;
17
- let path;
18
- try {
19
- path = original.split("/").map(segment => encodeURIComponent(decodeURIComponent(segment))).join("/");
20
- }
21
- catch {
18
+ const path = canonicalPath(original);
19
+ if (path === undefined) {
22
20
  if (!isApi(original))
23
21
  return undefined;
24
22
  fail("Malformed request path.");
@@ -1,6 +1,8 @@
1
1
  /** REST request handling for a generated route table: matching, body and parameter decoding, dispatch and errors. */
2
2
  import type { Caller, Target, Authorize, Gateway, RequestContext, GatewayOptions } from "./contracts.js";
3
3
  import { type Conversions } from "./surface.js";
4
+ /** The routers' path grammar, for Admin's router: its mount, route patterns and the canonical request path. */
5
+ export { ADMIN_API_BASE, canonicalPath, routeSource } from "./route-pattern.js";
4
6
  type Paging = {
5
7
  limit?: number;
6
8
  cursor?: string;
@@ -55,4 +57,3 @@ export declare function rest<Env, Database, Handlers = never>(routes: readonly R
55
57
  export declare function nativeRest<Env, Database, Handlers = never>(routes: readonly Route<Database, Handlers, Env>[], options: Options<Env, Database> & {
56
58
  handlers?: Handlers;
57
59
  }): (request: Request, env: Env, ctx?: RequestContext) => Promise<Response | undefined>;
58
- export {};
@@ -1,5 +1,6 @@
1
1
  import { ContractError } from "./contract-error.js";
2
2
  import { invalid } from "./surface.js";
3
+ export { ADMIN_API_BASE, canonicalPath, routeSource } from "./route-pattern.js";
3
4
  const headers = { "cache-control": "private, no-store" };
4
5
  const fail = message => invalid(message, "rest");
5
6
  export async function body(request, limit = 1_000_000) {
@@ -0,0 +1,26 @@
1
+ /**
2
+ * The one path grammar of the generated REST router, the Admin router and describe's `matchTarget`: a route template's
3
+ * pattern, the request path it is tested against and REST precedence. Pure; no database, application or other module.
4
+ */
5
+ /** Where Admin serves its API. Admin route templates and `admin:` describe ids are relative to it. */
6
+ export declare const ADMIN_API_BASE = "/admin/api";
7
+ /**
8
+ * An http Trigger path as a describe template: `{name}` becomes `:name`. The compiler refuses other braces and
9
+ * `:`-prefixed segments in these paths, so `routePath` inverts it exactly.
10
+ */
11
+ export declare const pathTemplate: (path: string) => string;
12
+ /** A describe template as the route path the routers compile: `:name` segments become `{name}`. */
13
+ export declare const routePath: (template: string) => string;
14
+ /**
15
+ * A template's anchored pattern source and capture keys. A `{name}` segment captures one segment; with `suffix`,
16
+ * `{name*}` captures the rest of the path. Other segments, `:name` included, match literally in encoded form. One
17
+ * trailing slash is optional.
18
+ */
19
+ export declare function routeSource(template: string, base?: string, suffix?: boolean): {
20
+ source: string;
21
+ keys: string[];
22
+ };
23
+ /** The path the routers test: each segment decoded and re-encoded, so equivalent escapes match alike. Malformed escapes are undefined. */
24
+ export declare function canonicalPath(pathname: string): string | undefined;
25
+ /** REST precedence over route paths: fewer parameters, then a literal before a parameter from the left, then the path. */
26
+ export declare function routeOrder(a: string, b: string): number;
@@ -0,0 +1,34 @@
1
+ export const ADMIN_API_BASE = "/admin/api";
2
+ export const pathTemplate = (path) => path.replace(/\{([^}]+)\}/g, ":$1");
3
+ export const routePath = (template) => template.split("/").map(segment => segment.startsWith(":") ? `{${segment.slice(1)}}` : segment).join("/");
4
+ export function routeSource(template, base = "", suffix = false) {
5
+ const keys = [];
6
+ const path = template.split("/").map(segment => {
7
+ const rest = suffix ? /^\{([a-zA-Z]\w*)\*\}$/.exec(segment) : null, one = /^\{([^}]+)\}$/.exec(segment);
8
+ if (rest || one) {
9
+ keys.push((rest ?? one)[1]);
10
+ return rest ? "(.*)" : "([^/]+)";
11
+ }
12
+ return encodeURIComponent(segment).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
13
+ }).join("/");
14
+ return { source: `^${base}${path}/?$`, keys };
15
+ }
16
+ export function canonicalPath(pathname) {
17
+ try {
18
+ return pathname.split("/").map(segment => encodeURIComponent(decodeURIComponent(segment))).join("/");
19
+ }
20
+ catch {
21
+ return undefined;
22
+ }
23
+ }
24
+ const isParameter = (segment) => /^\{[^}]+\}$/.test(segment);
25
+ export function routeOrder(a, b) {
26
+ const x = a.split("/"), y = b.split("/");
27
+ const count = (segments) => segments.filter(isParameter).length;
28
+ if (count(x) !== count(y))
29
+ return count(x) - count(y);
30
+ for (let i = 0; i < Math.min(x.length, y.length); i++)
31
+ if (isParameter(x[i]) !== isParameter(y[i]))
32
+ return isParameter(x[i]) ? 1 : -1;
33
+ return a < b ? -1 : a > b ? 1 : 0;
34
+ }
@@ -30,6 +30,13 @@ export interface Rules {
30
30
  /** Core's new entry id as the dialect prints it (an SQL expression). */
31
31
  readonly newId: string;
32
32
  }
33
+ /** A Schema lock (#1822): while `sql` (Core's `when`, printed for the dialect) holds for the existing row, a caller's write on `on` is refused. */
34
+ export interface Lock {
35
+ readonly name: string;
36
+ readonly on: readonly ("update" | "delete")[];
37
+ readonly sql: string;
38
+ readonly message: string;
39
+ }
33
40
  /** What a Schema adds to the rules. The table is the collection's name. */
34
41
  export interface Fact {
35
42
  readonly u?: readonly Extract<Term, readonly [string, 'in', ...unknown[]]>[];
@@ -39,6 +46,8 @@ export interface Fact {
39
46
  readonly ttl?: readonly [column: string, seconds: number];
40
47
  /** Opt-in publishing lifecycle. */
41
48
  readonly pub?: 1;
49
+ /** The columns `publish` requires non-null, each to its declared field: an update of a published row cannot clear one (#1822). */
50
+ readonly req?: Readonly<Record<string, string>>;
42
51
  /** Declared date-time and date columns (`created_at` and `updated_at` are always date-times). */
43
52
  readonly ts?: readonly string[];
44
53
  readonly d?: readonly string[];
@@ -52,6 +61,8 @@ export interface Fact {
52
61
  readonly same: readonly (readonly [parent: string, child: string])[];
53
62
  readonly terms: readonly Term[];
54
63
  }[];
64
+ /** Schema locks; a trusted caller is not bound by them. */
65
+ readonly lock?: readonly Lock[];
55
66
  /** Generated parent policy and inherited read-only columns; no authored schema is interpreted. */
56
67
  readonly parent?: {
57
68
  readonly on: string;
@@ -66,6 +77,8 @@ export interface Fact {
66
77
  };
67
78
  }
68
79
  export declare const SCOPE = "$scope", TTL = "$ttl";
80
+ /** The probe's answer for an update that would leave a published row unpublishable; no lock name has a dot. */
81
+ export declare const REQUIRED = "publishing.required";
69
82
  /** A Schema's rules: the shared ones with its scope and TTL put in, and a term dropped where it has none. The generator proves this equals Core's for every Schema. */
70
83
  export declare function expand(r: Rules, f: Fact): {
71
84
  caller: Term[];
@@ -148,6 +161,7 @@ export interface TransactionOptions {
148
161
  }
149
162
  export interface Store<T extends Record<string, Table>> {
150
163
  collection<K extends keyof T & string>(name: K): Collection<T[K]>;
164
+ /** Atomic unused Writes from this Store. Inside a transaction, joins it; any rejection makes the outer transaction rollback-only, even if caught. */
151
165
  batch<const W extends readonly Write<unknown>[]>(writes: W): Promise<{
152
166
  -readonly [K in keyof W]: Awaited<W[K]>;
153
167
  }>;
@@ -158,7 +172,7 @@ type BoundOperations<O> = {
158
172
  [N in keyof O[K]]: O[K][N] extends (db: any, caller: Caller, input: infer I, ...args: any[]) => infer R ? (input: I) => R : never;
159
173
  };
160
174
  };
161
- type Tx<T extends Record<string, Table>, O> = Pick<Store<T>, "collection"> & BoundOperations<O>;
175
+ type Tx<T extends Record<string, Table>, O> = Store<T> & BoundOperations<O>;
162
176
  type Interactive<T extends Record<string, Table>, O> = {
163
177
  transaction<R>(fn: (tx: Tx<T, O>) => Promise<R>, options?: TransactionOptions): Promise<R>;
164
178
  };
@@ -6,6 +6,7 @@ import { condition } from "./where.js";
6
6
  import { decodeCursor, encodeCursor } from "./paging.js";
7
7
  export { decodeCursor, encodeCursor };
8
8
  export const SCOPE = "$scope", TTL = "$ttl";
9
+ export const REQUIRED = "publishing.required";
9
10
  const SYSTEM = ["id", "version", "created_at", "updated_at", "author_id", "status", "published_at", "_rid"];
10
11
  export function expand(r, f) {
11
12
  const all = (l) => l.flatMap(([c, ...t]) => (c = c === SCOPE ? f.s : c === TTL ? f.ttl?.[0] : c === "status" && !f.pub ? "" : c) ? [[c, ...t.map(v => v.k === "cutoff" ? { ...v, seconds: f.ttl[1] } : v)]] : []);
@@ -67,8 +68,8 @@ export function createStore(d, drv, rules, facts, operations) {
67
68
  p.policy.set(trusted, preparedPolicy);
68
69
  return preparedPolicy;
69
70
  };
70
- return ((db, caller, options) => make(db, caller, false, options));
71
- function make(db, caller, inTransaction = false, options) {
71
+ return ((db, caller, options) => make(db, caller, undefined, options));
72
+ function make(db, caller, transaction, options) {
72
73
  const pending = new WeakMap();
73
74
  const write = (build, project, path) => {
74
75
  const state = { build: () => { checkFresh(caller, false, path); return build(); }, project };
@@ -94,7 +95,8 @@ export function createStore(d, drv, rules, facts, operations) {
94
95
  return fn && (cache[name] = (input) => fn(db, caller, input, options && { ...options, store }));
95
96
  } })];
96
97
  })),
97
- ...(!inTransaction && { batch: async (writes) => {
98
+ batch: async (writes) => {
99
+ try {
98
100
  const states = writes.map(w => pending.get(w));
99
101
  if (new Set(writes).size !== writes.length || states.some(s => !s || s.claimed))
100
102
  throw new Error("Batch needs unused Writes from this Store.");
@@ -102,14 +104,26 @@ export function createStore(d, drv, rules, facts, operations) {
102
104
  s.claimed = true;
103
105
  const rows = writes.length ? await drv.procedure(db, states.map(s => s.build()), "store/batch") : [];
104
106
  return states.map((s, i) => s.project(rows[i]));
105
- } }),
107
+ }
108
+ catch (error) {
109
+ if (transaction)
110
+ transaction.failure ??= { error };
111
+ throw error;
112
+ }
113
+ },
106
114
  ...(drv.transaction && { transaction: async (fn, txOptions) => {
107
- if (inTransaction)
115
+ if (transaction)
108
116
  throw new Error("Nested transactions are not supported.");
109
- return drv.transaction(db, tx => fn(make(tx, caller, true, options), tx), txOptions);
117
+ const state = {};
118
+ return drv.transaction(db, async (tx) => {
119
+ const result = await fn(make(tx, caller, state, options), tx);
120
+ if (state.failure)
121
+ throw state.failure.error;
122
+ return result;
123
+ }, txOptions);
110
124
  } }),
111
125
  };
112
- if (inTransaction)
126
+ if (transaction)
113
127
  transactionStores.add(store);
114
128
  return store;
115
129
  }
@@ -132,11 +146,18 @@ export function createStore(d, drv, rules, facts, operations) {
132
146
  return out;
133
147
  };
134
148
  const rows = (sql, values) => drv.read(db, scoped(sql, caller), values);
135
- const clean = (list) => { for (const r of list) {
136
- delete r._rid;
137
- if (f.s)
138
- delete r[f.s];
139
- } return list; };
149
+ const clean = (list) => {
150
+ for (const r of list) {
151
+ delete r._rid;
152
+ if (f.s)
153
+ delete r[f.s];
154
+ const v = Number(r.version);
155
+ if (!Number.isSafeInteger(v))
156
+ throw new ContractError("SQL_TYPE", `version ${r.version} is beyond Number.MAX_SAFE_INTEGER.`, p.name);
157
+ r.version = v;
158
+ }
159
+ return list;
160
+ };
140
161
  const select = () => pol.select ??= `SELECT r.*, ${wires(p, "r.")}${pol.projection ? ", " + pol.projection : ""}${pol.from}`;
141
162
  const where = (conds) => { const all = pol.sql ? [pol.sql, ...conds] : conds; return all.length ? ` WHERE ${all.join(" AND ")}` : ""; };
142
163
  const base = () => evaluate(d, pol.recipes, caller);
@@ -189,10 +210,22 @@ export function createStore(d, drv, rules, facts, operations) {
189
210
  return [d.ident(name), mark];
190
211
  });
191
212
  const refs = (child) => trusted ? [] : (f.ref ?? []).flatMap(r => { const v = marks.get(r.column); return v ? [`EXISTS (SELECT 1 FROM ${d.ident(r.table)} _mantle_ref WHERE ${[`_mantle_ref.${d.ident(r.field)} = ${v}`, ...r.same.map(([parent, column]) => `_mantle_ref.${d.ident(parent)} = ${child(column) ?? "NULL"}`), ...r.terms.map(t => renderTerm(d, t, "_mantle_ref.", recipe))].join(" AND ")})`] : []; });
192
- const guard = (id, also = []) => ` WHERE ${[`${ID} = ${push(id)}`, ...[...pol.terms, x.lock].map(t => renderTerm(d, t, t[1] === "parent" ? d.ident(p.name) + "." : "", recipe)), ...also].join(" AND ")}`;
193
- const step = (sql) => ({ sql: scoped(sql, caller), binds: values, row: true });
213
+ const guard = (id, also = [], versioned = true) => ` WHERE ${[`${ID} = ${push(id)}`, ...[...pol.terms, ...versioned ? [x.lock] : []].map(t => renderTerm(d, t, t[1] === "parent" ? d.ident(p.name) + "." : "", recipe)), ...also].join(" AND ")}`;
214
+ const step = (sql, lock) => ({ sql: scoped(sql, caller), binds: values, row: true, ...(lock ? { lock } : {}) });
194
215
  return { given, marks, recipe, refs, guard, step, into, returning: () => ` RETURNING *, ${p.returning ??= wires(p, "")}` };
195
216
  };
217
+ const cleared = (set) => {
218
+ const req = f.req ?? {};
219
+ return [...new Set(Object.entries(set).flatMap(([name, v]) => v === null && Object.hasOwn(req, name.toLowerCase()) ? [req[name.toLowerCase()]] : []))];
220
+ };
221
+ const locked = (id, verb, fields = []) => {
222
+ const locks = trusted ? [] : (f.lock ?? []).filter(l => l.on.includes(verb)), required = fields.length ? `${d.ident("status")} <> 'published'` : undefined;
223
+ if (!locks.length && !required)
224
+ return { terms: [] };
225
+ const w = writer(), where = w.guard(id, [], false), cases = [...locks.map(l => `WHEN (${l.sql}) IS NOT FALSE THEN '${l.name}'`), ...required ? [`WHEN NOT (${required}) THEN '${REQUIRED}'`] : []];
226
+ const { sql, binds } = w.step(`SELECT CASE ${cases.join(" ")} END AS ${d.ident("lock")} FROM ${w.into}${where}`);
227
+ return { terms: [...locks.map(l => `NOT (${l.sql})`), ...required ? [required] : []], lock: { sql, binds, schema: p.name, verb, locks: Object.fromEntries(locks.map(l => [l.name, l.message])), ...required ? { required: `This entry is published: publish requires ${fields.join(", ")}, so an update cannot clear ${fields.length > 1 ? "them" : "it"}.` } : {} } };
228
+ };
196
229
  return {
197
230
  async findById(id) {
198
231
  const values = base();
@@ -229,14 +262,14 @@ export function createStore(d, drv, rules, facts, operations) {
229
262
  const w = writer(lock), assigns = w.given(set).map(([name, mark]) => `${name} = ${mark}`);
230
263
  for (const [column, v] of x.sets)
231
264
  assigns.push(`${d.ident(column)} = ${v === "bump" ? `${d.ident(column)} + 1` : w.recipe(v)}`);
232
- const refs = w.refs(column => `${w.into}.${d.ident(column)}`);
233
- return w.step(`UPDATE ${w.into} SET ${assigns.join(", ")}${w.guard(id, refs)}${w.returning()}`);
265
+ const refs = w.refs(column => `${w.into}.${d.ident(column)}`), held = locked(id, "update", cleared(set));
266
+ return w.step(`UPDATE ${w.into} SET ${assigns.join(", ")}${w.guard(id, [...refs, ...held.terms])}${w.returning()}`, held.lock);
234
267
  }, rows => clean(rows)[0], `${p.name}/update`);
235
268
  },
236
269
  delete(id, { lock }) {
237
270
  return write(() => {
238
- const w = writer(lock);
239
- return w.step(`DELETE FROM ${w.into}${w.guard(id)}`);
271
+ const w = writer(lock), held = locked(id, "delete");
272
+ return w.step(`DELETE FROM ${w.into}${w.guard(id, held.terms)}`, held.lock);
240
273
  }, () => undefined, `${p.name}/delete`);
241
274
  },
242
275
  };
@@ -93,9 +93,9 @@ export function emitAdminBindings(input, refuse) {
93
93
  const defaults = Object.fromEntries(Object.entries(checks(s, true).properties ?? {}).flatMap(([key, p]) => p.default !== undefined ? [[key, p.default]] : []));
94
94
  const primary = isPrimaryCollection(s, Object.values(plan.schemas));
95
95
  const schemaCode = `{
96
- name:${q(s.name)},primary:${primary},descriptor:allCollections[${q(s.name)}],publishing:${!!s.publishing},readOnly:${s.schema.readOnly === true || declared.some(f => s.fields[f] === "geo")},fields:names${n},defaults:${q(defaults)},
96
+ name:${q(s.name)},primary:${primary},descriptor:allCollections[${q(s.name)}],publishing:${!!s.publishing},${s.liveEdits ? "liveEdits:true," : ""}readOnly:${s.schema.readOnly === true || declared.some(f => s.fields[f] === "geo")},fields:names${n},defaults:${q(defaults)},
97
97
  binding:${entrySpec},
98
- query:query${n},validate:(data,full)=>{const checked=Object.fromEntries(Object.entries(data).filter(([key,v])=>v!==null||${q(s.schema.required ?? [])}.includes(key)));(full?full${n}:partial${n})(checked);${sqlite ? "" : "nativeText(data);"}${yearZero}return data;},
98
+ query:query${n},validate:(data,full)=>{const checked=Object.fromEntries(Object.entries(data).filter(([key,v])=>v!==null||${s.publishing ? "full&&" : ""}${q(s.schema.required ?? [])}.includes(key)));(full?full${n}:partial${n})(checked);${sqlite ? "" : "nativeText(data);"}${yearZero}return data;},
99
99
  values:values${n},row:${named},
100
100
  ${sqlite ? "" : "native:nativeText,"}
101
101
  ${s.publishing ? `transition:async(db,caller,verb,id,expectedVersion)=>{const run=await ({publish:()=>import("./procedures/${encodeURIComponent(s.name + ".publish")}.js"),unpublish:()=>import("./procedures/${encodeURIComponent(s.name + ".unpublish")}.js"),archive:()=>import("./procedures/${encodeURIComponent(s.name + ".archive")}.js")})[verb]();const version=wireRows([{version:expectedVersion}],[["version","int"]])[0]!.version;return run.default(db,caller,{id,expectedVersion:version} as Parameters<typeof run.default>[2]);},` : ""}