@aotter/mantle 0.1.0-alpha.9 → 0.1.2-alpha.1

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 (104) hide show
  1. package/README.md +38 -38
  2. package/dist/cli/generate.d.ts +10 -1
  3. package/dist/cli/generate.d.ts.map +1 -1
  4. package/dist/cli/generate.js +49 -4
  5. package/dist/cli/generate.js.map +1 -1
  6. package/dist/cli/main.js +0 -8
  7. package/dist/cli/main.js.map +1 -1
  8. package/docs/adapter-guide.md +20 -24
  9. package/docs/adr/0008-structured-diagnostic-shape.md +7 -2
  10. package/docs/adr/0014-auth-better-auth-and-multi-tenant-mcp.md +100 -0
  11. package/docs/adr/0018-core-starters-repository-boundary.md +3 -0
  12. package/docs/adr/0019-sealed-manifest-runtime-pipeline.md +37 -1
  13. package/docs/adr/0020-builtin-handler-contracts-and-matched-upsert.md +199 -0
  14. package/docs/adr/0021-retire-starter-scaffolding.md +30 -0
  15. package/docs/adr/0022-caller-observed-version-occ.md +212 -0
  16. package/docs/adr/0023-port-failure-contract.md +69 -0
  17. package/docs/adr/README.md +18 -3
  18. package/docs/adr/adr-lite-803-request-diagnostics.md +38 -0
  19. package/docs/adr/adr-lite-808-route-readiness.md +47 -0
  20. package/docs/adr/adr-lite-809-bounded-public-content.md +71 -0
  21. package/docs/adr/adr-lite-812-native-parity.md +149 -0
  22. package/docs/adr/adr-lite-823-home-statistics.md +63 -0
  23. package/docs/adr/adr-lite-842-mcp-authoring.md +30 -0
  24. package/docs/adr/adr-lite-861-admin-webmcp.md +42 -0
  25. package/docs/adr/adr-lite-909-admin-ui-kit.md +37 -0
  26. package/docs/api-mcp-authorization.md +3 -664
  27. package/docs/auth-hosting-model.md +4 -4
  28. package/docs/cloudflare-low-level-composition.md +3 -104
  29. package/docs/deferred-lifecycle-queues.md +2 -243
  30. package/docs/design-atoms.md +16 -796
  31. package/docs/direct-authoring.md +5 -0
  32. package/docs/examples/minimal-worker/README.md +37 -0
  33. package/docs/examples/minimal-worker/manifests/site.yaml +25 -0
  34. package/docs/examples/minimal-worker/package.json +26 -0
  35. package/docs/examples/minimal-worker/smoke.mjs +23 -0
  36. package/docs/examples/minimal-worker/src/index.ts +4 -0
  37. package/docs/examples/minimal-worker/tsconfig.json +17 -0
  38. package/docs/examples/minimal-worker/wrangler.jsonc +22 -0
  39. package/docs/handbook/cloudflare/authentication.md +167 -0
  40. package/docs/handbook/cloudflare/bindings.md +233 -0
  41. package/docs/handbook/cloudflare/conventional-worker.md +162 -0
  42. package/docs/handbook/cloudflare/deferred-hooks-queues.md +171 -0
  43. package/docs/handbook/cloudflare/deploy-and-operate.md +98 -0
  44. package/docs/handbook/cloudflare/low-level-composition.md +169 -0
  45. package/docs/handbook/cloudflare/media-r2.md +216 -0
  46. package/docs/handbook/cloudflare/public-web.md +144 -0
  47. package/docs/handbook/concepts/authorization.md +116 -0
  48. package/docs/handbook/concepts/four-atoms.md +173 -0
  49. package/docs/handbook/concepts/lifecycle-and-locales.md +142 -0
  50. package/docs/handbook/concepts/mcp-and-agents.md +109 -0
  51. package/docs/handbook/concepts/procedures-and-triggers.md +221 -0
  52. package/docs/handbook/concepts/runtime-and-adapters.md +177 -0
  53. package/docs/handbook/concepts/views.md +183 -0
  54. package/docs/handbook/examples/commerce-transaction.md +813 -0
  55. package/docs/handbook/examples/guarded-api.md +427 -0
  56. package/docs/handbook/examples/intake-form.md +319 -0
  57. package/docs/handbook/examples/legal-documents.md +218 -0
  58. package/docs/handbook/examples/procurement-approvals.md +240 -0
  59. package/docs/handbook/examples/publication.md +240 -0
  60. package/docs/handbook/examples/reservation.md +220 -0
  61. package/docs/handbook/navigation.json +63 -0
  62. package/docs/handbook/reference/authorization.md +295 -0
  63. package/docs/handbook/reference/diagnostics.md +183 -0
  64. package/docs/handbook/reference/manifest.md +132 -0
  65. package/docs/handbook/reference/procedure.md +263 -0
  66. package/docs/handbook/reference/schema.md +247 -0
  67. package/docs/handbook/reference/site-config.md +233 -0
  68. package/docs/handbook/reference/surface.md +207 -0
  69. package/docs/handbook/reference/trigger.md +193 -0
  70. package/docs/handbook/reference/view.md +256 -0
  71. package/docs/handbook/start/project-and-cli.md +122 -0
  72. package/docs/handbook/start/quickstart-worker.md +193 -0
  73. package/docs/labels.md +3 -1
  74. package/docs/media-uploads.md +3 -184
  75. package/docs/migration-0.1.2.md +34 -1
  76. package/docs/performance-harness.md +125 -8
  77. package/docs/release-process.md +85 -231
  78. package/docs/schema-indexes.md +3 -180
  79. package/docs/sealed-pipeline-ownership.md +5 -4
  80. package/docs/spec-only-host-adoption.md +158 -0
  81. package/docs/transaction-patterns.md +5 -0
  82. package/package.json +21 -25
  83. package/skills/README.md +4 -4
  84. package/skills/develop/SKILL.md +23 -28
  85. package/skills/install/SKILL.md +55 -147
  86. package/skills/provision/SKILL.md +4 -3
  87. package/skills/theme/SKILL.md +6 -6
  88. package/skills/update/SKILL.md +32 -68
  89. package/dist/cli/create.d.ts +0 -2
  90. package/dist/cli/create.d.ts.map +0 -1
  91. package/dist/cli/create.js +0 -243
  92. package/dist/cli/create.js.map +0 -1
  93. package/dist/cli/update.d.ts +0 -2
  94. package/dist/cli/update.d.ts.map +0 -1
  95. package/dist/cli/update.js +0 -413
  96. package/dist/cli/update.js.map +0 -1
  97. package/dist/provision/renderProvisionBundle.d.ts +0 -70
  98. package/dist/provision/renderProvisionBundle.d.ts.map +0 -1
  99. package/dist/provision/renderProvisionBundle.js +0 -367
  100. package/dist/provision/renderProvisionBundle.js.map +0 -1
  101. package/dist/provision.d.ts +0 -2
  102. package/dist/provision.d.ts.map +0 -1
  103. package/dist/provision.js +0 -2
  104. package/dist/provision.js.map +0 -1
