@aotter/mantle 0.2.0-alpha.10 → 0.2.0-alpha.11

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 (121) hide show
  1. package/dist/auth/adapter.js +1 -28
  2. package/dist/auth/admin.js +2 -5
  3. package/dist/auth/caller.js +0 -27
  4. package/dist/auth/consent.js +100 -0
  5. package/dist/auth/hardening.js +0 -7
  6. package/dist/auth/identity.js +0 -11
  7. package/dist/auth/oauth.js +0 -32
  8. package/dist/auth/resource-seeds.js +0 -2
  9. package/dist/auth/route-http.js +33 -0
  10. package/dist/auth/routes.js +5 -151
  11. package/dist/build/compiler.d.ts +12 -611
  12. package/dist/build/compiler.js +3 -7029
  13. package/dist/bun/assets.js +0 -2
  14. package/dist/domain/adminOrder.js +0 -2
  15. package/dist/domain/columns.js +0 -15
  16. package/dist/domain/describe/chunk-SGMRVDGE.js +1 -0
  17. package/dist/domain/describe/index.js +1 -115
  18. package/dist/domain/describe/target-hash.js +1 -12
  19. package/dist/domain/entry.js +0 -14
  20. package/dist/domain/generated-tools.js +0 -1
  21. package/dist/domain/input.js +0 -2
  22. package/dist/domain/paging.js +0 -17
  23. package/dist/domain/recipes.js +0 -2
  24. package/dist/domain/userReferences.js +0 -1
  25. package/dist/domain/wireScalars.js +0 -6
  26. package/dist/infrastructure/adminEntries.js +9 -6
  27. package/dist/infrastructure/adminPaging.js +0 -2
  28. package/dist/infrastructure/artifacts.js +0 -4
  29. package/dist/infrastructure/authSchema.js +0 -9
  30. package/dist/infrastructure/bundle.js +0 -29
  31. package/dist/infrastructure/cli.js +0 -7
  32. package/dist/infrastructure/generate.js +0 -14
  33. package/dist/infrastructure/generateAuth.js +1 -9
  34. package/dist/infrastructure/help.js +0 -5
  35. package/dist/infrastructure/init.js +0 -4
  36. package/dist/infrastructure/inputs.js +0 -9
  37. package/dist/infrastructure/mounts.js +0 -3
  38. package/dist/infrastructure/wrangler.js +0 -6
  39. package/dist/kernel/authorization.js +0 -1
  40. package/dist/kernel/binds.js +1 -11
  41. package/dist/kernel/caller-identity.d.ts +0 -3
  42. package/dist/kernel/caller-identity.js +0 -15
  43. package/dist/kernel/caller-mode.d.ts +4 -0
  44. package/dist/kernel/caller-mode.js +14 -0
  45. package/dist/kernel/contracts.d.ts +1 -9
  46. package/dist/kernel/contracts.js +1 -42
  47. package/dist/kernel/cron.js +0 -4
  48. package/dist/kernel/dialect/common.js +0 -3
  49. package/dist/kernel/dialect/postgres.js +0 -2
  50. package/dist/kernel/dialect/sqlite.js +1 -6
  51. package/dist/kernel/drain.js +2 -8
  52. package/dist/kernel/driver/bun-sqlite.js +0 -4
  53. package/dist/kernel/driver/d1.js +0 -1
  54. package/dist/kernel/driver/postgres.js +0 -15
  55. package/dist/kernel/driver/transaction-context.js +0 -1
  56. package/dist/kernel/email.js +0 -4
  57. package/dist/kernel/host-access.js +2 -3
  58. package/dist/kernel/host-caller.d.ts +1 -1
  59. package/dist/kernel/host-caller.js +3 -6
  60. package/dist/kernel/json-body.js +0 -2
  61. package/dist/kernel/mcp-apps.js +0 -5
  62. package/dist/kernel/mcp-call.js +0 -1
  63. package/dist/kernel/mcp-handler.js +2 -26
  64. package/dist/kernel/mcp.js +0 -5
  65. package/dist/kernel/media.js +0 -18
  66. package/dist/kernel/paging.js +1 -9
  67. package/dist/kernel/publishing.js +1 -2
  68. package/dist/kernel/r2-media.js +0 -7
  69. package/dist/kernel/rest-dispatch.js +0 -1
  70. package/dist/kernel/rest-router.js +2 -3
  71. package/dist/kernel/rest.js +0 -3
  72. package/dist/kernel/scalars.js +1 -9
  73. package/dist/kernel/search.js +0 -1
  74. package/dist/kernel/site.js +0 -2
  75. package/dist/kernel/store.js +1 -26
  76. package/dist/kernel/surface-failure.d.ts +10 -0
  77. package/dist/kernel/surface-failure.js +29 -0
  78. package/dist/kernel/surface.js +0 -3
  79. package/dist/kernel/sweep.js +0 -1
  80. package/dist/kernel/translations.js +0 -1
  81. package/dist/kernel/user-caller.js +0 -6
  82. package/dist/kernel/user-fields.d.ts +1 -1
  83. package/dist/kernel/user-fields.js +1 -11
  84. package/dist/kernel/validation.js +0 -8
  85. package/dist/kernel/web-gateway.js +0 -2
  86. package/dist/kernel/web-head.js +0 -1
  87. package/dist/kernel/web-index.js +0 -1
  88. package/dist/kernel/web-response.js +0 -1
  89. package/dist/kernel/web-serve.js +97 -0
  90. package/dist/kernel/web.d.ts +1 -1
  91. package/dist/kernel/web.js +10 -91
  92. package/dist/kernel/where.js +0 -6
  93. package/dist/usecase/adminBindings.js +28 -21
  94. package/dist/usecase/adminClient.js +0 -21
  95. package/dist/usecase/adminDescriptors.js +2 -9
  96. package/dist/usecase/generate.js +0 -6
  97. package/dist/usecase/handlers.js +0 -3
  98. package/dist/usecase/mcp-media.js +0 -1
  99. package/dist/usecase/mcp.js +0 -4
  100. package/dist/usecase/paging.js +0 -2
  101. package/dist/usecase/publishing.js +0 -4
  102. package/dist/usecase/rest.js +0 -1
  103. package/dist/usecase/site.js +0 -1
  104. package/dist/usecase/stores.js +1 -13
  105. package/dist/usecase/tool-version.js +0 -3
  106. package/dist/usecase/translations.js +0 -2
  107. package/dist/usecase/validation.js +0 -20
  108. package/docs/examples/README.md +1 -1
  109. package/docs/examples/app-shell/package.json +3 -3
  110. package/docs/examples/app-shell/src/routes.ts +3 -1
  111. package/docs/examples/app-shell/src/service.ts +2 -1
  112. package/docs/handbook/cloudflare/service-entry.md +3 -0
  113. package/docs/handbook/examples/hub.md +1 -1
  114. package/docs/handbook/guides/agent-setup.md +4 -4
  115. package/docs/handbook/guides/public-pages.md +6 -0
  116. package/docs/handbook/reference/admin-descriptors.md +4 -1
  117. package/docs/handbook/reference/schema.md +1 -1
  118. package/docs/handbook/releases/index.md +3 -3
  119. package/package.json +2 -2
  120. package/skills/README.md +1 -1
  121. package/dist/domain/describe/chunk-ZVXBGP7Y.js +0 -424
