@aotter/mantle 0.1.0-alpha.1 → 0.1.0-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 +9 -5
- package/dist/generate.js +19 -2
- package/dist/generate.js.map +1 -1
- package/dist/harness-cli.js +1 -1
- package/dist/harness-cli.js.map +1 -1
- package/docs/adapter-guide.md +1 -1
- package/docs/adr/0001-four-atom-manifest-model.md +37 -304
- package/docs/adr/0002-closed-enums-for-bindings.md +6 -15
- package/docs/adr/0007-ai-as-primary-author.md +3 -1
- package/docs/adr/0008-structured-diagnostic-shape.md +2 -2
- package/docs/adr/0009-consumer-supplied-manifests.md +6 -5
- package/docs/adr/0010-locale-and-translates.md +13 -8
- package/docs/adr/0011-adapter-port-spec.md +13 -19
- package/docs/adr/0012-views-as-public-rest.md +5 -9
- package/docs/adr/0014-auth-better-auth-and-multi-tenant-mcp.md +13 -37
- package/docs/adr/README.md +4 -4
- package/docs/api-mcp-authorization.md +7 -0
- package/docs/design-atoms.md +66 -255
- package/docs/labels.md +4 -2
- package/docs/release-process.md +14 -3
- package/docs/schema-indexes.md +1 -1
- package/package.json +8 -8
- package/skills/develop/SKILL.md +15 -6
- package/skills/plugin/SKILL.md +1 -1
- package/skills/provision/SKILL.md +4 -0
- package/skills/theme/SKILL.md +1 -1
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),
|
|
@@ -33,7 +26,7 @@ primitives Postgres has shipped for 30 years.
|
|
|
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
28
|
| **`Procedure`** | `CREATE FUNCTION ... LANGUAGE plpgsql` | **no** (transport-agnostic; needs a `Trigger` to bind it) | **yes — handler ref to consumer's TS file** |
|
|
36
|
-
| **`Trigger`** | `CREATE TRIGGER` +
|
|
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
|
|
@@ -53,7 +46,6 @@ apiVersion: cms.mantle.aotter.net/v1
|
|
|
53
46
|
kind: Schema | View | Procedure | Trigger
|
|
54
47
|
metadata:
|
|
55
48
|
name: posts # required, kebab-case, unique within deployment
|
|
56
|
-
labels: { ... } # optional, free-form
|
|
57
49
|
spec:
|
|
58
50
|
... # kind-specific
|
|
59
51
|
```
|
|
@@ -73,7 +65,7 @@ A logical feature commonly bundles a Procedure + a Trigger (and often a
|
|
|
73
65
|
Schema and a View). Put related atoms in one file separated by `---`:
|
|
74
66
|
|
|
75
67
|
```yaml
|
|
76
|
-
# manifests/
|
|
68
|
+
# manifests/site.yaml
|
|
77
69
|
apiVersion: cms.mantle.aotter.net/v1
|
|
78
70
|
kind: Procedure
|
|
79
71
|
metadata: { name: send-contact-message }
|
|
@@ -93,8 +85,8 @@ spec:
|
|
|
93
85
|
target: { procedure: send-contact-message }
|
|
94
86
|
```
|
|
95
87
|
|
|
96
|
-
One file, two
|
|
97
|
-
|
|
88
|
+
One fixed file, two documents. Add every atom to `manifests/site.yaml` with
|
|
89
|
+
`---`; the loader rejects other manifest filenames.
|
|
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,17 @@ 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
|
+
**`spec.searchableFields`** — optional allowlist of top-level string
|
|
132
|
+
properties used by Admin and Staff MCP substring search. Entry id is always
|
|
133
|
+
searchable. This is separate from `indexes`; ordinary B-tree indexes do not
|
|
134
|
+
accelerate leading-wildcard substring search.
|
|
135
|
+
|
|
136
|
+
**`spec.uiSchema.list.filterField`** — optional Admin-only primary filter for
|
|
137
|
+
an operational collection. The field must declare a non-empty string `enum`
|
|
138
|
+
and be the first field of an `indexes` or `uniqueIndexes` tuple. Admin reuses
|
|
139
|
+
the enum for sidebar links and list tabs; Staff MCP uses declared Views for
|
|
140
|
+
richer query capabilities instead of reading UI configuration.
|
|
141
|
+
|
|
138
142
|
**`spec.localized: bool`** (default `false`, ADR-0010) — opt-in per
|
|
139
143
|
Schema. Localized Schemas store locale in `data.locale`; non-localized
|
|
140
144
|
Schemas reject `data.locale` writes. Site config must declare the set
|
|
@@ -144,25 +148,22 @@ validates manifest shape.
|
|
|
144
148
|
|
|
145
149
|
**`spec.translates: { parent, on }`** (ADR-0010) — declares this
|
|
146
150
|
Schema as the translation companion to a non-localized parent, joined
|
|
147
|
-
on the named field (typically `slug`).
|
|
151
|
+
on the named field (typically `slug`). Requires an explicit
|
|
152
|
+
`localized: true`. The
|
|
148
153
|
admin UI surfaces the child only as locale tabs in the parent's editor.
|
|
149
154
|
|
|
155
|
+
Use a standalone localized Schema only when each locale row is an
|
|
156
|
+
independent record. When several locale rows are versions of one entity,
|
|
157
|
+
declare a non-localized parent plus a localized `translates` child; that
|
|
158
|
+
relationship is what powers translation grouping and completeness in Admin.
|
|
159
|
+
|
|
150
160
|
#### Lifecycle
|
|
151
161
|
|
|
152
|
-
**`spec.lifecycle: '
|
|
162
|
+
**`spec.lifecycle: 'publishing' | 'operational'`** — controls the
|
|
153
163
|
entry's state machine.
|
|
154
164
|
|
|
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,
|
|
165
|
+
- `publishing` (default) — `draft → published → archived`. No approval queue.
|
|
166
|
+
- `operational` — records that are not authored content: orders,
|
|
166
167
|
inventory snapshots, grant/audit rows — anything written by
|
|
167
168
|
Procedures as a side effect rather than drafted by a person. No
|
|
168
169
|
content workflow applies: entries are live (`published`) the moment
|
|
@@ -175,8 +176,8 @@ entry's state machine.
|
|
|
175
176
|
`update_record_<schema>` for these Schemas instead of draft tools.
|
|
176
177
|
|
|
177
178
|
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 `
|
|
179
|
+
site-wide lifecycle setting; one Schema can be `publishing` while another
|
|
180
|
+
is `operational`.
|
|
180
181
|
|
|
181
182
|
**Property-level extensions** (JSON Schema vendor keywords, all optional):
|
|
182
183
|
|
|
@@ -204,17 +205,11 @@ wire.
|
|
|
204
205
|
| `now` | Server timestamp at write (ISO-8601 string with timezone) | `createdAt`, `submittedAt`, `grantedAt` |
|
|
205
206
|
|
|
206
207
|
**v0.1 stamping behavior**:
|
|
207
|
-
- Stamped on `INSERT` only.
|
|
208
|
-
re-stamps on every UPDATE) is DRAFT — it lands when first use case
|
|
209
|
-
surfaces.
|
|
208
|
+
- Stamped on `INSERT` only.
|
|
210
209
|
- Caller-supplied value: rejected at input validation. The Procedure's
|
|
211
210
|
effective input schema strips bound fields before checking caller
|
|
212
211
|
input, so callers can't accidentally send them.
|
|
213
212
|
- 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
213
|
|
|
219
214
|
**Why a closed enum** (highest-leverage discipline in the spec): bind
|
|
220
215
|
values are server-controlled identity + time facts. If we accepted
|
|
@@ -241,8 +236,7 @@ detection). The admin uses declared refs to show related rows and to nest
|
|
|
241
236
|
required child collections; it does not infer relationships from field names.
|
|
242
237
|
Declare a single-field `indexes` entry for the ref field (for example,
|
|
243
238
|
`indexes: [[authorId]]`) when reverse lookups must stay bounded. Mantle adds
|
|
244
|
-
the native entry-order columns to that access path.
|
|
245
|
-
it to enforced.
|
|
239
|
+
the native entry-order columns to that access path.
|
|
246
240
|
|
|
247
241
|
**Example**:
|
|
248
242
|
```yaml
|
|
@@ -286,12 +280,6 @@ MCP tool schemas preserve the hint for agents. Admin surfaces expose
|
|
|
286
280
|
media-shaped fields but first-party upload hosting is optional and not
|
|
287
281
|
part of first-run provisioning.
|
|
288
282
|
|
|
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
283
|
**Postgres analogue**: `CREATE TABLE posts (id UUID PRIMARY KEY, ...,
|
|
296
284
|
UNIQUE (slug, locale));`
|
|
297
285
|
|
|
@@ -363,11 +351,8 @@ query, no `LIMIT n+1` probe.
|
|
|
363
351
|
|
|
364
352
|
**v0.1 filter AST**: comparison operators (`eq`, `gt`, `gte`, `lt`,
|
|
365
353
|
`lte`) plus `and` / `or`. Comparison `value` may be a literal or a
|
|
366
|
-
`{ $param: <name> }` sentinel.
|
|
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.
|
|
354
|
+
`{ $param: <name> }` sentinel. Param refs must be required. Field-to-field
|
|
355
|
+
comparisons are not supported; compare a field to a literal or param.
|
|
371
356
|
|
|
372
357
|
**Postgres analogue**: `CREATE VIEW recent_published AS SELECT ...
|
|
373
358
|
FROM posts WHERE status = 'published' ORDER BY updated_at DESC LIMIT
|
|
@@ -383,8 +368,8 @@ provides the handler function with auto-generated TS types.
|
|
|
383
368
|
`Procedure` is **transport-agnostic and not directly exposed**. It does
|
|
384
369
|
not contain HTTP paths, methods, or MCP tool names. To call a Procedure
|
|
385
370
|
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
|
-
|
|
371
|
+
at it. The same Procedure can be bound by multiple Triggers (HTTP + MCP +
|
|
372
|
+
lifecycle, all sharing one handler).
|
|
388
373
|
|
|
389
374
|
```yaml
|
|
390
375
|
apiVersion: cms.mantle.aotter.net/v1
|
|
@@ -426,9 +411,6 @@ export const handlers = {
|
|
|
426
411
|
- `ctx.auth.scope: <scope>` — verified credential carries the exact opaque,
|
|
427
412
|
consumer-owned scope; repeat to require multiple scopes
|
|
428
413
|
|
|
429
|
-
Anything beyond this (`any:`, `owns:`, `withinMinutes:`, `contains:`,
|
|
430
|
-
`requires.window`, `requires.quota`, `errors`, `retry`) is DRAFT.
|
|
431
|
-
|
|
432
414
|
Both Procedures and Views may add one dynamic guard beside `auth`:
|
|
433
415
|
|
|
434
416
|
```yaml
|
|
@@ -485,7 +467,7 @@ spec:
|
|
|
485
467
|
```
|
|
486
468
|
|
|
487
469
|
The same Procedure can have multiple Triggers — that's how it becomes
|
|
488
|
-
|
|
470
|
+
an HTTP endpoint and an MCP tool without duplicating
|
|
489
471
|
handler logic. Each transport is one Trigger; the Procedure body is
|
|
490
472
|
shared.
|
|
491
473
|
|
|
@@ -496,17 +478,14 @@ tool on `surface: public | staff`), or `lifecycle` (entry-writer hook). For `lif
|
|
|
496
478
|
Lifecycle hooks are wired through `LifecycleHookingEntryRepository`, so
|
|
497
479
|
MCP, admin, and builtin write paths share the same hook behavior.
|
|
498
480
|
|
|
499
|
-
- `cron` / `queue` are **DRAFT (v0.2+)** — speculative, gated by
|
|
500
|
-
concrete consumer demand. Same appendix § "DRAFT (v0.2+)."
|
|
501
|
-
|
|
502
481
|
The state-machine "lifecycle" from the Schema atom
|
|
503
|
-
(`Schema.spec.lifecycle:
|
|
482
|
+
(`Schema.spec.lifecycle: publishing | operational`) is a separate domain
|
|
504
483
|
that shares the word. The Schema setting governs which states an
|
|
505
484
|
entry can be in; shipped lifecycle Triggers govern what fires around
|
|
506
485
|
mutations.
|
|
507
486
|
|
|
508
487
|
**Postgres analogue**: `CREATE TRIGGER ... AFTER INSERT ON posts
|
|
509
|
-
EXECUTE FUNCTION ...` (lifecycle)
|
|
488
|
+
EXECUTE FUNCTION ...` (lifecycle), plus
|
|
510
489
|
`CREATE FUNCTION` exposed via PostgREST routes (http) — Postgres has
|
|
511
490
|
had all of these via extensions for years, just split across multiple
|
|
512
491
|
mechanisms. We unify them under one atom.
|
|
@@ -584,7 +563,7 @@ different constraints.
|
|
|
584
563
|
| `UNAUTHENTICATED` | `401` | no active session (admin API: missing/expired session cookie) |
|
|
585
564
|
| `AUTH_DENIED` | `403` | `requires.auth` predicate evaluated false (or admin: caller lacks required staff role) |
|
|
586
565
|
| `ENTITLEMENT_REQUIRED` | `402` | consumer guard denies current payment/membership/transaction entitlement |
|
|
587
|
-
| `NOT_FOUND` | `404` | resource not found —
|
|
566
|
+
| `NOT_FOUND` | `404` | resource not found — entry id or View name at `/api/views/<name>` |
|
|
588
567
|
| `HANDLER_NOT_REGISTERED` | `500` | `handler.ref` key not registered at boot |
|
|
589
568
|
| `DISPATCHER_NOT_BUILT` | `501` | runtime feature not implemented in this SDK build |
|
|
590
569
|
| `INTERNAL_ERROR` | `500` | uncaught handler exception |
|
|
@@ -595,10 +574,7 @@ may also fire in earlier loops (e.g. `HANDLER_NOT_REGISTERED`
|
|
|
595
574
|
fires at boot — `phase: "boot"` — and that's where it should be
|
|
596
575
|
caught; the runtime occurrence is defense-in-depth).
|
|
597
576
|
|
|
598
|
-
|
|
599
|
-
`QUOTA_EXCEEDED → 429` arrives with `requires.quota`).
|
|
600
|
-
|
|
601
|
-
## RBAC — what v0.1 ships, what's DRAFT
|
|
577
|
+
## RBAC
|
|
602
578
|
|
|
603
579
|
v0.1 auth gates use `requires.auth.all` with `ctx.user`,
|
|
604
580
|
`ctx.staff: [<roles>]`, `ctx.auth`, and `ctx.auth.scope` predicates on
|
|
@@ -606,16 +582,6 @@ Procedures and Views. This covers staff-only, logged-in-only,
|
|
|
606
582
|
credential-protected, and delegated-scope targets. One Procedure-backed guard
|
|
607
583
|
handles mutable consumer business state without widening the static grammar.
|
|
608
584
|
|
|
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
585
|
## How to think when extending this CMS
|
|
620
586
|
|
|
621
587
|
1. **What entities does the feature need?** → `Schema` for each.
|
|
@@ -624,13 +590,11 @@ sub-specs in the DRAFT spec. See "Future grammar" appendix.
|
|
|
624
590
|
external read endpoints).
|
|
625
591
|
3. **What operations?** → `Procedure` for each. One Procedure = one
|
|
626
592
|
typed function call. Compose in handler code, not in YAML.
|
|
627
|
-
4. **What invokes them?** → `Trigger` per
|
|
628
|
-
can
|
|
629
|
-
handler).
|
|
593
|
+
4. **What invokes them?** → `Trigger` per declared HTTP, MCP, or lifecycle
|
|
594
|
+
source. Site-owned events can call the same Procedure binding directly.
|
|
630
595
|
5. **Who's allowed?** → `Procedure/View.spec.requires.auth` for static
|
|
631
596
|
identity/scope; optional `requires.guard.procedure` for one live,
|
|
632
|
-
consumer-owned business check.
|
|
633
|
-
future grammar.
|
|
597
|
+
consumer-owned business check.
|
|
634
598
|
|
|
635
599
|
If you find yourself wanting a 5th kind, **stop**. Sketch the same
|
|
636
600
|
thing as a composition of the four; almost always it works.
|
|
@@ -722,8 +686,7 @@ grammar, leftmost-prefix rules, SQL helper, and query-plan examples.
|
|
|
722
686
|
comfortable. Anonymous public responses can hit version-local Workers
|
|
723
687
|
Cache before the Worker; origin misses use indexed D1 reads.
|
|
724
688
|
- **Mid-scale (10k – 100k entries)**: declare measured list/filter
|
|
725
|
-
access paths with ordered `indexes`.
|
|
726
|
-
may eventually gain `x-mantle-ref` auto-lift; that remains DRAFT.
|
|
689
|
+
access paths with ordered `indexes`.
|
|
727
690
|
- **Hard D1 limits**: 1 MB max row size; 5,000 rows per query result;
|
|
728
691
|
single-writer per database (concurrent writes serialize).
|
|
729
692
|
- **Cross-region read**: D1 is region-pinned; first origin hit from a
|
|
@@ -753,7 +716,6 @@ When this path is taken (post-v0.1; not implemented yet), the
|
|
|
753
716
|
| `json_each` → array operators (`@>`, `&&`) | none — predicates stay declarative |
|
|
754
717
|
| SDK-stamped `now` → `DEFAULT now()` (optional) | none |
|
|
755
718
|
| 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
719
|
|
|
758
720
|
Migration shape: `entries` table dumped from D1, restored to PG. Stay
|
|
759
721
|
single-table-with-discriminator (drop-in) or split into
|
|
@@ -761,68 +723,12 @@ table-per-collection (more PG-idiomatic, longer migration). Both shapes
|
|
|
761
723
|
remain valid `cms.mantle.aotter.net/v1` deployments.
|
|
762
724
|
|
|
763
725
|
**v0.1 commitment**: D1 only via the Cloudflare adapter. Hyperdrive +
|
|
764
|
-
PG
|
|
765
|
-
|
|
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.
|
|
726
|
+
PG is an upgrade route, not an implemented adapter. SDKs and starters target
|
|
727
|
+
D1 exclusively.
|
|
775
728
|
|
|
776
|
-
|
|
777
|
-
narrow at v0.1.0 and grows in two tiers:
|
|
729
|
+
## Detailed shipped grammar
|
|
778
730
|
|
|
779
|
-
|
|
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.
|
|
787
|
-
|
|
788
|
-
### v0.1.0 shipped
|
|
789
|
-
|
|
790
|
-
#### `Trigger.source.kind: lifecycle` — bind a Procedure to a Schema event
|
|
791
|
-
|
|
792
|
-
Grammar lives in v0.1.0. Runtime is the
|
|
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.
|
|
796
|
-
|
|
797
|
-
#### `Procedure.handler.kind: builtin` — SDK-supplied CRUD Procedure
|
|
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
|
|
731
|
+
### `handler.kind: builtin` — thin shortcut over the storage adapter for trivial CRUD-shaped Procedures
|
|
826
732
|
|
|
827
733
|
Use when the body is "insert a row" / "update a row" / "delete a
|
|
828
734
|
row"; reach for `ref` when there is real business logic. Shape:
|
|
@@ -839,11 +745,11 @@ spec:
|
|
|
839
745
|
|
|
840
746
|
| op | Behavior |
|
|
841
747
|
|---|---|
|
|
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:
|
|
748
|
+
| `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
749
|
| `update` | UPDATE in place. `input.id` + `input.expectedVersion` (OCC) required. Bumps version. |
|
|
844
750
|
| `upsert` | If `input.id` resolves, behaves as `update`; else as `create`. |
|
|
845
751
|
| `delete` | Hard DELETE by id. |
|
|
846
|
-
| `archive` | Soft-archive (status='archived').
|
|
752
|
+
| `archive` | Soft-archive a publishing entry (`status='archived'`). |
|
|
847
753
|
|
|
848
754
|
The Procedure's `input` is the contract with the *caller*. It MAY
|
|
849
755
|
declare fields the Schema does not (e.g. a Turnstile token). The
|
|
@@ -853,11 +759,10 @@ the side-channel fields pass validation. To act on those fields
|
|
|
853
759
|
(read the token, call the vendor), declare a `before_create`
|
|
854
760
|
lifecycle Trigger — see below.
|
|
855
761
|
|
|
856
|
-
`request_publish` and `publish` are intentionally not in the
|
|
857
|
-
|
|
858
|
-
primitives.
|
|
762
|
+
`request_publish` and `publish` are intentionally not in the builtin
|
|
763
|
+
vocabulary. They are lifecycle operations, not CRUD primitives.
|
|
859
764
|
|
|
860
|
-
|
|
765
|
+
### `Trigger.source.kind: lifecycle` — bind a Procedure to a Schema event
|
|
861
766
|
|
|
862
767
|
Shape:
|
|
863
768
|
|
|
@@ -879,12 +784,15 @@ spec:
|
|
|
879
784
|
|---|---|
|
|
880
785
|
| `before_create` | Before INSERT. Throw cancels. |
|
|
881
786
|
| `after_create` | After INSERT. Default best-effort. |
|
|
882
|
-
| `before_update` | Before UPDATE. Throw cancels. |
|
|
883
|
-
| `after_update` | After UPDATE. Default best-effort. |
|
|
787
|
+
| `before_update` | Before UPDATE or a status transition whose target is not `published` (including unpublish and archive). Throw cancels. |
|
|
788
|
+
| `after_update` | After UPDATE or a status transition whose target is not `published` (including unpublish and archive). Default best-effort. |
|
|
884
789
|
| `before_delete` | Before DELETE. Throw cancels. |
|
|
885
790
|
| `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 `
|
|
791
|
+
| `before_publish` | Before any supported status transition to `published` (the shipped workflow is `publishing`). |
|
|
792
|
+
| `after_publish` | After any supported status transition to `published` (the shipped workflow is `publishing`). |
|
|
793
|
+
|
|
794
|
+
v0.1 has no separate unpublish or archive hooks. Do not treat
|
|
795
|
+
`before_update` / `after_update` as edit-only hooks.
|
|
888
796
|
|
|
889
797
|
**Atomicity defaults by phase**:
|
|
890
798
|
- `before_*`: `errorPolicy: abort`. Handler throw cancels the
|
|
@@ -915,9 +823,8 @@ post-mutation row for every `after_*`.
|
|
|
915
823
|
|
|
916
824
|
**Hook ordering**: when multiple lifecycle Triggers bind the same
|
|
917
825
|
`(schema, hook)`, the runtime fires them **alphabetically by
|
|
918
|
-
`Trigger.metadata.name`**.
|
|
919
|
-
|
|
920
|
-
`020-rate-limit`).
|
|
826
|
+
`Trigger.metadata.name`**. Choose names that sort correctly
|
|
827
|
+
(`010-bot-check`, `020-rate-limit`).
|
|
921
828
|
|
|
922
829
|
For deferred delivery, that ordered Trigger-name list is captured in
|
|
923
830
|
one strict versioned event. Every captured Trigger runs before a
|
|
@@ -928,103 +835,7 @@ Cloudflare wiring, the 128 KB platform limit, retry/DLQ configuration,
|
|
|
928
835
|
idempotency, and legacy-envelope draining are specified in
|
|
929
836
|
[Deferred lifecycle hooks on Cloudflare Queues](deferred-lifecycle-queues.md).
|
|
930
837
|
|
|
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.
|
|
838
|
+
`before_publish` and `after_publish` wrap the shipped publishing transition.
|
|
1028
839
|
|
|
1029
840
|
## Lineage — the academic foundations
|
|
1030
841
|
|
|
@@ -1037,6 +848,6 @@ locking. v0.1 is the floor, not the ceiling.
|
|
|
1037
848
|
|
|
1038
849
|
The composite design — declarative resources + ECA-fired procedures +
|
|
1039
850
|
policy-gated execution — is sometimes labeled **Active Database +
|
|
1040
|
-
Policy-Based Management**. Postgres + PostgREST
|
|
851
|
+
Policy-Based Management**. Postgres + PostgREST is the
|
|
1041
852
|
canonical full-stack reference; we abstract that pattern up to the
|
|
1042
853
|
application layer with K8s-style YAML manifests.
|
package/docs/labels.md
CHANGED
|
@@ -33,7 +33,7 @@ For package README or package-local docs changes, prefer the package area label
|
|
|
33
33
|
| `area:skills` | `skills/*` agent briefs and install/extend/provision workflows. |
|
|
34
34
|
| `area:admin-ui` | `packages/mantle-admin-ui` React admin SPA. |
|
|
35
35
|
| `area:docs` | Repo-wide human docs, governance docs, ADR text, release docs, root README content, and cross-cutting documentation work. |
|
|
36
|
-
| `area:adapter` | Adapter boundary work spanning Cloudflare
|
|
36
|
+
| `area:adapter` | Adapter boundary work spanning Cloudflare or future adapters. |
|
|
37
37
|
|
|
38
38
|
## Release and review gates
|
|
39
39
|
|
|
@@ -41,7 +41,8 @@ For package README or package-local docs changes, prefer the package area label
|
|
|
41
41
|
|---|---|---|---|
|
|
42
42
|
| `release-gate` | Blocks a public release or release-quality claim. | Must be resolved or explicitly deferred before release. | The gate is closed or deferred by maintainer decision. |
|
|
43
43
|
| `v0.1.0` | Required for the v0.1.0 release gate. | Track in the v0.1.0 release pass. | The item lands or is moved out of v0.1.0. |
|
|
44
|
-
| `breaking-change` | Semver-relevant breaking change. |
|
|
44
|
+
| `breaking-change` | Semver-relevant breaking change. | Generated release notes must call it out. | The change is redesigned to be non-breaking. |
|
|
45
|
+
| `skip-release-notes` | Release bookkeeping with no user-facing change. | Excluded from generated GitHub Release notes. | The PR contains a user-facing change. |
|
|
45
46
|
| `needs-adr` | Architecture, trust boundary, package boundary, or long-lived decision needs an ADR or ADR-lite proposal. | Do not merge implementation until the decision is captured. | ADR/proposal lands or maintainer confirms an existing ADR covers it. |
|
|
46
47
|
| `needs-grammar-revise` | Manifest grammar or closed-enum change. | Requires grammar-revise round before code/types/starters change. | Grammar decision lands or the change no longer affects grammar. |
|
|
47
48
|
| `needs-discussion` | Not converged enough for implementation. | Do not start coding from this issue. | Closing criteria are met and scope is concrete. |
|
|
@@ -60,6 +61,7 @@ gh label create "area:admin-ui" --description "React admin UI" --color "1d76db"
|
|
|
60
61
|
gh label create "area:docs" --description "Documentation and governance" --color "1d76db"
|
|
61
62
|
gh label create "area:adapter" --description "Adapter boundary and future adapter work" --color "1d76db"
|
|
62
63
|
gh label create "breaking-change" --description "Semver-relevant breaking change" --color "b60205"
|
|
64
|
+
gh label create "skip-release-notes" --description "Release bookkeeping only; omit from generated GitHub notes" --color "ededed"
|
|
63
65
|
gh label create "needs-adr" --description "Requires an ADR or ADR-lite decision before merge" --color "d93f0b"
|
|
64
66
|
gh label create "needs-grammar-revise" --description "Requires manifest grammar review before implementation" --color "d93f0b"
|
|
65
67
|
gh label create "needs-discussion" --description "Not converged enough for implementation" --color "fbca04"
|
package/docs/release-process.md
CHANGED
|
@@ -62,8 +62,9 @@ decision instead of starting another local redesign loop.
|
|
|
62
62
|
## Release PR
|
|
63
63
|
|
|
64
64
|
1. Fetch Core and Starter remotes and choose the next unused version.
|
|
65
|
-
2.
|
|
66
|
-
|
|
65
|
+
2. Preview GitHub's generated notes for the merged commits since the previous
|
|
66
|
+
tag. Correct PR titles and labels before release; do not duplicate the notes
|
|
67
|
+
in `CHANGELOG.md`. Label the release-only PR `skip-release-notes`.
|
|
67
68
|
3. Set that exact version in every workspace package and in all four agent
|
|
68
69
|
plugin manifests. Set `.agents/plugins/marketplace.json` to the immutable
|
|
69
70
|
`v<version>` ref.
|
|
@@ -77,6 +78,16 @@ decision instead of starting another local redesign loop.
|
|
|
77
78
|
packed-consumer gate. Review and merge a same-repository PR into `develop`;
|
|
78
79
|
the controller rejects a direct-push release commit.
|
|
79
80
|
|
|
81
|
+
Preview the native notes before merging the release PR:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
gh api --method POST repos/aotter/mantle/releases/generate-notes \
|
|
85
|
+
-f tag_name=vX.Y.Z \
|
|
86
|
+
-f target_commitish="$(git rev-parse origin/develop)" \
|
|
87
|
+
-f previous_tag_name=vPREVIOUS \
|
|
88
|
+
--jq .body
|
|
89
|
+
```
|
|
90
|
+
|
|
80
91
|
The five public packages publish in dependency order:
|
|
81
92
|
|
|
82
93
|
1. `@aotter/mantle-spec`
|
|
@@ -179,7 +190,7 @@ deployment was started.
|
|
|
179
190
|
## Fix-forward policy
|
|
180
191
|
|
|
181
192
|
- Broken public package or Starter bundle: publish the next alpha and explain
|
|
182
|
-
the re-spin in
|
|
193
|
+
the re-spin in the fix PR and generated GitHub Release notes.
|
|
183
194
|
- Use `npm deprecate` to steer consumers away from a broken version.
|
|
184
195
|
- Unpublish only for secrets, private files, or similarly severe exposure;
|
|
185
196
|
npm versions cannot be reused and registry metadata may remain unavailable
|