@aotter/mantle 0.0.11-alpha.73 → 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 +13 -9
- 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 +3 -6
- 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 +25 -43
- package/docs/adr/0012-views-as-public-rest.md +6 -10
- 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 +57 -2
- package/docs/cloudflare-low-level-composition.md +1 -1
- package/docs/deferred-lifecycle-queues.md +0 -3
- package/docs/design-atoms.md +75 -259
- package/docs/labels.md +4 -2
- package/docs/performance-harness.md +18 -19
- package/docs/release-process.md +19 -6
- package/docs/schema-indexes.md +1 -1
- package/package.json +11 -11
- package/skills/develop/SKILL.md +17 -8
- package/skills/plugin/SKILL.md +1 -1
- package/skills/provision/SKILL.md +4 -0
- package/skills/theme/SKILL.md +1 -1
|
@@ -78,14 +78,14 @@ the `cms.mantle.aotter.net/v1` API group:
|
|
|
78
78
|
| `Schema` | `CREATE TABLE` | no | no |
|
|
79
79
|
| `View` | `CREATE VIEW` | yes (auto-mounted at `GET /api/views/<name>`; see ADR-0012) | no |
|
|
80
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` +
|
|
81
|
+
| `Trigger` | `CREATE TRIGGER` + route/tool binding | yes (the binding atom) | no |
|
|
82
82
|
|
|
83
83
|
### Path A: Trigger does all binding
|
|
84
84
|
|
|
85
85
|
A Procedure is **not externally exposed by itself**. To make it
|
|
86
|
-
callable over HTTP / MCP /
|
|
86
|
+
callable over HTTP / MCP / lifecycle, declare a
|
|
87
87
|
`Trigger` whose `target.procedure` points at it. The same
|
|
88
|
-
Procedure can be bound by multiple Triggers (HTTP +
|
|
88
|
+
Procedure can be bound by multiple Triggers (HTTP + MCP,
|
|
89
89
|
all sharing one handler).
|
|
90
90
|
|
|
91
91
|
This is "Path A" relative to an alternative considered (and
|
|
@@ -135,9 +135,6 @@ existing atoms cannot express it.
|
|
|
135
135
|
by multi-doc YAML grouping (see § Authoring shape below).
|
|
136
136
|
- "Procedure is not directly exposed" is a teaching point; new
|
|
137
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
138
|
- The PG-1:1 pitch breaks down once authors look at the actual
|
|
142
139
|
storage layer (D1 today). The mapping is conceptual; the
|
|
143
140
|
runtime is SQLite + JSON.
|
|
@@ -150,12 +147,9 @@ existing atoms cannot express it.
|
|
|
150
147
|
multi-doc YAML keeping atoms separate while the file count
|
|
151
148
|
stays low; reviewers should push back when a single Procedure's
|
|
152
149
|
handler becomes a workflow engine.
|
|
153
|
-
- **Trigger.source.kind expansion pressure.** v0.1
|
|
154
|
-
`
|
|
155
|
-
|
|
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.
|
|
150
|
+
- **Trigger.source.kind expansion pressure.** v0.1 ships `http`, `mcp`,
|
|
151
|
+
and `lifecycle`. Each new source kind requires a complete grammar and
|
|
152
|
+
runtime design pass.
|
|
159
153
|
- **The PG-1:1 pitch may become a constraint.** If a future
|
|
160
154
|
capability has no PG analogue, framing pressure will push to
|
|
161
155
|
shoehorn it into the table or to break the pitch. Either is
|
|
@@ -178,8 +172,8 @@ Rejected: under R7's reframing, the right axis is contract /
|
|
|
178
172
|
implementation × state / event, not "PG attachment." Schema
|
|
179
173
|
holds state; Procedure holds operations against state; folding
|
|
180
174
|
them conflates "what data exists" with "what can happen to data."
|
|
181
|
-
Also the LOC win that motivated the fold (92→58 via default
|
|
182
|
-
was recovered without folding
|
|
175
|
+
Also the LOC win that motivated the fold (92→58 via default CRUD)
|
|
176
|
+
was recovered without folding through the shipped
|
|
183
177
|
`handler.kind: builtin` shortcut.
|
|
184
178
|
|
|
185
179
|
**(d) 4 kinds with `Procedure` retaining inline
|
|
@@ -217,251 +211,12 @@ executor, and Procedure dispatcher live in this repository.
|
|
|
217
211
|
|
|
218
212
|
---
|
|
219
213
|
|
|
220
|
-
##
|
|
214
|
+
## Grammar changes
|
|
221
215
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
### Context
|
|
227
|
-
|
|
228
|
-
The 4-atom design experiment (same 6 rounds + R6.5 + R7 cited
|
|
229
|
-
above) produced a rich inner grammar:
|
|
230
|
-
|
|
231
|
-
- 18 grammar keys across the 4 atoms
|
|
232
|
-
- 7 meta-rules (DRY across `requires` siblings; one-site
|
|
233
|
-
placeholder evaluation; SDK helper doctrine; closed
|
|
234
|
-
`x-mantle-bind` enum; list-shape for cross-resource refs;
|
|
235
|
-
singleton-fallback exception; ctx.system origin invariant)
|
|
236
|
-
- 2 closed enums (`x-mantle-bind`, `ctx.*` predicate identity)
|
|
237
|
-
- 2 placeholder namespaces (`:ctx.*` identity bindings, `$.*`
|
|
238
|
-
data-flow bindings)
|
|
239
|
-
- ~36 cognitive surface units (consumer-cc count)
|
|
240
|
-
|
|
241
|
-
The pressure-tested grammar covered features as ambitious as
|
|
242
|
-
`Trigger.target.project` (declarative cross-Schema aggregate
|
|
243
|
-
projection in same-tx), `Schema.spec.policies.{visible,
|
|
244
|
-
readable, writable}` (PG-RLS-style row/field policy),
|
|
245
|
-
`View.recursive` (declarative recursive CTEs),
|
|
246
|
-
`requires.window/quota/owns/contains` (temporal predicates,
|
|
247
|
-
rate caps, row ownership, array containment), and
|
|
248
|
-
`handler.kind: builtin` (default-CRUD shortcut).
|
|
249
|
-
|
|
250
|
-
Shipping all of that in v0.1 would mean roughly 5–10× the
|
|
251
|
-
dispatcher / validator / type-system surface v0.1 actually
|
|
252
|
-
needs, plus an OpenAPI emission story for keys (recursive CTEs,
|
|
253
|
-
RLS) that have no clean OpenAPI mapping.
|
|
254
|
-
|
|
255
|
-
The user principle, articulated 2026-04-30 EOD:
|
|
256
|
-
|
|
257
|
-
> 本次 iteration 只匡出四個 yml 最 minimum essential 的 grammar,
|
|
258
|
-
> 四個都蓋出來且能動,之後當應用場景擴充真的有需要再往上蓋。
|
|
259
|
-
|
|
260
|
-
(Translation: this iteration only ships the absolute minimum
|
|
261
|
-
grammar to make the 4 atoms work end-to-end; each extension
|
|
262
|
-
arrives when a real application use case demands it.)
|
|
263
|
-
|
|
264
|
-
This is YAGNI applied to grammar surface, motivated by:
|
|
265
|
-
|
|
266
|
-
- The runtime cost above.
|
|
267
|
-
- The risk that speculative grammar locks the wrong shape —
|
|
268
|
-
features designed in the abstract often look different once a
|
|
269
|
-
real use case applies pressure (R3-R5 of the design experiment
|
|
270
|
-
showed multiple grammar shapes proposed and revised as
|
|
271
|
-
features unfolded).
|
|
272
|
-
- The maintenance cost of keys nobody uses but everybody must
|
|
273
|
-
understand.
|
|
274
|
-
|
|
275
|
-
### Decision
|
|
276
|
-
|
|
277
|
-
The v0.1 ship targets **minimum essential grammar only**. Rich
|
|
278
|
-
grammar from the design experiment is preserved as DRAFT in the
|
|
279
|
-
manifest grammar reference; it lands when a concrete use case
|
|
280
|
-
forces it.
|
|
281
|
-
|
|
282
|
-
#### v0.1 grammar lock per atom
|
|
283
|
-
|
|
284
|
-
**Schema (v0.1)**:
|
|
285
|
-
- `spec.schema:` (JSON Schema 2020-12 body)
|
|
286
|
-
- `spec.uniqueIndexes:` (composite uniques)
|
|
287
|
-
- `spec.indexes:` (ordered composite, non-unique access paths)
|
|
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
|
-
- `spec.requires.{auth, guard}:` (same authorization contract as Procedure)
|
|
298
|
-
|
|
299
|
-
**Procedure (v0.1)**:
|
|
300
|
-
- `spec.requires.auth.all:` (closed predicate vocabulary)
|
|
301
|
-
- `spec.requires.guard.procedure:` (one consumer-owned dynamic guard)
|
|
302
|
-
- `spec.input:` (JSON Schema 2020-12)
|
|
303
|
-
- `spec.output:` (JSON Schema 2020-12)
|
|
304
|
-
- `spec.handler.{kind: ref, ref: <opaque-key>}` — author-supplied
|
|
305
|
-
handler function
|
|
306
|
-
- `spec.handler.{kind: builtin, op: <create | update | upsert |
|
|
307
|
-
delete>, schema: <Schema name>}` — SDK-supplied CRUD shortcut
|
|
308
|
-
(promoted to v0.1.0; runtime implemented by `InvokeBuiltinUseCase`)
|
|
309
|
-
|
|
310
|
-
**Trigger (v0.1)**:
|
|
311
|
-
- `spec.source.kind: http` — public HTTP endpoint
|
|
312
|
-
- `spec.source.{method, path}` (when `kind: http`)
|
|
313
|
-
- `spec.source.kind: lifecycle` — entry-writer hook (promoted to
|
|
314
|
-
v0.1.0; runtime implemented by `LifecycleHookingEntryRepository`)
|
|
315
|
-
- `spec.source.{schema, on, errorPolicy}` (when `kind: lifecycle`)
|
|
316
|
-
- `spec.source.kind: mcp` plus `surface: public | staff` — MCP tool
|
|
317
|
-
exposure for a declared Procedure
|
|
318
|
-
- `spec.target.procedure:`
|
|
319
|
-
|
|
320
|
-
#### v0.1 closed enums
|
|
321
|
-
|
|
322
|
-
- `x-mantle-bind: {ctx.user, ctx.staff, now}`
|
|
323
|
-
- `ctx.*` predicate vocabulary: `{user, staff, auth, auth.scope}` (no
|
|
324
|
-
`system` until a use case forces it)
|
|
325
|
-
- `Trigger.source.kind: {http, lifecycle, mcp}`
|
|
326
|
-
- `Procedure.handler.kind: {ref, builtin}`
|
|
327
|
-
- `BuiltinOp: {create, update, upsert, delete}`
|
|
328
|
-
- `LifecycleHook: {before_create, after_create, before_update,
|
|
329
|
-
after_update, before_delete, after_delete, before_publish,
|
|
330
|
-
after_publish}`
|
|
331
|
-
|
|
332
|
-
#### What's DRAFT (do not implement, do not type)
|
|
333
|
-
|
|
334
|
-
- Schema: `policies.{visible, readable, writable, owner}`,
|
|
335
|
-
`x-mantle-ref` auto-lift to virtual column,
|
|
336
|
-
computed columns via projection Trigger
|
|
337
|
-
- View: `recursive`, `gatedBy`, `join`, `policies.skip`,
|
|
338
|
-
filter AST extensions (`contains`, `not`, `in`, `like`)
|
|
339
|
-
- Procedure: `requires.auth.{any | all}` with disjunction;
|
|
340
|
-
`owns:`, `contains:`, `withinMinutes:`, `requires.window`,
|
|
341
|
-
`requires.quota`, `errors`, `retry`
|
|
342
|
-
- Trigger: `source.kind: {cron, queue}`,
|
|
343
|
-
`target.project`, `atomicity`
|
|
344
|
-
(`source.kind: mcp` was promoted in alpha.16 — #281)
|
|
345
|
-
- Cross-cutting: `ctx.system`, `$.*` placeholder namespace,
|
|
346
|
-
`staffBypass:`
|
|
347
|
-
|
|
348
|
-
#### Discipline
|
|
349
|
-
|
|
350
|
-
- The v0.1 validator **rejects DRAFT keys at parse time** with a
|
|
351
|
-
`DRAFT_KEY_USED` warning (not error — see the
|
|
352
|
-
AI-as-primary-author contract on permissive bias). The
|
|
353
|
-
diagnostic explicitly references the future-grammar reference
|
|
354
|
-
so authors know the key is reserved, not
|
|
355
|
-
unsupported-and-forgotten.
|
|
356
|
-
- Each DRAFT key has a documented **landing condition** (the
|
|
357
|
-
use case that surfaces it). Promoting a key to v0.1+ requires
|
|
358
|
-
showing that condition has been met — not just "it would be
|
|
359
|
-
nice to have."
|
|
360
|
-
- Each promotion goes through a 3-agent design review
|
|
361
|
-
(yml-editor proposes / code-impler tests buildability /
|
|
362
|
-
fresh-dev verifies clarity) before locking.
|
|
363
|
-
- The v0.1 floor is the floor, not the ceiling. The atom set
|
|
364
|
-
(4) is locked; the grammar inside each atom is permitted to
|
|
365
|
-
grow.
|
|
366
|
-
|
|
367
|
-
### Consequences
|
|
368
|
-
|
|
369
|
-
#### Pros
|
|
370
|
-
|
|
371
|
-
- v0.1 dispatcher / validator / types are tractable in a small
|
|
372
|
-
number of weeks, not many months.
|
|
373
|
-
- Author cognitive surface is small: 4 atoms × ~5 keys each ≈
|
|
374
|
-
20 things to learn, instead of 18 keys with ~36 cognitive
|
|
375
|
-
surface units.
|
|
376
|
-
- DRAFT keys document where the spec is **going**, so authors
|
|
377
|
-
with future requirements can tell whether the project is on
|
|
378
|
-
trajectory or not — without those features being prematurely
|
|
379
|
-
committed.
|
|
380
|
-
- Each DRAFT promotion gets a real use case attached, so the
|
|
381
|
-
grammar evolves under empirical pressure rather than
|
|
382
|
-
speculation. This was the design experiment's strongest
|
|
383
|
-
signal: features like `View.recursive` and
|
|
384
|
-
`Trigger.target.project` looked very different once a real
|
|
385
|
-
feature (threaded comments, cross-Schema invariants) applied
|
|
386
|
-
pressure.
|
|
387
|
-
|
|
388
|
-
#### Costs
|
|
389
|
-
|
|
390
|
-
- Authors with ambitions beyond v0.1 (private posts, role-gated
|
|
391
|
-
reads, rate limits, recursive views) must wait or write
|
|
392
|
-
handler-side TS. For some projects this is enough to shop
|
|
393
|
-
elsewhere; that's acceptable for an OSS v0.1.
|
|
394
|
-
- DRAFT documentation is itself a maintenance burden — the
|
|
395
|
-
reference has to stay coherent as the spec evolves; stale
|
|
396
|
-
DRAFT entries (features no longer planned) need pruning.
|
|
397
|
-
- The "warning, not error, on DRAFT keys" rule means an author
|
|
398
|
-
who copies a DRAFT example into a manifest doesn't fail —
|
|
399
|
-
they get a warning. This requires the validator to know the
|
|
400
|
-
full DRAFT vocabulary, not just the v0.1 vocabulary.
|
|
401
|
-
|
|
402
|
-
#### Risks
|
|
403
|
-
|
|
404
|
-
- **Grammar surface grows ad-hoc.** Each DRAFT promotion looks
|
|
405
|
-
reasonable; together they reproduce the over-grammar v0.1
|
|
406
|
-
was avoiding. Mitigation: 3-agent review per promotion;
|
|
407
|
-
promotions land in batches with documented use cases, not
|
|
408
|
-
one-by-one.
|
|
409
|
-
- **DRAFT becomes vaporware.** Some DRAFT features are listed
|
|
410
|
-
but never land because their use case never surfaces.
|
|
411
|
-
Mitigation: this is fine — the appendix is documenting
|
|
412
|
-
shapes, not commitments. If a DRAFT entry stays cold for >12
|
|
413
|
-
months, prune it from the appendix and let the future
|
|
414
|
-
proposer re-derive it.
|
|
415
|
-
- **Authors hit walls and route around the SDK.** "I need
|
|
416
|
-
`View.recursive` so I'll just write raw SQL in a Procedure
|
|
417
|
-
handler" — over time this drifts the actual extension shape
|
|
418
|
-
away from what the SDK eventually builds. Mitigation: when
|
|
419
|
-
ad-hoc handler patterns repeat, that's the use case that
|
|
420
|
-
promotes the DRAFT key. Watch handler implementations for
|
|
421
|
-
recurring patterns.
|
|
422
|
-
|
|
423
|
-
### Alternatives considered
|
|
424
|
-
|
|
425
|
-
**(a) Ship the full experiment grammar in v0.1**.
|
|
426
|
-
Rejected: 5–10× the dispatcher work; locks shapes in the
|
|
427
|
-
abstract before real use cases apply pressure; author surface
|
|
428
|
-
explodes.
|
|
429
|
-
|
|
430
|
-
**(b) Don't document DRAFT at all; ship v0.1 spec only**.
|
|
431
|
-
Rejected: authors with future requirements have no way to
|
|
432
|
-
evaluate trajectory; the design experiment's results would be
|
|
433
|
-
lost institutionally; future proposals would re-litigate solved
|
|
434
|
-
questions. The DRAFT reference is a forward-looking invariant.
|
|
435
|
-
|
|
436
|
-
**(c) Ship a subset of DRAFT in v0.1 (e.g. `policies.visible`
|
|
437
|
-
and `requires.auth.any`)**.
|
|
438
|
-
Rejected: any line we draw between v0.1 and DRAFT has the same
|
|
439
|
-
litigation problem. The principled cut is "minimum essential to
|
|
440
|
-
make 4 atoms work end-to-end"; anything richer needs a specific
|
|
441
|
-
motivation. None of the DRAFT features had that motivation in
|
|
442
|
-
the v0.1 ship list.
|
|
443
|
-
|
|
444
|
-
**(d) Promote DRAFT keys lazily — implement at runtime when
|
|
445
|
-
authors ask, no spec lock**.
|
|
446
|
-
Rejected: spec coherence demands keys are spec'd before they
|
|
447
|
-
ship. Authors writing against runtime-only features get
|
|
448
|
-
silently broken on SDK upgrades.
|
|
449
|
-
|
|
450
|
-
### How to apply
|
|
451
|
-
|
|
452
|
-
- Authoring v0.1 manifests: stick to the v0.1 vocabulary above.
|
|
453
|
-
If something seems missing, check the DRAFT reference; if it's
|
|
454
|
-
there and you have a concrete use case, file an issue
|
|
455
|
-
describing the use case.
|
|
456
|
-
- SDK code: implement only v0.1 keys in the dispatcher /
|
|
457
|
-
validator / types. DRAFT keys are documented but not handled.
|
|
458
|
-
- DRAFT promotion: needs a documented use case + 3-agent design
|
|
459
|
-
review + ADR landing the promotion (with cross-link to the
|
|
460
|
-
use case).
|
|
461
|
-
- Pruning DRAFT: if an entry has stayed cold for 12+ months,
|
|
462
|
-
propose removal in a small ADR. Removal is reversible (the
|
|
463
|
-
shape is in the experiment transcripts and prior ADR
|
|
464
|
-
history).
|
|
216
|
+
The parser accepts only grammar implemented by the current SDK. Unknown keys and
|
|
217
|
+
closed-enum values fail validation; Mantle does not reserve speculative syntax.
|
|
218
|
+
Add new grammar only alongside a concrete use case, runtime behavior, validation,
|
|
219
|
+
and an ADR update. Earlier experiments remain available in git history.
|
|
465
220
|
|
|
466
221
|
---
|
|
467
222
|
|
|
@@ -482,7 +237,7 @@ bundles multiple atoms. A contact form is at minimum:
|
|
|
482
237
|
inserts)
|
|
483
238
|
- A `Trigger` binding the Procedure to `POST /api/contact`
|
|
484
239
|
|
|
485
|
-
Three atoms, three
|
|
240
|
+
Three atoms, three documents. With one file per atom, a
|
|
486
241
|
small site with 10 features has 30+ manifest files. An early
|
|
487
242
|
design-experiment vibe-user called this out:
|
|
488
243
|
|
|
@@ -496,10 +251,10 @@ Two paths surfaced to address the file count:
|
|
|
496
251
|
#### Path A — Multi-doc YAML
|
|
497
252
|
|
|
498
253
|
YAML's standard `---` separator allows multiple documents in
|
|
499
|
-
one file.
|
|
254
|
+
one file. The fixed site manifest carries all atoms:
|
|
500
255
|
|
|
501
256
|
```yaml
|
|
502
|
-
# manifests/
|
|
257
|
+
# manifests/site.yaml
|
|
503
258
|
apiVersion: cms.mantle.aotter.net/v1
|
|
504
259
|
kind: Schema
|
|
505
260
|
metadata: { name: contact-messages }
|
|
@@ -525,7 +280,7 @@ spec:
|
|
|
525
280
|
```
|
|
526
281
|
|
|
527
282
|
One file, three atoms, conceptual separation preserved. The
|
|
528
|
-
loader
|
|
283
|
+
loader reads `site.yaml` and parses each `---` block as a separate
|
|
529
284
|
manifest.
|
|
530
285
|
|
|
531
286
|
#### Path B — Inline shortcut on Procedure (`expose:`)
|
|
@@ -560,7 +315,7 @@ Path B created **two ways to bind a Procedure to HTTP**:
|
|
|
560
315
|
|
|
561
316
|
- For a Procedure with one HTTP transport: inline `expose:` on
|
|
562
317
|
Procedure
|
|
563
|
-
- For a Procedure with multiple transports (HTTP +
|
|
318
|
+
- For a Procedure with multiple transports (HTTP + MCP):
|
|
564
319
|
separate `Trigger` per transport
|
|
565
320
|
|
|
566
321
|
This is the exact "two ways to do the same thing" failure mode.
|
|
@@ -582,31 +337,20 @@ multi") but creates permanent tax:
|
|
|
582
337
|
|
|
583
338
|
#### What multi-doc YAML accomplishes
|
|
584
339
|
|
|
585
|
-
- File count drops without inventing redundancy. A
|
|
586
|
-
|
|
340
|
+
- File count drops without inventing redundancy. A site has one
|
|
341
|
+
authored manifest file regardless of how many atoms it requires.
|
|
587
342
|
- Atom separation stays honest. Every atom has its own
|
|
588
343
|
envelope (`apiVersion`, `kind`, `metadata`, `spec`); there
|
|
589
344
|
is no "shortcut form."
|
|
590
|
-
- Related atoms sit next to each other for readers reviewing
|
|
591
|
-
feature.
|
|
345
|
+
- Related atoms sit next to each other for readers reviewing the site.
|
|
592
346
|
- The PG-1:1 framing stays clean: Procedure is
|
|
593
347
|
internal-callable, Trigger is the binding atom.
|
|
594
348
|
|
|
595
|
-
####
|
|
596
|
-
|
|
597
|
-
- One feature → one file. Name the file by feature
|
|
598
|
-
(`contact.yaml`, `posts.yaml`, `comments.yaml`), not by atom
|
|
599
|
-
type.
|
|
600
|
-
- The shared file holds the Procedure + its Trigger + (when
|
|
601
|
-
feature-specific) the Schema and View.
|
|
602
|
-
- Schemas that are referenced by many features can live in
|
|
603
|
-
their own `<schema-name>.schema.yaml` file.
|
|
604
|
-
- Views with no Procedure / Trigger neighbors live in
|
|
605
|
-
`<view-name>.view.yaml`.
|
|
349
|
+
#### Fixed file contract
|
|
606
350
|
|
|
607
|
-
The
|
|
608
|
-
|
|
609
|
-
|
|
351
|
+
The authored file is always `manifests/site.yaml`. The common loader rejects
|
|
352
|
+
a missing or differently named file, and every `---` document becomes one
|
|
353
|
+
atom. This is enforced so humans and agents never need a file-discovery rule.
|
|
610
354
|
|
|
611
355
|
### Consequences
|
|
612
356
|
|
|
@@ -616,8 +360,7 @@ block as a manifest.
|
|
|
616
360
|
OpenAPI emitter, runtime dispatcher all walk one shape.
|
|
617
361
|
- Conceptual atom separation preserved. The 4-atom story holds
|
|
618
362
|
for every feature.
|
|
619
|
-
- File count
|
|
620
|
-
files, not 30+.
|
|
363
|
+
- File count is fixed at one authored manifest.
|
|
621
364
|
- Cardinality changes (1 HTTP → HTTP + cron) are additive (add
|
|
622
365
|
a Trigger); no refactor needed.
|
|
623
366
|
- AI authors learn one mental model, not two.
|
|
@@ -630,9 +373,8 @@ block as a manifest.
|
|
|
630
373
|
- Some loaders (older / non-compliant) don't handle multi-doc.
|
|
631
374
|
Standard `js-yaml` / `yaml` libraries do; the SDK uses
|
|
632
375
|
`yaml` (already a dependency).
|
|
633
|
-
-
|
|
634
|
-
|
|
635
|
-
must `grep` instead.
|
|
376
|
+
- Authors use `grep` by atom name inside `manifests/site.yaml` instead of
|
|
377
|
+
discovering feature-specific filenames.
|
|
636
378
|
- Editor support: YAML language servers handle multi-doc, but
|
|
637
379
|
some IDE features (e.g. JSON Schema validation per document
|
|
638
380
|
in a multi-doc file) work less smoothly than in single-doc
|
|
@@ -647,18 +389,9 @@ block as a manifest.
|
|
|
647
389
|
ways is permanent tax that grows with the codebase.
|
|
648
390
|
Mitigation: this section is the answer to that argument; flag
|
|
649
391
|
attempts and link here.
|
|
650
|
-
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
Mitigation: file-organization convention is suggestion, not
|
|
654
|
-
rule; authors are free to split when it stops helping.
|
|
655
|
-
- **Co-location breaks discoverability for shared atoms.**
|
|
656
|
-
A Schema referenced by 4 features can live in any of those
|
|
657
|
-
4 files (or its own). Without grep / IDE go-to-def, finding
|
|
658
|
-
the Schema definition requires knowing where the author
|
|
659
|
-
put it. Mitigation: Loop 1 (`mantle validate`) emits the
|
|
660
|
-
filesystem path in `VIEW_FROM_UNKNOWN_SCHEMA` diagnostics;
|
|
661
|
-
IDE-shaped tooling (LSP — DRAFT) closes the rest.
|
|
392
|
+
- **`site.yaml` can grow.** Search by `kind` plus `metadata.name`; reconsider
|
|
393
|
+
splitting only when real projects show that one deterministic location is
|
|
394
|
+
worse than file discovery.
|
|
662
395
|
|
|
663
396
|
### Alternatives considered
|
|
664
397
|
|
|
@@ -685,14 +418,14 @@ explicit `Trigger` + explicit `expose:`).
|
|
|
685
418
|
|
|
686
419
|
### How to apply
|
|
687
420
|
|
|
688
|
-
- Authoring:
|
|
421
|
+
- Authoring: put every atom in `manifests/site.yaml`, separated by `---`.
|
|
689
422
|
- Reviewing: when an author writes a Procedure with an inline
|
|
690
423
|
HTTP binding, push back with this section.
|
|
691
424
|
- Refactoring: a feature that grew from one HTTP binding to
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
- Tooling: SDK loader
|
|
695
|
-
|
|
425
|
+
HTTP + MCP doesn't change the Procedure; it adds one Trigger
|
|
426
|
+
in the same file.
|
|
427
|
+
- Tooling: SDK loader reads `<manifest-root>/site.yaml` and parses each `---`
|
|
428
|
+
block; static validator emits diagnostics
|
|
696
429
|
with file path + manifest pointer
|
|
697
|
-
(`
|
|
430
|
+
(`site.yaml#/2/spec/from`) so multi-doc location stays
|
|
698
431
|
precise.
|
|
@@ -96,26 +96,19 @@ The vocabulary is exactly:
|
|
|
96
96
|
|
|
97
97
|
- `ctx.user` — caller is signed in as an end-user
|
|
98
98
|
- `ctx.staff: [<role>, ...]` — caller is signed in as staff in one of these roles
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
`contains:`, quota predicates) is DRAFT — see
|
|
102
|
-
[ADR-0001](0001-four-atom-manifest-model.md) § Future grammar
|
|
103
|
-
discipline.
|
|
99
|
+
- `ctx.auth` — caller supplied an adapter-verified credential
|
|
100
|
+
- `ctx.auth.scope: <scope>` — credential carries the exact scope
|
|
104
101
|
|
|
105
102
|
### Adding a new entry is an explicit grammar-revise round
|
|
106
103
|
|
|
107
|
-
New `x-mantle-bind` values or `ctx.*` predicates do not get added
|
|
108
|
-
ad-hoc. They go through the discipline gate documented in
|
|
109
|
-
[ADR-0001](0001-four-atom-manifest-model.md) § Future grammar
|
|
110
|
-
discipline:
|
|
104
|
+
New `x-mantle-bind` values or `ctx.*` predicates do not get added ad hoc:
|
|
111
105
|
|
|
112
106
|
1. A documented use case showing the existing closed set cannot
|
|
113
107
|
express the requirement.
|
|
114
108
|
2. A design pass on what the new value's runtime semantics are
|
|
115
109
|
(where does it come from? when is it null? what happens at
|
|
116
110
|
the storage layer?).
|
|
117
|
-
3. A spec doc revision
|
|
118
|
-
classification.
|
|
111
|
+
3. A spec doc revision.
|
|
119
112
|
4. Code that updates the validator to accept the new value.
|
|
120
113
|
|
|
121
114
|
This treats the closed set as load-bearing infrastructure, not
|
|
@@ -208,10 +201,8 @@ attributes.
|
|
|
208
201
|
- New manifest using a value not in the enum: parse error,
|
|
209
202
|
exact diagnostic shape with `candidates` populated. AI authors
|
|
210
203
|
fix in one turn.
|
|
211
|
-
- New use case wanting an entry not in the enum:
|
|
212
|
-
|
|
213
|
-
discipline. Document the use case in the spec PR; do not
|
|
214
|
-
fast-track.
|
|
204
|
+
- New use case wanting an entry not in the enum: document the concrete
|
|
205
|
+
semantics and add validation, runtime behavior, and docs together.
|
|
215
206
|
- Lookups (e.g. "stamp the team_id"): handler-side TS, not
|
|
216
207
|
binding metadata. The Procedure handler has the lookup
|
|
217
208
|
context anyway.
|
|
@@ -100,6 +100,8 @@ What it catches:
|
|
|
100
100
|
`View.filter` refer only to declared properties of that Schema
|
|
101
101
|
- `Schema.uniqueIndexes` and `Schema.indexes` contain valid ordered
|
|
102
102
|
tuples of scalar properties declared in `spec.schema.properties`
|
|
103
|
+
- `Schema.searchableFields` contains unique top-level string properties
|
|
104
|
+
- `Schema.uiSchema.list.filterField` names an indexed operational string enum
|
|
103
105
|
- `x-mantle-bind: <value>` is in the closed enum
|
|
104
106
|
- `requires.auth.all` predicates are in the v0.1 vocabulary
|
|
105
107
|
- `Trigger.source.path` does not collide with another Trigger
|
|
@@ -264,7 +266,7 @@ Concrete artifacts today:
|
|
|
264
266
|
admin: write a post, update a draft, request publish, list
|
|
265
267
|
submissions.
|
|
266
268
|
- Future ops-role verbs that don't exist yet but have an obvious
|
|
267
|
-
operator use case ("translate this draft", "
|
|
269
|
+
operator use case ("translate this draft", "publish this entry").
|
|
268
270
|
When added, they land as **new SDK builtin tools** — declared in
|
|
269
271
|
the MCP catalog source under the closed Day-1 set, not
|
|
270
272
|
auto-generated from consumer Procedures.
|
|
@@ -126,7 +126,7 @@ code string. That was retired because:
|
|
|
126
126
|
### `path` format
|
|
127
127
|
|
|
128
128
|
- For static validation: filesystem path + JSON Pointer fragment,
|
|
129
|
-
e.g. `manifests/
|
|
129
|
+
e.g. `manifests/site.yaml#/3/spec/from`.
|
|
130
130
|
- For a consumer test diagnostic: test file path + assertion location when
|
|
131
131
|
available, e.g. `tests/handlers/contact.test.ts:42`.
|
|
132
132
|
- For boot-time: manifest pointer (no on-disk path because boot
|
|
@@ -250,7 +250,7 @@ Diagnostic candidate (alternative (d) below) applies to zod —
|
|
|
250
250
|
and AI authors see the same structure.
|
|
251
251
|
- Single source of truth. The interface, code constants, and
|
|
252
252
|
formatter all live in `@aotter/mantle-spec`; the runtime
|
|
253
|
-
package and adapters import them. A future
|
|
253
|
+
package and adapters import them. A future adapter
|
|
254
254
|
inherits the shape for free.
|
|
255
255
|
|
|
256
256
|
### Costs
|
|
@@ -22,9 +22,10 @@ YAML machinery into the runtime bundle and coupled consumers to Wrangler
|
|
|
22
22
|
|
|
23
23
|
## Decision
|
|
24
24
|
|
|
25
|
-
Consumers own
|
|
26
|
-
validates
|
|
27
|
-
types
|
|
25
|
+
Consumers own exactly `manifests/site.yaml`. The installed `mantle` CLI parses
|
|
26
|
+
and validates its multi-document YAML, then writes the machine-owned runtime
|
|
27
|
+
module and handler types. A missing or differently named file fails before
|
|
28
|
+
generation:
|
|
28
29
|
|
|
29
30
|
```bash
|
|
30
31
|
pnpm exec mantle generate
|
|
@@ -72,7 +73,7 @@ runtime boot validation covers cross-manifest and registered-handler facts.
|
|
|
72
73
|
|
|
73
74
|
The external [`aotter/mantle-starters`](https://github.com/aotter/mantle-starters)
|
|
74
75
|
repository owns the blank project and typed overlays. Materialized projects
|
|
75
|
-
carry their own `manifests
|
|
76
|
+
carry their own `manifests/site.yaml`, generated module, handlers, and package scripts;
|
|
76
77
|
they consume the exact packed Core artifact rather than a workspace link.
|
|
77
78
|
|
|
78
79
|
## Consequences
|
|
@@ -128,7 +129,7 @@ runtime.
|
|
|
128
129
|
|
|
129
130
|
When authoring or reviewing a generated project:
|
|
130
131
|
|
|
131
|
-
1. Edit
|
|
132
|
+
1. Edit only `manifests/site.yaml`; do not create feature-named manifest files.
|
|
132
133
|
2. Run the installed `pnpm exec mantle generate`.
|
|
133
134
|
3. Import `manifest` from `.mantle/generated/site.js` into the conventional
|
|
134
135
|
Worker, or pass it as `CmsConfig.manifests` in low-level composition.
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
**Deciders**: phsu
|
|
8
8
|
|
|
9
|
-
**Related**: [ADR-0001](0001-four-atom-manifest-model.md) (the Schema atom this extends
|
|
9
|
+
**Related**: [ADR-0001](0001-four-atom-manifest-model.md) (the Schema atom this extends).
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
@@ -70,7 +70,7 @@ matches the principle that locale is opt-in.
|
|
|
70
70
|
|
|
71
71
|
The boot validator only inspects the manifest at this layer. It
|
|
72
72
|
checks shape (every `localized: true` Schema is well-formed, every
|
|
73
|
-
`translates:` block resolves) and rejects
|
|
73
|
+
`translates:` block resolves) and rejects unsupported keys; it does **not**
|
|
74
74
|
read D1 to confirm that the site actually has any locales configured.
|
|
75
75
|
That cross-check is deferred to runtime (Layer 3).
|
|
76
76
|
|
|
@@ -206,29 +206,35 @@ apiVersion: cms.mantle.aotter.net/v1
|
|
|
206
206
|
kind: Schema
|
|
207
207
|
metadata: { name: products }
|
|
208
208
|
spec:
|
|
209
|
+
title: Products
|
|
210
|
+
localized: false
|
|
209
211
|
schema:
|
|
212
|
+
type: object
|
|
210
213
|
properties:
|
|
211
214
|
slug: { type: string }
|
|
212
215
|
sku: { type: string }
|
|
213
216
|
price: { type: number }
|
|
214
217
|
required: [slug, sku, price]
|
|
215
|
-
|
|
218
|
+
uniqueIndexes: [[slug]]
|
|
216
219
|
---
|
|
217
220
|
apiVersion: cms.mantle.aotter.net/v1
|
|
218
221
|
kind: Schema
|
|
219
222
|
metadata: { name: product-translations }
|
|
220
223
|
spec:
|
|
224
|
+
title: Product translations
|
|
221
225
|
localized: true
|
|
222
226
|
translates:
|
|
223
227
|
parent: products
|
|
224
228
|
on: slug
|
|
225
229
|
schema:
|
|
230
|
+
type: object
|
|
226
231
|
properties:
|
|
227
232
|
slug: { type: string }
|
|
233
|
+
locale: { type: string }
|
|
228
234
|
title: { type: string }
|
|
229
235
|
description: { type: string }
|
|
230
|
-
required: [slug, title]
|
|
231
|
-
|
|
236
|
+
required: [slug, locale, title]
|
|
237
|
+
uniqueIndexes: [[slug, locale]]
|
|
232
238
|
```
|
|
233
239
|
|
|
234
240
|
`Schema.spec.translates` declares the parent/child relationship as
|
|
@@ -245,9 +251,6 @@ all treat the relation as known structure rather than convention:
|
|
|
245
251
|
- Admin UI groups parent + per-locale translation entries together.
|
|
246
252
|
- Boot validate enforces parent existence and join-field presence in
|
|
247
253
|
both parent and child JSON Schemas (manifest shape, no D1 reads).
|
|
248
|
-
- View executor (when `View.join` lands per the future-grammar
|
|
249
|
-
appendix) can auto-join parent + child without per-View
|
|
250
|
-
configuration.
|
|
251
254
|
- AI authoring an entry against the child knows from the manifest
|
|
252
255
|
that there's a parent it must reference by `slug`.
|
|
253
256
|
|
|
@@ -262,6 +265,8 @@ Validation rules introduced:
|
|
|
262
265
|
- `TRANSLATES_REQUIRES_LOCALIZED` — `translates: ...` declared on a
|
|
263
266
|
Schema where `localized` isn't `true`. (A non-localized translation
|
|
264
267
|
table makes no sense.)
|
|
268
|
+
- `TRANSLATES_REQUIRES_CONTENT_FIELD` — the child declares only its join
|
|
269
|
+
field and `locale`, with no locale-specific payload to translate.
|
|
265
270
|
|
|
266
271
|
## Consequences
|
|
267
272
|
|