@@ -1,796 +1,16 @@
1
- # Design atoms — the 4 things this SDK exposes
2
-
3
- > If you're an AI working in a project that consumes `@aotter/mantle-*`,
4
- > read this first. It explains the entire surface area in one page.
5
- >
6
- > **Status**: v0.1 shipped grammar. Unknown keys and enum values are rejected.
7
- >
8
- > **This is the reference manual** — what the system is. For *why* it
9
- > ended up this shape (alternatives considered, trade-offs accepted),
10
- > see the Architecture Decision Records under [`docs/adr/`](adr/README.md).
11
- > For the SDK's contract with its primary author (CLI feedback loops,
12
- > structured diagnostics, deterministic authoring), see
13
- > [ADR-0007](adr/0007-ai-as-primary-author.md).
14
-
15
- ## TL;DR
16
-
17
- > See ADR-0001 for rationale — why these 4 atoms, why the PG-1:1
18
- > mapping pitch, and the design history.
19
-
20
- The SDK exposes **exactly 4 declarative resource kinds**, scoped to the
21
- **`cms.mantle.aotter.net/v1`** group of the mantle universe. They map 1-to-1 to the
22
- primitives Postgres has shipped for 30 years.
23
-
24
- | Our atom | Postgres equivalent | Externally exposed by itself? | Has user code? |
25
- |---|---|---|---|
26
- | **`Schema`** | `CREATE TABLE` | no (manipulated via View / Procedure) | no |
27
- | **`View`** | `CREATE VIEW` | **yes** (auto-mounted on its declared public/staff REST and MCP surface; see ADR-0012) | no |
28
- | **`Procedure`** | `CREATE FUNCTION ... LANGUAGE plpgsql` | **no** (transport-agnostic; needs a `Trigger` to bind it) | **yes — handler ref to the consumer's registry** |
29
- | **`Trigger`** | `CREATE TRIGGER` + route/tool binding | yes (the binding atom — turns Procedures into HTTP endpoints, MCP tools, and lifecycle hooks) | no |
30
-
31
- The **read/write asymmetry is by design** and matches HTTP safe-vs-unsafe
32
- semantics. Reads (`View`) are idempotent and cacheable; the SDK
33
- auto-mounts them. Writes (`Procedure`) change state; they require an
34
- explicit `Trigger` to gain an external surface, with the auth/path
35
- choices made deliberately.
36
-
37
- **Composition rule**: anything more "domain-shaped" than these (a Form,
38
- Membership, Email, Webhook, Workflow, ScheduledJob) is **not** an atom.
39
- It's something you compose **in the consumer's project** by combining
40
- these four plus your own TypeScript.
41
-
42
- ## Manifest envelope
43
-
44
- ```yaml
45
- apiVersion: cms.mantle.aotter.net/v1
46
- kind: Schema | View | Procedure | Trigger
47
- metadata:
48
- name: posts # required, non-empty, unique within its kind
49
- spec:
50
- ... # kind-specific
51
- ```
52
-
53
- There is **no manifest `namespace` field**. Resource names are unique within
54
- their kind, so a Schema and View may share a name. SaaS multi-tenancy belongs
55
- in the consumer's app layer, not in manifest metadata.
56
-
57
- ### Multi-doc YAML — keeping file count down
58
-
59
- > See [ADR-0001 §"Authoring shape: multi-doc YAML"](adr/0001-four-atom-manifest-model.md#authoring-shape-multi-doc-yaml-was-poc-adr-0006)
60
- > for rationale — why multi-doc YAML over an inline
61
- > `Procedure.expose:` shortcut.
62
-
63
- A logical feature commonly bundles a Procedure + a Trigger (and often a
64
- Schema and a View). Put related atoms in one file separated by `---`:
65
-
66
- ```yaml
67
- # manifests/contact.yml
68
- apiVersion: cms.mantle.aotter.net/v1
69
- kind: Procedure
70
- metadata: { name: send-contact-message }
71
- spec:
72
- input: { type: object, required: [name, message], properties: { ... } }
73
- output: { type: object, properties: { ok: { type: boolean } } }
74
- handler:
75
- kind: ref
76
- ref: send-contact-message # opaque registration key
77
-
78
- ---
79
- apiVersion: cms.mantle.aotter.net/v1
80
- kind: Trigger
81
- metadata: { name: contact-http }
82
- spec:
83
- source: { kind: http, method: POST, path: /api/contact }
84
- target: { procedure: send-contact-message }
85
- ```
86
-
87
- Use one file or split features across files. Mantle reads every immediate
88
- `.yaml` and `.yml` file in `./manifests` by default; `---` separates multiple
89
- documents in one file.
90
-
91
- ## What each atom is for (v0.1 minimum essential grammar)
92
-
93
- ### 1. `Schema` — the entity (internal)
94
-
95
- A relation. Defines what data exists. You declare the JSON Schema for
96
- each row, its indexes, and the binding directives. Schema is **not
97
- directly externally exposed** — clients don't write to a Schema URL; they
98
- hit a Procedure that the SDK translates into Schema mutations.
99
-
100
- ```yaml
101
- apiVersion: cms.mantle.aotter.net/v1
102
- kind: Schema
103
- metadata: { name: posts }
104
- spec:
105
- title: Posts # required: human-readable label for the admin UI
106
- localized: true # opt-in: row carries data.locale (ADR-0010)
107
- lifecycle: publishing # default; use 'operational' for live records
108
- schema:
109
- $schema: https://json-schema.org/draft/2020-12/schema
110
- type: object
111
- required: [title, slug, content]
112
- properties:
113
- title: { type: string, minLength: 1, maxLength: 200 }
114
- slug: { type: string }
115
- locale: { type: string } # NOT in `required:`; the runtime locale
116
- # gate (mantle-runtime helpers.ts, per
117
- # ADR-0010) enforces presence on writes
118
- # against localized Schemas.
119
- content: { type: string }
120
- authorId: { type: string, format: uuid, x-mantle-bind: ctx.user }
121
- createdAt: { type: integer, x-mcp-hint: timestamp-ms, x-mantle-bind: now }
122
- searchableFields: [title, slug] # id is always searched
123
- uniqueIndexes: [[slug, locale]]
124
- indexes: [[locale, title]] # ordered, non-unique hot path
125
- ```
126
-
127
- **`spec.title`** — admin UI label. Required. AI authors must populate
128
- in the user's primary language (the install-time chosen locale); the
129
- SPA shows this everywhere instead of `metadata.name`.
130
-
131
- Schema titles and optional descriptions accept either a string or a locale
132
- map such as `{ en: Products, "zh-TW": 商品 }`. Procedure titles and
133
- descriptions and View titles use the same shape; all are optional.
134
-
135
- **`spec.searchableFields`** — optional allowlist of top-level string
136
- properties used by Admin and Staff MCP substring search. Entry id is always
137
- searchable. This is separate from `indexes`; ordinary B-tree indexes do not
138
- accelerate leading-wildcard substring search.
139
-
140
- **`spec.uiSchema.list.filterField`** — optional Admin-only primary filter for
141
- an operational collection. The field must declare a non-empty string `enum`
142
- and be the first field of an `indexes` or `uniqueIndexes` tuple. Admin reuses
143
- the enum for sidebar links and list tabs; Staff MCP uses declared Views for
144
- richer query capabilities instead of reading UI configuration.
145
-
146
- Operational list data is also explicit: `uiSchema.list.primaryField` names the
147
- linked leading value and `uiSchema.list.columns` names the remaining scalar
148
- columns. Without them Admin shows only platform metadata. Form-only choices use
149
- `uiSchema.fields.<field>.widget: textarea`; rich content remains declared with
150
- `x-mcp-hint: markdown|html|richtext`. Neither setting changes runtime or MCP
151
- input validation.
152
-
153
- A staff-operable Procedure may declare `uiSchema.collectionAction: orders` to
154
- appear as an action in that collection's Admin header. The target must be an
155
- existing Schema. This binding is Admin-only; the Procedure input, authorization,
156
- runtime handler, and MCP exposure remain unchanged.
157
-
158
- **`spec.localized: bool`** (default `false`, ADR-0010) — opt-in per
159
- Schema. Localized Schemas store locale in `data.locale`; non-localized
160
- Schemas reject `data.locale` writes. Site config must declare the set
161
- of allowed locales (`site_config.locales`). The runtime layer reads
162
- `site_config` per-request to resolve the active locale set; boot only
163
- validates manifest shape.
164
-
165
- **`spec.translates: { parent, on }`** (ADR-0010) — declares this
166
- Schema as the translation companion to a non-localized parent, joined
167
- on the named field (typically `slug`). Requires an explicit
168
- `localized: true`. The
169
- admin UI surfaces the child only as locale tabs in the parent's editor.
170
-
171
- Use a standalone localized Schema only when each locale row is an
172
- independent record. When several locale rows are versions of one entity,
173
- declare a non-localized parent plus a localized `translates` child; that
174
- relationship is what powers translation grouping and completeness in Admin.
175
-
176
- #### Lifecycle
177
-
178
- **`spec.lifecycle: 'publishing' | 'operational'`** — controls the
179
- entry's state machine.
180
-
181
- - `publishing` (default) — `draft → published → archived`. No approval queue.
182
- - `operational` — records that are not authored content: orders,
183
- inventory snapshots, grant/audit rows — anything written by
184
- Procedures as a side effect rather than drafted by a person. No
185
- content workflow applies: entries are live (`published`) the moment
186
- they are created, are editable in place regardless of status, and
187
- have **no** publish / unpublish / archive transitions (all reject
188
- with `CONFLICT`). The admin console renders these collections flat —
189
- no draft/published filter buckets, no publish controls. Declare it
190
- on any Schema whose rows a human should *inspect and correct*, never
191
- *stage and publish*. Staff MCP emits `create_record_<schema>` and
192
- `update_record_<schema>` for these Schemas instead of draft tools.
193
-
194
- The modes are **per-Schema and mix freely** within a site. There is no
195
- site-wide lifecycle setting; one Schema can be `publishing` while another
196
- is `operational`.
197
-
198
- **Root `schema.readOnly: true`** — standard JSON Schema annotation for a
199
- Procedure-managed collection. Admin and Staff MCP keep list/detail access and
200
- declared row Procedures, but suppress and reject generic create, update,
201
- status-change, and delete operations. Trusted Procedure handlers may still use
202
- the runtime write use cases to maintain the projection. Put this on operational
203
- mirrors and audit rows whose authority lives outside generic authoring.
204
-
205
- **Property-level extensions** (JSON Schema vendor keywords, all optional):
206
-
207
- These are the standard `x-` prefix that JSON Schema reserves for
208
- extensions. Three are part of `cms.mantle.aotter.net/v1`:
209
-
210
- #### `x-mantle-bind: <value>` — server-stamped fields
211
-
212
- > See ADR-0002 for rationale — why this is a closed enum and not an
213
- > open expression language.
214
-
215
- Marks a property as **server-controlled**. On create, the SDK overwrites any
216
- caller value with the bound value; updates preserve the existing stamp. This
217
- eliminates the entire
218
- class of handler-side `userId: ctx.user.id` boilerplate, and gives
219
- the dispatcher an authoritative "who/when" tag without trusting the
220
- wire.
221
-
222
- **Closed enum** (any new value requires an explicit grammar-revise round):
223
-
224
- | Value | Resolves to | Typical use |
225
- |---|---|---|
226
- | `ctx.user` | UUID of the signed-in end-user (from session cookie); `null` if anonymous | row ownership: `authorId`, `submittedBy`, `creatorId` |
227
- | `ctx.staff` | UUID of the staff member acting (when a staff session is active); `null` for end-user-only paths | audit trail: `approvedBy`, `moderatedBy`, `grantedBy` |
228
- | `now` | Server timestamp at write (Unix epoch milliseconds) | `createdAt`, `submittedAt`, `grantedAt` |
229
-
230
- **v0.1 stamping behavior**:
231
- - Stamped on `INSERT` only.
232
- - Caller-supplied create value: ignored and replaced by the server stamp.
233
- - Visible in View output: yes, as ordinary columns. No special masking.
234
-
235
- **Why a closed enum** (highest-leverage discipline in the spec): bind
236
- values are server-controlled identity + time facts. If we accepted
237
- arbitrary expressions (`x-mantle-bind: ${request.headers["x-team"]}`),
238
- we'd be reinventing a templating language. Keeping the set finite and
239
- named bounds the semantic surface forever.
240
-
241
- **Example** (from a hypothetical extended `posts` Schema):
242
- ```yaml
243
- properties:
244
- title: { type: string, minLength: 1, maxLength: 200 }
245
- authorId: { type: string, format: uuid, x-mantle-bind: ctx.user }
246
- createdAt: { type: integer, x-mcp-hint: timestamp-ms, x-mantle-bind: now }
247
- # caller sends only: { title }
248
- # SDK stamps: { title, authorId: <session UUID>, createdAt: <epoch ms> }
249
- ```
250
-
251
- #### `x-mantle-ref: <other-schema-name>` — cross-Schema reference
252
-
253
- Informational FK marker on a string-typed field that holds an ID
254
- referencing rows in another Schema. Not enforced in v0.1 (the SDK
255
- passes it through; no foreign-key constraint, no cascade, no orphan
256
- detection). The admin uses declared refs to show related rows and to nest
257
- required child collections; it does not infer relationships from field names.
258
- Declare a single-field `indexes` entry for the ref field (for example,
259
- `indexes: [[authorId]]`) when reverse lookups must stay bounded. Mantle adds
260
- the native entry-order columns to that access path.
261
-
262
- On a Procedure input property, the Admin also uses `x-mantle-ref` to expose
263
- that Procedure in the referenced collection row's three-dot menu. It locks
264
- the referenced value to the selected row and infers the remaining form from
265
- the Procedure input schema. The value comes from a same-named Schema property,
266
- then a lone single-field unique index, and finally the entry `id`.
267
-
268
- **Example**:
269
- ```yaml
270
- authorId:
271
- type: string
272
- format: uuid
273
- x-mantle-ref: authors # this UUID points at a row in `authors` Schema
274
- ```
275
-
276
- **Implementation status**: declared in
277
- `@aotter/mantle-spec` as the `MANTLE_REF_KEYWORD` constant; SDK
278
- currently passes it through to consumers (admin UI uses it for picker
279
- widgets) but does not enforce referential integrity at write time.
280
-
281
- #### `x-mcp-hint: <hint-string>` — agent / widget intent
282
-
283
- Descriptive hint for AI agents and admin UI widgets. The string is
284
- accepted as free-form for forward compatibility, but conventional
285
- values (`markdown`, `richtext`, `code`, `media`, `media-image`,
286
- `media-video`, `media-file`) tell consumers how to render or generate
287
- the field's value. `idempotency-key` asks the Admin to generate and hide a
288
- stable UUID for one form invocation; other callers must generate one and reuse
289
- it when retrying the same operation.
290
-
291
- **Examples** (from the publication/blog starter manifests):
292
- ```yaml
293
- content:
294
- type: string
295
- x-mcp-hint: markdown # admin uses markdown editor; agents know to author markdown
296
-
297
- coverUrl:
298
- type: string
299
- format: uri
300
- x-mcp-hint: media-image # media-shaped URL; first-run uses external URLs
301
- ```
302
-
303
- **Implementation status**: declared in
304
- `@aotter/mantle-spec` as the `MCP_HINT_KEYWORD` constant. The
305
- conventional values are `markdown`, `richtext`, `code`, `media`,
306
- `media-image`, `media-video`, `media-file`, `money-minor`, and
307
- `timestamp-ms` (the `media*` subset marks media-shaped fields).
308
- MCP tool schemas preserve the hint for agents. Admin surfaces expose
309
- media-shaped fields but first-party upload hosting is optional and not
310
- part of first-run provisioning.
311
-
312
- **Postgres analogue**: `CREATE TABLE posts (id UUID PRIMARY KEY, ...,
313
- UNIQUE (slug, locale));`
314
-
315
- ### 2. `View` — the read surface (auto-exposed by surface)
316
-
317
- A named, read-only SQL query over Schemas. Every Schema is available to SQL as
318
- a logical table named after `Schema.metadata.name`; storage internals stay
319
- hidden. No Trigger is required. `surface` is required: `surface: public` mounts
320
- at `GET /api/views/<name>` and becomes `query_view_<name>` on `/mcp`, while
321
- `surface: staff` mounts at `GET /admin/api/views/<name>` behind the
322
- staff gate and appears only on `/mcp/staff`. See ADR-0012 for the full design
323
- rationale.
324
-
325
- ```yaml
326
- apiVersion: cms.mantle.aotter.net/v1
327
- kind: View
328
- metadata: { name: recent-published }
329
- spec:
330
- surface: public
331
- sql: |
332
- SELECT id, title, slug, locale, publishedAt, updatedAt
333
- FROM posts
334
- WHERE status = 'published'
335
- ORDER BY updatedAt DESC
336
- limit: 20
337
- ```
338
-
339
- **Param-driven Views** declare `spec.params` (a JSON Schema with
340
- `type: object`); SQL references required properties as named `:params`. The
341
- runtime validates and binds them; it never interpolates caller values:
342
-
343
- ```yaml
344
- apiVersion: cms.mantle.aotter.net/v1
345
- kind: View
346
- metadata: { name: posts-by-locale }
347
- spec:
348
- surface: public
349
- sql: |
350
- SELECT id, slug, locale, title, updatedAt
351
- FROM post-translations
352
- WHERE status = 'published' AND locale = :locale
353
- ORDER BY slug ASC
354
- params:
355
- type: object
356
- properties:
357
- locale: { type: string }
358
- required: [locale]
359
- limit: 100
360
- ```
361
-
362
- REST callers paginate via reserved query-string knobs `?page=&show=`
363
- (1-indexed page, server caps `show` at `View.spec.limit`). Reserved
364
- names — `page` / `show` / `cursor` — must NOT appear in
365
- `spec.params.properties` (the parser rejects with
366
- `VIEW_PARAMS_RESERVED_NAME`).
367
-
368
- Staff report lists may opt into the Admin's standard columns, substring
369
- search, and exact filters without changing the public REST/MCP contract:
370
-
371
- ```yaml
372
- uiSchema:
373
- list:
374
- columns: [orderNumber, customerName, orderStatus]
375
- searchFields: [orderNumber, customerName, customerEmail]
376
- filterFields: [orderStatus]
377
- ```
378
-
379
- These names are SQL output aliases. The Admin applies search and filters
380
- before pagination and carries them into
381
- `GET /admin/api/views/<name>/export`; the CSV contains all matching rows,
382
- not only the visible page.
383
-
384
- Views may declare the same `requires.auth.all` predicates and optional
385
- `requires.guard.procedure` as Procedures. Static auth runs before parameter
386
- validation; the guard receives validated params and authorizes the whole
387
- query. It does not rewrite SQL or filter individual rows. REST and MCP View
388
- calls share this path.
389
-
390
- Response envelope:
391
-
392
- ```json
393
- { "ok": true, "data": { "rows": [...], "page": 1, "show": 20, "hasMore": true } }
394
- ```
395
-
396
- `hasMore` is the lazy form: `rows.length === show` ⇒ `true`. No COUNT
397
- query, no `LIMIT n+1` probe.
398
-
399
- The previous `from` / `fields` / `filter` / `orderBy` declarative form remains
400
- accepted for existing manifests. New Views should use one `SELECT`; writes,
401
- multiple statements, semicolons, and combining SQL with the legacy clauses are
402
- rejected.
403
-
404
- **Postgres analogue**: `CREATE VIEW recent_published AS SELECT ...
405
- FROM posts WHERE status = 'published' ORDER BY updated_at DESC LIMIT
406
- 20;`. PG views are read-API surfaces by virtue of being queryable —
407
- ours work the same way, just over HTTP.
408
-
409
- ### 3. `Procedure` — the typed callable (internal until bound)
410
-
411
- The **only atom with a code seam**. YAML declares typed input + typed
412
- output + auth requirement + handler reference; the consumer's project
413
- provides the handler function with auto-generated TS types.
414
-
415
- `Procedure` is **transport-agnostic and not directly exposed**. It does
416
- not contain HTTP paths, methods, or MCP tool names. To call a Procedure
417
- from outside the SDK runtime, declare a `Trigger` whose `target` points
418
- at it. The same Procedure can be bound by multiple Triggers (HTTP + MCP +
419
- lifecycle, all sharing one handler).
420
-
421
- ```yaml
422
- apiVersion: cms.mantle.aotter.net/v1
423
- kind: Procedure
424
- metadata: { name: send-contact-message }
425
- spec:
426
- requires:
427
- auth:
428
- all: [ctx.user] # logged-in users only
429
- input:
430
- type: object
431
- required: [name, message]
432
- properties:
433
- name: { type: string, minLength: 1, maxLength: 80 }
434
- email: { type: string, format: email }
435
- message: { type: string, minLength: 1, maxLength: 4000 }
436
- output:
437
- type: object
438
- properties:
439
- ok: { type: boolean }
440
- handler:
441
- kind: ref
442
- ref: send-contact-message # opaque registration key (NOT a path)
443
- ```
444
-
445
- ```ts
446
- // src/mantle/config.ts
447
- import { sendContactMessage } from "./handlers/send-contact-message";
448
-
449
- export const handlers = {
450
- "send-contact-message": sendContactMessage,
451
- };
452
- ```
453
-
454
- **v0.1 `requires.auth`**: `{ all: [<predicate>] }` only. Predicates:
455
- - `ctx.user` — caller is any signed-in end-user
456
- - `ctx.staff: [<role>, ...]` — caller is staff in one of these roles
457
- - `ctx.auth` — caller supplied any adapter-verified credential
458
- - `ctx.auth.scope: <scope>` — verified credential carries the exact opaque,
459
- consumer-owned scope; repeat to require multiple scopes
460
-
461
- Both Procedures and Views may add one dynamic guard beside `auth`:
462
-
463
- ```yaml
464
- requires:
465
- auth:
466
- all:
467
- - ctx.auth
468
- - { "ctx.auth.scope": "orders:read" }
469
- guard:
470
- procedure: require-active-access
471
- ```
472
-
473
- The guard is an ordinary declared `handler.kind: ref` Procedure. It receives
474
- the validated target input/View params and the same `HandlerContext`; success
475
- permits the target. Any guard diagnostic, invalid output, missing handler, or
476
- throw fails closed. Guards cannot be builtin, self-referential, or guarded
477
- themselves. Current payments, membership, and entitlement state belongs in
478
- the consumer guard handler, not in a new atom or Core repository. See
479
- [API and MCP authorization](api-mcp-authorization.md).
480
-
481
- **v0.1.0 `handler.kind`**: `ref` (author-supplied function) or
482
- `builtin` (SDK-supplied CRUD shortcut). For `builtin`, declare
483
- `op: <create | update | upsert | delete | archive>` and
484
- `schema: <Schema name>` in place of `ref`. The runtime dispatch path is
485
- implemented by `InvokeBuiltinUseCase`; parser and boot validation fail closed
486
- on unknown ops, Schemas, or incompatible lifecycle use.
487
-
488
- **Postgres analogue**: `CREATE FUNCTION send_contact_message(input
489
- JSONB) RETURNS JSONB LANGUAGE plpgsql AS $$ ... $$;`. PG functions are
490
- internal callables — `pg_proc` rows scoped to a schema, not externally
491
- addressable. Exposing them via HTTP requires PostgREST or a custom
492
- RPC layer; in our world, that layer is `Trigger`.
493
-
494
- ### 4. `Trigger` — the event binding (the whole external surface for writes)
495
-
496
- Says "when X happens, run Procedure Y." Every external surface for a
497
- write — HTTP endpoint, MCP tool, cron job, lifecycle hook, queue
498
- consumer — is a `Trigger`. There is no other way to expose a
499
- Procedure.
500
-
501
- ```yaml
502
- apiVersion: cms.mantle.aotter.net/v1
503
- kind: Trigger
504
- metadata: { name: contact-http }
505
- spec:
506
- source:
507
- kind: http # v0.1 also supports mcp and lifecycle
508
- method: POST # POST | PUT | PATCH | DELETE
509
- path: /api/contact # OpenAPI {param} syntax for path params
510
- # path params auto-bind to identically-named input fields
511
- # input MUST declare them; no optional segments
512
- target:
513
- procedure: send-contact-message
514
- ```
515
-
516
- The same Procedure can have multiple Triggers — that's how it becomes
517
- an HTTP endpoint and an MCP tool without duplicating
518
- handler logic. Each transport is one Trigger; the Procedure body is
519
- shared.
520
-
521
- **v0.1 `Trigger.source.kind`**: `http` (public endpoint), `mcp` (named
522
- tool on `surface: public | staff`), or `lifecycle` (entry-writer hook). For `lifecycle`, declare `schema`,
523
- `on: [<hook>, ...]` from `LifecycleHook`, and optional `errorPolicy`
524
- (`abort` is the `before_*` default; `continue` is the `after_*` default).
525
- Lifecycle hooks are wired through `LifecycleHookingEntryRepository`, so
526
- MCP, admin, and builtin write paths share the same hook behavior.
527
-
528
- The state-machine "lifecycle" from the Schema atom
529
- (`Schema.spec.lifecycle: publishing | operational`) is a separate domain
530
- that shares the word. The Schema setting governs which states an
531
- entry can be in; shipped lifecycle Triggers govern what fires around
532
- mutations.
533
-
534
- **Postgres analogue**: `CREATE TRIGGER ... AFTER INSERT ON posts
535
- EXECUTE FUNCTION ...` (lifecycle), plus
536
- `CREATE FUNCTION` exposed via PostgREST routes (http) — Postgres has
537
- had all of these via extensions for years, just split across multiple
538
- mechanisms. We unify them under one atom.
539
-
540
- ## OpenAPI emission — manifests in, OpenAPI out
541
-
542
- > See the `mantle-spec` README for rationale — why OpenAPI is
543
- > emission target, not manifest shape (keeps MCP and other source
544
- > kinds first-class peers, not `x-` extensions on an HTTP-flavored
545
- > type).
546
-
547
- The `Trigger { source.kind: http }` + `Procedure` pair is **not** an
548
- OpenAPI Operation Object. It's a smaller, transport-neutral pair. The
549
- SDK emits standard OpenAPI 3.1 from the manifest set so existing
550
- tooling (Swagger UI, Redoc, openapi-generator, Stainless, Speakeasy)
551
- works unchanged:
552
-
553
- ```bash
554
- $ mantle emit-openapi --output ./openapi.json
555
- ```
556
-
557
- Mapping:
558
- - Each `Trigger { source.kind: http, target.procedure: X }` → one
559
- OpenAPI Operation Object at `path` + `method`
560
- - The target Procedure's `input` → OpenAPI request body schema
561
- - The target Procedure's `output` → OpenAPI 200 response schema
562
- - configured cookie, OAuth bearer, API-key, and personal-token schemes →
563
- accurate OpenAPI `security` alternatives for auth-gated targets
564
- - repeated `ctx.auth.scope` predicates → OAuth scopes plus
565
- `x-mantle-required-scopes`
566
- - `requires.guard.procedure` → `x-mantle-guard-procedure` plus a `402`
567
- response
568
- - Error code → HTTP status mapping (below) → OpenAPI 4xx/5xx response
569
- shapes
570
-
571
- MCP Procedure tools emit from `Trigger.source.kind: mcp`; Views emit on their
572
- declared surface. Catalog filtering is discovery UX only. Every `tools/call`
573
- re-runs static auth and the dynamic guard. Required scopes/guard behavior stay
574
- in the standard Tool description rather than a non-standard required-scopes
575
- field.
576
-
577
- ## Manifest validation — JSON Schema in, zod at runtime
578
-
579
- The author writes JSON Schema; the runtime validates with zod.
580
- That split is intentional and load-bearing.
581
-
582
- - **Authoring contract** = JSON Schema. `Schema.spec.schema`,
583
- `Procedure.spec.input`, `Procedure.spec.output` are all
584
- draft-2020-12 JSON Schema documents. This is what AI authors and
585
- human authors write, what the admin UI feeds JSON Forms, and what
586
- the OpenAPI emitter relays unchanged.
587
- - **Runtime engine** = zod 4. The `@aotter/mantle-spec`
588
- package ships a JSON-Schema → zod converter; the runtime calls the converted zod
589
- schema on every Procedure invocation, View parameter parse, and
590
- manifest boot check.
591
-
592
- **Why zod, not Ajv**: Cloudflare Workers' default Content-Security
593
- Policy posture and bundle-size budget make Ajv (which generates
594
- validators via `new Function(...)`) a poor fit. zod is interpreted,
595
- ships small, and has no eval-shaped code paths. The price is a
596
- narrower JSON Schema feature set; the converter documents which
597
- keywords it supports.
598
-
599
- The split also means: **manifests stay portable**. A consumer who
600
- later swaps to a non-Workers adapter inherits the same JSON Schema
601
- manifests; only the runtime validator changes if the adapter has
602
- different constraints.
603
-
604
- ## Error code → HTTP status table
605
-
606
- | Code | HTTP | When |
607
- |---|---|---|
608
- | `INPUT_VALIDATION_FAILED` | `400` | Procedure input fails zod-converted schema |
609
- | `UNAUTHENTICATED` | `401` | no active session (admin API: missing/expired session cookie) |
610
- | `AUTH_DENIED` | `403` | `requires.auth` predicate evaluated false (or admin: caller lacks required staff role) |
611
- | `ENTITLEMENT_REQUIRED` | `402` | consumer guard denies current payment/membership/transaction entitlement |
612
- | `NOT_FOUND` | `404` | resource not found — entry id or View name at `/api/views/<name>` |
613
- | `HANDLER_NOT_REGISTERED` | `500` | `handler.ref` key not registered at boot |
614
- | `DISPATCHER_NOT_BUILT` | `501` | runtime feature not implemented in this SDK build |
615
- | `INTERNAL_ERROR` | `500` | uncaught handler exception |
616
- | `OUTPUT_VALIDATION_FAILED` | `500` | handler returned a value not matching its declared output schema |
617
-
618
- All emitted with `phase: "runtime"` per ADR-0008. The same code
619
- may also fire in earlier loops (e.g. `HANDLER_NOT_REGISTERED`
620
- fires at boot — `phase: "boot"` — and that's where it should be
621
- caught; the runtime occurrence is defense-in-depth).
622
-
623
- ## RBAC
624
-
625
- v0.1 auth gates use `requires.auth.all` with `ctx.user`,
626
- `ctx.staff: [<roles>]`, `ctx.auth`, and `ctx.auth.scope` predicates on
627
- Procedures and Views. This covers staff-only, logged-in-only,
628
- credential-protected, and delegated-scope targets. One Procedure-backed guard
629
- handles mutable consumer business state without widening the static grammar.
630
-
631
- ## How to think when extending this CMS
632
-
633
- 1. **What entities does the feature need?** → `Schema` for each.
634
- 2. **What named queries?** → `View` for each. Ad-hoc reads happen in
635
- Procedure handlers; only sanctioned ones get a View (and become
636
- external read endpoints).
637
- 3. **What operations?** → `Procedure` for each. One Procedure = one
638
- typed function call. Compose in handler code, not in YAML.
639
- 4. **What invokes them?** → `Trigger` per declared HTTP, MCP, or lifecycle
640
- source. Site-owned events can call the same Procedure binding directly.
641
- 5. **Who's allowed?** → `Procedure/View.spec.requires.auth` for static
642
- identity/scope; optional `requires.guard.procedure` for one live,
643
- consumer-owned business check.
644
-
645
- If you find yourself wanting a 5th kind, **stop**. Sketch the same
646
- thing as a composition of the four; almost always it works.
647
-
648
- ### Postgres heuristic for "is this an atom?"
649
-
650
- > If it's a `CREATE X` in Postgres that an **application developer**
651
- > would write (not a DBA, not the query planner), it maps to one of
652
- > our 4 atoms. If it's a `CREATE X` that PG only needs because PG is a
653
- > database engine — `INDEX`, `MATERIALIZED VIEW`, `TABLESPACE`,
654
- > `STATISTICS`, `EVENT TRIGGER`, `EXTENSION`, `LANGUAGE`,
655
- > `PUBLICATION`, `FOREIGN TABLE`, `RULE` — it **doesn't** map, and
656
- > you likely don't need it either. Type-system extensions (`TYPE`,
657
- > `DOMAIN`, `SEQUENCE`, `COLLATION`, `CAST`, `AGGREGATE`, `OPERATOR`)
658
- > fold into JSON Schema inside `Schema` rather than becoming new
659
- > kinds.
660
-
661
- Postgres exposes ~25 object kinds total; an application developer typically
662
- only writes 4–6 of them. The rest belong behind the selected storage adapter.
663
-
664
- ## Storage adapters
665
-
666
- Manifests compile to semantic storage ports; they do not select a database.
667
- Use the shared SQLite adapter for SQLite-compatible hosts, or implement the
668
- same ports over PostgreSQL, MongoDB, or existing application repositories.
669
- See the [adapter guide](adapter-guide.md); SQLite index lowering is documented
670
- separately in [Schema indexes](schema-indexes.md).
671
-
672
- ## Detailed shipped grammar
673
-
674
- ### `handler.kind: builtin` — thin shortcut over the storage adapter for trivial CRUD-shaped Procedures
675
-
676
- Use when the body is "insert a row" / "update a row" / "delete a
677
- row"; reach for `ref` when there is real business logic. Shape:
678
-
679
- ```yaml
680
- spec:
681
- input: { ... JSON Schema for the request body ... }
682
- output: { ... JSON Schema for the response body ... }
683
- handler:
684
- kind: builtin
685
- op: create | update | upsert | delete | archive
686
- schema: <Schema metadata.name>
687
- ```
688
-
689
- | op | Behavior |
690
- |---|---|
691
- | `create` | INSERT a new row. Project `input ∩ Schema.spec.schema.properties`; stamp `x-mantle-bind` fields; generated id; status is `draft`, or immediately `published` for `lifecycle: operational`. |
692
- | `update` | UPDATE in place. `input.id` + `input.expectedVersion` (OCC) required. Bumps version. |
693
- | `upsert` | If `input.id` resolves, behaves as `update`; else as `create`. |
694
- | `delete` | Hard DELETE by id. |
695
- | `archive` | Soft-archive a publishing entry (`status='archived'`). |
696
-
697
- The Procedure's `input` is the contract with the *caller*. It MAY
698
- declare fields the Schema does not (e.g. a Turnstile token). The
699
- builtin op silently projects `input ∩ Schema.properties` and ignores
700
- the rest; JSON Schema's default `additionalProperties: true` lets
701
- the side-channel fields pass validation. To act on those fields
702
- (read the token, call the vendor), declare a `before_create`
703
- lifecycle Trigger — see below.
704
-
705
- `request_publish` and `publish` are intentionally not in the builtin
706
- vocabulary. They are lifecycle operations, not CRUD primitives.
707
-
708
- ### `Trigger.source.kind: lifecycle` — bind a Procedure to a Schema event
709
-
710
- Shape:
711
-
712
- ```yaml
713
- apiVersion: cms.mantle.aotter.net/v1
714
- kind: Trigger
715
- metadata: { name: contact-bot-check }
716
- spec:
717
- source:
718
- kind: lifecycle
719
- schema: contact-messages
720
- on: [before_create] # one or more hooks
721
- errorPolicy: abort # default: abort for before_*, continue for after_*
722
- target:
723
- procedure: bot-check # any Procedure — re-bound across schemas
724
- ```
725
-
726
- | Hook | Fires |
727
- |---|---|
728
- | `before_create` | Before INSERT. Throw cancels. |
729
- | `after_create` | After INSERT. Default best-effort. |
730
- | `before_update` | Before UPDATE or a status transition whose target is not `published` (including unpublish and archive). Throw cancels. |
731
- | `after_update` | After UPDATE or a status transition whose target is not `published` (including unpublish and archive). Default best-effort. |
732
- | `before_delete` | Before DELETE. Throw cancels. |
733
- | `after_delete` | After DELETE. Default best-effort. |
734
- | `before_publish` | Before any supported status transition to `published` (the shipped workflow is `publishing`). |
735
- | `after_publish` | After any supported status transition to `published` (the shipped workflow is `publishing`). |
736
-
737
- v0.1 has no separate unpublish or archive hooks. Do not treat
738
- `before_update` / `after_update` as edit-only hooks.
739
-
740
- **Atomicity defaults by phase**:
741
- - `before_*`: `errorPolicy: abort`. Handler throw cancels the
742
- surrounding mutation; caller receives the diagnostic.
743
- - `after_*`: `errorPolicy: continue`. The committed mutation remains
744
- successful. On the inline / `ctx.waitUntil` path, a failure is logged
745
- and swallowed. With an optional `DeferredHookDispatcher`, failures
746
- surface to the delivery adapter so its at-least-once retry/DLQ policy
747
- can run; they still never roll back the entry mutation.
748
-
749
- Authors override either default by declaring `errorPolicy: abort |
750
- continue` explicitly.
751
-
752
- **Hook handler input** is phase-specific. Synchronous `before_*` hooks
753
- receive the original pre-projection Procedure input, so a
754
- `before_create` hook on `contact-messages` can read the caller's
755
- `recaptchaToken` even though the row never stores it. Every `after_*`
756
- hook receives only persisted `entry.data`; deferred envelopes never
757
- retain arbitrary request input.
758
-
759
- Handlers also receive
760
- `ctx.event = { id, trigger, hook, schema, entry }`. `id` is stable
761
- across enqueue fallback and deferred retries; `trigger` is the current
762
- `Trigger.metadata.name`. Deferred handlers use `${id}:${trigger}` as
763
- their idempotency key. `entry` is null only on `before_create`; it is
764
- the pre-mutation row for the other `before_*` hooks and the persisted
765
- post-mutation row for every `after_*`.
766
-
767
- **Hook ordering**: when multiple lifecycle Triggers bind the same
768
- `(schema, hook)`, the runtime fires them **alphabetically by
769
- `Trigger.metadata.name`**. Choose names that sort correctly
770
- (`010-bot-check`, `020-rate-limit`).
771
-
772
- For deferred delivery, that ordered Trigger-name list is captured in
773
- one strict versioned event. Every captured Trigger runs before a
774
- failure is returned to the Queue, so retry may replay Triggers that
775
- already succeeded. Queue acceptance is not transactional with D1,
776
- `waitUntil` fallback is best-effort, and exactly-once is not promised.
777
- Cloudflare wiring, the 128 KB platform limit, retry/DLQ configuration,
778
- idempotency, and legacy-envelope draining are specified in
779
- [Deferred lifecycle hooks on Cloudflare Queues](deferred-lifecycle-queues.md).
780
-
781
- `before_publish` and `after_publish` wrap the shipped publishing transition.
782
-
783
- ## Lineage — the academic foundations
784
-
785
- | Atom | Theory |
786
- |---|---|
787
- | `Schema` | Codd, "A Relational Model of Data..." (1970); SQL-92 base tables |
788
- | `View` | Codd 1970; relational algebra (σ π ⋈ γ); SEQUEL/SQL DML |
789
- | `Procedure` | SQL/PSM (1996); typed I/O; Birrell & Nelson RPC (1984); Moggi monads (1991) |
790
- | `Trigger` | Active Database / ECA rules (Diaz & Paton, *Active Database Systems* 1999) |
791
-
792
- The composite design — declarative resources + ECA-fired procedures +
793
- policy-gated execution — is sometimes labeled **Active Database +
794
- Policy-Based Management**. Postgres + PostgREST is the
795
- canonical full-stack reference; we abstract that pattern up to the
796
- application layer with K8s-style YAML manifests.
1
+ # Manifest reference has moved
2
+
3
+ The version-matched [handbook](handbook/reference/manifest.md) is the single
4
+ grammar reference. This compatibility URL contains no duplicate grammar.
5
+
6
+ - [Four atoms](handbook/concepts/four-atoms.md)
7
+ - [Schema](handbook/reference/schema.md)
8
+ - [View](handbook/reference/view.md)
9
+ - [Procedure and builtin handlers](handbook/reference/procedure.md)
10
+ - [Trigger](handbook/reference/trigger.md)
11
+ - [Authorization](handbook/reference/authorization.md)
12
+ - [Diagnostics](handbook/reference/diagnostics.md)
13
+
14
+ Builder consumers must load these pages from the same installed SDK as their
15
+ runtime. The old Markdown section headings are retired; see
16
+ [#837](https://github.com/aotter/mantle/issues/837).