@@ -1,4 +1,3 @@
1
- /** Selected web authorization loads on a request, outside the ordinary web startup graph. */
2
1
  import { web } from "./web.js";
3
2
  import { gatewayCaller, hostFailure } from "./host-caller.js";
4
3
  import { accessRequest } from "./host-access.js";
@@ -34,7 +33,6 @@ export function gatewayWeb(options, table) {
34
33
  throw error;
35
34
  }
36
35
  } }]));
37
- // The ordinary renderer validates query/path fields; wrapped pages decide before any SQL.
38
36
  const result = await site[method](request, env, options.gateway ? undefined : ctx);
39
37
  if (failure)
40
38
  throw failure;
@@ -1,4 +1,3 @@
1
- /** HTML head formatting loads only when the application asks for head tags. */
2
1
  const escape = (v) => String(v).replace(/[&<>"']/g, c => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c]));
3
2
  export function headTags(canonical, row, markdown, locales) {
4
3
  let tags = '<link rel="canonical" href="' + escape(canonical) + '">';
@@ -1,4 +1,3 @@
1
- /** /llms.txt and /sitemap.xml bodies; loaded only when an index is requested, so page Workers do not pay for it at startup. */
2
1
  const escape = (v) => String(v).replace(/[&<>"']/g, c => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c]));
3
2
  const label = (v) => String(v).replace(/[\r\n]/g, " ").replace(/[\\[\]]/g, "\\$&");
4
3
  const size = (s) => new TextEncoder().encode(s).length;
@@ -1,4 +1,3 @@
1
- /** Response hashing is needed only when a Web route actually renders. */
2
1
  export async function webResponse(request, body, type, cacheControl) {
3
2
  const hash = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(body));
4
3
  const etag = '"' + Array.from(new Uint8Array(hash), b => b.toString(16).padStart(2, '0')).join('') + '"';
@@ -0,0 +1,97 @@
1
+ import { forwarded, wire, ContractError } from "./contracts.js";
2
+ import { decode } from "./surface.js";
3
+ const NO_STORE = { "cache-control": "no-store" };
4
+ const notFound = () => new Response(null, { status: 404, headers: NO_STORE });
5
+ export function serve(options, views, pages, query) {
6
+ const origin = new URL(options.origin);
7
+ const context = async (request, env, ctx) => ({ db: options.db, caller: forwarded(ctx, options.gateway)
8
+ ? (await import("./host-caller.js")).gatewayCaller(ctx, options.gateway, options.fields)
9
+ : typeof options.caller === "function" ? await options.caller(request, env, { readOnly: ["GET", "HEAD"].includes(request.method) }) : options.caller ?? { kind: "anonymous" } });
10
+ const urlFor = (path, row, view) => origin.origin + path.replace(/\{([^{}]+)\}/g, (_, key) => {
11
+ let value = row[key];
12
+ if (views.get(view).booleans?.includes(key) && (value === 0 || value === 1))
13
+ value = Boolean(value);
14
+ if (value === null || value === undefined)
15
+ throw new TypeError("List View must return every page parameter: " + key);
16
+ const segment = String(value);
17
+ if (segment === "." || segment === "..")
18
+ return "";
19
+ return encodeURIComponent(segment).replace(/[()]/g, c => "%" + c.charCodeAt(0).toString(16).toUpperCase());
20
+ });
21
+ const validUrl = (path, row, view) => { if ([...path.matchAll(/\{([^{}]+)\}/g)].some(([, key]) => row[key] === "." || row[key] === ".."))
22
+ return; return urlFor(path, row, view); };
23
+ const match = (path) => {
24
+ for (const entry of pages) {
25
+ const hit = entry.pattern.exec(path);
26
+ if (!hit)
27
+ continue;
28
+ return { ...entry, params: new URLSearchParams(decode(entry.keys, hit, "Malformed URL parameter.", "web/path")) };
29
+ }
30
+ };
31
+ const read = async (entry, ctx) => (await query(entry.page.view, ctx.db, ctx.caller, entry.params)).rows[0];
32
+ const response = async (request, body, type, caller) => {
33
+ const { webResponse } = await import('./web-response.js');
34
+ return webResponse(request, body, type, caller.kind === "anonymous" ? (options.cacheControl ?? "public, max-age=60") : "private, no-store");
35
+ };
36
+ return {
37
+ async fetch(request, env, execution) {
38
+ let caller;
39
+ try {
40
+ if (request.method !== "GET" && request.method !== "HEAD")
41
+ return;
42
+ const path = new URL(request.url).pathname, index = path === "/llms.txt" || path === "/sitemap.xml";
43
+ const entry = !index && path.endsWith(".md") ? match(path.slice(0, -3)) : undefined, markdown = entry?.page.markdown;
44
+ if (!index && !markdown)
45
+ return;
46
+ const ctx = await context(request, env, execution);
47
+ caller = ctx.caller;
48
+ if (index) {
49
+ const sitemap = path === "/sitemap.xml";
50
+ const body = await (await import("./web-index.js")).webIndex(sitemap, options, pages, (list, cursor) => query(list, ctx.db, ctx.caller, new URLSearchParams(), cursor), validUrl);
51
+ return response(request, body, sitemap ? "application/xml" : "text/plain", ctx.caller);
52
+ }
53
+ const exact = match(path);
54
+ if (exact)
55
+ try {
56
+ if (await read(exact, ctx))
57
+ return;
58
+ }
59
+ catch (error) {
60
+ if (wire(error).status !== 400)
61
+ throw error;
62
+ }
63
+ const row = await read(entry, ctx);
64
+ if (!row)
65
+ return notFound();
66
+ const body = row[markdown];
67
+ if (body == null)
68
+ return notFound();
69
+ if (typeof body !== "string")
70
+ throw new TypeError("Markdown field must return text.");
71
+ return response(request, body, "text/markdown", ctx.caller);
72
+ }
73
+ catch (error) {
74
+ return (await import("./host-caller.js")).hostFailure(error, caller, "web");
75
+ }
76
+ },
77
+ async head(request, env, execution) {
78
+ try {
79
+ const path = new URL(request.url).pathname, entry = match(path);
80
+ if (!entry)
81
+ return "";
82
+ const ctx = await context(request, env, execution), row = await read(entry, ctx);
83
+ if (!row)
84
+ return "";
85
+ const canonical = validUrl(entry.path, row, entry.page.view);
86
+ if (!canonical)
87
+ return "";
88
+ return (await import("./web-head.js")).headTags(canonical, row, !!entry.page.markdown, options.locales);
89
+ }
90
+ catch (error) {
91
+ if (error instanceof ContractError && error.code === "HOST_CONTRACT")
92
+ throw error;
93
+ return "";
94
+ }
95
+ },
96
+ };
97
+ }
@@ -1,5 +1,5 @@
1
1
  /** Web output for a generated public View table: /llms.txt, /sitemap.xml, Markdown pages and head tags. SQL owns visibility. */
2
- import { type Caller, type Gateway, type RequestContext } from "./contracts.js";
2
+ import type { Caller, Gateway, RequestContext } from "./contracts.js";
3
3
  import type { UserFields } from "./user-fields.js";
4
4
  import { type Conversions } from "./surface.js";
5
5
  /** An app's public Views: row type and input keys per View, and the Views a list may name (input that accepts no fields). */
@@ -1,8 +1,4 @@
1
- /** Web output for a generated public View table: /llms.txt, /sitemap.xml, Markdown pages and head tags. SQL owns visibility. */
2
- import { forwarded, wire, ContractError } from "./contracts.js";
3
- import { convert, decode } from "./surface.js";
4
- const NO_STORE = { "cache-control": "no-store" };
5
- const notFound = () => new Response(null, { status: 404, headers: NO_STORE });
1
+ import { convert } from "./surface.js";
6
2
  export function web(options, table) {
7
3
  const views = new Map(table);
8
4
  const query = (name, db, caller, params, cursor) => {
@@ -23,35 +19,6 @@ export function web(options, table) {
23
19
  const pattern = path.split(/(\{[^{}]+\})/).map(part => part.startsWith("{") ? (keys.push(part.slice(1, -1)), "([^/]+)") : part.replace(/[.*+?^$()|[\]\\]/g, "\\$&")).join("");
24
20
  return { path, page, keys, pattern: new RegExp("^" + pattern + "$") };
25
21
  }).sort((a, b) => a.keys.length - b.keys.length);
26
- const context = async (request, env, ctx) => ({ db: options.db, caller: forwarded(ctx, options.gateway)
27
- ? (await import("./host-caller.js")).gatewayCaller(ctx, options.gateway, options.fields)
28
- : typeof options.caller === "function" ? await options.caller(request, env, { readOnly: ["GET", "HEAD"].includes(request.method) }) : options.caller ?? { kind: "anonymous" } });
29
- const urlFor = (path, row, view) => origin.origin + path.replace(/\{([^{}]+)\}/g, (_, key) => {
30
- let value = row[key];
31
- if (views.get(view).booleans?.includes(key) && (value === 0 || value === 1))
32
- value = Boolean(value);
33
- if (value === null || value === undefined)
34
- throw new TypeError("List View must return every page parameter: " + key);
35
- const segment = String(value);
36
- if (segment === "." || segment === "..")
37
- return "";
38
- return encodeURIComponent(segment).replace(/[()]/g, c => "%" + c.charCodeAt(0).toString(16).toUpperCase());
39
- });
40
- const validUrl = (path, row, view) => { if ([...path.matchAll(/\{([^{}]+)\}/g)].some(([, key]) => row[key] === "." || row[key] === ".."))
41
- return; return urlFor(path, row, view); };
42
- const match = (path) => {
43
- for (const entry of pages) {
44
- const hit = entry.pattern.exec(path);
45
- if (!hit)
46
- continue;
47
- return { ...entry, params: new URLSearchParams(decode(entry.keys, hit, "Malformed URL parameter.", "web/path")) };
48
- }
49
- };
50
- const read = async (entry, ctx) => (await query(entry.page.view, ctx.db, ctx.caller, entry.params)).rows[0];
51
- const response = async (request, body, type, caller) => {
52
- const { webResponse } = await import('./web-response.js');
53
- return webResponse(request, body, type, caller.kind === "anonymous" ? (options.cacheControl ?? "public, max-age=60") : "private, no-store");
54
- };
55
22
  if (options.gateway || options.authorize) {
56
23
  let handle;
57
24
  const load = async () => handle ??= (await import("./web-gateway.js")).gatewayWeb(options, table);
@@ -60,65 +27,17 @@ export function web(options, table) {
60
27
  head: async (request, env, ctx) => (await load()).head(request, env, ctx),
61
28
  };
62
29
  }
30
+ let served;
31
+ const serve = async () => served ??= (await import("./web-serve.js")).serve(options, views, pages, query);
63
32
  return {
64
33
  async fetch(request, env, execution) {
65
- let caller;
66
- try {
67
- if (request.method !== "GET" && request.method !== "HEAD")
68
- return;
69
- const path = new URL(request.url).pathname, index = path === "/llms.txt" || path === "/sitemap.xml";
70
- const entry = !index && path.endsWith(".md") ? match(path.slice(0, -3)) : undefined, markdown = entry?.page.markdown;
71
- if (!index && !markdown)
72
- return;
73
- const ctx = await context(request, env, execution);
74
- caller = ctx.caller;
75
- if (index) {
76
- const sitemap = path === "/sitemap.xml";
77
- const body = await (await import("./web-index.js")).webIndex(sitemap, options, pages, (list, cursor) => query(list, ctx.db, ctx.caller, new URLSearchParams(), cursor), validUrl);
78
- return response(request, body, sitemap ? "application/xml" : "text/plain", ctx.caller);
79
- }
80
- const exact = match(path);
81
- if (exact)
82
- try {
83
- if (await read(exact, ctx))
84
- return;
85
- }
86
- catch (error) {
87
- if (wire(error).status !== 400)
88
- throw error;
89
- }
90
- const row = await read(entry, ctx);
91
- if (!row)
92
- return notFound();
93
- const body = row[markdown];
94
- if (body == null)
95
- return notFound();
96
- if (typeof body !== "string")
97
- throw new TypeError("Markdown field must return text.");
98
- return response(request, body, "text/markdown", ctx.caller);
99
- }
100
- catch (error) {
101
- return (await import("./host-caller.js")).hostFailure(error, caller, "web");
102
- }
103
- },
104
- async head(request, env, execution) {
105
- try {
106
- const path = new URL(request.url).pathname, entry = match(path);
107
- if (!entry)
108
- return "";
109
- const ctx = await context(request, env, execution), row = await read(entry, ctx);
110
- if (!row)
111
- return "";
112
- const canonical = validUrl(entry.path, row, entry.page.view);
113
- if (!canonical)
114
- return "";
115
- return (await import("./web-head.js")).headTags(canonical, row, !!entry.page.markdown, options.locales);
116
- }
117
- catch (error) {
118
- if (error instanceof ContractError && error.code === "HOST_CONTRACT")
119
- throw error;
120
- return "";
121
- }
34
+ if (request.method !== "GET" && request.method !== "HEAD")
35
+ return;
36
+ const path = new URL(request.url).pathname;
37
+ if (path !== "/llms.txt" && path !== "/sitemap.xml" && !path.endsWith(".md"))
38
+ return;
39
+ return (served ?? await serve()).fetch(request, env, execution);
122
40
  },
41
+ head: async (request, env, execution) => (served ?? await serve()).head(request, env, execution),
123
42
  };
124
43
  }
@@ -1,10 +1,4 @@
1
1
  const COMPARE = { gt: ">", gte: ">=", lt: "<", lte: "<=" };
2
- /**
3
- * `column` is already quoted. `bind` registers one parameter and returns its placeholder. `insensitive` folds both sides of
4
- * eq/ne/in/notIn with LOWER (text only) and picks the dialect's case-insensitive LIKE. A null eq/ne is IS [NOT] NULL.
5
- * `like` takes a finished pattern (`escapeLike` the literal parts).
6
- * @internal
7
- */
8
2
  export function condition(d, column, operator, value, bind, insensitive = false) {
9
3
  const lhs = insensitive ? `LOWER(${column})` : column;
10
4
  const rhs = (v) => insensitive ? `LOWER(${bind(v)})` : bind(v);
@@ -1,5 +1,4 @@
1
1
  import { userReferences } from "../domain/userReferences.js";
2
- /** Selected application bindings: compiled checks/native SQL; only the app depends on Admin adapters. */
3
2
  import { isPrimaryCollection } from "../build/compiler.js";
4
3
  import { enumOptions } from "../build/compiler.js";
5
4
  import { databaseImport, isSqlite, literal as q } from "../domain/entry.js";
@@ -14,7 +13,6 @@ const wire = (fields, sqlite) => {
14
13
  const geo = fields.filter(([, type]) => type === "geo").map(([field]) => `row[${q(field)}]=row[${q(field + "_lat")}] == null || row[${q(field + "_lng")}] == null ? null : {lat:row[${q(field + "_lat")}],lng:row[${q(field + "_lng")}]};delete row[${q(field + "_lat")}];delete row[${q(field + "_lng")}];`);
15
14
  return `(raw:Row):Row=>{const row=${columns.length ? `wireRows([raw],${q(columns)})[0]!` : geo.length ? "{...raw}" : "raw"};${geo.join("")}return row;}`;
16
15
  };
17
- /** Core's titled string choices are enum checks; constrained or general oneOf remains refused. */
18
16
  function adminProperty(property) {
19
17
  const choices = enumOptions(property);
20
18
  if (!property.oneOf || property.enum || !choices || !property.oneOf.every(branch => Object.entries(branch).every(([key, value]) => key === "const" || key === "title" || key === "type" && value === "string")) || new Set(choices.map(c => c.value)).size !== choices.length)
@@ -30,19 +28,31 @@ const allowed = (requires, path) => {
30
28
  const lines = authLines(requires, path);
31
29
  return lines.length ? `(caller:Caller)=>{try{${lines.join("")}return true;}catch{return false;}}` : "(_caller:Caller)=>true";
32
30
  };
33
- /** Share repeated compiler SQL constants; generated modules evaluate no decoder or kernel at import. */
34
- function sqlConstants(value) {
35
- const counts = new Map();
36
- const visit = (v) => { if (typeof v === "string" && v.length > 80)
37
- counts.set(v, (counts.get(v) ?? 0) + 1);
38
- else if (Array.isArray(v))
39
- v.forEach(visit);
40
- else if (v && typeof v === "object")
41
- Object.values(v).forEach(visit); };
42
- visit(value);
43
- const pool = new Map([...counts].filter(([, count]) => count > 1).map(([text], n) => [text, `sql${n}`]));
44
- const literal = (v) => typeof v === "string" ? pool.get(v) ?? q(v) : Array.isArray(v) ? "[" + v.map(literal).join(",") + "]" : v && typeof v === "object" ? "{" + Object.entries(v).map(([name, child]) => `${name === "__proto__" ? `[${q(name)}]` : q(name)}:${literal(child)}`).join(",") + "}" : q(v);
45
- return { code: literal(value), constants: [...pool].map(([text, name]) => `const ${name}=${q(text)};`) };
31
+ function sqlConstants(values) {
32
+ const counts = new Map(), seen = new Map(), pool = new Map(), constants = [];
33
+ let named = 0;
34
+ const key = (v) => typeof v === "string" ? v.length > 80 ? q(v) : undefined : v && typeof v === "object" ? (text => text.length > 80 ? text : undefined)(q(v)) : undefined;
35
+ const children = (v) => v && typeof v === "object" ? Object.values(v) : [];
36
+ const tally = (v, by = 1) => { const k = key(v); if (k) {
37
+ counts.set(k, (counts.get(k) ?? 0) + by);
38
+ seen.set(k, v);
39
+ } children(v).forEach(child => tally(child, by)); };
40
+ values.forEach(value => children(value).forEach(child => tally(child)));
41
+ for (const [k, v] of [...seen].sort(([a], [b]) => b.length - a.length)) {
42
+ const copies = counts.get(k);
43
+ if (copies > 1) {
44
+ pool.set(k, "");
45
+ children(v).forEach(child => tally(child, 1 - copies));
46
+ }
47
+ }
48
+ const print = (v) => Array.isArray(v) ? "[" + v.map(reference).join(",") + "]" : v && typeof v === "object" ? "{" + Object.entries(v).map(([name, child]) => `${name === "__proto__" ? `[${q(name)}]` : q(name)}:${reference(child)}`).join(",") + "}" : q(v);
49
+ const reference = (v) => { const k = key(v); if (!k || pool.get(k) === undefined)
50
+ return print(v); if (pool.get(k) === "") {
51
+ const name = `sql${named++}`;
52
+ pool.set(k, name);
53
+ constants.push(`const ${name}=${print(v)}${typeof v === "string" ? "" : " as const"};`);
54
+ } return pool.get(k); };
55
+ return { code: values.map(print), constants };
46
56
  }
47
57
  export function emitAdminBindings(input, refuse) {
48
58
  if (!input.admin)
@@ -54,9 +64,7 @@ export function emitAdminBindings(input, refuse) {
54
64
  const schemas = [], views = [], adminViews = [], operations = [], pageSpecs = [];
55
65
  for (const [n, s] of Object.values(plan.schemas).entries()) {
56
66
  const schemaPath = `schemas/${s.name}/admin`, declared = Object.keys(s.fields).filter(f => f !== s.scope), writable = declared.filter(f => scalar(s.fields[f]) || s.fields[f] === "json");
57
- // readOnly properties are server-maintained: values() pins them to their default, and they have no Admin UPDATE.
58
67
  const server = Object.fromEntries(writable.filter(f => serverOwned(s.schema, f)).map(f => [f, Object.entries(s.schema.properties ?? {}).find(([k]) => k.toLowerCase() === f)?.[1]?.default ?? null]));
59
- // Experimental geo collections remain readable; edits require a declared Procedure.
60
68
  const validators = emitValidation({ views: {}, procedures: { full: { input: checks(s, true) }, partial: { input: checks(s, false) } } }, (path, message) => refuse(schemaPath + "/" + path, message));
61
69
  for (const mode of ["full", "partial"]) {
62
70
  const name = `validation/admin/${encodeURIComponent(s.name)}-${mode}.ts`;
@@ -103,7 +111,6 @@ export function emitAdminBindings(input, refuse) {
103
111
  const rename = Object.fromEntries(Object.entries(v.columns ?? {}).map(([key, col]) => [key, key === col.field ? plan.schemas[col.schema].names[col.field] ?? key : key]));
104
112
  imports.push(`const viewNames${n}=${q(rename)} as const;`);
105
113
  const pageData = input.adminPages?.[name];
106
- // A supplied page map omits only Views its producer already refused (at `views/<name>` or `views/<name>/admin`).
107
114
  if (!pageData) {
108
115
  if (!input.adminPages)
109
116
  refuse(`views/${name}/admin`, "Admin list was not generated: no compiler-authorized matching pages were supplied for this View.");
@@ -123,7 +130,7 @@ export function emitAdminBindings(input, refuse) {
123
130
  const filterReads = Object.entries(filters).map(([field, s]) => { const type = [s.type].flat().find(t => t && t !== "null"); return `if(params.has(${q("filter." + field)})&&params.get(${q("filter." + field)})!=="")filters[${q(field)}]=${type === "boolean" ? `params.get(${q("filter." + field)})==="true"?true:params.get(${q("filter." + field)})==="false"?false:invalid("Expected true or false.",${q("views/" + name)})` : ["number", "integer"].includes(type ?? "") ? `Number(params.get(${q("filter." + field)}))` : `params.get(${q("filter." + field)})!`};`; });
124
131
  const cursorKinds = (input.paging?.[name]?.types ?? []).map(type => type === "bool" ? "bool" : type === "integer" || type === "int8" ? "int" : type === "int4" ? "int4" : type.startsWith("numeric(") ? "numeric" : type === "real" || type === "float8" ? "float" : type === "date" ? "date" : type === "timestamptz" ? "timestamp" : "text");
125
132
  const modes = Object.fromEntries(Object.entries({ caller: pageData.caller, trusted: pageData.trusted }).map(([mode, p]) => [mode, { ...p, binds: recipesOf(p, Object.keys(v.input?.properties ?? {}), message => refuse(`views/${name}/admin`, message)) }]));
126
- pageSpecs.push(`const viewPage${n}=${q({ path: "views/" + name, cap: v.stmts[0]?.SelectStmt.limitCount?.A_Const?.ival?.ival ?? 500, cursorKinds, ...modes })} as const;`);
133
+ pageSpecs.push({ path: "views/" + name, cap: v.stmts[0]?.SelectStmt.limitCount?.A_Const?.ival?.ival ?? 500, cursorKinds, ...modes });
127
134
  imports.push(`const allowsView${n}=${allowed(v.requires, `views/${name}`)};`);
128
135
  if (v.surface === "admin")
129
136
  adminViews.push(`view${n}`);
@@ -134,8 +141,8 @@ export function emitAdminBindings(input, refuse) {
134
141
  imports.push(`import {validate as operation${n}} from "./validation/procedures/${encoded}.js";`);
135
142
  operations.push(`{${input.description.targets.find(t => t._meta.id === `procedure:${name}`)?._meta.readOnly ? "readOnly:true," : ""}${user.used || p.trustedOnly ? `check:(caller)=>{${authLines(undefined, `procedures/${name}`, lowered.procedures[name]?.caller?.flatMap(s => s.binds), sqlite, p.trustedOnly).join("")}},` : ""}name:${q(name)},allows:${allowed(p.requires, `procedures/${name}`)},validate:operation${n},${converted ? `convert:${converted},` : ""}run:${ref ? `async(db,caller,input)=>(await import("./generated.js")).store(db,caller,{handlers:options.handlers}).procedures[${q(name)}](input)` : `async(db,caller,input)=>(await import("./procedures/${encoded}.js")).default(db,caller,input)`}}`);
136
143
  }
137
- const entryData = sqlConstants(input.adminEntries);
138
- imports.push(...views.map((view, n) => `const view${n}=${view} satisfies View<Database>;`), ...pageSpecs, ...entryData.constants, `const entrySpecs=${entryData.code} as const;`);
144
+ const { code: [entrySpecs, ...pages], constants } = sqlConstants([input.adminEntries, ...pageSpecs]);
145
+ imports.push(...views.map((view, n) => `const view${n}=${view} satisfies View<Database>;`), ...constants, ...pages.map((page, n) => `const viewPage${n}=${page} as const;`), `const entrySpecs=${entrySpecs} as const;`);
139
146
  imports.push(`export const statisticsView=(db:Database,caller:Caller,collection:keyof typeof entrySpecs,args:{from:number;to:number;bucketMs:number})=>nativeStatistics(entrySpecs,adapter,db,caller,collection,args);`);
140
147
  const metadataGuards = [...Object.entries(plan.views).map(([name, v]) => `case ${q("view:" + name)}: return (${allowed(v.requires, `views/${name}`)})(caller);`), ...Object.entries(plan.procedures).map(([name, p]) => `case ${q("procedure:" + name)}: return (${allowed(p.requires, `procedures/${name}`)})(caller);`), ...Object.values(plan.schemas).filter(s => s.publishing).flatMap(s => ["publish", "unpublish", "archive"].map(verb => `case ${q("procedure:" + s.name + "." + verb)}: return isAdmin(caller);`))];
141
148
  files["admin-bindings.ts"] = `${imports.filter(Boolean).join("\n")}\nexport function adminBindings${refs ? "<const H extends Handlers>(options:{handlers:H & Record<Exclude<keyof H,keyof Handlers>,never>})" : "(_options?:{handlers?:Handlers})"}{\nreturn {metadata:{...bootstrap,accessTargets:()=>import("./access-targets.js").then(m=>m.targets),${plan.access ? `userFields:${q(plan.access.userFields)} as const,` : ""}allows:(target:import("@aotter/mantle/contracts").Target,caller:Caller)=>{switch(target.kind+":"+target.name){${metadataGuards.join("")}default:return target.kind==="schema";}}},adapter,schemas:[${schemas.join(",\n")}] satisfies readonly EntrySchema<Database>[],views:[${adminViews.join(",")}],developerViews:[${views.map((_, n) => `view${n}`).join(",")}],operations:[${operations.join(",\n")}] satisfies readonly Operation<Database,${refs ? "Handlers" : "never"}>[]};\n}\n`;
@@ -1,12 +1,5 @@
1
- /** `admin-client.ts`: the types Admin's data hooks bind to, for the application's browser-side `admin.tsx`. */
2
1
  import { BUILT_IN_NAV_GROUPS, mcpTools } from "../build/compiler.js";
3
2
  import { q } from "../domain/entry.js";
4
- /**
5
- * The compatibility test of the application's Admin MCP App (`node --test generated/admin-mcp-app.check.mjs`): it loads the App
6
- * `mantle bundle` built in the official MCP Apps host and fails on a network request, a console error or an exception. It needs
7
- * `playwright-core` (a devDependency) and Chrome: `npx playwright-core install chromium`, or `MANTLE_TEST_CHROMIUM=<executable>`.
8
- * Node strips the types of `admin-descriptors.ts` (Node 22.18 or newer).
9
- */
10
3
  const MCP_APP_TEST = `import { existsSync } from "node:fs";
11
4
  import { test } from "node:test";
12
5
  import { checkMcpApp } from "@aotter/mantle-admin/testing";
@@ -20,40 +13,26 @@ test("the Admin MCP App works in an MCP host", { timeout: 300_000 }, async () =>
20
13
  await checkMcpApp({ catalogue: mcpAppCatalogues.admin, html });
21
14
  });
22
15
  `;
23
- /** A Schema, View input or Procedure input as a closed object type; one that is only a `$ref` stays open. */
24
16
  const closed = (typeOf, schema) => !schema ? "{}" : schema.properties || schema.type === "object" && !schema["$ref"] ? typeOf({ ...schema, additionalProperties: false }) : "{ readonly [key: string]: unknown }";
25
- /**
26
- * Types are what the routes take and return, not the Store's rows: an entry's `data` keeps declared field names, an input is
27
- * what the route validates, a View row is its wire row under the names the browser route gives its columns (an MCP App's
28
- * `tools/call` returns the SQL names; the hooks rename them with `rowNames`), with `true` when it is an admin MCP tool, else null, which
29
- * types `pages[].mcp.renders` (types only; nothing ships). The file imports `generated/*` as types only, so nothing of the
30
- * Worker enters the browser bundle, and it is refused in the Worker's startup graph (`mantle bundle`).
31
- */
32
17
  export function emitAdminClient(plan, typeOf) {
33
18
  const tools = mcpTools(plan, "admin"), hasTool = (kind, source) => tools.some(t => t.kind === kind && t.source === source);
34
19
  const views = Object.entries(plan.views).filter(([, v]) => v.surface === "admin").sort(([a], [b]) => a < b ? -1 : 1);
35
20
  const operations = Object.entries(plan.procedures).filter(([name]) => hasTool("procedure", name)).sort(([a], [b]) => a < b ? -1 : 1);
36
21
  const schemas = Object.values(plan.schemas).sort((a, b) => a.name < b.name ? -1 : 1);
37
- // the scope column belongs to the caller, so an entry's data has no such field
38
22
  const dataOf = (s) => {
39
23
  const scope = s.scope ? s.names[s.scope] ?? s.scope : undefined, { [scope ?? ""]: _scope, ...properties } = s.schema.properties ?? {};
40
- // a draft of a publishing collection is only partly validated, so none of its fields is certain to be there
41
24
  const required = s.publishing ? [] : s.schema.required?.filter(f => f !== scope);
42
25
  const schema = { ...s.schema, properties, ...(required ? { required } : {}) };
43
26
  return s.translates ? typeOf(schema) : closed(typeOf, schema);
44
27
  };
45
- // the names the browser route gives a View's columns where they differ from the SQL output names, as it applies them
46
28
  const renames = views.map(([, v]) => Object.fromEntries(Object.entries(v.columns ?? {}).flatMap(([key, c]) => {
47
29
  const named = key === c.field ? plan.schemas[c.schema].names[c.field] ?? key : key;
48
30
  return named !== key ? [[key, named]] : [];
49
31
  })));
50
- // every sidebar group an admin Schema or View places an item in; `defineAdmin` must declare the ones Admin does not have
51
- // (the generate-time check of `uiSchema.nav.group` is this type)
52
32
  const placed = (ui) => { const group = ui?.["nav"]?.group; return typeof group === "string" ? [group] : []; };
53
33
  const used = [...new Set([...schemas.flatMap(s => placed(s.uiSchema)), ...views.flatMap(([, v]) => placed(v.uiSchema))])].sort();
54
34
  const custom = used.filter(group => !BUILT_IN_NAV_GROUPS.has(group));
55
35
  const union = (names) => names.length ? names.map(name => q(name)).join(" | ") : "never";
56
- // whether an admin View is an MCP tool, which a page declared for an MCP App (`pages[].mcp.renders`) must have
57
36
  const toolOf = (name) => hasTool("view", name) || null;
58
37
  const rows = views.map(([name], n) => `import type { WireRow as View${n}Wire } from "./views/${encodeURIComponent(name)}.js";`);
59
38
  const sql = operations.some(([, p]) => "sql" in p.handler);
@@ -1,4 +1,3 @@
1
- /** Build-time projections only; generated exports are static, independently tree-shakable data. */
2
1
  import { isPrimaryCollection, mcpTools, mcpToolNameSegment, toolTitle, collectionOf, developerConsole, rowPreconditions, appCatalog, entrySchema } from "../build/compiler.js";
3
2
  import { q } from "../domain/entry.js";
4
3
  import { advertised } from "./validation.js";
@@ -9,7 +8,6 @@ export function emitAdminDescriptors({ plan, admin = false }, diagnostics) {
9
8
  const placed = [...schemaList.map(s => ["schemas/" + s.name, s.uiSchema, s]), ...Object.entries(plan.views).map(([name, v]) => ["views/" + name, v.uiSchema, null])];
10
9
  for (const [path, ui, s] of placed) {
11
10
  const nav = (ui?.["nav"] ?? {}), group = nav["group"];
12
- // a child folded under its parent, or a translation on its parent's language tabs, has no sidebar entry to place
13
11
  if (s && !isPrimaryCollection(s, schemaList))
14
12
  for (const key of ["group", "order", "hidden"])
15
13
  if (nav[key] !== undefined)
@@ -46,7 +44,6 @@ export function emitAdminDescriptors({ plan, admin = false }, diagnostics) {
46
44
  const target = plan.procedures[t.source].target;
47
45
  return target ? [[t.name, { path: "/admin/c/" + encodeURIComponent(plan.schemas[target.schema.toLowerCase()]?.name ?? target.schema), entry: true }]] : [];
48
46
  })) };
49
- // These are the actual generated transitions, not authored ref handlers or SQL IR.
50
47
  for (const schema of schemas.filter(s => s.publishing))
51
48
  for (const [verb] of publishingTransitions) {
52
49
  const name = schema.name + "." + verb, tool = mcpToolNameSegment(verb + "_" + schema.name);
@@ -67,12 +64,9 @@ export function emitAdminDescriptors({ plan, admin = false }, diagnostics) {
67
64
  snapshot.interfaces.callable.push({ kind: "procedure", name: tool, target: name, surface: "admin", audience: "admin", title: null, description, input: publishingInput, output: null, trigger: null });
68
65
  }
69
66
  const data = { allCollections, views, operations, fieldNames: Object.fromEntries(schemas.map(s => [s.name, s.names])), viewNames, authorizations, developerConsole: snapshot, mcpAppCatalogues: catalogues, webmcp };
70
- return { "admin-descriptors.ts": "// Generated static Admin metadata. Import only the exports the application uses.\n" + literals(data) + "\nexport const collections = [" + schemas.filter(s => !s.translates).map(s => "allCollections[" + q(s.name) + "]").join(",") + "] as const;\nexport const bootstrap = {collections, allCollections, operations, views, webmcp, developerConsole} as const;\n" };
67
+ return { "admin-descriptors.ts": "// Generated static Admin metadata. Import only the exports the application uses.\n" + literals(data) + "\nexport const collections = [" + schemas.filter(s => !s.translates).map(s => "allCollections[" + q(s.name) + "]").join(",") + "] as const;\nexport const bootstrap = {collections, allCollections, operations, views, webmcp, developerConsole: () => import(\"./admin-developer-console.js\").then(m => m.developerConsole)} as const;\n",
68
+ "admin-developer-console.ts": "// Generated developer snapshot, loaded by bootstrap.developerConsole.\n" + literals({ developerConsole: snapshot }) + "\n" };
71
69
  }
72
- /**
73
- * Each export as a JSON literal; a subtree of 40 or more bytes that would be printed more than once is printed once, as a
74
- * module-private constant the exports reference (a bundler keeps only those an imported export reaches).
75
- */
76
70
  function literals(exports) {
77
71
  const data = JSON.parse(q(exports)), count = new Map(), hoisted = new Map(), constants = [];
78
72
  const each = (value, visit) => { if (value && typeof value === "object")
@@ -83,7 +77,6 @@ function literals(exports) {
83
77
  count.set(json, (count.get(json) ?? 0) + by); each(value, (json, child) => tally(json, child, by)); };
84
78
  for (const value of Object.values(data))
85
79
  each(value, tally);
86
- // Longest first: once a subtree is printed once, what it contains occurs (copies - 1) times fewer per occurrence in it.
87
80
  for (const json of [...count.keys()].sort((a, b) => b.length - a.length)) {
88
81
  const copies = count.get(json);
89
82
  if (copies > 1) {
@@ -1,5 +1,4 @@
1
1
  import { userReferences } from "../domain/userReferences.js";
2
- /** Canonical lowered programs become ordinary target-specific TypeScript, never a runtime plan. */
3
2
  import { parseNumeric, tableOutputs } from "../build/compiler.js";
4
3
  import { boundaryType, nativeType, outputType, wireType } from "../domain/columns.js";
5
4
  import { databaseImport, driverFor, entry, exportName, generatedFiles, isSqlite as sqlite, q } from "../domain/entry.js";
@@ -66,7 +65,6 @@ export function generate(input) {
66
65
  ? type === "bool" || parseNumeric(type) ? "number" : typeOf(field)
67
66
  : parseNumeric(type) ? "number | string" : typeOf(field);
68
67
  const nullable = field.nullable === true || Array.isArray(field.type) && field.type.includes("null") || !schema.required?.includes(name);
69
- // D1 cannot bind undefined. Bound fields are explicit, null represents absence.
70
68
  return `readonly ${q(name)}: ${driverType}${nullable ? " | null" : ""};`;
71
69
  });
72
70
  return `{ ${properties.join(" ")} ${schema.additionalProperties !== false ? "readonly [key:string]: unknown;" : ""} }`;
@@ -77,9 +75,7 @@ export function generate(input) {
77
75
  refuse(path, "Input properties must have unique case-insensitive names. Rename input properties to be unique without regard to case.");
78
76
  return fields;
79
77
  };
80
- // The dialect functions the bind recipes call, so a bundle carries only those (`binds` takes this object).
81
78
  const values = new Set();
82
- /** A statement as a constant: its text and how to bind it. */
83
79
  const step = (s, path, fields, row = false) => {
84
80
  const recipes = recipesOf(s, fields, message => refuse(path, message));
85
81
  for (const r of recipes) {
@@ -111,9 +107,7 @@ export function generate(input) {
111
107
  }
112
108
  const output = [], direct = tableOutputs(view, plan.schemas);
113
109
  for (const key of keys) {
114
- // A table column read unchanged keeps its NOT NULL (and gives a native column such as `id` its type); anything else may be NULL.
115
110
  const type = outputType(plan, name, key) ?? direct[key]?.type;
116
- // Declared timestamps and dates arrive formatted by the SQL itself (domain/wireScalars).
117
111
  output.push(`readonly ${q(key)}: ${nativeType(type, isSqlite, direct[key]?.notNull)};`);
118
112
  }
119
113
  const fieldNames = inputFields(view.input, path + "/input");
@@ -2,18 +2,15 @@ import { authLines as auth } from "../domain/input.js";
2
2
  import { q } from "../domain/entry.js";
3
3
  export function emitHandlers(plan, interactive, typeOf, declarations, procedures) {
4
4
  const refs = Object.entries(plan.procedures).sort().map(([name, procedure], index) => ({ name, procedure, index })).filter(({ procedure }) => "ref" in procedure.handler);
5
- // An empty contract keeps app wiring (`handlers: Handlers = {}`) identical whether or not any ref Procedure exists.
6
5
  if (!refs.length) {
7
6
  declarations.push("export interface Handlers {}");
8
7
  return [];
9
8
  }
10
9
  declarations.push('export type HandlerStore = Pick<ReturnType<typeof makeStore>, "collection" | "views" | "procedures">;',
11
- // D1 has no interactive transactions, so its handlers get no `transaction`: using one there fails typecheck.
12
10
  `export interface HandlerContext { readonly caller: Caller; readonly db: Database; readonly store: HandlerStore;${interactive ? " readonly transaction: <R>(fn: (ctx: { readonly store: HandlerStore; readonly db: Database }) => Promise<R>) => Promise<R>;" : ""} }`, "export interface Handlers {", ...refs.map(({ name, index }) => ` readonly [${q(name)}]: (ctx: HandlerContext, input: Procedure${index}Input) => Promise<Procedure${index}Output>;`), "}");
13
11
  for (const { name, procedure, index } of refs) {
14
12
  const path = `procedures/${name}`, fn = `procedure${index}`;
15
13
  declarations.push(`export type Procedure${index}Input = ${typeOf(procedure.input ?? { type: "object", additionalProperties: false })};`, `export type Procedure${index}Output = ${typeOf(procedure.output)};`, `async function ${fn}(db: Database, caller: Caller, raw: Procedure${index}Input, context: { handlers: Handlers; store: HandlerContext["store"] }): Promise<Procedure${index}Output> {`, ` if ("authenticatedAs" in caller && caller.authenticatedAs === "snapshot") (await import("@aotter/mantle/host-caller")).checkFresh(caller, false, ${q(path)});`, ...auth(procedure.requires, path), " const handlers = context?.handlers;", ` const handler = handlers && Object.hasOwn(handlers, ${q(name)}) ? handlers[${q(name)}] : undefined;`, ` if (!handler) throw new ContractError("HANDLER_NOT_REGISTERED", "Procedure handler is not registered.", ${q(path)});`,
16
- // TODO #1470: REST/MCP use shared generated validators at the boundary; bare calls stay typed.
17
14
  ` return handler({ caller, db, store: context.store${interactive ? ", transaction: handlerTransaction(context.store, db)" : ""} }, raw);`, "}");
18
15
  procedures.push(`[${q(name)}]: ${fn}`);
19
16
  }
@@ -1,7 +1,6 @@
1
1
  import { emitValidation } from "./validation.js";
2
2
  import { mediaDefinitions } from "../domain/generated-tools.js";
3
3
  export { mediaDefinitions };
4
- /** `definitions` are the versioned media tools, computed once by the MCP emitter. */
5
4
  export function emitMcpMedia(definitions, refuse) {
6
5
  const inputs = Object.fromEntries(definitions.map(tool => [tool.name, { input: tool.inputSchema }]));
7
6
  const checks = Object.fromEntries(Object.entries(emitValidation({ views: {}, procedures: inputs }, refuse)).map(([path, code]) => [path.replace("validation/procedures/", "validation/media/"), code]));
@@ -1,4 +1,3 @@
1
- /** Emit the selected MCP boundary; the official transport loads on its first request. */
2
1
  import { mcpToolNameSegment, procedureAnnotations, toolTitle, viewAnnotations } from "../build/compiler.js";
3
2
  import { emitMcpMedia, mediaDefinitions } from "./mcp-media.js";
4
3
  import { advertised, emitValidation } from "./validation.js";
@@ -37,7 +36,6 @@ export function emitMcp(plan, lowered, target, description, refuse, media = fals
37
36
  const module = `./${outputConversions(plan, name, target).some(([, type]) => type === "int") ? "paged/wire/views" : "paged/views"}/${file}.js`;
38
37
  imports.push(`const run${index}: typeof import(${q(module)}).default = async (...args) => (await import(${q(module)})).default(...args);`);
39
38
  }
40
- // The kernel adds a View tool's `limit` and `cursor` paging properties.
41
39
  const { inputSchema, description: about } = advertised(kind === "views" ? operation.input ?? { type: "object", additionalProperties: false } : operation.input, operation.description ?? operation.title ?? name), schema = literal(inputSchema);
42
40
  if (kind === "views" && ("limit" in (operation.input?.properties ?? {}) || "cursor" in (operation.input?.properties ?? {})))
43
41
  refuse(`views/${name}`, "MCP View input reserves limit and cursor for paging.");
@@ -49,7 +47,6 @@ export function emitMcp(plan, lowered, target, description, refuse, media = fals
49
47
  const output = kind === "procedures" ? ref ? (procedure?.output.type === "object" ? procedure.output : undefined) : { type: "object", properties: { results: { type: "array", items: { type: "array", items: { type: "object" } } } }, required: ["results"] } : undefined;
50
48
  const annotations = kind === "views" ? viewAnnotations : procedure ? procedureAnnotations(procedure) : operation.mcp;
51
49
  const wire = kind === "views" ? outputConversions(plan, name, target) : [];
52
- // Hashed as listed: the kernel adds a View tool's paging properties exactly so.
53
50
  const listed = versioned({ name: tool, title: operation.title ?? toolTitle(name), description: about, inputSchema: kind === "views" ? { ...inputSchema, properties: { ...inputSchema?.properties, limit: { type: "integer", minimum: 1, maximum: 500 }, cursor: { type: "string" } } } : inputSchema, outputSchema: output, annotations });
54
51
  hasWire ||= wire.length > 0;
55
52
  const fields = [...(description.targets.find(t => t._meta.id === `${kind === "views" ? "view" : "procedure"}:${name}`)?._meta.readOnly ? ["readOnly: true"] : []), `target: ${q({ kind: kind === "views" ? "view" : "procedure", name })}${guard(operation.requires, `${kind}/${name}`, binds, procedure?.trustedOnly)}`, `name: ${q(tool)}`, `surface: ${q(surface)}`, `title: ${q(listed.title)}`, `description: ${q(listed.description)}`, "inputSchema", ...(output ? [`outputSchema: ${literal(output)}`] : []), ...(annotations ? [`annotations: ${q(annotations)}`] : []), ...((operation.requires?.auth?.all.length ?? 0) > 0 ? ["closed: true"] : []), ...(scopes.length ? [`scopes: ${q(scopes)}`] : []), `validate: ${kind === "views" ? `validate${index}` : `async (raw) => (await import("./validation/procedures/${file}.js")).validate(raw)`}`, ...(convert ? [`convert: ${convert}`] : []),
@@ -77,7 +74,6 @@ export function emitMcp(plan, lowered, target, description, refuse, media = fals
77
74
  }
78
75
  if (hasWire)
79
76
  imports.push('import { wireRows } from "@aotter/mantle/contracts";');
80
- // An input schema several tools share is one constant.
81
77
  const uses = new Map(), shared = new Map();
82
78
  for (const { schema } of tools)
83
79
  uses.set(schema, (uses.get(schema) ?? 0) + 1);
@@ -1,5 +1,4 @@
1
1
  import { userReferences } from "../domain/userReferences.js";
2
- /** Selected View boundary: Core prints keyset SQL once; bare operations retain native rows. */
3
2
  import { parseNumeric } from "../build/compiler.js";
4
3
  import { outputConversions } from "../domain/columns.js";
5
4
  import { authLines } from "../domain/input.js";
@@ -17,7 +16,6 @@ export function emitPaging(plan, lowered, target, refuse, proofs, boundary = fal
17
16
  const proof = proofs?.[name];
18
17
  if (boundary && (!proof || !proof.types.length && !proof.single))
19
18
  continue;
20
- // Internal bare queries remain available even when they cannot be safely paged.
21
19
  if (!proof || !proof.types.length && !proof.single) {
22
20
  if (view.surface !== "internal")
23
21
  refuse(path, proof?.reason ?? "Paged Views need a complete typed NOT NULL key tuple, indexed Schema order and a non-fanning relation, or a proved singleton. For a lookup by a unique field, end ORDER BY with the Schema's id (`ORDER BY t.id`).");
@@ -1,4 +1,3 @@
1
- /** Opt-in lifecycle: one atomic, version-guarded statement per transition, with Core's row rules. */
2
1
  import { rowPolicy } from "../build/compiler.js";
3
2
  import { dialect as sqlite } from "../kernel/dialect/sqlite.js";
4
3
  import { dialect as postgres } from "../kernel/dialect/postgres.js";
@@ -32,8 +31,6 @@ export function emitPublishing(plan, isSqlite, declarations, procedures, entries
32
31
  const schema = plan.schemas[s.name];
33
32
  const recipes = [{ k: "now" }, { k: "input", name: "id" }, { k: "input", name: "expectedVersion" }];
34
33
  const conditions = [`id = ${d.param(2)}`, `version = ${d.param(3)}`, `status IN (${from.map(v => `'${v}'`).join(", ")})`];
35
- // Same visibility as Store and Admin writes, translation parent included (a child under an out-of-set parent is missing);
36
- // the status term is replaced by the transition's own `status IN (...)`.
37
34
  for (const term of rowPolicy(schema, trusted ? "trusted" : "caller", plan.schemas))
38
35
  if (term[1] !== "admin")
39
36
  conditions.push(renderTerm(d, term, term[1] === "parent" ? d.ident(s.name) + "." : "", recipe => (recipes.push(recipe), d.param(recipes.length))));
@@ -56,7 +53,6 @@ export function emitPublishing(plan, isSqlite, declarations, procedures, entries
56
53
  }
57
54
  }
58
55
  }
59
- /** Publishing lists get one index, prefixed by scope when present. */
60
56
  export function publishingSql(schemas, isSqlite) {
61
57
  const d = isSqlite ? sqlite : postgres;
62
58
  return Object.values(schemas).filter(s => s.publishing).map(s => `CREATE INDEX ${d.ident(`_mantle_ix_${s.name}_published`)} ON ${d.ident(s.name)} (${[...(s.scope ? [s.scope] : []), ...(s.userScopes ?? []).map(u => u.column), "published_at", "id"].map(d.ident).join(", ")})`);