@aotter/mantle 0.1.0-alpha.9 → 0.1.2-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -38
- package/dist/cli/generate.d.ts +10 -1
- package/dist/cli/generate.d.ts.map +1 -1
- package/dist/cli/generate.js +49 -4
- package/dist/cli/generate.js.map +1 -1
- package/dist/cli/main.js +0 -8
- package/dist/cli/main.js.map +1 -1
- package/dist/codegen/emitMantleModule.js +8 -0
- package/dist/codegen/emitMantleModule.js.map +1 -1
- package/docs/adapter-guide.md +23 -26
- package/docs/adr/0008-structured-diagnostic-shape.md +7 -2
- package/docs/adr/0010-locale-and-translates.md +14 -16
- package/docs/adr/0011-adapter-port-spec.md +2 -1
- package/docs/adr/0014-auth-better-auth-and-multi-tenant-mcp.md +100 -0
- package/docs/adr/0018-core-starters-repository-boundary.md +3 -0
- package/docs/adr/0019-sealed-manifest-runtime-pipeline.md +38 -2
- package/docs/adr/0020-builtin-handler-contracts-and-matched-upsert.md +199 -0
- package/docs/adr/0021-retire-starter-scaffolding.md +30 -0
- package/docs/adr/0022-caller-observed-version-occ.md +212 -0
- package/docs/adr/0023-port-failure-contract.md +69 -0
- package/docs/adr/0024-manifest-native-schema-tables.md +260 -0
- package/docs/adr/README.md +19 -3
- package/docs/adr/adr-lite-803-request-diagnostics.md +38 -0
- package/docs/adr/adr-lite-808-route-readiness.md +47 -0
- package/docs/adr/adr-lite-809-bounded-public-content.md +71 -0
- package/docs/adr/adr-lite-812-native-parity.md +150 -0
- package/docs/adr/adr-lite-823-home-statistics.md +64 -0
- package/docs/adr/adr-lite-842-mcp-authoring.md +30 -0
- package/docs/adr/adr-lite-861-admin-webmcp.md +42 -0
- package/docs/adr/adr-lite-909-admin-ui-kit.md +37 -0
- package/docs/api-mcp-authorization.md +3 -664
- package/docs/auth-hosting-model.md +4 -4
- package/docs/cloudflare-low-level-composition.md +3 -104
- package/docs/deferred-lifecycle-queues.md +2 -243
- package/docs/design-atoms.md +16 -796
- package/docs/direct-authoring.md +5 -0
- package/docs/examples/minimal-worker/README.md +37 -0
- package/docs/examples/minimal-worker/manifests/site.yaml +25 -0
- package/docs/examples/minimal-worker/package.json +26 -0
- package/docs/examples/minimal-worker/smoke.mjs +23 -0
- package/docs/examples/minimal-worker/src/index.ts +4 -0
- package/docs/examples/minimal-worker/tsconfig.json +17 -0
- package/docs/examples/minimal-worker/wrangler.jsonc +22 -0
- package/docs/handbook/cloudflare/authentication.md +167 -0
- package/docs/handbook/cloudflare/bindings.md +236 -0
- package/docs/handbook/cloudflare/conventional-worker.md +162 -0
- package/docs/handbook/cloudflare/deferred-hooks-queues.md +171 -0
- package/docs/handbook/cloudflare/deploy-and-operate.md +105 -0
- package/docs/handbook/cloudflare/low-level-composition.md +169 -0
- package/docs/handbook/cloudflare/media-r2.md +216 -0
- package/docs/handbook/cloudflare/public-web.md +144 -0
- package/docs/handbook/concepts/authorization.md +116 -0
- package/docs/handbook/concepts/four-atoms.md +173 -0
- package/docs/handbook/concepts/lifecycle-and-locales.md +142 -0
- package/docs/handbook/concepts/mcp-and-agents.md +109 -0
- package/docs/handbook/concepts/procedures-and-triggers.md +221 -0
- package/docs/handbook/concepts/runtime-and-adapters.md +177 -0
- package/docs/handbook/concepts/views.md +191 -0
- package/docs/handbook/examples/commerce-transaction.md +813 -0
- package/docs/handbook/examples/guarded-api.md +427 -0
- package/docs/handbook/examples/intake-form.md +319 -0
- package/docs/handbook/examples/legal-documents.md +218 -0
- package/docs/handbook/examples/procurement-approvals.md +240 -0
- package/docs/handbook/examples/publication.md +240 -0
- package/docs/handbook/examples/reservation.md +220 -0
- package/docs/handbook/navigation.json +63 -0
- package/docs/handbook/reference/authorization.md +295 -0
- package/docs/handbook/reference/diagnostics.md +183 -0
- package/docs/handbook/reference/manifest.md +132 -0
- package/docs/handbook/reference/procedure.md +263 -0
- package/docs/handbook/reference/schema.md +250 -0
- package/docs/handbook/reference/site-config.md +233 -0
- package/docs/handbook/reference/surface.md +207 -0
- package/docs/handbook/reference/trigger.md +193 -0
- package/docs/handbook/reference/view.md +256 -0
- package/docs/handbook/start/project-and-cli.md +122 -0
- package/docs/handbook/start/quickstart-worker.md +193 -0
- package/docs/labels.md +3 -1
- package/docs/media-uploads.md +3 -184
- package/docs/migration-0.1.2.md +60 -3
- package/docs/performance-harness.md +127 -10
- package/docs/release-process.md +85 -231
- package/docs/schema-indexes.md +3 -180
- package/docs/sealed-pipeline-ownership.md +6 -5
- package/docs/spec-only-host-adoption.md +158 -0
- package/docs/transaction-patterns.md +5 -0
- package/package.json +21 -25
- package/skills/README.md +4 -4
- package/skills/develop/SKILL.md +23 -28
- package/skills/install/SKILL.md +55 -147
- package/skills/provision/SKILL.md +4 -3
- package/skills/theme/SKILL.md +6 -6
- package/skills/update/SKILL.md +32 -68
- package/dist/cli/create.d.ts +0 -2
- package/dist/cli/create.d.ts.map +0 -1
- package/dist/cli/create.js +0 -243
- package/dist/cli/create.js.map +0 -1
- package/dist/cli/update.d.ts +0 -2
- package/dist/cli/update.d.ts.map +0 -1
- package/dist/cli/update.js +0 -413
- package/dist/cli/update.js.map +0 -1
- package/dist/provision/renderProvisionBundle.d.ts +0 -70
- package/dist/provision/renderProvisionBundle.d.ts.map +0 -1
- package/dist/provision/renderProvisionBundle.js +0 -367
- package/dist/provision/renderProvisionBundle.js.map +0 -1
- package/dist/provision.d.ts +0 -2
- package/dist/provision.d.ts.map +0 -1
- package/dist/provision.js +0 -2
- package/dist/provision.js.map +0 -1
package/docs/design-atoms.md
CHANGED
|
@@ -1,796 +1,16 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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).
|