@aotter/mantle 0.1.2-alpha.6 → 0.1.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 (110) hide show
  1. package/README.md +15 -10
  2. package/dist/cli/generate.d.ts +9 -0
  3. package/dist/cli/generate.d.ts.map +1 -1
  4. package/dist/cli/generate.js +40 -1
  5. package/dist/cli/generate.js.map +1 -1
  6. package/dist/cli/main.d.ts +1 -1
  7. package/dist/cli/main.d.ts.map +1 -1
  8. package/dist/cli/main.js +37 -9
  9. package/dist/cli/main.js.map +1 -1
  10. package/docs/adapter-guide.md +6 -1
  11. package/docs/adr/adr-lite-845-frontend-client.md +38 -0
  12. package/docs/agent-prompts.md +91 -0
  13. package/docs/api-mcp-authorization.md +1 -1
  14. package/docs/auth-hosting-model.md +1 -1
  15. package/docs/examples/README.md +22 -0
  16. package/docs/examples/builtin-commerce.md +269 -0
  17. package/docs/examples/builtin-intake.md +143 -0
  18. package/docs/examples/builtin-legal-documents.md +189 -0
  19. package/docs/examples/builtin-procurement.md +241 -0
  20. package/docs/examples/builtin-publication.md +241 -0
  21. package/docs/examples/builtin-reservation.md +149 -0
  22. package/docs/examples/cf-primitives-commerce-inventory.md +809 -0
  23. package/docs/examples/cf-primitives-guarded-api.md +429 -0
  24. package/docs/examples/cf-primitives-intake-hooks.md +319 -0
  25. package/docs/examples/host-chatgpt-sites/.openai/hosting.json +1 -0
  26. package/docs/examples/host-chatgpt-sites/README.md +53 -0
  27. package/docs/examples/host-chatgpt-sites/drizzle/0000_sites_users.sql +8 -0
  28. package/docs/examples/host-chatgpt-sites/drizzle/0001_mantle.sql +294 -0
  29. package/docs/examples/host-chatgpt-sites/drizzle/0002_article_cover.sql +5 -0
  30. package/docs/examples/host-chatgpt-sites/drizzle/meta/_journal.json +1 -0
  31. package/docs/examples/host-chatgpt-sites/manifests/site.yaml +27 -0
  32. package/docs/examples/host-chatgpt-sites/package.json +1 -0
  33. package/docs/examples/host-chatgpt-sites/public/site.css +1 -0
  34. package/docs/examples/host-chatgpt-sites/scripts/build.mjs +12 -0
  35. package/docs/examples/host-chatgpt-sites/scripts/check.mjs +99 -0
  36. package/docs/examples/host-chatgpt-sites/scripts/migration.mjs +9 -0
  37. package/docs/examples/host-chatgpt-sites/src/chatgpt-auth.ts +63 -0
  38. package/docs/examples/host-chatgpt-sites/src/index.ts +46 -0
  39. package/docs/examples/host-chatgpt-sites/src/mcp.ts +49 -0
  40. package/docs/examples/host-chatgpt-sites/src/media.ts +109 -0
  41. package/docs/examples/host-chatgpt-sites/src/r2-lab.ts +38 -0
  42. package/docs/examples/host-chatgpt-sites/src/storage-fingerprint.json +1 -0
  43. package/docs/examples/host-chatgpt-sites/src/web.ts +35 -0
  44. package/docs/examples/host-chatgpt-sites/tsconfig.json +1 -0
  45. package/docs/examples/host-chatgpt-sites/wrangler.jsonc +10 -0
  46. package/docs/examples/host-local-admin-otp/.dev.vars.example +3 -0
  47. package/docs/examples/host-local-admin-otp/README.md +70 -0
  48. package/docs/examples/host-local-admin-otp/ensure-dev-vars.mjs +5 -0
  49. package/docs/examples/host-local-admin-otp/package.json +29 -0
  50. package/docs/examples/host-local-admin-otp/public/.gitkeep +1 -0
  51. package/docs/examples/host-local-admin-otp/smoke.mjs +141 -0
  52. package/docs/examples/host-local-admin-otp/src/index.ts +54 -0
  53. package/docs/examples/host-local-admin-otp/wrangler.jsonc +23 -0
  54. package/docs/examples/{minimal-worker → host-minimal-worker}/README.md +9 -5
  55. package/docs/examples/host-minimal-worker/manifests/site.yaml +25 -0
  56. package/docs/examples/{minimal-worker → host-minimal-worker}/package.json +3 -3
  57. package/docs/examples/host-minimal-worker/tsconfig.json +17 -0
  58. package/docs/handbook/cloudflare/authentication.md +17 -2
  59. package/docs/handbook/cloudflare/bindings.md +9 -7
  60. package/docs/handbook/cloudflare/chatgpt-sites.md +29 -0
  61. package/docs/handbook/cloudflare/conventional-worker.md +3 -3
  62. package/docs/handbook/cloudflare/deferred-hooks-queues.md +0 -1
  63. package/docs/handbook/cloudflare/deploy-and-operate.md +14 -20
  64. package/docs/handbook/cloudflare/media-r2.md +2 -2
  65. package/docs/handbook/cloudflare/public-web.md +1 -1
  66. package/docs/handbook/cloudflare/site-chrome.md +75 -0
  67. package/docs/handbook/concepts/authorization.md +2 -2
  68. package/docs/handbook/concepts/four-atoms.md +2 -2
  69. package/docs/handbook/concepts/lifecycle-and-locales.md +1 -1
  70. package/docs/handbook/concepts/mcp-and-agents.md +1 -1
  71. package/docs/handbook/concepts/procedures-and-triggers.md +1 -1
  72. package/docs/handbook/concepts/runtime-and-adapters.md +2 -2
  73. package/docs/handbook/concepts/views.md +2 -2
  74. package/docs/handbook/examples/commerce-transaction.md +4 -806
  75. package/docs/handbook/examples/commerce.md +11 -0
  76. package/docs/handbook/examples/guarded-api.md +3 -420
  77. package/docs/handbook/examples/hub.md +10 -0
  78. package/docs/handbook/examples/intake-form.md +6 -313
  79. package/docs/handbook/examples/intake-hooks.md +11 -0
  80. package/docs/handbook/examples/legal-documents.md +3 -211
  81. package/docs/handbook/examples/procurement-approvals.md +3 -233
  82. package/docs/handbook/examples/publication.md +3 -233
  83. package/docs/handbook/examples/reservation.md +3 -213
  84. package/docs/handbook/navigation.json +17 -2
  85. package/docs/handbook/reference/authorization.md +1 -1
  86. package/docs/handbook/reference/procedure.md +2 -2
  87. package/docs/handbook/reference/schema.md +3 -3
  88. package/docs/handbook/reference/site-config.md +5 -16
  89. package/docs/handbook/reference/surface.md +3 -7
  90. package/docs/handbook/sites/equipment-checkout.md +231 -0
  91. package/docs/handbook/sites/host-reference.md +113 -0
  92. package/docs/handbook/sites/index.md +111 -0
  93. package/docs/handbook/start/project-and-cli.md +22 -14
  94. package/docs/handbook/start/quickstart-admin.md +239 -0
  95. package/docs/handbook/start/quickstart-worker.md +22 -23
  96. package/docs/migration-0.1.2.md +26 -0
  97. package/docs/release-process.md +92 -7
  98. package/docs/sealed-pipeline-ownership.md +2 -2
  99. package/docs/spec-only-host-adoption.md +3 -4
  100. package/docs/transaction-patterns.md +2 -2
  101. package/package.json +15 -15
  102. package/skills/README.md +18 -2
  103. package/skills/develop/SKILL.md +32 -23
  104. package/skills/install/SKILL.md +50 -11
  105. package/skills/provision/SKILL.md +21 -5
  106. /package/docs/examples/{minimal-worker → host-local-admin-otp}/manifests/site.yaml +0 -0
  107. /package/docs/examples/{minimal-worker → host-local-admin-otp}/tsconfig.json +0 -0
  108. /package/docs/examples/{minimal-worker → host-minimal-worker}/smoke.mjs +0 -0
  109. /package/docs/examples/{minimal-worker → host-minimal-worker}/src/index.ts +0 -0
  110. /package/docs/examples/{minimal-worker → host-minimal-worker}/wrangler.jsonc +0 -0
