@aotter/mantle 0.1.0-alpha.1 → 0.1.0-alpha.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +105 -26
- package/dist/admin.d.ts +2 -0
- package/dist/admin.d.ts.map +1 -0
- package/dist/admin.js +2 -0
- package/dist/admin.js.map +1 -0
- package/dist/bun.d.ts +2 -0
- package/dist/bun.d.ts.map +1 -0
- package/dist/bun.js +2 -0
- package/dist/bun.js.map +1 -0
- package/dist/cli/create.d.ts +2 -0
- package/dist/cli/create.d.ts.map +1 -0
- package/dist/cli/create.js +246 -0
- package/dist/cli/create.js.map +1 -0
- package/dist/cli/generate.d.ts.map +1 -0
- package/dist/cli/generate.js +101 -0
- package/dist/cli/generate.js.map +1 -0
- package/dist/cli/harness.d.ts +3 -0
- package/dist/cli/harness.d.ts.map +1 -0
- package/dist/{harness-cli.js → cli/harness.js} +13 -8
- package/dist/cli/harness.js.map +1 -0
- package/dist/cli/main.d.ts +3 -0
- package/dist/cli/main.d.ts.map +1 -0
- package/dist/{cli.js → cli/main.js} +6 -8
- package/dist/cli/main.js.map +1 -0
- package/dist/cli/skills.d.ts +3 -0
- package/dist/cli/skills.d.ts.map +1 -0
- package/dist/{skills.js → cli/skills.js} +51 -6
- package/dist/cli/skills.js.map +1 -0
- package/dist/cli/update.d.ts.map +1 -0
- package/dist/{update.js → cli/update.js} +72 -46
- package/dist/cli/update.js.map +1 -0
- package/dist/codegen/emitMantleModule.d.ts +16 -0
- package/dist/codegen/emitMantleModule.d.ts.map +1 -0
- package/dist/codegen/emitMantleModule.js +217 -0
- package/dist/codegen/emitMantleModule.js.map +1 -0
- package/dist/codegen.d.ts +2 -0
- package/dist/codegen.d.ts.map +1 -0
- package/dist/codegen.js +2 -0
- package/dist/codegen.js.map +1 -0
- package/dist/provision/renderProvisionBundle.d.ts +70 -0
- package/dist/provision/renderProvisionBundle.d.ts.map +1 -0
- package/dist/provision/renderProvisionBundle.js +367 -0
- package/dist/provision/renderProvisionBundle.js.map +1 -0
- package/dist/provision.d.ts +2 -0
- package/dist/provision.d.ts.map +1 -0
- package/dist/provision.js +2 -0
- package/dist/provision.js.map +1 -0
- package/dist/vercel-libsql.d.ts +2 -0
- package/dist/vercel-libsql.d.ts.map +1 -0
- package/dist/vercel-libsql.js +2 -0
- package/dist/vercel-libsql.js.map +1 -0
- package/dist/vercel.d.ts +2 -0
- package/dist/vercel.d.ts.map +1 -0
- package/dist/vercel.js +2 -0
- package/dist/vercel.js.map +1 -0
- package/dist/web.d.ts +2 -0
- package/dist/web.d.ts.map +1 -0
- package/dist/web.js +2 -0
- package/dist/web.js.map +1 -0
- package/docs/adapter-guide.md +94 -47
- package/docs/adr/0001-four-atom-manifest-model.md +48 -307
- package/docs/adr/0002-closed-enums-for-bindings.md +6 -15
- package/docs/adr/0007-ai-as-primary-author.md +6 -4
- package/docs/adr/0008-structured-diagnostic-shape.md +2 -2
- package/docs/adr/0009-consumer-supplied-manifests.md +15 -9
- package/docs/adr/0010-locale-and-translates.md +13 -8
- package/docs/adr/0011-adapter-port-spec.md +29 -21
- package/docs/adr/0012-views-as-public-rest.md +5 -9
- package/docs/adr/0014-auth-better-auth-and-multi-tenant-mcp.md +124 -43
- package/docs/adr/0018-core-starters-repository-boundary.md +22 -1
- package/docs/adr/0019-sealed-manifest-runtime-pipeline.md +204 -0
- package/docs/adr/README.md +16 -14
- package/docs/api-mcp-authorization.md +28 -42
- package/docs/assets/mantle-admin-operations.png +0 -0
- package/docs/assets/mantle-hero.jpg +0 -0
- package/docs/auth-hosting-model.md +15 -11
- package/docs/cloudflare-low-level-composition.md +30 -18
- package/docs/deferred-lifecycle-queues.md +20 -38
- package/docs/design-atoms.md +155 -401
- package/docs/labels.md +5 -3
- package/docs/media-uploads.md +9 -2
- package/docs/migration-0.1.2.md +45 -0
- package/docs/performance-harness.md +7 -6
- package/docs/release-process.md +86 -25
- package/docs/schema-indexes.md +1 -1
- package/docs/sealed-pipeline-ownership.md +100 -0
- package/package.json +84 -14
- package/skills/README.md +39 -7
- package/skills/develop/SKILL.md +31 -17
- package/skills/install/SKILL.md +26 -25
- package/skills/media-gc/SKILL.md +85 -0
- package/skills/plugin/SKILL.md +2 -1
- package/skills/provision/SKILL.md +24 -2
- package/skills/theme/SKILL.md +11 -1
- package/skills/update/SKILL.md +1 -0
- package/dist/cli.d.ts +0 -3
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/generate.d.ts.map +0 -1
- package/dist/generate.js +0 -181
- package/dist/generate.js.map +0 -1
- package/dist/harness-cli.d.ts +0 -3
- package/dist/harness-cli.d.ts.map +0 -1
- package/dist/harness-cli.js.map +0 -1
- package/dist/skills.d.ts +0 -2
- package/dist/skills.d.ts.map +0 -1
- package/dist/skills.js.map +0 -1
- package/dist/update.d.ts.map +0 -1
- package/dist/update.js.map +0 -1
- /package/dist/{generate.d.ts → cli/generate.d.ts} +0 -0
- /package/dist/{update.d.ts → cli/update.d.ts} +0 -0
package/docs/design-atoms.md
CHANGED
|
@@ -3,14 +3,7 @@
|
|
|
3
3
|
> If you're an AI working in a project that consumes `@aotter/mantle-*`,
|
|
4
4
|
> read this first. It explains the entire surface area in one page.
|
|
5
5
|
>
|
|
6
|
-
> **Status**: v0.1 grammar
|
|
7
|
-
> grammar (policies, recursive views, temporal predicates, quotas,
|
|
8
|
-
> projection triggers, cron/queue sources, and extended lifecycle hooks) is
|
|
9
|
-
> reserved as **DRAFT** — see "Future grammar" appendix. Editorial
|
|
10
|
-
> lifecycle is the one shipped grammar key whose runtime is **deferred
|
|
11
|
-
> to v0.1.x**: parser and boot accept the shape, while
|
|
12
|
-
> `request_publish` rejects it with `LIFECYCLE_NOT_IN_V010` until the approval
|
|
13
|
-
> queue lands. Do not use editorial for a v0.1 publishing workflow.
|
|
6
|
+
> **Status**: v0.1 shipped grammar. Unknown keys and enum values are rejected.
|
|
14
7
|
>
|
|
15
8
|
> **This is the reference manual** — what the system is. For *why* it
|
|
16
9
|
> ended up this shape (alternatives considered, trade-offs accepted),
|
|
@@ -32,8 +25,8 @@ primitives Postgres has shipped for 30 years.
|
|
|
32
25
|
|---|---|---|---|
|
|
33
26
|
| **`Schema`** | `CREATE TABLE` | no (manipulated via View / Procedure) | no |
|
|
34
27
|
| **`View`** | `CREATE VIEW` | **yes** (auto-mounted on its declared public/staff REST and MCP surface; see ADR-0012) | no |
|
|
35
|
-
| **`Procedure`** | `CREATE FUNCTION ... LANGUAGE plpgsql` | **no** (transport-agnostic; needs a `Trigger` to bind it) | **yes — handler ref to consumer's
|
|
36
|
-
| **`Trigger`** | `CREATE TRIGGER` +
|
|
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 |
|
|
37
30
|
|
|
38
31
|
The **read/write asymmetry is by design** and matches HTTP safe-vs-unsafe
|
|
39
32
|
semantics. Reads (`View`) are idempotent and cacheable; the SDK
|
|
@@ -52,16 +45,14 @@ these four plus your own TypeScript.
|
|
|
52
45
|
apiVersion: cms.mantle.aotter.net/v1
|
|
53
46
|
kind: Schema | View | Procedure | Trigger
|
|
54
47
|
metadata:
|
|
55
|
-
name: posts # required,
|
|
56
|
-
labels: { ... } # optional, free-form
|
|
48
|
+
name: posts # required, non-empty, unique within its kind
|
|
57
49
|
spec:
|
|
58
50
|
... # kind-specific
|
|
59
51
|
```
|
|
60
52
|
|
|
61
|
-
There is **no `namespace` field**. Resource names are unique within
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
`tenant_id` column gated by RBAC, not in the SDK metadata layer.
|
|
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.
|
|
65
56
|
|
|
66
57
|
### Multi-doc YAML — keeping file count down
|
|
67
58
|
|
|
@@ -73,7 +64,7 @@ A logical feature commonly bundles a Procedure + a Trigger (and often a
|
|
|
73
64
|
Schema and a View). Put related atoms in one file separated by `---`:
|
|
74
65
|
|
|
75
66
|
```yaml
|
|
76
|
-
# manifests/contact.
|
|
67
|
+
# manifests/contact.yml
|
|
77
68
|
apiVersion: cms.mantle.aotter.net/v1
|
|
78
69
|
kind: Procedure
|
|
79
70
|
metadata: { name: send-contact-message }
|
|
@@ -93,8 +84,9 @@ spec:
|
|
|
93
84
|
target: { procedure: send-contact-message }
|
|
94
85
|
```
|
|
95
86
|
|
|
96
|
-
|
|
97
|
-
|
|
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.
|
|
98
90
|
|
|
99
91
|
## What each atom is for (v0.1 minimum essential grammar)
|
|
100
92
|
|
|
@@ -112,7 +104,7 @@ metadata: { name: posts }
|
|
|
112
104
|
spec:
|
|
113
105
|
title: Posts # required: human-readable label for the admin UI
|
|
114
106
|
localized: true # opt-in: row carries data.locale (ADR-0010)
|
|
115
|
-
lifecycle:
|
|
107
|
+
lifecycle: publishing # default; use 'operational' for live records
|
|
116
108
|
schema:
|
|
117
109
|
$schema: https://json-schema.org/draft/2020-12/schema
|
|
118
110
|
type: object
|
|
@@ -126,7 +118,8 @@ spec:
|
|
|
126
118
|
# against localized Schemas.
|
|
127
119
|
content: { type: string }
|
|
128
120
|
authorId: { type: string, format: uuid, x-mantle-bind: ctx.user }
|
|
129
|
-
createdAt: { type:
|
|
121
|
+
createdAt: { type: integer, x-mcp-hint: timestamp-ms, x-mantle-bind: now }
|
|
122
|
+
searchableFields: [title, slug] # id is always searched
|
|
130
123
|
uniqueIndexes: [[slug, locale]]
|
|
131
124
|
indexes: [[locale, title]] # ordered, non-unique hot path
|
|
132
125
|
```
|
|
@@ -135,6 +128,33 @@ spec:
|
|
|
135
128
|
in the user's primary language (the install-time chosen locale); the
|
|
136
129
|
SPA shows this everywhere instead of `metadata.name`.
|
|
137
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
|
+
|
|
138
158
|
**`spec.localized: bool`** (default `false`, ADR-0010) — opt-in per
|
|
139
159
|
Schema. Localized Schemas store locale in `data.locale`; non-localized
|
|
140
160
|
Schemas reject `data.locale` writes. Site config must declare the set
|
|
@@ -144,25 +164,22 @@ validates manifest shape.
|
|
|
144
164
|
|
|
145
165
|
**`spec.translates: { parent, on }`** (ADR-0010) — declares this
|
|
146
166
|
Schema as the translation companion to a non-localized parent, joined
|
|
147
|
-
on the named field (typically `slug`).
|
|
167
|
+
on the named field (typically `slug`). Requires an explicit
|
|
168
|
+
`localized: true`. The
|
|
148
169
|
admin UI surfaces the child only as locale tabs in the parent's editor.
|
|
149
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
|
+
|
|
150
176
|
#### Lifecycle
|
|
151
177
|
|
|
152
|
-
**`spec.lifecycle: '
|
|
178
|
+
**`spec.lifecycle: 'publishing' | 'operational'`** — controls the
|
|
153
179
|
entry's state machine.
|
|
154
180
|
|
|
155
|
-
- `
|
|
156
|
-
|
|
157
|
-
in v0.1.0.**
|
|
158
|
-
- `editorial` — the six-state machine with an approval queue
|
|
159
|
-
(`draft → review → approved → scheduled → published → archived`,
|
|
160
|
-
with `published` returnable to `draft` for republish). The grammar and
|
|
161
|
-
state-machine vocabulary are reserved for forward compatibility, but the
|
|
162
|
-
approval/request-publish runtime is on the v0.1.x roadmap. In v0.1,
|
|
163
|
-
`request_publish` rejects with `LIFECYCLE_NOT_IN_V010`; do not declare
|
|
164
|
-
editorial for a current publishing workflow.
|
|
165
|
-
- `none` — **operational records**, not authored content: orders,
|
|
181
|
+
- `publishing` (default) — `draft → published → archived`. No approval queue.
|
|
182
|
+
- `operational` — records that are not authored content: orders,
|
|
166
183
|
inventory snapshots, grant/audit rows — anything written by
|
|
167
184
|
Procedures as a side effect rather than drafted by a person. No
|
|
168
185
|
content workflow applies: entries are live (`published`) the moment
|
|
@@ -175,8 +192,15 @@ entry's state machine.
|
|
|
175
192
|
`update_record_<schema>` for these Schemas instead of draft tools.
|
|
176
193
|
|
|
177
194
|
The modes are **per-Schema and mix freely** within a site. There is no
|
|
178
|
-
site-wide lifecycle setting; one Schema can be `
|
|
179
|
-
is `
|
|
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.
|
|
180
204
|
|
|
181
205
|
**Property-level extensions** (JSON Schema vendor keywords, all optional):
|
|
182
206
|
|
|
@@ -188,9 +212,9 @@ extensions. Three are part of `cms.mantle.aotter.net/v1`:
|
|
|
188
212
|
> See ADR-0002 for rationale — why this is a closed enum and not an
|
|
189
213
|
> open expression language.
|
|
190
214
|
|
|
191
|
-
Marks a property as **server-controlled**.
|
|
192
|
-
|
|
193
|
-
|
|
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
|
|
194
218
|
class of handler-side `userId: ctx.user.id` boilerplate, and gives
|
|
195
219
|
the dispatcher an authoritative "who/when" tag without trusting the
|
|
196
220
|
wire.
|
|
@@ -201,20 +225,12 @@ wire.
|
|
|
201
225
|
|---|---|---|
|
|
202
226
|
| `ctx.user` | UUID of the signed-in end-user (from session cookie); `null` if anonymous | row ownership: `authorId`, `submittedBy`, `creatorId` |
|
|
203
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` |
|
|
204
|
-
| `now` | Server timestamp at write (
|
|
228
|
+
| `now` | Server timestamp at write (Unix epoch milliseconds) | `createdAt`, `submittedAt`, `grantedAt` |
|
|
205
229
|
|
|
206
230
|
**v0.1 stamping behavior**:
|
|
207
|
-
- Stamped on `INSERT` only.
|
|
208
|
-
|
|
209
|
-
surfaces.
|
|
210
|
-
- Caller-supplied value: rejected at input validation. The Procedure's
|
|
211
|
-
effective input schema strips bound fields before checking caller
|
|
212
|
-
input, so callers can't accidentally send them.
|
|
231
|
+
- Stamped on `INSERT` only.
|
|
232
|
+
- Caller-supplied create value: ignored and replaced by the server stamp.
|
|
213
233
|
- Visible in View output: yes, as ordinary columns. No special masking.
|
|
214
|
-
- Used by `requires.auth.owns:` predicate (DRAFT): the predicate
|
|
215
|
-
compares a row's `x-mantle-bind: ctx.user` field to the current
|
|
216
|
-
`:ctx.user` to verify ownership without configuring per-Schema
|
|
217
|
-
owner-column names.
|
|
218
234
|
|
|
219
235
|
**Why a closed enum** (highest-leverage discipline in the spec): bind
|
|
220
236
|
values are server-controlled identity + time facts. If we accepted
|
|
@@ -227,9 +243,9 @@ named bounds the semantic surface forever.
|
|
|
227
243
|
properties:
|
|
228
244
|
title: { type: string, minLength: 1, maxLength: 200 }
|
|
229
245
|
authorId: { type: string, format: uuid, x-mantle-bind: ctx.user }
|
|
230
|
-
createdAt: { type:
|
|
246
|
+
createdAt: { type: integer, x-mcp-hint: timestamp-ms, x-mantle-bind: now }
|
|
231
247
|
# caller sends only: { title }
|
|
232
|
-
# SDK stamps: { title, authorId: <session UUID>, createdAt: <
|
|
248
|
+
# SDK stamps: { title, authorId: <session UUID>, createdAt: <epoch ms> }
|
|
233
249
|
```
|
|
234
250
|
|
|
235
251
|
#### `x-mantle-ref: <other-schema-name>` — cross-Schema reference
|
|
@@ -241,8 +257,13 @@ detection). The admin uses declared refs to show related rows and to nest
|
|
|
241
257
|
required child collections; it does not infer relationships from field names.
|
|
242
258
|
Declare a single-field `indexes` entry for the ref field (for example,
|
|
243
259
|
`indexes: [[authorId]]`) when reverse lookups must stay bounded. Mantle adds
|
|
244
|
-
the native entry-order columns to that access path.
|
|
245
|
-
|
|
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`.
|
|
246
267
|
|
|
247
268
|
**Example**:
|
|
248
269
|
```yaml
|
|
@@ -263,7 +284,9 @@ Descriptive hint for AI agents and admin UI widgets. The string is
|
|
|
263
284
|
accepted as free-form for forward compatibility, but conventional
|
|
264
285
|
values (`markdown`, `richtext`, `code`, `media`, `media-image`,
|
|
265
286
|
`media-video`, `media-file`) tell consumers how to render or generate
|
|
266
|
-
the field's value.
|
|
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.
|
|
267
290
|
|
|
268
291
|
**Examples** (from the publication/blog starter manifests):
|
|
269
292
|
```yaml
|
|
@@ -286,21 +309,16 @@ MCP tool schemas preserve the hint for agents. Admin surfaces expose
|
|
|
286
309
|
media-shaped fields but first-party upload hosting is optional and not
|
|
287
310
|
part of first-run provisioning.
|
|
288
311
|
|
|
289
|
-
---
|
|
290
|
-
|
|
291
|
-
Anything beyond these three (policies, computed columns, RBAC grants,
|
|
292
|
-
owner declarations, `x-mantle-bind` enum extension) is **DRAFT** — see
|
|
293
|
-
appendix.
|
|
294
|
-
|
|
295
312
|
**Postgres analogue**: `CREATE TABLE posts (id UUID PRIMARY KEY, ...,
|
|
296
313
|
UNIQUE (slug, locale));`
|
|
297
314
|
|
|
298
315
|
### 2. `View` — the read surface (auto-exposed by surface)
|
|
299
316
|
|
|
300
|
-
A named,
|
|
301
|
-
|
|
302
|
-
`
|
|
303
|
-
|
|
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
|
|
304
322
|
staff gate and appears only on `/mcp/staff`. See ADR-0012 for the full design
|
|
305
323
|
rationale.
|
|
306
324
|
|
|
@@ -309,34 +327,35 @@ apiVersion: cms.mantle.aotter.net/v1
|
|
|
309
327
|
kind: View
|
|
310
328
|
metadata: { name: recent-published }
|
|
311
329
|
spec:
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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
|
|
318
336
|
limit: 20
|
|
319
337
|
```
|
|
320
338
|
|
|
321
339
|
**Param-driven Views** declare `spec.params` (a JSON Schema with
|
|
322
|
-
`type: object`);
|
|
323
|
-
|
|
340
|
+
`type: object`); SQL references required properties as named `:params`. The
|
|
341
|
+
runtime validates and binds them; it never interpolates caller values:
|
|
324
342
|
|
|
325
343
|
```yaml
|
|
326
344
|
apiVersion: cms.mantle.aotter.net/v1
|
|
327
345
|
kind: View
|
|
328
346
|
metadata: { name: posts-by-locale }
|
|
329
347
|
spec:
|
|
330
|
-
|
|
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
|
|
331
354
|
params:
|
|
332
355
|
type: object
|
|
333
356
|
properties:
|
|
334
357
|
locale: { type: string }
|
|
335
358
|
required: [locale]
|
|
336
|
-
filter:
|
|
337
|
-
and:
|
|
338
|
-
- eq: { field: status, value: published }
|
|
339
|
-
- eq: { field: locale, value: { $param: locale } }
|
|
340
359
|
limit: 100
|
|
341
360
|
```
|
|
342
361
|
|
|
@@ -346,6 +365,22 @@ names — `page` / `show` / `cursor` — must NOT appear in
|
|
|
346
365
|
`spec.params.properties` (the parser rejects with
|
|
347
366
|
`VIEW_PARAMS_RESERVED_NAME`).
|
|
348
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
|
+
|
|
349
384
|
Views may declare the same `requires.auth.all` predicates and optional
|
|
350
385
|
`requires.guard.procedure` as Procedures. Static auth runs before parameter
|
|
351
386
|
validation; the guard receives validated params and authorizes the whole
|
|
@@ -361,13 +396,10 @@ Response envelope:
|
|
|
361
396
|
`hasMore` is the lazy form: `rows.length === show` ⇒ `true`. No COUNT
|
|
362
397
|
query, no `LIMIT n+1` probe.
|
|
363
398
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
comparisons are not supported yet; compare a field to a literal or
|
|
369
|
-
param. Anything else (`contains`, `recursive`, `gatedBy`,
|
|
370
|
-
`join.aggregate`, `policies.skip`) is DRAFT.
|
|
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.
|
|
371
403
|
|
|
372
404
|
**Postgres analogue**: `CREATE VIEW recent_published AS SELECT ...
|
|
373
405
|
FROM posts WHERE status = 'published' ORDER BY updated_at DESC LIMIT
|
|
@@ -383,8 +415,8 @@ provides the handler function with auto-generated TS types.
|
|
|
383
415
|
`Procedure` is **transport-agnostic and not directly exposed**. It does
|
|
384
416
|
not contain HTTP paths, methods, or MCP tool names. To call a Procedure
|
|
385
417
|
from outside the SDK runtime, declare a `Trigger` whose `target` points
|
|
386
|
-
at it. The same Procedure can be bound by multiple Triggers (HTTP +
|
|
387
|
-
|
|
418
|
+
at it. The same Procedure can be bound by multiple Triggers (HTTP + MCP +
|
|
419
|
+
lifecycle, all sharing one handler).
|
|
388
420
|
|
|
389
421
|
```yaml
|
|
390
422
|
apiVersion: cms.mantle.aotter.net/v1
|
|
@@ -426,9 +458,6 @@ export const handlers = {
|
|
|
426
458
|
- `ctx.auth.scope: <scope>` — verified credential carries the exact opaque,
|
|
427
459
|
consumer-owned scope; repeat to require multiple scopes
|
|
428
460
|
|
|
429
|
-
Anything beyond this (`any:`, `owns:`, `withinMinutes:`, `contains:`,
|
|
430
|
-
`requires.window`, `requires.quota`, `errors`, `retry`) is DRAFT.
|
|
431
|
-
|
|
432
461
|
Both Procedures and Views may add one dynamic guard beside `auth`:
|
|
433
462
|
|
|
434
463
|
```yaml
|
|
@@ -485,7 +514,7 @@ spec:
|
|
|
485
514
|
```
|
|
486
515
|
|
|
487
516
|
The same Procedure can have multiple Triggers — that's how it becomes
|
|
488
|
-
|
|
517
|
+
an HTTP endpoint and an MCP tool without duplicating
|
|
489
518
|
handler logic. Each transport is one Trigger; the Procedure body is
|
|
490
519
|
shared.
|
|
491
520
|
|
|
@@ -496,17 +525,14 @@ tool on `surface: public | staff`), or `lifecycle` (entry-writer hook). For `lif
|
|
|
496
525
|
Lifecycle hooks are wired through `LifecycleHookingEntryRepository`, so
|
|
497
526
|
MCP, admin, and builtin write paths share the same hook behavior.
|
|
498
527
|
|
|
499
|
-
- `cron` / `queue` are **DRAFT (v0.2+)** — speculative, gated by
|
|
500
|
-
concrete consumer demand. Same appendix § "DRAFT (v0.2+)."
|
|
501
|
-
|
|
502
528
|
The state-machine "lifecycle" from the Schema atom
|
|
503
|
-
(`Schema.spec.lifecycle:
|
|
529
|
+
(`Schema.spec.lifecycle: publishing | operational`) is a separate domain
|
|
504
530
|
that shares the word. The Schema setting governs which states an
|
|
505
531
|
entry can be in; shipped lifecycle Triggers govern what fires around
|
|
506
532
|
mutations.
|
|
507
533
|
|
|
508
534
|
**Postgres analogue**: `CREATE TRIGGER ... AFTER INSERT ON posts
|
|
509
|
-
EXECUTE FUNCTION ...` (lifecycle)
|
|
535
|
+
EXECUTE FUNCTION ...` (lifecycle), plus
|
|
510
536
|
`CREATE FUNCTION` exposed via PostgREST routes (http) — Postgres has
|
|
511
537
|
had all of these via extensions for years, just split across multiple
|
|
512
538
|
mechanisms. We unify them under one atom.
|
|
@@ -558,9 +584,8 @@ That split is intentional and load-bearing.
|
|
|
558
584
|
draft-2020-12 JSON Schema documents. This is what AI authors and
|
|
559
585
|
human authors write, what the admin UI feeds JSON Forms, and what
|
|
560
586
|
the OpenAPI emitter relays unchanged.
|
|
561
|
-
- **Runtime engine** = zod
|
|
562
|
-
package ships a JSON-Schema → zod converter
|
|
563
|
-
(`src/json-schema-zod.ts`); the runtime calls the converted zod
|
|
587
|
+
- **Runtime engine** = zod 4. The `@aotter/mantle-spec`
|
|
588
|
+
package ships a JSON-Schema → zod converter; the runtime calls the converted zod
|
|
564
589
|
schema on every Procedure invocation, View parameter parse, and
|
|
565
590
|
manifest boot check.
|
|
566
591
|
|
|
@@ -584,7 +609,7 @@ different constraints.
|
|
|
584
609
|
| `UNAUTHENTICATED` | `401` | no active session (admin API: missing/expired session cookie) |
|
|
585
610
|
| `AUTH_DENIED` | `403` | `requires.auth` predicate evaluated false (or admin: caller lacks required staff role) |
|
|
586
611
|
| `ENTITLEMENT_REQUIRED` | `402` | consumer guard denies current payment/membership/transaction entitlement |
|
|
587
|
-
| `NOT_FOUND` | `404` | resource not found —
|
|
612
|
+
| `NOT_FOUND` | `404` | resource not found — entry id or View name at `/api/views/<name>` |
|
|
588
613
|
| `HANDLER_NOT_REGISTERED` | `500` | `handler.ref` key not registered at boot |
|
|
589
614
|
| `DISPATCHER_NOT_BUILT` | `501` | runtime feature not implemented in this SDK build |
|
|
590
615
|
| `INTERNAL_ERROR` | `500` | uncaught handler exception |
|
|
@@ -595,10 +620,7 @@ may also fire in earlier loops (e.g. `HANDLER_NOT_REGISTERED`
|
|
|
595
620
|
fires at boot — `phase: "boot"` — and that's where it should be
|
|
596
621
|
caught; the runtime occurrence is defense-in-depth).
|
|
597
622
|
|
|
598
|
-
|
|
599
|
-
`QUOTA_EXCEEDED → 429` arrives with `requires.quota`).
|
|
600
|
-
|
|
601
|
-
## RBAC — what v0.1 ships, what's DRAFT
|
|
623
|
+
## RBAC
|
|
602
624
|
|
|
603
625
|
v0.1 auth gates use `requires.auth.all` with `ctx.user`,
|
|
604
626
|
`ctx.staff: [<roles>]`, `ctx.auth`, and `ctx.auth.scope` predicates on
|
|
@@ -606,16 +628,6 @@ Procedures and Views. This covers staff-only, logged-in-only,
|
|
|
606
628
|
credential-protected, and delegated-scope targets. One Procedure-backed guard
|
|
607
629
|
handles mutable consumer business state without widening the static grammar.
|
|
608
630
|
|
|
609
|
-
**Not yet shipped** (DRAFT):
|
|
610
|
-
- Row-level read visibility (private posts, friend-only audiences)
|
|
611
|
-
- Field-level write policies (this user can update body but not
|
|
612
|
-
parent_id)
|
|
613
|
-
- Cross-Schema policy inheritance (gatedBy)
|
|
614
|
-
- Quotas, edit-windows, rate limits
|
|
615
|
-
|
|
616
|
-
These all live as `Schema.spec.policies.*` and `Procedure.spec.requires.*`
|
|
617
|
-
sub-specs in the DRAFT spec. See "Future grammar" appendix.
|
|
618
|
-
|
|
619
631
|
## How to think when extending this CMS
|
|
620
632
|
|
|
621
633
|
1. **What entities does the feature need?** → `Schema` for each.
|
|
@@ -624,13 +636,11 @@ sub-specs in the DRAFT spec. See "Future grammar" appendix.
|
|
|
624
636
|
external read endpoints).
|
|
625
637
|
3. **What operations?** → `Procedure` for each. One Procedure = one
|
|
626
638
|
typed function call. Compose in handler code, not in YAML.
|
|
627
|
-
4. **What invokes them?** → `Trigger` per
|
|
628
|
-
can
|
|
629
|
-
handler).
|
|
639
|
+
4. **What invokes them?** → `Trigger` per declared HTTP, MCP, or lifecycle
|
|
640
|
+
source. Site-owned events can call the same Procedure binding directly.
|
|
630
641
|
5. **Who's allowed?** → `Procedure/View.spec.requires.auth` for static
|
|
631
642
|
identity/scope; optional `requires.guard.procedure` for one live,
|
|
632
|
-
consumer-owned business check.
|
|
633
|
-
future grammar.
|
|
643
|
+
consumer-owned business check.
|
|
634
644
|
|
|
635
645
|
If you find yourself wanting a 5th kind, **stop**. Sketch the same
|
|
636
646
|
thing as a composition of the four; almost always it works.
|
|
@@ -648,181 +658,20 @@ thing as a composition of the four; almost always it works.
|
|
|
648
658
|
> fold into JSON Schema inside `Schema` rather than becoming new
|
|
649
659
|
> kinds.
|
|
650
660
|
|
|
651
|
-
Postgres exposes ~25 object kinds total; an application developer
|
|
652
|
-
|
|
653
|
-
Cloudflare D1 / KV abstract away.
|
|
654
|
-
|
|
655
|
-
## Storage backend — D1 today, Postgres-via-Hyperdrive tomorrow
|
|
656
|
-
|
|
657
|
-
> See the `mantle-cloudflare` README for rationale — why D1 for
|
|
658
|
-
> v0.1 (no-CC OSS onboarding, platform-native), and the documented
|
|
659
|
-
> PG path.
|
|
660
|
-
|
|
661
|
-
### How `Schema` data is stored on D1 today
|
|
662
|
-
|
|
663
|
-
All collections share one `entries` table (defined by the
|
|
664
|
-
Cloudflare adapter's storage migrations in
|
|
665
|
-
`@aotter/mantle-cloudflare`):
|
|
666
|
-
|
|
667
|
-
```sql
|
|
668
|
-
CREATE TABLE entries (
|
|
669
|
-
id TEXT PRIMARY KEY,
|
|
670
|
-
collection TEXT NOT NULL, -- discriminator: 'posts', 'comments', ...
|
|
671
|
-
status TEXT NOT NULL, -- 'draft' / 'published' / ...
|
|
672
|
-
version INTEGER NOT NULL,
|
|
673
|
-
data TEXT NOT NULL, -- JSON blob: every Schema property (incl. locale, per ADR-0010)
|
|
674
|
-
created_at INTEGER NOT NULL,
|
|
675
|
-
updated_at INTEGER NOT NULL,
|
|
676
|
-
author_id TEXT REFERENCES users(id)
|
|
677
|
-
);
|
|
678
|
-
```
|
|
679
|
-
|
|
680
|
-
Reserved metadata are native columns; **everything in your Schema's
|
|
681
|
-
`spec.schema.properties` lives inside the `data` JSON blob**. Locale
|
|
682
|
-
on localized Schemas lives at `data.locale` (per ADR-0010); for
|
|
683
|
-
indexing it surfaces as a JSON-extracted virtual generated column
|
|
684
|
-
(`json_extract(data, '$.locale')`) with a partial unique index, not as
|
|
685
|
-
a top-level column.
|
|
686
|
-
|
|
687
|
-
`uniqueIndexes` and ordered, non-unique `indexes` declarations compile
|
|
688
|
-
into affinity-correct virtual generated columns plus partial B-tree
|
|
689
|
-
indexes via the spec engine's DDL emitter (`@aotter/mantle-spec`).
|
|
690
|
-
Different collections coexist on the same table without colliding
|
|
691
|
-
because each generated-column expression is gated by `WHEN collection
|
|
692
|
-
= '<name>'`. See [Schema indexes on D1](schema-indexes.md) for the
|
|
693
|
-
grammar, leftmost-prefix rules, SQL helper, and query-plan examples.
|
|
694
|
-
|
|
695
|
-
### What works well on D1
|
|
696
|
-
|
|
697
|
-
- D1's SQLite has the **JSON1 extension built in**: `json_extract`,
|
|
698
|
-
`json_each`, `json_object`, `json_array`, `json_set`, `json_valid`,
|
|
699
|
-
plus `->` / `->>` operators.
|
|
700
|
-
- Reserved-column queries (`status = 'published'`, `ORDER BY
|
|
701
|
-
updated_at`) use native indexes — fast.
|
|
702
|
-
- `uniqueIndexes`- and `indexes`-declared paths use virtual columns +
|
|
703
|
-
partial indexes. Core-compiled Views and repository lookups reference
|
|
704
|
-
those columns instead of repeating `json_extract`.
|
|
705
|
-
|
|
706
|
-
### Where D1's JSON support has limits (vs Postgres)
|
|
707
|
-
|
|
708
|
-
| Concern | D1 / SQLite | Postgres |
|
|
709
|
-
|---|---|---|
|
|
710
|
-
| JSON storage | TEXT — re-parsed on every `json_extract` | JSONB — stored as compact binary; faster repeated extraction |
|
|
711
|
-
| Index any JSON path | must declare each path explicitly via virtual column + index | GIN index on JSONB indexes all paths automatically |
|
|
712
|
-
| Array containment | `EXISTS (SELECT 1 FROM json_each(...) WHERE value = ?)` — full scan unless indexed | `data @> ARRAY[x]` with GIN — native operator |
|
|
713
|
-
| Path operators | `->`, `->>` only | `->`, `->>`, `#>`, `#>>`, `@>`, `?`, `?|`, `?&` |
|
|
714
|
-
| `DEFAULT now()` | not at column level — SDK-stamped via `x-mantle-bind: now` | native column default |
|
|
715
|
-
| Foreign key on JSON path | not supported on `VIRTUAL` columns; needs `STORED` (extra space) | not natively, but B-tree on expression OK |
|
|
716
|
-
| Row-level security | none — SDK-side filter rewriting | native RLS policies |
|
|
717
|
-
| Multi-statement transactions | session API serializes; not true ACID across statements | full ACID transactions |
|
|
718
|
-
|
|
719
|
-
### Practical scale envelope on D1
|
|
720
|
-
|
|
721
|
-
- **Blog-scale (< 10k entries / collection)**: today's design is
|
|
722
|
-
comfortable. Anonymous public responses can hit version-local Workers
|
|
723
|
-
Cache before the Worker; origin misses use indexed D1 reads.
|
|
724
|
-
- **Mid-scale (10k – 100k entries)**: declare measured list/filter
|
|
725
|
-
access paths with ordered `indexes`. Cross-collection JSON-path joins
|
|
726
|
-
may eventually gain `x-mantle-ref` auto-lift; that remains DRAFT.
|
|
727
|
-
- **Hard D1 limits**: 1 MB max row size; 5,000 rows per query result;
|
|
728
|
-
single-writer per database (concurrent writes serialize).
|
|
729
|
-
- **Cross-region read**: D1 is region-pinned; first origin hit from a
|
|
730
|
-
far region may pay replica latency. Eligible anonymous responses are
|
|
731
|
-
then served by version-local Workers Cache.
|
|
732
|
-
|
|
733
|
-
### Scale-up path: D1 → Postgres via Cloudflare Hyperdrive
|
|
734
|
-
|
|
735
|
-
Cloudflare does not run a first-party Postgres service. The supported
|
|
736
|
-
path when D1 limits bind is **Cloudflare Hyperdrive** (a connection
|
|
737
|
-
pooler + query-cache that lets a Worker connect to any external
|
|
738
|
-
Postgres at near-edge latency):
|
|
739
|
-
|
|
740
|
-
- **Neon** (serverless Postgres with scale-to-zero) — closest
|
|
741
|
-
ergonomic match; tightest CF integration.
|
|
742
|
-
- **Supabase** (PG + auth + storage) — works out of the box.
|
|
743
|
-
- **AWS RDS / Google Cloud SQL / Azure DB / self-hosted PG** — also
|
|
744
|
-
supported via Hyperdrive.
|
|
745
|
-
|
|
746
|
-
When this path is taken (post-v0.1; not implemented yet), the
|
|
747
|
-
**author-facing YAML stays unchanged**. The SDK's compile target swaps:
|
|
748
|
-
|
|
749
|
-
| What changes (SDK runtime) | Author impact |
|
|
750
|
-
|---|---|
|
|
751
|
-
| `data TEXT` → `data JSONB` | none |
|
|
752
|
-
| Virtual generated columns → optional (PG can use GIN) | author may stop declaring `indexes` if defaults suffice; the same YAML remains valid |
|
|
753
|
-
| `json_each` → array operators (`@>`, `&&`) | none — predicates stay declarative |
|
|
754
|
-
| SDK-stamped `now` → `DEFAULT now()` (optional) | none |
|
|
755
|
-
| SDK-side policy rewriting → native RLS (optional) | none |
|
|
756
|
-
| `Trigger.target.project` single-writer DO → real same-transaction trigger | atomicity guarantee strengthens; YAML grammar unchanged (the `atomicity:` declaration just stops needing the eventual-consistency caveat) |
|
|
757
|
-
|
|
758
|
-
Migration shape: `entries` table dumped from D1, restored to PG. Stay
|
|
759
|
-
single-table-with-discriminator (drop-in) or split into
|
|
760
|
-
table-per-collection (more PG-idiomatic, longer migration). Both shapes
|
|
761
|
-
remain valid `cms.mantle.aotter.net/v1` deployments.
|
|
762
|
-
|
|
763
|
-
**v0.1 commitment**: D1 only via the Cloudflare adapter. Hyperdrive +
|
|
764
|
-
PG path is documented as the upgrade route but not implemented. SDKs
|
|
765
|
-
and starters target D1 exclusively. The Netlify adapter package
|
|
766
|
-
(`@aotter/mantle-netlify`) is a README stub for v0.2; its
|
|
767
|
-
existence is an engineering forcing function ensuring the runtime
|
|
768
|
-
package stays portable across adapters.
|
|
769
|
-
|
|
770
|
-
## Roadmap — what's not in v0.1.0
|
|
771
|
-
|
|
772
|
-
> See [ADR-0001 §"Future grammar discipline"](adr/0001-four-atom-manifest-model.md#future-grammar-discipline-was-poc-adr-0005)
|
|
773
|
-
> for the v0.1 minimum-vs-roadmap discipline, the promotion process,
|
|
774
|
-
> and the YAGNI argument that gates speculative shipping.
|
|
775
|
-
|
|
776
|
-
The atoms are locked at 4. Their **inner grammar** is intentionally
|
|
777
|
-
narrow at v0.1.0 and grows in two tiers:
|
|
778
|
-
|
|
779
|
-
1. **v0.1.0 shipped** — grammar parses and runtime behavior is wired
|
|
780
|
-
in the current rebuild.
|
|
781
|
-
2. **v0.1.x committed** — on the patch-release roadmap. Spec is
|
|
782
|
-
documented; implementation lands within the v0.1 series. The unsupported
|
|
783
|
-
runtime path fails closed with a code naming the feature.
|
|
784
|
-
3. **DRAFT (v0.2+)** — speculative, gated by concrete consumer
|
|
785
|
-
demand. Parser/static validation rejects with `DRAFT_KEY_USED`. May or may
|
|
786
|
-
not ship — depends on whether real use cases apply pressure.
|
|
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.
|
|
787
663
|
|
|
788
|
-
|
|
664
|
+
## Storage adapters
|
|
789
665
|
|
|
790
|
-
|
|
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).
|
|
791
671
|
|
|
792
|
-
|
|
793
|
-
`LifecycleHookingEntryRepository` decorator wrapping the entry-writer
|
|
794
|
-
chokepoint so MCP / admin / builtin paths all fire the same hooks.
|
|
795
|
-
Full shape lives further down.
|
|
672
|
+
## Detailed shipped grammar
|
|
796
673
|
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
Grammar lives in v0.1.0. Runtime is the
|
|
800
|
-
`InvokeBuiltinUseCase` that dispatches `op: create | update | upsert
|
|
801
|
-
| delete | archive` against the entry-writer chokepoint with `x-mantle-bind`
|
|
802
|
-
stamping and `input ∩ Schema.properties` projection. Full shape lives
|
|
803
|
-
further down.
|
|
804
|
-
|
|
805
|
-
#### `Trigger.source.kind: mcp` and shared authorization
|
|
806
|
-
|
|
807
|
-
An MCP Trigger binds a declared Procedure to either the public or staff MCP
|
|
808
|
-
surface. Procedures/Views share `ctx.auth`/scope predicates and optional guard
|
|
809
|
-
orchestration across REST and MCP. Staff role is loaded live for each protected
|
|
810
|
-
call; staff Views remain absent and un-callable on public MCP.
|
|
811
|
-
|
|
812
|
-
### Detailed shipped grammar and v0.1.x reservation
|
|
813
|
-
|
|
814
|
-
> The `handler.kind: builtin` and `Trigger.source.kind: lifecycle`
|
|
815
|
-
> sections below describe shipped v0.1.0 behavior. Only the
|
|
816
|
-
> `Schema.spec.lifecycle: editorial` subsection remains v0.1.x-committed.
|
|
817
|
-
|
|
818
|
-
#### `Schema.spec.lifecycle: editorial` runtime
|
|
819
|
-
|
|
820
|
-
Grammar and boot already accept the key, but `request_publish` emits
|
|
821
|
-
`LIFECYCLE_NOT_IN_V010` because the approval-queue runtime ships in v0.1.x.
|
|
822
|
-
When that runtime lands, the same manifest can use the publish workflow without
|
|
823
|
-
a grammar change.
|
|
824
|
-
|
|
825
|
-
#### `handler.kind: builtin` — thin shortcut over the storage adapter for trivial CRUD-shaped Procedures
|
|
674
|
+
### `handler.kind: builtin` — thin shortcut over the storage adapter for trivial CRUD-shaped Procedures
|
|
826
675
|
|
|
827
676
|
Use when the body is "insert a row" / "update a row" / "delete a
|
|
828
677
|
row"; reach for `ref` when there is real business logic. Shape:
|
|
@@ -839,11 +688,11 @@ spec:
|
|
|
839
688
|
|
|
840
689
|
| op | Behavior |
|
|
841
690
|
|---|---|
|
|
842
|
-
| `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:
|
|
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`. |
|
|
843
692
|
| `update` | UPDATE in place. `input.id` + `input.expectedVersion` (OCC) required. Bumps version. |
|
|
844
693
|
| `upsert` | If `input.id` resolves, behaves as `update`; else as `create`. |
|
|
845
694
|
| `delete` | Hard DELETE by id. |
|
|
846
|
-
| `archive` | Soft-archive (status='archived').
|
|
695
|
+
| `archive` | Soft-archive a publishing entry (`status='archived'`). |
|
|
847
696
|
|
|
848
697
|
The Procedure's `input` is the contract with the *caller*. It MAY
|
|
849
698
|
declare fields the Schema does not (e.g. a Turnstile token). The
|
|
@@ -853,11 +702,10 @@ the side-channel fields pass validation. To act on those fields
|
|
|
853
702
|
(read the token, call the vendor), declare a `before_create`
|
|
854
703
|
lifecycle Trigger — see below.
|
|
855
704
|
|
|
856
|
-
`request_publish` and `publish` are intentionally not in the
|
|
857
|
-
|
|
858
|
-
primitives.
|
|
705
|
+
`request_publish` and `publish` are intentionally not in the builtin
|
|
706
|
+
vocabulary. They are lifecycle operations, not CRUD primitives.
|
|
859
707
|
|
|
860
|
-
|
|
708
|
+
### `Trigger.source.kind: lifecycle` — bind a Procedure to a Schema event
|
|
861
709
|
|
|
862
710
|
Shape:
|
|
863
711
|
|
|
@@ -879,12 +727,15 @@ spec:
|
|
|
879
727
|
|---|---|
|
|
880
728
|
| `before_create` | Before INSERT. Throw cancels. |
|
|
881
729
|
| `after_create` | After INSERT. Default best-effort. |
|
|
882
|
-
| `before_update` | Before UPDATE. Throw cancels. |
|
|
883
|
-
| `after_update` | After UPDATE. 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. |
|
|
884
732
|
| `before_delete` | Before DELETE. Throw cancels. |
|
|
885
733
|
| `after_delete` | After DELETE. Default best-effort. |
|
|
886
|
-
| `before_publish` | Before any supported status transition to `published` (the shipped workflow is `
|
|
887
|
-
| `after_publish` | After any supported status transition to `published` (the shipped workflow is `
|
|
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.
|
|
888
739
|
|
|
889
740
|
**Atomicity defaults by phase**:
|
|
890
741
|
- `before_*`: `errorPolicy: abort`. Handler throw cancels the
|
|
@@ -915,9 +766,8 @@ post-mutation row for every `after_*`.
|
|
|
915
766
|
|
|
916
767
|
**Hook ordering**: when multiple lifecycle Triggers bind the same
|
|
917
768
|
`(schema, hook)`, the runtime fires them **alphabetically by
|
|
918
|
-
`Trigger.metadata.name`**.
|
|
919
|
-
|
|
920
|
-
`020-rate-limit`).
|
|
769
|
+
`Trigger.metadata.name`**. Choose names that sort correctly
|
|
770
|
+
(`010-bot-check`, `020-rate-limit`).
|
|
921
771
|
|
|
922
772
|
For deferred delivery, that ordered Trigger-name list is captured in
|
|
923
773
|
one strict versioned event. Every captured Trigger runs before a
|
|
@@ -928,103 +778,7 @@ Cloudflare wiring, the 128 KB platform limit, retry/DLQ configuration,
|
|
|
928
778
|
idempotency, and legacy-envelope draining are specified in
|
|
929
779
|
[Deferred lifecycle hooks on Cloudflare Queues](deferred-lifecycle-queues.md).
|
|
930
780
|
|
|
931
|
-
`before_publish` and `after_publish`
|
|
932
|
-
transition. When editorial approval lands, the same hooks wrap its final
|
|
933
|
-
transition to `published`; no second hook grammar is planned.
|
|
934
|
-
|
|
935
|
-
### DRAFT (v0.2+, speculative)
|
|
936
|
-
|
|
937
|
-
Each item below lands when the first concrete real-world use case
|
|
938
|
-
forces it, not on speculation. Today, do not implement; parser/static
|
|
939
|
-
validation rejects it with `DRAFT_KEY_USED`.
|
|
940
|
-
|
|
941
|
-
#### Schema future
|
|
942
|
-
- **`x-mantle-ref` auto-lift to virtual column** — when a property
|
|
943
|
-
carrying `x-mantle-ref: <other-schema>` is referenced in any registered
|
|
944
|
-
View's `filter:` or future `join:`, the SDK auto-creates the virtual
|
|
945
|
-
column + non-unique index without requiring an explicit `indexes`
|
|
946
|
-
declaration. Solves cross-collection JSON-path join
|
|
947
|
-
performance without making authors think about it. Pure SDK
|
|
948
|
-
behavior; no new YAML grammar.
|
|
949
|
-
- **`spec.policies.visible`** — row-level read predicate auto-AND'd
|
|
950
|
-
into Views from this Schema. Lands with first private/audience
|
|
951
|
-
feature.
|
|
952
|
-
- **`spec.policies.readable.fields`** — field-level wire-mask. Lands
|
|
953
|
-
when staff-only fields surface (e.g. `threadLength` visible to
|
|
954
|
-
editors only).
|
|
955
|
-
- **`spec.policies.writable.{fields, create}`** — field-level write
|
|
956
|
-
gate auto-applied to all writers.
|
|
957
|
-
- **`spec.policies.owner`** — names the ownership column for `owns:`
|
|
958
|
-
predicate. Currently implicit (`authorId`).
|
|
959
|
-
- **Computed columns via projection Trigger** — see `Trigger.target`.
|
|
960
|
-
|
|
961
|
-
#### View future
|
|
962
|
-
- **`recursive: { parent, rootWhen, pathBy, depthAs?, pathAs?, maxDepth }`**
|
|
963
|
-
— declarative recursive CTE. Lands with first threaded-reads feature
|
|
964
|
-
(comments, taxonomies).
|
|
965
|
-
- **`params: { <name>: <jsonschema> }`** — caller-bound view parameters.
|
|
966
|
-
- **`gatedBy: [{ schema, policy, on }]`** — cross-Schema visibility
|
|
967
|
-
inheritance.
|
|
968
|
-
- **`join: [{ as, from, on, project?, aggregate?: count|exists, bind? }]`**
|
|
969
|
-
— joined relations with optional reducer (per-row count or boolean).
|
|
970
|
-
- **`policies: { skip: [<policy-name> | field, ...] }`** — explicit
|
|
971
|
-
bypass for admin/audit Views.
|
|
972
|
-
- Filter AST extension: `contains` (array containment), `not`, `in`, `like`.
|
|
973
|
-
|
|
974
|
-
#### Procedure future
|
|
975
|
-
- **`requires.auth.any`** with disjunction; the shipped `all` predicate
|
|
976
|
-
vocabulary extends to `owns: { schema, idFrom }`, `contains: {
|
|
977
|
-
schema, idFrom, field, valueFrom }`.
|
|
978
|
-
- **`requires.window.{withinMinutes, column?}`** — temporal
|
|
979
|
-
precondition, sibling-modifier of `owns:`. DRY rule: window MUST
|
|
980
|
-
sit beside exactly one `(schema, id)`-binding predicate in the same
|
|
981
|
-
any/all group.
|
|
982
|
-
- **`requires.quota.{key, limit, per, overrideFrom?}`** — declarative
|
|
983
|
-
rate cap. Override Schema lookup keyed by `ctx.user`. Counter
|
|
984
|
-
substrate: Durable Object.
|
|
985
|
-
- **`errors.{onHandlerThrow, onRecursionDepth, maxDepth}`** + **`retry.{
|
|
986
|
-
limit, backoff, retryOn, idempotencyKey?}`** — failure-policy
|
|
987
|
-
declared. Retry + `op: create` requires `idempotencyKey:` (parse
|
|
988
|
-
error otherwise).
|
|
989
|
-
|
|
990
|
-
#### Trigger future
|
|
991
|
-
- **`source.kind: cron`** with `expr:` — scheduled invocation.
|
|
992
|
-
- **`source.kind: queue`** — async fan-out / message-driven invocation.
|
|
993
|
-
- **`source.kind: lifecycle.foo`** — DRAFT extensions to the shipped
|
|
994
|
-
lifecycle hooks (e.g. `before_archive`, `after_request_publish`).
|
|
995
|
-
The 8 hooks listed in the detailed shipped section are the floor,
|
|
996
|
-
not the ceiling.
|
|
997
|
-
|
|
998
|
-
(Full lifecycle Trigger spec for the 8 shipped hooks lives
|
|
999
|
-
in the detailed section above. The remaining DRAFT items
|
|
1000
|
-
below are the speculative v0.2+ shapes that haven't yet been promoted
|
|
1001
|
-
to a committed roadmap.)
|
|
1002
|
-
|
|
1003
|
-
- **`target.project: { schema, where, set: { <col>: { aggregate, from,
|
|
1004
|
-
where } } }`** — declarative aggregate-projection across Schemas.
|
|
1005
|
-
Replaces hand-written counter handlers. Same-transaction on PG;
|
|
1006
|
-
bounded-eventual on D1 via single-writer DO.
|
|
1007
|
-
- **`atomicity: same-transaction | best-effort`** — explicit guarantee
|
|
1008
|
-
declaration when projection Triggers ship.
|
|
1009
|
-
|
|
1010
|
-
#### Cross-cutting future
|
|
1011
|
-
- **Closed `ctx.*` predicate identity**: v0.1 `{ user, staff, auth,
|
|
1012
|
-
auth.scope }`
|
|
1013
|
-
extends to `{ ..., system }` when SDK-internal Trigger executor
|
|
1014
|
-
paths land. Multi-tenant deployments would add `{ tenant }` via
|
|
1015
|
-
grammar-revise round if/when that product shape is pursued. New
|
|
1016
|
-
entries require explicit grammar-revise (closed enum is
|
|
1017
|
-
load-bearing infrastructure).
|
|
1018
|
-
- **Placeholder namespace `$.*`**: `$.input.<f>`, `$.row.<f>`, `$.op`,
|
|
1019
|
-
`$.params.<f>`, `$.source` — write-time data-flow bindings inside
|
|
1020
|
-
policy ASTs and handler signatures. Distinct from `:ctx.*` identity
|
|
1021
|
-
bindings; one-site evaluation rule.
|
|
1022
|
-
- **`Schema.spec.staffBypass: [<role>]`** — DRY shortcut once
|
|
1023
|
-
`ctx.staff: [editor, owner]` repetition crosses 4+ schemas.
|
|
1024
|
-
|
|
1025
|
-
Each extension goes through a 3-agent review (yml-editor proposes /
|
|
1026
|
-
code-impler tests buildability / fresh-dev verifies clarity) before
|
|
1027
|
-
locking. v0.1 is the floor, not the ceiling.
|
|
781
|
+
`before_publish` and `after_publish` wrap the shipped publishing transition.
|
|
1028
782
|
|
|
1029
783
|
## Lineage — the academic foundations
|
|
1030
784
|
|
|
@@ -1037,6 +791,6 @@ locking. v0.1 is the floor, not the ceiling.
|
|
|
1037
791
|
|
|
1038
792
|
The composite design — declarative resources + ECA-fired procedures +
|
|
1039
793
|
policy-gated execution — is sometimes labeled **Active Database +
|
|
1040
|
-
Policy-Based Management**. Postgres + PostgREST
|
|
794
|
+
Policy-Based Management**. Postgres + PostgREST is the
|
|
1041
795
|
canonical full-stack reference; we abstract that pattern up to the
|
|
1042
796
|
application layer with K8s-style YAML manifests.
|