@aotter/mantle 0.0.11-alpha.16 → 0.0.11-alpha.18
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 +6 -0
- package/docs/adapter-guide.md +108 -0
- package/docs/adr/0001-four-atom-manifest-model.md +694 -0
- package/docs/adr/0002-closed-enums-for-bindings.md +226 -0
- package/docs/adr/0007-ai-as-primary-author.md +509 -0
- package/docs/adr/0008-structured-diagnostic-shape.md +353 -0
- package/docs/adr/0009-consumer-supplied-manifests.md +270 -0
- package/docs/adr/0010-locale-and-translates.md +450 -0
- package/docs/adr/0011-adapter-port-spec.md +272 -0
- package/docs/adr/0012-views-as-public-rest.md +137 -0
- package/docs/adr/0013-agent-provisioned-consumer-projects.md +147 -0
- package/docs/adr/0014-auth-better-auth-and-multi-tenant-mcp.md +418 -0
- package/docs/adr/0016-site-semantic-layer.md +55 -0
- package/docs/adr/0017-media-multi-variant-agent-side-optimization.md +181 -0
- package/docs/adr/README.md +57 -0
- package/docs/design-atoms.md +962 -0
- package/docs/design-references/editorial-blog-2026-05-05.md +218 -0
- package/docs/labels.md +67 -0
- package/docs/prompts/README.md +55 -0
- package/docs/prompts/publication.en.md +1 -0
- package/docs/prompts/publication.zh-TW.md +1 -0
- package/docs/release-process.md +555 -0
- package/package.json +10 -8
- package/skills/.keep +0 -0
- package/skills/README.md +33 -0
- package/skills/customize-design/SKILL.md +214 -0
- package/skills/extend/SKILL.md +214 -0
- package/skills/install/SKILL.md +339 -0
- package/skills/provision/SKILL.md +238 -0
|
@@ -0,0 +1,694 @@
|
|
|
1
|
+
# ADR-0001: 4-atom YAML manifest model under `cms.mantle.aotter.net/v1`
|
|
2
|
+
|
|
3
|
+
**Status:** Carried over from POC v0.0.x; refreshed and folded for v0.1.0 (incorporates POC ADRs 0005 + 0006).
|
|
4
|
+
|
|
5
|
+
**Date**: 2026-04-30 (POC origin), refreshed 2026-05-03 for v0.1.0 rebuild
|
|
6
|
+
|
|
7
|
+
**Deciders**: phsu
|
|
8
|
+
|
|
9
|
+
**Related**: ADR-0002 (closed enums for bindings)
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Context
|
|
14
|
+
|
|
15
|
+
The SDK's surface area to consumers is YAML manifests. The
|
|
16
|
+
question is: what set of resource kinds does that surface
|
|
17
|
+
expose? Too few and authors find themselves cramming concepts
|
|
18
|
+
into the wrong atom; too many and consumers face a long enum of
|
|
19
|
+
"what kind do I write today" with overlapping semantics.
|
|
20
|
+
|
|
21
|
+
Earlier iterations of this CMS experimented with 11 domain-shaped
|
|
22
|
+
kinds (`ContentSchema`, `ContentView`, `Form`, `Membership`,
|
|
23
|
+
`Email`, `Webhook`, `Workflow`, `ScheduledJob`, etc.). Two
|
|
24
|
+
problems surfaced:
|
|
25
|
+
|
|
26
|
+
1. **Authors couldn't find the right atom for a new feature.**
|
|
27
|
+
"Is a contact form a `Form`, a `Workflow`, an `Email`, or all
|
|
28
|
+
three?" Real features cross domain boundaries.
|
|
29
|
+
2. **Atoms drifted toward overlap.** A `Form` developed an
|
|
30
|
+
`email:` block; a `Webhook` developed a `validation:` block;
|
|
31
|
+
each new kind grew to subsume neighbors.
|
|
32
|
+
|
|
33
|
+
A multi-round design experiment (6 rounds + R6.5 falsification +
|
|
34
|
+
R7 reframing, run as a 4-agent debate over a blog feature
|
|
35
|
+
progression: like button → threaded comments → private posts →
|
|
36
|
+
rate-limited writes → cross-Schema invariants) reduced the kind
|
|
37
|
+
count to 4 and tested those 4 against 6 increasingly demanding
|
|
38
|
+
features without needing a 5th.
|
|
39
|
+
|
|
40
|
+
The relevant history that this ADR records (so future readers
|
|
41
|
+
don't re-litigate it):
|
|
42
|
+
|
|
43
|
+
- **R0 — "atoms ≡ PG primitives" claim**: the experiment opened
|
|
44
|
+
with the heuristic "4 kinds because Postgres has 4
|
|
45
|
+
application-developer primitives." Held for 6 rounds without
|
|
46
|
+
challenge.
|
|
47
|
+
- **R6.5 — falsification round**: an agent proposed cutting to
|
|
48
|
+
3 atoms by folding `Procedure` into `Schema.operations` (PG
|
|
49
|
+
functions attach to schemas in pg_proc). 92→58 LOC reduction
|
|
50
|
+
in the worked example by adding a default-CRUD shortcut.
|
|
51
|
+
Accepted by the round's judge.
|
|
52
|
+
- **R7 — user reframing**: reverted to 4 atoms, but for a
|
|
53
|
+
different reason than R0. The right axis isn't "PG
|
|
54
|
+
primitive"; it's "contract vs implementation × state vs
|
|
55
|
+
event":
|
|
56
|
+
|
|
57
|
+
| | request | event |
|
|
58
|
+
|---|---|---|
|
|
59
|
+
| external API | View (read) / write-API atom | (empty in v0.1) |
|
|
60
|
+
| internal | (Schema's writes are via the write-API atom) | Trigger (reaction) / Schema (state) |
|
|
61
|
+
|
|
62
|
+
**Design lesson** (worth surfacing because it shaped both this
|
|
63
|
+
ADR and the multi-doc YAML decision below): in multi-round
|
|
64
|
+
agent-team debates, decisions ratified in early rounds tend to
|
|
65
|
+
inherit forward through later rounds even when the local
|
|
66
|
+
conditions that justified them have changed. The orchestrator
|
|
67
|
+
must re-test prior naming / structural decisions whenever the
|
|
68
|
+
spec surface materially shifts; agent-team consensus is a strong
|
|
69
|
+
signal but not authoritative.
|
|
70
|
+
|
|
71
|
+
## Decision
|
|
72
|
+
|
|
73
|
+
The SDK exposes exactly **four declarative resource kinds** under
|
|
74
|
+
the `cms.mantle.aotter.net/v1` API group:
|
|
75
|
+
|
|
76
|
+
| Atom | Postgres equivalent | Externally exposed by itself? | Has user code? |
|
|
77
|
+
|---|---|---|---|
|
|
78
|
+
| `Schema` | `CREATE TABLE` | no | no |
|
|
79
|
+
| `View` | `CREATE VIEW` | yes (auto-mounted at `GET /api/views/<name>`; see ADR-0012) | no |
|
|
80
|
+
| `Procedure` | `CREATE FUNCTION ... LANGUAGE plpgsql` | **no** (transport-agnostic; needs a Trigger to bind) | **yes — handler ref to consumer's TS** |
|
|
81
|
+
| `Trigger` | `CREATE TRIGGER` + `pg_cron` + PostgREST route + `LISTEN/NOTIFY` | yes (the binding atom) | no |
|
|
82
|
+
|
|
83
|
+
### Path A: Trigger does all binding
|
|
84
|
+
|
|
85
|
+
A Procedure is **not externally exposed by itself**. To make it
|
|
86
|
+
callable over HTTP / MCP / cron / lifecycle / queue, declare a
|
|
87
|
+
`Trigger` whose `target.procedure` points at it. The same
|
|
88
|
+
Procedure can be bound by multiple Triggers (HTTP + cron + MCP,
|
|
89
|
+
all sharing one handler).
|
|
90
|
+
|
|
91
|
+
This is "Path A" relative to an alternative considered (and
|
|
92
|
+
rejected — see below) where Procedures carried an inline
|
|
93
|
+
`expose:` block.
|
|
94
|
+
|
|
95
|
+
### PG-1:1 mapping is the primary onboarding pitch
|
|
96
|
+
|
|
97
|
+
The mapping above is what new authors see first. Authors with
|
|
98
|
+
Postgres background orient instantly. Authors without it get a
|
|
99
|
+
mental anchor with 30 years of Codd/SQL/PostgREST literature
|
|
100
|
+
behind it.
|
|
101
|
+
|
|
102
|
+
### Composition rule
|
|
103
|
+
|
|
104
|
+
Anything more domain-shaped than these 4 (a `Form`,
|
|
105
|
+
`Membership`, `Email`, `Webhook`, `Workflow`, `ScheduledJob`)
|
|
106
|
+
is **not** an atom. It is composed in the consumer project by
|
|
107
|
+
combining the 4 atoms with consumer TypeScript. If a proposed
|
|
108
|
+
new feature wants a 5th kind, the proposer must show the 4
|
|
109
|
+
existing atoms cannot express it.
|
|
110
|
+
|
|
111
|
+
## Consequences
|
|
112
|
+
|
|
113
|
+
### Pros
|
|
114
|
+
|
|
115
|
+
- One question — "is it state, a query, an operation, or an
|
|
116
|
+
event-binding?" — picks the atom every time. No "form vs
|
|
117
|
+
workflow" gray zone.
|
|
118
|
+
- Read/write asymmetry (Views auto-mount, Procedures require
|
|
119
|
+
Triggers) matches HTTP safe-vs-unsafe semantics. Authors don't
|
|
120
|
+
have to remember which kinds the SDK auto-routes; the rule is
|
|
121
|
+
derivable from the atom's nature.
|
|
122
|
+
- PG mapping gives the spec 30 years of literature and battle
|
|
123
|
+
testing as background. Postgres has shipped these primitives
|
|
124
|
+
in the same shape since SQL-92 (Schema, View, Procedure) plus
|
|
125
|
+
Active Database / ECA rules from the 1990s (Trigger).
|
|
126
|
+
- Closed atom count makes the SDK's testing surface tractable:
|
|
127
|
+
every code path can be enumerated as
|
|
128
|
+
Schema-creation / View-query / Procedure-invocation /
|
|
129
|
+
Trigger-firing combinations.
|
|
130
|
+
|
|
131
|
+
### Costs
|
|
132
|
+
|
|
133
|
+
- Atoms are coarse. A "form-with-validation-and-email" feature
|
|
134
|
+
is 3 manifests (Schema + Procedure + Trigger), not 1. Mitigated
|
|
135
|
+
by multi-doc YAML grouping (see § Authoring shape below).
|
|
136
|
+
- "Procedure is not directly exposed" is a teaching point; new
|
|
137
|
+
authors initially try to call a Procedure URL and get 404.
|
|
138
|
+
Mitigated by the dispatcher emitting a hint in the 404 if a
|
|
139
|
+
Procedure exists with no Trigger binding (DRAFT — not yet
|
|
140
|
+
spec'd as a code).
|
|
141
|
+
- The PG-1:1 pitch breaks down once authors look at the actual
|
|
142
|
+
storage layer (D1 today). The mapping is conceptual; the
|
|
143
|
+
runtime is SQLite + JSON.
|
|
144
|
+
|
|
145
|
+
### Risks
|
|
146
|
+
|
|
147
|
+
- **Authors try to subsume domain concepts into the 4 atoms by
|
|
148
|
+
cramming.** A `Form` becomes "a Schema + a Procedure + an
|
|
149
|
+
ad-hoc validator block in the handler." Mitigated by
|
|
150
|
+
multi-doc YAML keeping atoms separate while the file count
|
|
151
|
+
stays low; reviewers should push back when a single Procedure's
|
|
152
|
+
handler becomes a workflow engine.
|
|
153
|
+
- **Trigger.source.kind expansion pressure.** v0.1 only ships
|
|
154
|
+
`http`. As MCP / cron / lifecycle / queue source kinds land
|
|
155
|
+
(DRAFT — see § Future grammar discipline below), the same
|
|
156
|
+
atom has to accommodate radically different invocation shapes.
|
|
157
|
+
The contract holds (one binding atom, many source kinds) but
|
|
158
|
+
each new kind is a design pass.
|
|
159
|
+
- **The PG-1:1 pitch may become a constraint.** If a future
|
|
160
|
+
capability has no PG analogue, framing pressure will push to
|
|
161
|
+
shoehorn it into the table or to break the pitch. Either is
|
|
162
|
+
defensible; the discipline is to flag the choice consciously.
|
|
163
|
+
|
|
164
|
+
## Alternatives considered
|
|
165
|
+
|
|
166
|
+
**(a) 11 domain-shaped kinds (early scaffolding)**.
|
|
167
|
+
Rejected: cross-cutting features didn't fit single kinds;
|
|
168
|
+
adjacent kinds drifted toward overlap. Authors couldn't pick.
|
|
169
|
+
|
|
170
|
+
**(b) 5 kinds — keep `Policy` as a separate atom**.
|
|
171
|
+
Rejected: row/field-level policy is sub-spec on the atom whose
|
|
172
|
+
data it concerns (Schema-attached visibility, Procedure-attached
|
|
173
|
+
auth gate). Lifting Policy to a kind invented a 5th locator
|
|
174
|
+
authors had to consult; folding it kept the locator count down.
|
|
175
|
+
|
|
176
|
+
**(c) 3 kinds — fold Procedure into `Schema.operations`** (R6.5).
|
|
177
|
+
Rejected: under R7's reframing, the right axis is contract /
|
|
178
|
+
implementation × state / event, not "PG attachment." Schema
|
|
179
|
+
holds state; Procedure holds operations against state; folding
|
|
180
|
+
them conflates "what data exists" with "what can happen to data."
|
|
181
|
+
Also the LOC win that motivated the fold (92→58 via default-CRUD)
|
|
182
|
+
was recovered without folding — captured in the DRAFT
|
|
183
|
+
`handler.kind: builtin` shortcut.
|
|
184
|
+
|
|
185
|
+
**(d) 4 kinds with `Procedure` retaining inline
|
|
186
|
+
`expose: { http: ... }` for single-source bindings**.
|
|
187
|
+
Rejected: created two ways to bind HTTP (inline `expose:` for
|
|
188
|
+
single-source, separate `Trigger` for multi-source). Doctrine
|
|
189
|
+
tax forever. Multi-doc YAML solves the file-count complaint
|
|
190
|
+
that motivated `expose:` without inventing redundant grammar.
|
|
191
|
+
See § Authoring shape below for the full retirement rationale.
|
|
192
|
+
|
|
193
|
+
## How to apply
|
|
194
|
+
|
|
195
|
+
- Adding a new feature: ask which of the 4 atoms holds each
|
|
196
|
+
piece. Use the "external by itself?" column to settle "do I
|
|
197
|
+
need a Trigger here?"
|
|
198
|
+
- Adding a new top-level kind: do not. First express as a
|
|
199
|
+
composition of the 4 + consumer TS. If the composition is
|
|
200
|
+
awkward, look for sub-spec on an existing atom before adding
|
|
201
|
+
a 5th kind.
|
|
202
|
+
- Renaming an atom: do not. The names are public surface; even
|
|
203
|
+
internally-motivated renames tend to drag schema-revise effort
|
|
204
|
+
across consumers, code, docs, and agent training data. Bias
|
|
205
|
+
toward keeping the names.
|
|
206
|
+
- Reading the spec: Schema = state, View = read API,
|
|
207
|
+
Procedure = write API (internal), Trigger = binding. That
|
|
208
|
+
4-word gloss covers ~90% of the questions authors ask.
|
|
209
|
+
|
|
210
|
+
## Implementation status
|
|
211
|
+
|
|
212
|
+
Names + PG-1:1 framing are documented in this ADR and the
|
|
213
|
+
manifest grammar reference. Manifest TS types live in
|
|
214
|
+
`packages/mantle-spec/`. Starter manifests under
|
|
215
|
+
`starters/` declare instances of each. Runtime dispatcher /
|
|
216
|
+
View executor / Procedure dispatcher are tracked under the
|
|
217
|
+
v0.1.0 ship list in [aotter/mantle](https://github.com/aotter/mantle).
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
## Future grammar discipline (was POC ADR-0005)
|
|
222
|
+
|
|
223
|
+
> Folded in 2026-05-03 from POC ADR-0005 ("v0.1 minimum essential
|
|
224
|
+
> grammar; rich grammar reserved as DRAFT"). The discipline below
|
|
225
|
+
> applies to every key inside every atom defined above.
|
|
226
|
+
|
|
227
|
+
### Context
|
|
228
|
+
|
|
229
|
+
The 4-atom design experiment (same 6 rounds + R6.5 + R7 cited
|
|
230
|
+
above) produced a rich inner grammar:
|
|
231
|
+
|
|
232
|
+
- 18 grammar keys across the 4 atoms
|
|
233
|
+
- 7 meta-rules (DRY across `requires` siblings; one-site
|
|
234
|
+
placeholder evaluation; SDK helper doctrine; closed
|
|
235
|
+
`x-mantle-bind` enum; list-shape for cross-resource refs;
|
|
236
|
+
singleton-fallback exception; ctx.system origin invariant)
|
|
237
|
+
- 2 closed enums (`x-mantle-bind`, `ctx.*` predicate identity)
|
|
238
|
+
- 2 placeholder namespaces (`:ctx.*` identity bindings, `$.*`
|
|
239
|
+
data-flow bindings)
|
|
240
|
+
- ~36 cognitive surface units (consumer-cc count)
|
|
241
|
+
|
|
242
|
+
The pressure-tested grammar covered features as ambitious as
|
|
243
|
+
`Trigger.target.project` (declarative cross-Schema aggregate
|
|
244
|
+
projection in same-tx), `Schema.spec.policies.{visible,
|
|
245
|
+
readable, writable}` (PG-RLS-style row/field policy),
|
|
246
|
+
`View.recursive` (declarative recursive CTEs),
|
|
247
|
+
`requires.window/quota/owns/contains` (temporal predicates,
|
|
248
|
+
rate caps, row ownership, array containment), and
|
|
249
|
+
`handler.kind: builtin` (default-CRUD shortcut).
|
|
250
|
+
|
|
251
|
+
Shipping all of that in v0.1 would mean roughly 5–10× the
|
|
252
|
+
dispatcher / validator / type-system surface v0.1 actually
|
|
253
|
+
needs, plus an OpenAPI emission story for keys (recursive CTEs,
|
|
254
|
+
RLS) that have no clean OpenAPI mapping.
|
|
255
|
+
|
|
256
|
+
The user principle, articulated 2026-04-30 EOD:
|
|
257
|
+
|
|
258
|
+
> 本次 iteration 只匡出四個 yml 最 minimum essential 的 grammar,
|
|
259
|
+
> 四個都蓋出來且能動,之後當應用場景擴充真的有需要再往上蓋。
|
|
260
|
+
|
|
261
|
+
(Translation: this iteration only ships the absolute minimum
|
|
262
|
+
grammar to make the 4 atoms work end-to-end; each extension
|
|
263
|
+
arrives when a real application use case demands it.)
|
|
264
|
+
|
|
265
|
+
This is YAGNI applied to grammar surface, motivated by:
|
|
266
|
+
|
|
267
|
+
- The runtime cost above.
|
|
268
|
+
- The risk that speculative grammar locks the wrong shape —
|
|
269
|
+
features designed in the abstract often look different once a
|
|
270
|
+
real use case applies pressure (R3-R5 of the design experiment
|
|
271
|
+
showed multiple grammar shapes proposed and revised as
|
|
272
|
+
features unfolded).
|
|
273
|
+
- The maintenance cost of keys nobody uses but everybody must
|
|
274
|
+
understand.
|
|
275
|
+
|
|
276
|
+
### Decision
|
|
277
|
+
|
|
278
|
+
The v0.1 ship targets **minimum essential grammar only**. Rich
|
|
279
|
+
grammar from the design experiment is preserved as DRAFT in the
|
|
280
|
+
manifest grammar reference; it lands when a concrete use case
|
|
281
|
+
forces it.
|
|
282
|
+
|
|
283
|
+
#### v0.1 grammar lock per atom
|
|
284
|
+
|
|
285
|
+
**Schema (v0.1)**:
|
|
286
|
+
- `spec.schema:` (JSON Schema 2020-12 body)
|
|
287
|
+
- `spec.uniqueIndexes:` (composite uniques)
|
|
288
|
+
- Property extensions: `x-mantle-bind`, `x-mantle-ref`, `x-mcp-hint`
|
|
289
|
+
|
|
290
|
+
**View (v0.1)**:
|
|
291
|
+
- `spec.from:` (source Schema)
|
|
292
|
+
- `spec.fields:` (projected fields)
|
|
293
|
+
- `spec.filter:` AST — `eq`, `gt`, `gte`, `lt`, `lte`, `and`, `or`
|
|
294
|
+
- `spec.orderBy:`
|
|
295
|
+
- `spec.limit:`
|
|
296
|
+
- `spec.params:` (required query params referenced by filter values)
|
|
297
|
+
|
|
298
|
+
**Procedure (v0.1)**:
|
|
299
|
+
- `spec.requires.auth.all:` (closed predicate vocabulary)
|
|
300
|
+
- `spec.input:` (JSON Schema 2020-12)
|
|
301
|
+
- `spec.output:` (JSON Schema 2020-12)
|
|
302
|
+
- `spec.handler.{kind: ref, ref: <opaque-key>}` — author-supplied
|
|
303
|
+
handler function
|
|
304
|
+
- `spec.handler.{kind: builtin, op: <create | update | upsert |
|
|
305
|
+
delete>, schema: <Schema name>}` — SDK-supplied CRUD shortcut
|
|
306
|
+
(promoted to v0.1.0; runtime implemented by `InvokeBuiltinUseCase`)
|
|
307
|
+
|
|
308
|
+
**Trigger (v0.1)**:
|
|
309
|
+
- `spec.source.kind: http` — public HTTP endpoint
|
|
310
|
+
- `spec.source.{method, path}` (when `kind: http`)
|
|
311
|
+
- `spec.source.kind: lifecycle` — entry-writer hook (promoted to
|
|
312
|
+
v0.1.0; runtime implemented by `LifecycleHookingEntryRepository`)
|
|
313
|
+
- `spec.source.{schema, on, errorPolicy}` (when `kind: lifecycle`)
|
|
314
|
+
- `spec.target.procedure:`
|
|
315
|
+
|
|
316
|
+
#### v0.1 closed enums
|
|
317
|
+
|
|
318
|
+
- `x-mantle-bind: {ctx.user, ctx.staff, now}`
|
|
319
|
+
- `ctx.*` predicate identity: `{user, staff}` (no `system` until a
|
|
320
|
+
use case forces it)
|
|
321
|
+
- `Trigger.source.kind: {http, lifecycle}`
|
|
322
|
+
- `Procedure.handler.kind: {ref, builtin}`
|
|
323
|
+
- `BuiltinOp: {create, update, upsert, delete}`
|
|
324
|
+
- `LifecycleHook: {before_create, after_create, before_update,
|
|
325
|
+
after_update, before_delete, after_delete, before_publish,
|
|
326
|
+
after_publish}`
|
|
327
|
+
|
|
328
|
+
#### What's DRAFT (do not implement, do not type)
|
|
329
|
+
|
|
330
|
+
- Schema: `policies.{visible, readable, writable, owner}`,
|
|
331
|
+
`indexedFields:`, `x-mantle-ref` auto-lift to virtual column,
|
|
332
|
+
computed columns via projection Trigger
|
|
333
|
+
- View: `recursive`, `gatedBy`, `join`, `policies.skip`,
|
|
334
|
+
filter AST extensions (`contains`, `not`, `in`, `like`)
|
|
335
|
+
- Procedure: `requires.auth.{any | all}` with disjunction;
|
|
336
|
+
`owns:`, `contains:`, `withinMinutes:`, `requires.window`,
|
|
337
|
+
`requires.quota`, `errors`, `retry`
|
|
338
|
+
- Trigger: `source.kind: {cron, queue}`,
|
|
339
|
+
`target.project`, `atomicity`
|
|
340
|
+
(`source.kind: mcp` was promoted in alpha.16 — #281)
|
|
341
|
+
- Cross-cutting: `ctx.system`, `$.*` placeholder namespace,
|
|
342
|
+
`staffBypass:`
|
|
343
|
+
|
|
344
|
+
#### Discipline
|
|
345
|
+
|
|
346
|
+
- The v0.1 validator **rejects DRAFT keys at parse time** with a
|
|
347
|
+
`DRAFT_KEY_USED` warning (not error — see the
|
|
348
|
+
AI-as-primary-author contract on permissive bias). The
|
|
349
|
+
diagnostic explicitly references the future-grammar reference
|
|
350
|
+
so authors know the key is reserved, not
|
|
351
|
+
unsupported-and-forgotten.
|
|
352
|
+
- Each DRAFT key has a documented **landing condition** (the
|
|
353
|
+
use case that surfaces it). Promoting a key to v0.1+ requires
|
|
354
|
+
showing that condition has been met — not just "it would be
|
|
355
|
+
nice to have."
|
|
356
|
+
- Each promotion goes through a 3-agent design review
|
|
357
|
+
(yml-editor proposes / code-impler tests buildability /
|
|
358
|
+
fresh-dev verifies clarity) before locking.
|
|
359
|
+
- The v0.1 floor is the floor, not the ceiling. The atom set
|
|
360
|
+
(4) is locked; the grammar inside each atom is permitted to
|
|
361
|
+
grow.
|
|
362
|
+
|
|
363
|
+
### Consequences
|
|
364
|
+
|
|
365
|
+
#### Pros
|
|
366
|
+
|
|
367
|
+
- v0.1 dispatcher / validator / types are tractable in a small
|
|
368
|
+
number of weeks, not many months.
|
|
369
|
+
- Author cognitive surface is small: 4 atoms × ~5 keys each ≈
|
|
370
|
+
20 things to learn, instead of 18 keys with ~36 cognitive
|
|
371
|
+
surface units.
|
|
372
|
+
- DRAFT keys document where the spec is **going**, so authors
|
|
373
|
+
with future requirements can tell whether the project is on
|
|
374
|
+
trajectory or not — without those features being prematurely
|
|
375
|
+
committed.
|
|
376
|
+
- Each DRAFT promotion gets a real use case attached, so the
|
|
377
|
+
grammar evolves under empirical pressure rather than
|
|
378
|
+
speculation. This was the design experiment's strongest
|
|
379
|
+
signal: features like `View.recursive` and
|
|
380
|
+
`Trigger.target.project` looked very different once a real
|
|
381
|
+
feature (threaded comments, cross-Schema invariants) applied
|
|
382
|
+
pressure.
|
|
383
|
+
|
|
384
|
+
#### Costs
|
|
385
|
+
|
|
386
|
+
- Authors with ambitions beyond v0.1 (private posts, role-gated
|
|
387
|
+
reads, rate limits, recursive views) must wait or write
|
|
388
|
+
handler-side TS. For some projects this is enough to shop
|
|
389
|
+
elsewhere; that's acceptable for an OSS v0.1.
|
|
390
|
+
- DRAFT documentation is itself a maintenance burden — the
|
|
391
|
+
reference has to stay coherent as the spec evolves; stale
|
|
392
|
+
DRAFT entries (features no longer planned) need pruning.
|
|
393
|
+
- The "warning, not error, on DRAFT keys" rule means an author
|
|
394
|
+
who copies a DRAFT example into a manifest doesn't fail —
|
|
395
|
+
they get a warning. This requires the validator to know the
|
|
396
|
+
full DRAFT vocabulary, not just the v0.1 vocabulary.
|
|
397
|
+
|
|
398
|
+
#### Risks
|
|
399
|
+
|
|
400
|
+
- **Grammar surface grows ad-hoc.** Each DRAFT promotion looks
|
|
401
|
+
reasonable; together they reproduce the over-grammar v0.1
|
|
402
|
+
was avoiding. Mitigation: 3-agent review per promotion;
|
|
403
|
+
promotions land in batches with documented use cases, not
|
|
404
|
+
one-by-one.
|
|
405
|
+
- **DRAFT becomes vaporware.** Some DRAFT features are listed
|
|
406
|
+
but never land because their use case never surfaces.
|
|
407
|
+
Mitigation: this is fine — the appendix is documenting
|
|
408
|
+
shapes, not commitments. If a DRAFT entry stays cold for >12
|
|
409
|
+
months, prune it from the appendix and let the future
|
|
410
|
+
proposer re-derive it.
|
|
411
|
+
- **Authors hit walls and route around the SDK.** "I need
|
|
412
|
+
`View.recursive` so I'll just write raw SQL in a Procedure
|
|
413
|
+
handler" — over time this drifts the actual extension shape
|
|
414
|
+
away from what the SDK eventually builds. Mitigation: when
|
|
415
|
+
ad-hoc handler patterns repeat, that's the use case that
|
|
416
|
+
promotes the DRAFT key. Watch handler implementations for
|
|
417
|
+
recurring patterns.
|
|
418
|
+
|
|
419
|
+
### Alternatives considered
|
|
420
|
+
|
|
421
|
+
**(a) Ship the full experiment grammar in v0.1**.
|
|
422
|
+
Rejected: 5–10× the dispatcher work; locks shapes in the
|
|
423
|
+
abstract before real use cases apply pressure; author surface
|
|
424
|
+
explodes.
|
|
425
|
+
|
|
426
|
+
**(b) Don't document DRAFT at all; ship v0.1 spec only**.
|
|
427
|
+
Rejected: authors with future requirements have no way to
|
|
428
|
+
evaluate trajectory; the design experiment's results would be
|
|
429
|
+
lost institutionally; future proposals would re-litigate solved
|
|
430
|
+
questions. The DRAFT reference is a forward-looking invariant.
|
|
431
|
+
|
|
432
|
+
**(c) Ship a subset of DRAFT in v0.1 (e.g. `policies.visible`
|
|
433
|
+
and `requires.auth.any`)**.
|
|
434
|
+
Rejected: any line we draw between v0.1 and DRAFT has the same
|
|
435
|
+
litigation problem. The principled cut is "minimum essential to
|
|
436
|
+
make 4 atoms work end-to-end"; anything richer needs a specific
|
|
437
|
+
motivation. None of the DRAFT features had that motivation in
|
|
438
|
+
the v0.1 ship list.
|
|
439
|
+
|
|
440
|
+
**(d) Promote DRAFT keys lazily — implement at runtime when
|
|
441
|
+
authors ask, no spec lock**.
|
|
442
|
+
Rejected: spec coherence demands keys are spec'd before they
|
|
443
|
+
ship. Authors writing against runtime-only features get
|
|
444
|
+
silently broken on SDK upgrades.
|
|
445
|
+
|
|
446
|
+
### How to apply
|
|
447
|
+
|
|
448
|
+
- Authoring v0.1 manifests: stick to the v0.1 vocabulary above.
|
|
449
|
+
If something seems missing, check the DRAFT reference; if it's
|
|
450
|
+
there and you have a concrete use case, file an issue
|
|
451
|
+
describing the use case.
|
|
452
|
+
- SDK code: implement only v0.1 keys in the dispatcher /
|
|
453
|
+
validator / types. DRAFT keys are documented but not handled.
|
|
454
|
+
- DRAFT promotion: needs a documented use case + 3-agent design
|
|
455
|
+
review + ADR landing the promotion (with cross-link to the
|
|
456
|
+
use case).
|
|
457
|
+
- Pruning DRAFT: if an entry has stayed cold for 12+ months,
|
|
458
|
+
propose removal in a small ADR. Removal is reversible (the
|
|
459
|
+
shape is in the experiment transcripts and prior ADR
|
|
460
|
+
history).
|
|
461
|
+
|
|
462
|
+
---
|
|
463
|
+
|
|
464
|
+
## Authoring shape: multi-doc YAML (was POC ADR-0006)
|
|
465
|
+
|
|
466
|
+
> Folded in 2026-05-03 from POC ADR-0006 ("Multi-doc YAML over
|
|
467
|
+
> inline shortcuts for atom co-location"). The decision below
|
|
468
|
+
> defines how authors lay manifests out on disk; it is the
|
|
469
|
+
> direct corollary of "Trigger does all binding" above.
|
|
470
|
+
|
|
471
|
+
### Context
|
|
472
|
+
|
|
473
|
+
The 4-atom model means a single user-visible feature commonly
|
|
474
|
+
bundles multiple atoms. A contact form is at minimum:
|
|
475
|
+
|
|
476
|
+
- A `Schema` for `contact-messages`
|
|
477
|
+
- A `Procedure` `send-contact-message` (handler validates +
|
|
478
|
+
inserts)
|
|
479
|
+
- A `Trigger` binding the Procedure to `POST /api/contact`
|
|
480
|
+
|
|
481
|
+
Three atoms, three manifests. With one file per manifest, a
|
|
482
|
+
small site with 10 features has 30+ manifest files. An early
|
|
483
|
+
design-experiment vibe-user called this out:
|
|
484
|
+
|
|
485
|
+
> 4 個 manifest 為了一顆 like 按鈕?要寫成這樣才能用?
|
|
486
|
+
|
|
487
|
+
(Translation: 4 manifests just for a like button? You have to
|
|
488
|
+
write all this to make it work?)
|
|
489
|
+
|
|
490
|
+
Two paths surfaced to address the file count:
|
|
491
|
+
|
|
492
|
+
#### Path A — Multi-doc YAML
|
|
493
|
+
|
|
494
|
+
YAML's standard `---` separator allows multiple documents in
|
|
495
|
+
one file. A "feature file" carries all atoms for that feature:
|
|
496
|
+
|
|
497
|
+
```yaml
|
|
498
|
+
# starters/<starter>/manifests/contact.yaml
|
|
499
|
+
apiVersion: cms.mantle.aotter.net/v1
|
|
500
|
+
kind: Schema
|
|
501
|
+
metadata: { name: contact-messages }
|
|
502
|
+
spec:
|
|
503
|
+
schema: { ... }
|
|
504
|
+
|
|
505
|
+
---
|
|
506
|
+
apiVersion: cms.mantle.aotter.net/v1
|
|
507
|
+
kind: Procedure
|
|
508
|
+
metadata: { name: send-contact-message }
|
|
509
|
+
spec:
|
|
510
|
+
input: { ... }
|
|
511
|
+
output: { ... }
|
|
512
|
+
handler: { kind: ref, ref: send-contact-message }
|
|
513
|
+
|
|
514
|
+
---
|
|
515
|
+
apiVersion: cms.mantle.aotter.net/v1
|
|
516
|
+
kind: Trigger
|
|
517
|
+
metadata: { name: contact-http }
|
|
518
|
+
spec:
|
|
519
|
+
source: { kind: http, method: POST, path: /api/contact }
|
|
520
|
+
target: { procedure: send-contact-message }
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
One file, three atoms, conceptual separation preserved. The
|
|
524
|
+
loader walks files and parses each `---` block as a separate
|
|
525
|
+
manifest.
|
|
526
|
+
|
|
527
|
+
#### Path B — Inline shortcut on Procedure (`expose:`)
|
|
528
|
+
|
|
529
|
+
Add a `Procedure.expose:` block that absorbs single-source HTTP
|
|
530
|
+
binding inline, eliminating the Trigger for the 1:1 case:
|
|
531
|
+
|
|
532
|
+
```yaml
|
|
533
|
+
apiVersion: cms.mantle.aotter.net/v1
|
|
534
|
+
kind: Procedure
|
|
535
|
+
metadata: { name: send-contact-message }
|
|
536
|
+
spec:
|
|
537
|
+
expose:
|
|
538
|
+
http: { method: POST, path: /api/contact }
|
|
539
|
+
input: { ... }
|
|
540
|
+
output: { ... }
|
|
541
|
+
handler: { kind: ref, ref: send-contact-message }
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
The two paths solve the same surface concern (file count) but
|
|
545
|
+
have very different structural implications.
|
|
546
|
+
|
|
547
|
+
### Decision
|
|
548
|
+
|
|
549
|
+
**Path A: Multi-doc YAML.** `Procedure.expose:` is permanently
|
|
550
|
+
retired. Trigger does ALL transport binding. Co-locate related
|
|
551
|
+
atoms in one file via `---`.
|
|
552
|
+
|
|
553
|
+
#### Why Path B was retired
|
|
554
|
+
|
|
555
|
+
Path B created **two ways to bind a Procedure to HTTP**:
|
|
556
|
+
|
|
557
|
+
- For a Procedure with one HTTP transport: inline `expose:` on
|
|
558
|
+
Procedure
|
|
559
|
+
- For a Procedure with multiple transports (HTTP + cron + MCP):
|
|
560
|
+
separate `Trigger` per transport
|
|
561
|
+
|
|
562
|
+
This is the exact "two ways to do the same thing" failure mode.
|
|
563
|
+
Doctrine resolves it on paper ("expose for single, Trigger for
|
|
564
|
+
multi") but creates permanent tax:
|
|
565
|
+
|
|
566
|
+
- Authors must remember which form to use, and switch when
|
|
567
|
+
cardinality changes (a single-HTTP Procedure that later
|
|
568
|
+
needs an MCP binding has to be refactored from `expose:` to
|
|
569
|
+
`Trigger`).
|
|
570
|
+
- The validator must understand both forms; OpenAPI emission
|
|
571
|
+
has two source paths to walk.
|
|
572
|
+
- The PG-1:1 mapping is muddied: "Procedure ≡ pg_proc" is clean
|
|
573
|
+
only if Procedure is internal-callable (no transport surface);
|
|
574
|
+
`expose:` gives Procedure transport semantics, weakening the
|
|
575
|
+
analogy.
|
|
576
|
+
- AI authors trying to introspect the manifest model get
|
|
577
|
+
conflicting signals about what the atom does.
|
|
578
|
+
|
|
579
|
+
#### What multi-doc YAML accomplishes
|
|
580
|
+
|
|
581
|
+
- File count drops without inventing redundancy. A feature →
|
|
582
|
+
one file, regardless of how many atoms it requires.
|
|
583
|
+
- Atom separation stays honest. Every atom has its own
|
|
584
|
+
envelope (`apiVersion`, `kind`, `metadata`, `spec`); there
|
|
585
|
+
is no "shortcut form."
|
|
586
|
+
- Related atoms sit next to each other for readers reviewing a
|
|
587
|
+
feature.
|
|
588
|
+
- The PG-1:1 framing stays clean: Procedure is
|
|
589
|
+
internal-callable, Trigger is the binding atom.
|
|
590
|
+
|
|
591
|
+
#### File organization conventions (suggested, not enforced)
|
|
592
|
+
|
|
593
|
+
- One feature → one file. Name the file by feature
|
|
594
|
+
(`contact.yaml`, `posts.yaml`, `comments.yaml`), not by atom
|
|
595
|
+
type.
|
|
596
|
+
- The shared file holds the Procedure + its Trigger + (when
|
|
597
|
+
feature-specific) the Schema and View.
|
|
598
|
+
- Schemas that are referenced by many features can live in
|
|
599
|
+
their own `<schema-name>.schema.yaml` file.
|
|
600
|
+
- Views with no Procedure / Trigger neighbors live in
|
|
601
|
+
`<view-name>.view.yaml`.
|
|
602
|
+
|
|
603
|
+
The validator does not enforce naming conventions; it walks
|
|
604
|
+
all `*.yaml` under the manifest root and parses every `---`
|
|
605
|
+
block as a manifest.
|
|
606
|
+
|
|
607
|
+
### Consequences
|
|
608
|
+
|
|
609
|
+
#### Pros
|
|
610
|
+
|
|
611
|
+
- Single mechanism for HTTP binding (Trigger). Validator,
|
|
612
|
+
OpenAPI emitter, runtime dispatcher all walk one shape.
|
|
613
|
+
- Conceptual atom separation preserved. The 4-atom story holds
|
|
614
|
+
for every feature.
|
|
615
|
+
- File count tractable. A typical small site is 5–20 manifest
|
|
616
|
+
files, not 30+.
|
|
617
|
+
- Cardinality changes (1 HTTP → HTTP + cron) are additive (add
|
|
618
|
+
a Trigger); no refactor needed.
|
|
619
|
+
- AI authors learn one mental model, not two.
|
|
620
|
+
|
|
621
|
+
#### Costs
|
|
622
|
+
|
|
623
|
+
- Author must learn YAML's multi-doc syntax (`---`). This is
|
|
624
|
+
standard YAML 1.2 and supported by every loader, but it is
|
|
625
|
+
one more piece of syntax than "one file = one document."
|
|
626
|
+
- Some loaders (older / non-compliant) don't handle multi-doc.
|
|
627
|
+
Standard `js-yaml` / `yaml` libraries do; the SDK uses
|
|
628
|
+
`yaml` (already a dependency).
|
|
629
|
+
- File-discovery patterns (`find . -name "send-contact-message.trigger.yaml"`)
|
|
630
|
+
no longer work for atoms inside multi-doc files. Authors
|
|
631
|
+
must `grep` instead.
|
|
632
|
+
- Editor support: YAML language servers handle multi-doc, but
|
|
633
|
+
some IDE features (e.g. JSON Schema validation per document
|
|
634
|
+
in a multi-doc file) work less smoothly than in single-doc
|
|
635
|
+
files. Acceptable for v0.1.
|
|
636
|
+
|
|
637
|
+
#### Risks
|
|
638
|
+
|
|
639
|
+
- **Author convenience pressure to reintroduce `expose:`.**
|
|
640
|
+
Someone shows up with "but for the 80% case where I have one
|
|
641
|
+
HTTP binding, the inline form is so much shorter!" The
|
|
642
|
+
argument is real; the answer is: doctrine that creates two
|
|
643
|
+
ways is permanent tax that grows with the codebase.
|
|
644
|
+
Mitigation: this section is the answer to that argument; flag
|
|
645
|
+
attempts and link here.
|
|
646
|
+
- **Multi-doc files grow unwieldy.** A "shop" feature might
|
|
647
|
+
have a Schema + 3 Procedures + 5 Triggers + 2 Views. One
|
|
648
|
+
file with 11 atoms is harder to navigate than 11 files.
|
|
649
|
+
Mitigation: file-organization convention is suggestion, not
|
|
650
|
+
rule; authors are free to split when it stops helping.
|
|
651
|
+
- **Co-location breaks discoverability for shared atoms.**
|
|
652
|
+
A Schema referenced by 4 features can live in any of those
|
|
653
|
+
4 files (or its own). Without grep / IDE go-to-def, finding
|
|
654
|
+
the Schema definition requires knowing where the author
|
|
655
|
+
put it. Mitigation: Loop 1 (`mantle validate`) emits the
|
|
656
|
+
filesystem path in `VIEW_FROM_UNKNOWN_SCHEMA` diagnostics;
|
|
657
|
+
IDE-shaped tooling (LSP — DRAFT) closes the rest.
|
|
658
|
+
|
|
659
|
+
### Alternatives considered
|
|
660
|
+
|
|
661
|
+
**(a) Path B: `Procedure.expose:`**.
|
|
662
|
+
Rejected (this section's main subject). Doctrine tax forever; PG
|
|
663
|
+
analogy weakened; cardinality refactor required when
|
|
664
|
+
single-source becomes multi-source.
|
|
665
|
+
|
|
666
|
+
**(b) One file per atom (early scaffolding default)**.
|
|
667
|
+
Rejected: file count complaint is real for multi-atom features.
|
|
668
|
+
Path A solves it without inventing redundancy.
|
|
669
|
+
|
|
670
|
+
**(c) New top-level kind `Feature` that bundles atoms**.
|
|
671
|
+
Rejected: turns a YAML file-organization concern into a spec
|
|
672
|
+
question. The 4-atom contract holds; an envelope-around-atoms
|
|
673
|
+
is structural noise.
|
|
674
|
+
|
|
675
|
+
**(d) Make Trigger optional with a default of "auto-mount the
|
|
676
|
+
Procedure at /api/p/<name>"**.
|
|
677
|
+
Rejected: hides the binding decision from the author, breaks
|
|
678
|
+
the PG-1:1 mapping (Procedures are not externally exposed by
|
|
679
|
+
themselves), and creates a third way to bind (this default +
|
|
680
|
+
explicit `Trigger` + explicit `expose:`).
|
|
681
|
+
|
|
682
|
+
### How to apply
|
|
683
|
+
|
|
684
|
+
- Authoring: one feature → one file with `---` separators.
|
|
685
|
+
- Reviewing: when an author writes a Procedure with an inline
|
|
686
|
+
HTTP binding, push back with this section.
|
|
687
|
+
- Refactoring: a feature that grew from one HTTP binding to
|
|
688
|
+
one HTTP + one cron + one MCP doesn't change the Procedure;
|
|
689
|
+
it adds two more Triggers in the same file.
|
|
690
|
+
- Tooling: SDK loader walks `*.yaml` under manifest root and
|
|
691
|
+
parses each `---` block; static validator emits diagnostics
|
|
692
|
+
with file path + manifest pointer
|
|
693
|
+
(`feature.yaml#/2/spec/from`) so multi-doc location stays
|
|
694
|
+
precise.
|