@@ -3,7 +3,7 @@ description: Enable staff media uploads on R2 — presigned PUT flow through Sta
3
3
  ---
4
4
  # Media uploads with R2
5
5
 
6
- R2-backed media is an optional post-launch capability for Workers whose staff or agents need to upload images and files. This page covers the upload flow, the Cloudflare and Worker configuration it needs, how a Schema references an asset, what a commit costs, and how to clean up.
6
+ R2-backed media is an optional post-launch capability for Workers whose staff or agents need to upload images and files. This page covers the native Cloudflare direct-to-R2 flow, the configuration it needs, how a Schema references an asset, what a commit costs, and how to clean up. For ChatGPT Sites with an R2 binding but no S3 credentials, use [the same-origin Worker flow](../sites/host-reference.md#media-with-only-an-r2-binding).
7
7
 
8
8
  ## When to enable it
9
9
 
@@ -31,7 +31,7 @@ wrangler r2 bucket dev-url enable <project>-media
31
31
 
32
32
  Then create an R2 S3 API token in the dashboard: open **R2**, open **Manage R2 API Tokens**, create an **Object Read & Write** token, and copy the Access Key ID and Secret Access Key.
33
33
 
34
- The R2 binding alone cannot issue presigned `PUT` URLs: presigning is an S3-protocol operation, so Mantle needs these credentials in addition to the binding. A deployment with the bucket bound but no credentials never registers the upload tools.
34
+ The R2 binding alone cannot issue presigned `PUT` URLs: presigning is an S3-protocol operation, so this **direct-to-R2 adapter** needs these credentials in addition to the binding. This is not a requirement of Mantle's `MediaStorage` port: a host may return an authenticated same-origin Worker `PUT` URL instead. Media tools appear whenever `mediaStorage` and a media purpose are bound, regardless of which transport the adapter uses.
35
35
 
36
36
  ## Wrangler configuration
37
37
 
@@ -75,7 +75,7 @@ export default createMantleWorker<Env>({
75
75
  });
76
76
  ```
77
77
 
78
- Templates return complete HTML strings; the renderer prepends the doctype. Escape every value you interpolate.
78
+ Templates return complete HTML strings; the renderer prepends the doctype. Escape every value you interpolate. Analytics, pixels and verification tags are host chrome — add them in the template or on Cloudflare. See [Site chrome](./site-chrome.md).
79
79
 
80
80
  ## Template signatures
81
81
 
@@ -0,0 +1,75 @@
1
+ ---
2
+ description: Host-owned site chrome — Cloudflare-first analytics, verification, robots, and what still belongs in mantle-web.
3
+ ---
4
+ # Site chrome
5
+
6
+ Core `siteConfig` is deployment identity: locales, brand, title, description, origin, icons and media. Visitor analytics, pixels, search-engine verification tokens, `ads.txt` and similar chrome are **host responsibility**. Admin Settings does not accept tracking IDs, and `@aotter/mantle-web` does not inject vendor snippets from `siteConfig`.
7
+
8
+ Cloudflare-native platform capabilities are the first-class place for chrome that the zone already covers. Do not invent a parallel “CDN settings center” inside Mantle.
9
+
10
+ ## Why Core does not inject
11
+
12
+ A headless runtime that rewrites the first `</head>` (or prepends to `<body>`) on a composed HTML string assumes one frontend shape: a complete SSR document that Core is allowed to mutate after the fact. Hosts are not one shape.
13
+
14
+ Frontends are plural. A Mantle consumer may serve SSR full HTML documents, a Vite/React (or other) SPA, a Worker plus static assets, Cloudflare Pages, or a custom wrapper that already owns `<html>`. Only the first of those even has a complete document string a runtime could splice into — and even then the attach point is the host template, not Core.
15
+
16
+ SPA, client-routed and hydration hosts attach analytics in **their own entry**: GTM or Zaraz, the app bootstrap, or Cloudflare Web Analytics. They do not expect Runtime or `mantle-web` to parse an HTML string and splice tags into it.
17
+
18
+ Silent injection from `siteConfig` would be a surprising side effect for any host that did not opt into “I emit a full document and you may rewrite it.” Core must not pretend to know every frontend’s attach point.
19
+
20
+ Therefore tracking and verification stay **host chrome**. Core `siteConfig` is not a tag-manager. The tables below say where to put chrome.
21
+
22
+ ## Two layers
23
+
24
+ | Concern | Prefer | Usually still app / mantle-web |
25
+ |---|---|---|
26
+ | `robots.txt`, simple redirects, some `/.well-known` files, Cloudflare Web Analytics | **Cloudflare-native** | — |
27
+ | Dynamic sitemap (grows with content), `llms.txt`, SEO `.md` mirrors | — | Application mount or `@aotter/mantle-web` composition, then serve publicly |
28
+
29
+ Turning on Cloudflare Web Analytics is **not** the same as having an indexable sitemap or `llms.txt`. Analytics answers “who visited”; discovery files answer “what can a crawler or agent list”. Keep those jobs on the layer that owns the data.
30
+
31
+ Cloud tenant UX for ads.txt / robots / sitemap tools lives on the host product. Core stays injection-free.
32
+
33
+ ## Cloudflare-first install
34
+
35
+ Use the dashboard, Wrangler, or an agent that can edit Worker / Pages config. None of these paths write Core Admin fields.
36
+
37
+ ### Analytics
38
+
39
+ Prefer **Cloudflare Web Analytics** (and Zaraz when you need a tag manager) for first-party traffic measurement. Enable it on the zone; no Core `siteConfig` key is required.
40
+
41
+ To add GA4 or Meta Pixel:
42
+
43
+ 1. Add the provider through **Cloudflare Zaraz** (recommended on a CF zone), or
44
+ 2. Emit the vendor snippet from **your** entry/list templates in `@aotter/mantle-web`, or from a host HTML wrapper.
45
+
46
+ Do not put Measurement IDs or Pixel IDs in `siteDefaults`. Core does not persist or inject them.
47
+
48
+ ### Search Console and Bing verification
49
+
50
+ Verify ownership with a host-owned method. On Cloudflare, prefer in this order:
51
+
52
+ 1. **DNS TXT** at the zone apex (Search Console and Bing Webmaster both accept this).
53
+ 2. **Cloudflare-managed HTML** via Zaraz or a small Worker / Pages header rewrite that adds `<meta name="google-site-verification">` / Bing equivalent.
54
+ 3. A **static verification file** at `/` or `/.well-known/` served from `public/` / the `ASSETS` binding.
55
+
56
+ HTML-tag verification belongs in the host template `<head>`, not in Core Settings.
57
+
58
+ ### robots.txt, redirects, well-known
59
+
60
+ Serve a static `robots.txt` from assets, or use Cloudflare Redirect Rules / Transform Rules / a tiny Worker route. `mountPublicRoutes` also emits `GET /robots.txt` (`Allow: /` plus the sitemap URL) when you opt into public pages — that is an application mount, not a Core Admin setting. See [Public web](./public-web.md).
61
+
62
+ Simple path redirects and most `/.well-known` files (including verification and `security.txt`) belong on the same Cloudflare-native layer.
63
+
64
+ ## What still belongs in the app
65
+
66
+ Dynamic **sitemap** and **`llms.txt`** grow with published content. Compose them with `@aotter/mantle-web` (`composeSitemap`, `composeLlmsTxt`) and mount the public URLs, or implement the same contract in the application. Cloudflare Web Analytics does not generate those files.
67
+
68
+ SEO `.md` mirrors are likewise application / `mantle-web` composition. Details: [Public web](./public-web.md) and [Site config](../reference/site-config.md).
69
+
70
+ ## Source
71
+
72
+ - [`packages/mantle-spec/src/domain/model/SiteConfig.ts`](../../../packages/mantle-spec/src/domain/model/SiteConfig.ts)
73
+ - [`packages/mantle-web/src/service/HtmlRenderer.ts`](../../../packages/mantle-web/src/service/HtmlRenderer.ts)
74
+ - [`docs/handbook/reference/site-config.md`](../reference/site-config.md)
75
+ - [`docs/handbook/cloudflare/public-web.md`](./public-web.md)
@@ -3,7 +3,7 @@ description: One authorization pipeline for REST, MCP and Admin — identity kin
3
3
  ---
4
4
  # Authorization
5
5
 
6
- Mantle has one authorization pipeline. A manifest HTTP Trigger, a View over REST, an MCP `tools/call` and an Admin operation all reach the same evaluator, so a rule written once holds on every surface. This page explains the model; the exact `requires` grammar is in the [authorization reference](../reference/authorization.md), and a worked ladder of four access levels is in [Guarded API access](../examples/guarded-api.md).
6
+ Mantle has one authorization pipeline. A manifest HTTP Trigger, a View over REST, an MCP `tools/call` and an Admin operation all reach the same evaluator, so a rule written once holds on every surface. This page explains the model; the exact `requires` grammar is in the [authorization reference](../reference/authorization.md), and a worked ladder of four access levels is in [Guarded API access](../../examples/cf-primitives-guarded-api.md).
7
7
 
8
8
  ## The fixed order
9
9
 
@@ -104,7 +104,7 @@ An MCP `tools/call` runs the identical evaluator, in the identical order, with t
104
104
  ## Related
105
105
 
106
106
  - [Authorization requirements](../reference/authorization.md) — the `requires` shape, predicate forms, guard rules, OpenAPI projection.
107
- - [Guarded API access](../examples/guarded-api.md) — a full resolver, guards and the REST/MCP outcome tables.
107
+ - [Guarded API access](../../examples/cf-primitives-guarded-api.md) — a full resolver, guards and the REST/MCP outcome tables.
108
108
  - [Reads: Views, REST and MCP](./views.md) — the `$ctx.user` identity-View sentinel.
109
109
  - [Authentication](../cloudflare/authentication.md) — sessions, first owner, role management routes.
110
110
 
@@ -26,9 +26,9 @@ The consequence for authors: a Procedure that has no Trigger is unreachable from
26
26
 
27
27
  ## The composition rule
28
28
 
29
- Anything more domain-shaped than these four is not an atom. A Form, Membership, Email, Webhook, Workflow or ScheduledJob is a composition of Schemas, Views, Procedures and Triggers plus your own TypeScript. An earlier iteration of this grammar shipped eleven domain-shaped kinds; authors could not decide whether a contact form was a Form, a Workflow or an Email, and each kind grew to subsume its neighbors.
29
+ Anything more domain-shaped than these four is not an atom. A Form, Membership, Email, Webhook, Workflow or ScheduledJob is a composition of Schemas, Views, Procedures and Triggers plus your own TypeScript. A fifth kind would recreate the same overlap: authors would have to decide whether a contact form was a Form, a Workflow or an Email.
30
30
 
31
- If you find yourself wanting a fifth kind, sketch the same thing as a composition of the four first. Almost always it works.
31
+ If you find yourself wanting another kind, sketch the same thing as a composition of the four first. Almost always it works.
32
32
 
33
33
  ## The Manifest envelope
34
34
 
@@ -129,7 +129,7 @@ The parent holds what every language shares and stays non-localized. The child s
129
129
 
130
130
  When you keep parallel locale rows in step, translate display strings only. Field names, option values, step identifiers and result keys must stay identical across locales, or the same View and the same MCP tool stop describing the same thing.
131
131
 
132
- The full worked version of this pattern, with a locale-parameterized public View and its REST response, is [Publication](../examples/publication.md).
132
+ The full worked version of this pattern, with a locale-parameterized public View and its REST response, is [Publication](../../examples/builtin-publication.md).
133
133
 
134
134
  ## Source
135
135
 
@@ -33,7 +33,7 @@ Procedures are never exposed on their own. A Procedure becomes a tool only throu
33
33
 
34
34
  ## The OAuth model
35
35
 
36
- The Cloudflare adapter runs one Better Auth 1.7 instance for staff identity, authorization, consent, client registration and MCP resource verification. Client identity is CIMD-first — the MCP 2026-07-28 Client ID Metadata Document profile, which is why the Worker needs the `global_fetch_strictly_public` flag to fetch client metadata across the public Internet boundary — with unauthenticated legacy DCR kept only as a bounded compatibility path for older clients, on the previous provider's 90-day default lifetime. One non-colon scope, `mcp`, is advertised in `scopes_supported`, because clients such as claude.ai reject colon-shaped scopes; per-surface enforcement then happens server-side, not through scope strings. Authorization is session-bound: the JWT's originating Better Auth session must still exist and be unexpired, so signing out of Admin also ends that session's MCP access, and a refresh token is not an independent authorization. Unauthenticated requests to either mount answer `401` with a `WWW-Authenticate` challenge pointing at the RFC 9728 protected-resource metadata document served under the auth mount. Authorization endpoints live under `/api/auth/oauth2/*` and are discovered from the advertised metadata, never hard-coded.
36
+ The Cloudflare adapter runs one Better Auth 1.7 instance for staff identity, authorization, consent, client registration and MCP resource verification. Client identity is CIMD-first — the MCP 2026-07-28 Client ID Metadata Document profile, which is why the Worker needs the `global_fetch_strictly_public` flag to fetch client metadata across the public Internet boundary. Unauthenticated Dynamic Client Registration remains available as a bounded path with a 90-day default lifetime for clients that do not present CIMD. One non-colon scope, `mcp`, is advertised in `scopes_supported`, because clients such as claude.ai reject colon-shaped scopes; per-surface enforcement then happens server-side, not through scope strings. Authorization is session-bound: the JWT's originating Better Auth session must still exist and be unexpired, so signing out of Admin also ends that session's MCP access, and a refresh token is not an independent authorization. Unauthenticated requests to either mount answer `401` with a `WWW-Authenticate` challenge pointing at the RFC 9728 protected-resource metadata document served under the auth mount. Authorization endpoints live under `/api/auth/oauth2/*` and are discovered from the advertised metadata, never hard-coded.
37
37
 
38
38
  ## Connecting a local client
39
39
 
@@ -209,7 +209,7 @@ What each piece is doing:
209
209
  - `submittedAt` is stamped by the runtime, so it is absent from the Procedure input and from Staff MCP authoring tools.
210
210
  - Adding `{ kind: mcp, surface: public }` as a fourth Trigger would publish the same Procedure as an agent tool without touching a handler.
211
211
 
212
- Register the two `ref` keys in the project's handlers map, then run the check loop from [Project layout and the CLI loop](../start/project-and-cli.md). The complete version with real Turnstile and email handlers is [Intake form](../examples/intake-form.md).
212
+ Register the two `ref` keys in the project's handlers map, then run the check loop from [Project layout and the CLI loop](../start/project-and-cli.md). The complete version with real Turnstile and email handlers is [Intake Turnstile and email hooks](../../examples/cf-primitives-intake-hooks.md). The Builder-safe Manifest without those hooks is [Intake form](../../examples/builtin-intake.md).
213
213
 
214
214
  ## Source
215
215
 
@@ -112,7 +112,7 @@ Bun, with the server and SQLite handle staying yours:
112
112
 
113
113
  ```ts
114
114
  import { Database } from "bun:sqlite";
115
- import { createBunMantle } from "@aotter/mantle-bun";
115
+ import { createBunMantle } from "@aotter/mantle-bun"; // experimental
116
116
 
117
117
  const database = new Database("app.sqlite");
118
118
  const mantle = createBunMantle({ plan, database, handlers });
@@ -128,7 +128,7 @@ Vercel Functions, with storage injected:
128
128
 
129
129
  ```ts
130
130
  import { SqliteMantleStorageAdapter } from "@aotter/mantle-runtime";
131
- import { createVercelMantle } from "@aotter/mantle-vercel";
131
+ import { createVercelMantle } from "@aotter/mantle-vercel"; // experimental
132
132
  import { LibsqlDatabaseDriver } from "@aotter/mantle-vercel/libsql";
133
133
 
134
134
  const mantle = createVercelMantle({
@@ -47,7 +47,7 @@ A View can filter to the calling user's own rows with the closed sentinel `{ "$c
47
47
  2. The View must require a signed-in user: `ctx.user` in `requires.auth.all` — otherwise `VIEW_FILTER_CTX_USER_REF_REQUIRES_AUTH`.
48
48
  3. The compared field must be the leftmost field of a declared index on the source Schema — otherwise `VIEW_FILTER_CTX_USER_REF_REQUIRES_INDEX`.
49
49
 
50
- This is a filter, not a row-level policy engine. `requires` authorizes the whole query; it does not inject per-row visibility predicates. Membership, payment and entitlement checks belong in a guard Procedure. See [Procurement approvals](../examples/procurement-approvals.md).
50
+ This is a filter, not a row-level policy engine. `requires` authorizes the whole query; it does not inject per-row visibility predicates. Membership, payment and entitlement checks belong in a guard Procedure. See [Procurement approvals](../../examples/builtin-procurement.md).
51
51
 
52
52
  ## Shared response cache
53
53
 
@@ -176,7 +176,7 @@ spec:
176
176
  limit: 200
177
177
  ```
178
178
 
179
- `tags` is an array property, so `json_each` unnests it and one request appears once per tag. The runtime wraps the whole statement as a subquery before applying pagination, which is why Admin's search and filters attach to the SQL output aliases — `tag` is a real filterable column even though no Schema property is named `tag`. See [Commerce and settlement](../examples/commerce-transaction.md) for the same technique over order lines.
179
+ `tags` is an array property, so `json_each` unnests it and one request appears once per tag. The runtime wraps the whole statement as a subquery before applying pagination, which is why Admin's search and filters attach to the SQL output aliases — `tag` is a real filterable column even though no Schema property is named `tag`. See [Commerce inventory](../../examples/cf-primitives-commerce-inventory.md) for the same technique over order lines.
180
180
 
181
181
  ## Source
182
182