@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.
@@ -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` + `pg_cron` + PostgREST route + `LISTEN/NOTIFY` | yes (the binding atom) | no |
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 / cron / lifecycle / queue, declare a
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 + cron + MCP,
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 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.
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-CRUD)
182
- was recovered without folding — captured in the DRAFT
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
- ## Future grammar discipline (was POC ADR-0005)
214
+ ## Grammar changes
221
215
 
222
- > Folded in 2026-05-03 from POC ADR-0005 ("v0.1 minimum essential
223
- > grammar; rich grammar reserved as DRAFT"). The discipline below
224
- > applies to every key inside every atom defined above.
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 manifests. With one file per manifest, a
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. A "feature file" carries all atoms for that feature:
254
+ one file. The fixed site manifest carries all atoms:
500
255
 
501
256
  ```yaml
502
- # manifests/contact.yaml
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 walks files and parses each `---` block as a separate
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 + cron + MCP):
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 feature →
586
- one file, regardless of how many atoms it requires.
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 a
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
- #### File organization conventions (suggested, not enforced)
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 validator does not enforce naming conventions; it walks
608
- all `*.yaml` under the manifest root and parses every `---`
609
- block as a manifest.
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 tractable. A typical small site is 5–20 manifest
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
- - File-discovery patterns (`find . -name "send-contact-message.trigger.yaml"`)
634
- no longer work for atoms inside multi-doc files. Authors
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
- - **Multi-doc files grow unwieldy.** A "shop" feature might
651
- have a Schema + 3 Procedures + 5 Triggers + 2 Views. One
652
- file with 11 atoms is harder to navigate than 11 files.
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: one feature → one file with `---` separators.
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
- one HTTP + one cron + one MCP doesn't change the Procedure;
693
- it adds two more Triggers in the same file.
694
- - Tooling: SDK loader walks `*.yaml` under manifest root and
695
- parses each `---` block; static validator emits diagnostics
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
- (`feature.yaml#/2/spec/from`) so multi-doc location stays
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
- Anything beyond (`any:` disjunction; `owns:`, `withinMinutes:`,
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, including the v0.1-vs-DRAFT
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: open a
212
- grammar-revise discussion per ADR-0001 § Future grammar
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", "schedule for Friday").
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/recent-published.view.yaml#/spec/from`.
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 `mantle-netlify`
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 YAML under `manifests/`. The installed `mantle` CLI parses and
26
- validates that YAML, then writes the machine-owned runtime module and handler
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/`, generated module, handlers, and package scripts;
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 YAML only under `manifests/`.
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; §"Future grammar discipline" covers the v0.1-vs-DRAFT window this lands in).
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 DRAFT keys; it does **not**
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
- unique: [slug]
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
- unique: [[slug, locale]]
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