@aotter/mantle 0.1.0-alpha.1 → 0.1.0-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,14 +3,7 @@
3
3
  > If you're an AI working in a project that consumes `@aotter/mantle-*`,
4
4
  > read this first. It explains the entire surface area in one page.
5
5
  >
6
- > **Status**: v0.1 grammar lock. Atoms are shipped; rich sub-spec
7
- > grammar (policies, recursive views, temporal predicates, quotas,
8
- > projection triggers, cron/queue sources, and extended lifecycle hooks) is
9
- > reserved as **DRAFT** — see "Future grammar" appendix. Editorial
10
- > lifecycle is the one shipped grammar key whose runtime is **deferred
11
- > to v0.1.x**: parser and boot accept the shape, while
12
- > `request_publish` rejects it with `LIFECYCLE_NOT_IN_V010` until the approval
13
- > queue lands. Do not use editorial for a v0.1 publishing workflow.
6
+ > **Status**: v0.1 shipped grammar. Unknown keys and enum values are rejected.
14
7
  >
15
8
  > **This is the reference manual** — what the system is. For *why* it
16
9
  > ended up this shape (alternatives considered, trade-offs accepted),
@@ -33,7 +26,7 @@ primitives Postgres has shipped for 30 years.
33
26
  | **`Schema`** | `CREATE TABLE` | no (manipulated via View / Procedure) | no |
34
27
  | **`View`** | `CREATE VIEW` | **yes** (auto-mounted on its declared public/staff REST and MCP surface; see ADR-0012) | no |
35
28
  | **`Procedure`** | `CREATE FUNCTION ... LANGUAGE plpgsql` | **no** (transport-agnostic; needs a `Trigger` to bind it) | **yes — handler ref to consumer's TS file** |
36
- | **`Trigger`** | `CREATE TRIGGER` + `pg_cron` + PostgREST route + `LISTEN/NOTIFY` | yes (the binding atom — turns Procedures into HTTP endpoints, cron jobs, MCP tools, lifecycle hooks) | no |
29
+ | **`Trigger`** | `CREATE TRIGGER` + route/tool binding | yes (the binding atom — turns Procedures into HTTP endpoints, MCP tools, and lifecycle hooks) | no |
37
30
 
38
31
  The **read/write asymmetry is by design** and matches HTTP safe-vs-unsafe
39
32
  semantics. Reads (`View`) are idempotent and cacheable; the SDK
@@ -53,7 +46,6 @@ apiVersion: cms.mantle.aotter.net/v1
53
46
  kind: Schema | View | Procedure | Trigger
54
47
  metadata:
55
48
  name: posts # required, kebab-case, unique within deployment
56
- labels: { ... } # optional, free-form
57
49
  spec:
58
50
  ... # kind-specific
59
51
  ```
@@ -73,7 +65,7 @@ A logical feature commonly bundles a Procedure + a Trigger (and often a
73
65
  Schema and a View). Put related atoms in one file separated by `---`:
74
66
 
75
67
  ```yaml
76
- # manifests/contact.yaml
68
+ # manifests/site.yaml
77
69
  apiVersion: cms.mantle.aotter.net/v1
78
70
  kind: Procedure
79
71
  metadata: { name: send-contact-message }
@@ -93,8 +85,8 @@ spec:
93
85
  target: { procedure: send-contact-message }
94
86
  ```
95
87
 
96
- One file, two atoms. File count stays low; conceptual atom separation
97
- stays honest.
88
+ One fixed file, two documents. Add every atom to `manifests/site.yaml` with
89
+ `---`; the loader rejects other manifest filenames.
98
90
 
99
91
  ## What each atom is for (v0.1 minimum essential grammar)
100
92
 
@@ -112,7 +104,7 @@ metadata: { name: posts }
112
104
  spec:
113
105
  title: Posts # required: human-readable label for the admin UI
114
106
  localized: true # opt-in: row carries data.locale (ADR-0010)
115
- lifecycle: simple # default; 'none' is operational, 'editorial' is reserved
107
+ lifecycle: publishing # default; use 'operational' for live records
116
108
  schema:
117
109
  $schema: https://json-schema.org/draft/2020-12/schema
118
110
  type: object
@@ -126,7 +118,8 @@ spec:
126
118
  # against localized Schemas.
127
119
  content: { type: string }
128
120
  authorId: { type: string, format: uuid, x-mantle-bind: ctx.user }
129
- createdAt: { type: string, format: date-time, x-mantle-bind: now }
121
+ createdAt: { type: integer, x-mcp-hint: timestamp-ms, x-mantle-bind: now }
122
+ searchableFields: [title, slug] # id is always searched
130
123
  uniqueIndexes: [[slug, locale]]
131
124
  indexes: [[locale, title]] # ordered, non-unique hot path
132
125
  ```
@@ -135,6 +128,17 @@ spec:
135
128
  in the user's primary language (the install-time chosen locale); the
136
129
  SPA shows this everywhere instead of `metadata.name`.
137
130
 
131
+ **`spec.searchableFields`** — optional allowlist of top-level string
132
+ properties used by Admin and Staff MCP substring search. Entry id is always
133
+ searchable. This is separate from `indexes`; ordinary B-tree indexes do not
134
+ accelerate leading-wildcard substring search.
135
+
136
+ **`spec.uiSchema.list.filterField`** — optional Admin-only primary filter for
137
+ an operational collection. The field must declare a non-empty string `enum`
138
+ and be the first field of an `indexes` or `uniqueIndexes` tuple. Admin reuses
139
+ the enum for sidebar links and list tabs; Staff MCP uses declared Views for
140
+ richer query capabilities instead of reading UI configuration.
141
+
138
142
  **`spec.localized: bool`** (default `false`, ADR-0010) — opt-in per
139
143
  Schema. Localized Schemas store locale in `data.locale`; non-localized
140
144
  Schemas reject `data.locale` writes. Site config must declare the set
@@ -144,25 +148,22 @@ validates manifest shape.
144
148
 
145
149
  **`spec.translates: { parent, on }`** (ADR-0010) — declares this
146
150
  Schema as the translation companion to a non-localized parent, joined
147
- on the named field (typically `slug`). Implies `localized: true`. The
151
+ on the named field (typically `slug`). Requires an explicit
152
+ `localized: true`. The
148
153
  admin UI surfaces the child only as locale tabs in the parent's editor.
149
154
 
155
+ Use a standalone localized Schema only when each locale row is an
156
+ independent record. When several locale rows are versions of one entity,
157
+ declare a non-localized parent plus a localized `translates` child; that
158
+ relationship is what powers translation grouping and completeness in Admin.
159
+
150
160
  #### Lifecycle
151
161
 
152
- **`spec.lifecycle: 'simple' | 'editorial' | 'none'`** — controls the
162
+ **`spec.lifecycle: 'publishing' | 'operational'`** — controls the
153
163
  entry's state machine.
154
164
 
155
- - `simple` (default) — `draft → published → archived`. No approval
156
- queue. **This is the only content-workflow mode whose runtime ships
157
- in v0.1.0.**
158
- - `editorial` — the six-state machine with an approval queue
159
- (`draft → review → approved → scheduled → published → archived`,
160
- with `published` returnable to `draft` for republish). The grammar and
161
- state-machine vocabulary are reserved for forward compatibility, but the
162
- approval/request-publish runtime is on the v0.1.x roadmap. In v0.1,
163
- `request_publish` rejects with `LIFECYCLE_NOT_IN_V010`; do not declare
164
- editorial for a current publishing workflow.
165
- - `none` — **operational records**, not authored content: orders,
165
+ - `publishing` (default) — `draft → published → archived`. No approval queue.
166
+ - `operational` — records that are not authored content: orders,
166
167
  inventory snapshots, grant/audit rows — anything written by
167
168
  Procedures as a side effect rather than drafted by a person. No
168
169
  content workflow applies: entries are live (`published`) the moment
@@ -175,8 +176,8 @@ entry's state machine.
175
176
  `update_record_<schema>` for these Schemas instead of draft tools.
176
177
 
177
178
  The modes are **per-Schema and mix freely** within a site. There is no
178
- site-wide lifecycle setting; one Schema can be `simple` while another
179
- is `editorial` and a third is `none`.
179
+ site-wide lifecycle setting; one Schema can be `publishing` while another
180
+ is `operational`.
180
181
 
181
182
  **Property-level extensions** (JSON Schema vendor keywords, all optional):
182
183
 
@@ -204,17 +205,11 @@ wire.
204
205
  | `now` | Server timestamp at write (ISO-8601 string with timezone) | `createdAt`, `submittedAt`, `grantedAt` |
205
206
 
206
207
  **v0.1 stamping behavior**:
207
- - Stamped on `INSERT` only. Update-stamping (e.g. `updatedAt: now` that
208
- re-stamps on every UPDATE) is DRAFT — it lands when first use case
209
- surfaces.
208
+ - Stamped on `INSERT` only.
210
209
  - Caller-supplied value: rejected at input validation. The Procedure's
211
210
  effective input schema strips bound fields before checking caller
212
211
  input, so callers can't accidentally send them.
213
212
  - Visible in View output: yes, as ordinary columns. No special masking.
214
- - Used by `requires.auth.owns:` predicate (DRAFT): the predicate
215
- compares a row's `x-mantle-bind: ctx.user` field to the current
216
- `:ctx.user` to verify ownership without configuring per-Schema
217
- owner-column names.
218
213
 
219
214
  **Why a closed enum** (highest-leverage discipline in the spec): bind
220
215
  values are server-controlled identity + time facts. If we accepted
@@ -241,8 +236,7 @@ detection). The admin uses declared refs to show related rows and to nest
241
236
  required child collections; it does not infer relationships from field names.
242
237
  Declare a single-field `indexes` entry for the ref field (for example,
243
238
  `indexes: [[authorId]]`) when reverse lookups must stay bounded. Mantle adds
244
- the native entry-order columns to that access path. Future grammar may upgrade
245
- it to enforced.
239
+ the native entry-order columns to that access path.
246
240
 
247
241
  **Example**:
248
242
  ```yaml
@@ -286,12 +280,6 @@ MCP tool schemas preserve the hint for agents. Admin surfaces expose
286
280
  media-shaped fields but first-party upload hosting is optional and not
287
281
  part of first-run provisioning.
288
282
 
289
- ---
290
-
291
- Anything beyond these three (policies, computed columns, RBAC grants,
292
- owner declarations, `x-mantle-bind` enum extension) is **DRAFT** — see
293
- appendix.
294
-
295
283
  **Postgres analogue**: `CREATE TABLE posts (id UUID PRIMARY KEY, ...,
296
284
  UNIQUE (slug, locale));`
297
285
 
@@ -363,11 +351,8 @@ query, no `LIMIT n+1` probe.
363
351
 
364
352
  **v0.1 filter AST**: comparison operators (`eq`, `gt`, `gte`, `lt`,
365
353
  `lte`) plus `and` / `or`. Comparison `value` may be a literal or a
366
- `{ $param: <name> }` sentinel. v0.1.0 enforces required-only param refs;
367
- optional-with-skip semantics are reserved for v0.1.x. Field-to-field
368
- comparisons are not supported yet; compare a field to a literal or
369
- param. Anything else (`contains`, `recursive`, `gatedBy`,
370
- `join.aggregate`, `policies.skip`) is DRAFT.
354
+ `{ $param: <name> }` sentinel. Param refs must be required. Field-to-field
355
+ comparisons are not supported; compare a field to a literal or param.
371
356
 
372
357
  **Postgres analogue**: `CREATE VIEW recent_published AS SELECT ...
373
358
  FROM posts WHERE status = 'published' ORDER BY updated_at DESC LIMIT
@@ -383,8 +368,8 @@ provides the handler function with auto-generated TS types.
383
368
  `Procedure` is **transport-agnostic and not directly exposed**. It does
384
369
  not contain HTTP paths, methods, or MCP tool names. To call a Procedure
385
370
  from outside the SDK runtime, declare a `Trigger` whose `target` points
386
- at it. The same Procedure can be bound by multiple Triggers (HTTP +
387
- cron + MCP + lifecycle, all sharing one handler).
371
+ at it. The same Procedure can be bound by multiple Triggers (HTTP + MCP +
372
+ lifecycle, all sharing one handler).
388
373
 
389
374
  ```yaml
390
375
  apiVersion: cms.mantle.aotter.net/v1
@@ -426,9 +411,6 @@ export const handlers = {
426
411
  - `ctx.auth.scope: <scope>` — verified credential carries the exact opaque,
427
412
  consumer-owned scope; repeat to require multiple scopes
428
413
 
429
- Anything beyond this (`any:`, `owns:`, `withinMinutes:`, `contains:`,
430
- `requires.window`, `requires.quota`, `errors`, `retry`) is DRAFT.
431
-
432
414
  Both Procedures and Views may add one dynamic guard beside `auth`:
433
415
 
434
416
  ```yaml
@@ -485,7 +467,7 @@ spec:
485
467
  ```
486
468
 
487
469
  The same Procedure can have multiple Triggers — that's how it becomes
488
- "an HTTP endpoint AND a cron job AND an MCP tool" without duplicating
470
+ an HTTP endpoint and an MCP tool without duplicating
489
471
  handler logic. Each transport is one Trigger; the Procedure body is
490
472
  shared.
491
473
 
@@ -496,17 +478,14 @@ tool on `surface: public | staff`), or `lifecycle` (entry-writer hook). For `lif
496
478
  Lifecycle hooks are wired through `LifecycleHookingEntryRepository`, so
497
479
  MCP, admin, and builtin write paths share the same hook behavior.
498
480
 
499
- - `cron` / `queue` are **DRAFT (v0.2+)** — speculative, gated by
500
- concrete consumer demand. Same appendix § "DRAFT (v0.2+)."
501
-
502
481
  The state-machine "lifecycle" from the Schema atom
503
- (`Schema.spec.lifecycle: simple | editorial`) is a separate domain
482
+ (`Schema.spec.lifecycle: publishing | operational`) is a separate domain
504
483
  that shares the word. The Schema setting governs which states an
505
484
  entry can be in; shipped lifecycle Triggers govern what fires around
506
485
  mutations.
507
486
 
508
487
  **Postgres analogue**: `CREATE TRIGGER ... AFTER INSERT ON posts
509
- EXECUTE FUNCTION ...` (lifecycle); `pg_cron` extension (cron); plus
488
+ EXECUTE FUNCTION ...` (lifecycle), plus
510
489
  `CREATE FUNCTION` exposed via PostgREST routes (http) — Postgres has
511
490
  had all of these via extensions for years, just split across multiple
512
491
  mechanisms. We unify them under one atom.
@@ -584,7 +563,7 @@ different constraints.
584
563
  | `UNAUTHENTICATED` | `401` | no active session (admin API: missing/expired session cookie) |
585
564
  | `AUTH_DENIED` | `403` | `requires.auth` predicate evaluated false (or admin: caller lacks required staff role) |
586
565
  | `ENTITLEMENT_REQUIRED` | `402` | consumer guard denies current payment/membership/transaction entitlement |
587
- | `NOT_FOUND` | `404` | resource not found — admin: approval id; runtime: View name at `/api/views/<name>` |
566
+ | `NOT_FOUND` | `404` | resource not found — entry id or View name at `/api/views/<name>` |
588
567
  | `HANDLER_NOT_REGISTERED` | `500` | `handler.ref` key not registered at boot |
589
568
  | `DISPATCHER_NOT_BUILT` | `501` | runtime feature not implemented in this SDK build |
590
569
  | `INTERNAL_ERROR` | `500` | uncaught handler exception |
@@ -595,10 +574,7 @@ may also fire in earlier loops (e.g. `HANDLER_NOT_REGISTERED`
595
574
  fires at boot — `phase: "boot"` — and that's where it should be
596
575
  caught; the runtime occurrence is defense-in-depth).
597
576
 
598
- Additional runtime codes activate as future grammar surfaces (e.g.
599
- `QUOTA_EXCEEDED → 429` arrives with `requires.quota`).
600
-
601
- ## RBAC — what v0.1 ships, what's DRAFT
577
+ ## RBAC
602
578
 
603
579
  v0.1 auth gates use `requires.auth.all` with `ctx.user`,
604
580
  `ctx.staff: [<roles>]`, `ctx.auth`, and `ctx.auth.scope` predicates on
@@ -606,16 +582,6 @@ Procedures and Views. This covers staff-only, logged-in-only,
606
582
  credential-protected, and delegated-scope targets. One Procedure-backed guard
607
583
  handles mutable consumer business state without widening the static grammar.
608
584
 
609
- **Not yet shipped** (DRAFT):
610
- - Row-level read visibility (private posts, friend-only audiences)
611
- - Field-level write policies (this user can update body but not
612
- parent_id)
613
- - Cross-Schema policy inheritance (gatedBy)
614
- - Quotas, edit-windows, rate limits
615
-
616
- These all live as `Schema.spec.policies.*` and `Procedure.spec.requires.*`
617
- sub-specs in the DRAFT spec. See "Future grammar" appendix.
618
-
619
585
  ## How to think when extending this CMS
620
586
 
621
587
  1. **What entities does the feature need?** → `Schema` for each.
@@ -624,13 +590,11 @@ sub-specs in the DRAFT spec. See "Future grammar" appendix.
624
590
  external read endpoints).
625
591
  3. **What operations?** → `Procedure` for each. One Procedure = one
626
592
  typed function call. Compose in handler code, not in YAML.
627
- 4. **What invokes them?** → `Trigger` per source. Multiple Triggers
628
- can target the same Procedure (HTTP + MCP + cron, all on one
629
- handler).
593
+ 4. **What invokes them?** → `Trigger` per declared HTTP, MCP, or lifecycle
594
+ source. Site-owned events can call the same Procedure binding directly.
630
595
  5. **Who's allowed?** → `Procedure/View.spec.requires.auth` for static
631
596
  identity/scope; optional `requires.guard.procedure` for one live,
632
- consumer-owned business check. Row-level and field-level rules remain
633
- future grammar.
597
+ consumer-owned business check.
634
598
 
635
599
  If you find yourself wanting a 5th kind, **stop**. Sketch the same
636
600
  thing as a composition of the four; almost always it works.
@@ -722,8 +686,7 @@ grammar, leftmost-prefix rules, SQL helper, and query-plan examples.
722
686
  comfortable. Anonymous public responses can hit version-local Workers
723
687
  Cache before the Worker; origin misses use indexed D1 reads.
724
688
  - **Mid-scale (10k – 100k entries)**: declare measured list/filter
725
- access paths with ordered `indexes`. Cross-collection JSON-path joins
726
- may eventually gain `x-mantle-ref` auto-lift; that remains DRAFT.
689
+ access paths with ordered `indexes`.
727
690
  - **Hard D1 limits**: 1 MB max row size; 5,000 rows per query result;
728
691
  single-writer per database (concurrent writes serialize).
729
692
  - **Cross-region read**: D1 is region-pinned; first origin hit from a
@@ -753,7 +716,6 @@ When this path is taken (post-v0.1; not implemented yet), the
753
716
  | `json_each` → array operators (`@>`, `&&`) | none — predicates stay declarative |
754
717
  | SDK-stamped `now` → `DEFAULT now()` (optional) | none |
755
718
  | SDK-side policy rewriting → native RLS (optional) | none |
756
- | `Trigger.target.project` single-writer DO → real same-transaction trigger | atomicity guarantee strengthens; YAML grammar unchanged (the `atomicity:` declaration just stops needing the eventual-consistency caveat) |
757
719
 
758
720
  Migration shape: `entries` table dumped from D1, restored to PG. Stay
759
721
  single-table-with-discriminator (drop-in) or split into
@@ -761,68 +723,12 @@ table-per-collection (more PG-idiomatic, longer migration). Both shapes
761
723
  remain valid `cms.mantle.aotter.net/v1` deployments.
762
724
 
763
725
  **v0.1 commitment**: D1 only via the Cloudflare adapter. Hyperdrive +
764
- PG path is documented as the upgrade route but not implemented. SDKs
765
- and starters target D1 exclusively. The Netlify adapter package
766
- (`@aotter/mantle-netlify`) is a README stub for v0.2; its
767
- existence is an engineering forcing function ensuring the runtime
768
- package stays portable across adapters.
769
-
770
- ## Roadmap — what's not in v0.1.0
771
-
772
- > See [ADR-0001 §"Future grammar discipline"](adr/0001-four-atom-manifest-model.md#future-grammar-discipline-was-poc-adr-0005)
773
- > for the v0.1 minimum-vs-roadmap discipline, the promotion process,
774
- > and the YAGNI argument that gates speculative shipping.
726
+ PG is an upgrade route, not an implemented adapter. SDKs and starters target
727
+ D1 exclusively.
775
728
 
776
- The atoms are locked at 4. Their **inner grammar** is intentionally
777
- narrow at v0.1.0 and grows in two tiers:
729
+ ## Detailed shipped grammar
778
730
 
779
- 1. **v0.1.0 shipped** — grammar parses and runtime behavior is wired
780
- in the current rebuild.
781
- 2. **v0.1.x committed** — on the patch-release roadmap. Spec is
782
- documented; implementation lands within the v0.1 series. The unsupported
783
- runtime path fails closed with a code naming the feature.
784
- 3. **DRAFT (v0.2+)** — speculative, gated by concrete consumer
785
- demand. Parser/static validation rejects with `DRAFT_KEY_USED`. May or may
786
- not ship — depends on whether real use cases apply pressure.
787
-
788
- ### v0.1.0 shipped
789
-
790
- #### `Trigger.source.kind: lifecycle` — bind a Procedure to a Schema event
791
-
792
- Grammar lives in v0.1.0. Runtime is the
793
- `LifecycleHookingEntryRepository` decorator wrapping the entry-writer
794
- chokepoint so MCP / admin / builtin paths all fire the same hooks.
795
- Full shape lives further down.
796
-
797
- #### `Procedure.handler.kind: builtin` — SDK-supplied CRUD Procedure
798
-
799
- Grammar lives in v0.1.0. Runtime is the
800
- `InvokeBuiltinUseCase` that dispatches `op: create | update | upsert
801
- | delete | archive` against the entry-writer chokepoint with `x-mantle-bind`
802
- stamping and `input ∩ Schema.properties` projection. Full shape lives
803
- further down.
804
-
805
- #### `Trigger.source.kind: mcp` and shared authorization
806
-
807
- An MCP Trigger binds a declared Procedure to either the public or staff MCP
808
- surface. Procedures/Views share `ctx.auth`/scope predicates and optional guard
809
- orchestration across REST and MCP. Staff role is loaded live for each protected
810
- call; staff Views remain absent and un-callable on public MCP.
811
-
812
- ### Detailed shipped grammar and v0.1.x reservation
813
-
814
- > The `handler.kind: builtin` and `Trigger.source.kind: lifecycle`
815
- > sections below describe shipped v0.1.0 behavior. Only the
816
- > `Schema.spec.lifecycle: editorial` subsection remains v0.1.x-committed.
817
-
818
- #### `Schema.spec.lifecycle: editorial` runtime
819
-
820
- Grammar and boot already accept the key, but `request_publish` emits
821
- `LIFECYCLE_NOT_IN_V010` because the approval-queue runtime ships in v0.1.x.
822
- When that runtime lands, the same manifest can use the publish workflow without
823
- a grammar change.
824
-
825
- #### `handler.kind: builtin` — thin shortcut over the storage adapter for trivial CRUD-shaped Procedures
731
+ ### `handler.kind: builtin` — thin shortcut over the storage adapter for trivial CRUD-shaped Procedures
826
732
 
827
733
  Use when the body is "insert a row" / "update a row" / "delete a
828
734
  row"; reach for `ref` when there is real business logic. Shape:
@@ -839,11 +745,11 @@ spec:
839
745
 
840
746
  | op | Behavior |
841
747
  |---|---|
842
- | `create` | INSERT a new row. Project `input ∩ Schema.spec.schema.properties`; stamp `x-mantle-bind` fields; generated id; status is `draft`, or immediately `published` for `lifecycle: none`. |
748
+ | `create` | INSERT a new row. Project `input ∩ Schema.spec.schema.properties`; stamp `x-mantle-bind` fields; generated id; status is `draft`, or immediately `published` for `lifecycle: operational`. |
843
749
  | `update` | UPDATE in place. `input.id` + `input.expectedVersion` (OCC) required. Bumps version. |
844
750
  | `upsert` | If `input.id` resolves, behaves as `update`; else as `create`. |
845
751
  | `delete` | Hard DELETE by id. |
846
- | `archive` | Soft-archive (status='archived'). The manifest validator permits this builtin only for `editorial` Schemas; the archive transition itself is runtime-wired, while editorial approval/request-publish remains deferred. |
752
+ | `archive` | Soft-archive a publishing entry (`status='archived'`). |
847
753
 
848
754
  The Procedure's `input` is the contract with the *caller*. It MAY
849
755
  declare fields the Schema does not (e.g. a Turnstile token). The
@@ -853,11 +759,10 @@ the side-channel fields pass validation. To act on those fields
853
759
  (read the token, call the vendor), declare a `before_create`
854
760
  lifecycle Trigger — see below.
855
761
 
856
- `request_publish` and `publish` are intentionally not in the
857
- builtin vocabulary. They are editorial-workflow operations, not CRUD
858
- primitives.
762
+ `request_publish` and `publish` are intentionally not in the builtin
763
+ vocabulary. They are lifecycle operations, not CRUD primitives.
859
764
 
860
- #### `Trigger.source.kind: lifecycle` — bind a Procedure to a Schema event
765
+ ### `Trigger.source.kind: lifecycle` — bind a Procedure to a Schema event
861
766
 
862
767
  Shape:
863
768
 
@@ -879,12 +784,15 @@ spec:
879
784
  |---|---|
880
785
  | `before_create` | Before INSERT. Throw cancels. |
881
786
  | `after_create` | After INSERT. Default best-effort. |
882
- | `before_update` | Before UPDATE. Throw cancels. |
883
- | `after_update` | After UPDATE. Default best-effort. |
787
+ | `before_update` | Before UPDATE or a status transition whose target is not `published` (including unpublish and archive). Throw cancels. |
788
+ | `after_update` | After UPDATE or a status transition whose target is not `published` (including unpublish and archive). Default best-effort. |
884
789
  | `before_delete` | Before DELETE. Throw cancels. |
885
790
  | `after_delete` | After DELETE. Default best-effort. |
886
- | `before_publish` | Before any supported status transition to `published` (the shipped workflow is `simple`). |
887
- | `after_publish` | After any supported status transition to `published` (the shipped workflow is `simple`). |
791
+ | `before_publish` | Before any supported status transition to `published` (the shipped workflow is `publishing`). |
792
+ | `after_publish` | After any supported status transition to `published` (the shipped workflow is `publishing`). |
793
+
794
+ v0.1 has no separate unpublish or archive hooks. Do not treat
795
+ `before_update` / `after_update` as edit-only hooks.
888
796
 
889
797
  **Atomicity defaults by phase**:
890
798
  - `before_*`: `errorPolicy: abort`. Handler throw cancels the
@@ -915,9 +823,8 @@ post-mutation row for every `after_*`.
915
823
 
916
824
  **Hook ordering**: when multiple lifecycle Triggers bind the same
917
825
  `(schema, hook)`, the runtime fires them **alphabetically by
918
- `Trigger.metadata.name`**. A `priority: number` key is reserved for
919
- v0.2; today, choose names that sort correctly (`010-bot-check`,
920
- `020-rate-limit`).
826
+ `Trigger.metadata.name`**. Choose names that sort correctly
827
+ (`010-bot-check`, `020-rate-limit`).
921
828
 
922
829
  For deferred delivery, that ordered Trigger-name list is captured in
923
830
  one strict versioned event. Every captured Trigger runs before a
@@ -928,103 +835,7 @@ Cloudflare wiring, the 128 KB platform limit, retry/DLQ configuration,
928
835
  idempotency, and legacy-envelope draining are specified in
929
836
  [Deferred lifecycle hooks on Cloudflare Queues](deferred-lifecycle-queues.md).
930
837
 
931
- `before_publish` and `after_publish` already wrap the shipped simple publish
932
- transition. When editorial approval lands, the same hooks wrap its final
933
- transition to `published`; no second hook grammar is planned.
934
-
935
- ### DRAFT (v0.2+, speculative)
936
-
937
- Each item below lands when the first concrete real-world use case
938
- forces it, not on speculation. Today, do not implement; parser/static
939
- validation rejects it with `DRAFT_KEY_USED`.
940
-
941
- #### Schema future
942
- - **`x-mantle-ref` auto-lift to virtual column** — when a property
943
- carrying `x-mantle-ref: <other-schema>` is referenced in any registered
944
- View's `filter:` or future `join:`, the SDK auto-creates the virtual
945
- column + non-unique index without requiring an explicit `indexes`
946
- declaration. Solves cross-collection JSON-path join
947
- performance without making authors think about it. Pure SDK
948
- behavior; no new YAML grammar.
949
- - **`spec.policies.visible`** — row-level read predicate auto-AND'd
950
- into Views from this Schema. Lands with first private/audience
951
- feature.
952
- - **`spec.policies.readable.fields`** — field-level wire-mask. Lands
953
- when staff-only fields surface (e.g. `threadLength` visible to
954
- editors only).
955
- - **`spec.policies.writable.{fields, create}`** — field-level write
956
- gate auto-applied to all writers.
957
- - **`spec.policies.owner`** — names the ownership column for `owns:`
958
- predicate. Currently implicit (`authorId`).
959
- - **Computed columns via projection Trigger** — see `Trigger.target`.
960
-
961
- #### View future
962
- - **`recursive: { parent, rootWhen, pathBy, depthAs?, pathAs?, maxDepth }`**
963
- — declarative recursive CTE. Lands with first threaded-reads feature
964
- (comments, taxonomies).
965
- - **`params: { <name>: <jsonschema> }`** — caller-bound view parameters.
966
- - **`gatedBy: [{ schema, policy, on }]`** — cross-Schema visibility
967
- inheritance.
968
- - **`join: [{ as, from, on, project?, aggregate?: count|exists, bind? }]`**
969
- — joined relations with optional reducer (per-row count or boolean).
970
- - **`policies: { skip: [<policy-name> | field, ...] }`** — explicit
971
- bypass for admin/audit Views.
972
- - Filter AST extension: `contains` (array containment), `not`, `in`, `like`.
973
-
974
- #### Procedure future
975
- - **`requires.auth.any`** with disjunction; the shipped `all` predicate
976
- vocabulary extends to `owns: { schema, idFrom }`, `contains: {
977
- schema, idFrom, field, valueFrom }`.
978
- - **`requires.window.{withinMinutes, column?}`** — temporal
979
- precondition, sibling-modifier of `owns:`. DRY rule: window MUST
980
- sit beside exactly one `(schema, id)`-binding predicate in the same
981
- any/all group.
982
- - **`requires.quota.{key, limit, per, overrideFrom?}`** — declarative
983
- rate cap. Override Schema lookup keyed by `ctx.user`. Counter
984
- substrate: Durable Object.
985
- - **`errors.{onHandlerThrow, onRecursionDepth, maxDepth}`** + **`retry.{
986
- limit, backoff, retryOn, idempotencyKey?}`** — failure-policy
987
- declared. Retry + `op: create` requires `idempotencyKey:` (parse
988
- error otherwise).
989
-
990
- #### Trigger future
991
- - **`source.kind: cron`** with `expr:` — scheduled invocation.
992
- - **`source.kind: queue`** — async fan-out / message-driven invocation.
993
- - **`source.kind: lifecycle.foo`** — DRAFT extensions to the shipped
994
- lifecycle hooks (e.g. `before_archive`, `after_request_publish`).
995
- The 8 hooks listed in the detailed shipped section are the floor,
996
- not the ceiling.
997
-
998
- (Full lifecycle Trigger spec for the 8 shipped hooks lives
999
- in the detailed section above. The remaining DRAFT items
1000
- below are the speculative v0.2+ shapes that haven't yet been promoted
1001
- to a committed roadmap.)
1002
-
1003
- - **`target.project: { schema, where, set: { <col>: { aggregate, from,
1004
- where } } }`** — declarative aggregate-projection across Schemas.
1005
- Replaces hand-written counter handlers. Same-transaction on PG;
1006
- bounded-eventual on D1 via single-writer DO.
1007
- - **`atomicity: same-transaction | best-effort`** — explicit guarantee
1008
- declaration when projection Triggers ship.
1009
-
1010
- #### Cross-cutting future
1011
- - **Closed `ctx.*` predicate identity**: v0.1 `{ user, staff, auth,
1012
- auth.scope }`
1013
- extends to `{ ..., system }` when SDK-internal Trigger executor
1014
- paths land. Multi-tenant deployments would add `{ tenant }` via
1015
- grammar-revise round if/when that product shape is pursued. New
1016
- entries require explicit grammar-revise (closed enum is
1017
- load-bearing infrastructure).
1018
- - **Placeholder namespace `$.*`**: `$.input.<f>`, `$.row.<f>`, `$.op`,
1019
- `$.params.<f>`, `$.source` — write-time data-flow bindings inside
1020
- policy ASTs and handler signatures. Distinct from `:ctx.*` identity
1021
- bindings; one-site evaluation rule.
1022
- - **`Schema.spec.staffBypass: [<role>]`** — DRY shortcut once
1023
- `ctx.staff: [editor, owner]` repetition crosses 4+ schemas.
1024
-
1025
- Each extension goes through a 3-agent review (yml-editor proposes /
1026
- code-impler tests buildability / fresh-dev verifies clarity) before
1027
- locking. v0.1 is the floor, not the ceiling.
838
+ `before_publish` and `after_publish` wrap the shipped publishing transition.
1028
839
 
1029
840
  ## Lineage — the academic foundations
1030
841
 
@@ -1037,6 +848,6 @@ locking. v0.1 is the floor, not the ceiling.
1037
848
 
1038
849
  The composite design — declarative resources + ECA-fired procedures +
1039
850
  policy-gated execution — is sometimes labeled **Active Database +
1040
- Policy-Based Management**. Postgres + PostgREST + pg_cron is the
851
+ Policy-Based Management**. Postgres + PostgREST is the
1041
852
  canonical full-stack reference; we abstract that pattern up to the
1042
853
  application layer with K8s-style YAML manifests.
package/docs/labels.md CHANGED
@@ -33,7 +33,7 @@ For package README or package-local docs changes, prefer the package area label
33
33
  | `area:skills` | `skills/*` agent briefs and install/extend/provision workflows. |
34
34
  | `area:admin-ui` | `packages/mantle-admin-ui` React admin SPA. |
35
35
  | `area:docs` | Repo-wide human docs, governance docs, ADR text, release docs, root README content, and cross-cutting documentation work. |
36
- | `area:adapter` | Adapter boundary work spanning Cloudflare, Netlify stub, or future adapters. |
36
+ | `area:adapter` | Adapter boundary work spanning Cloudflare or future adapters. |
37
37
 
38
38
  ## Release and review gates
39
39
 
@@ -41,7 +41,8 @@ For package README or package-local docs changes, prefer the package area label
41
41
  |---|---|---|---|
42
42
  | `release-gate` | Blocks a public release or release-quality claim. | Must be resolved or explicitly deferred before release. | The gate is closed or deferred by maintainer decision. |
43
43
  | `v0.1.0` | Required for the v0.1.0 release gate. | Track in the v0.1.0 release pass. | The item lands or is moved out of v0.1.0. |
44
- | `breaking-change` | Semver-relevant breaking change. | Changelog and release notes must call it out. | The change is redesigned to be non-breaking. |
44
+ | `breaking-change` | Semver-relevant breaking change. | Generated release notes must call it out. | The change is redesigned to be non-breaking. |
45
+ | `skip-release-notes` | Release bookkeeping with no user-facing change. | Excluded from generated GitHub Release notes. | The PR contains a user-facing change. |
45
46
  | `needs-adr` | Architecture, trust boundary, package boundary, or long-lived decision needs an ADR or ADR-lite proposal. | Do not merge implementation until the decision is captured. | ADR/proposal lands or maintainer confirms an existing ADR covers it. |
46
47
  | `needs-grammar-revise` | Manifest grammar or closed-enum change. | Requires grammar-revise round before code/types/starters change. | Grammar decision lands or the change no longer affects grammar. |
47
48
  | `needs-discussion` | Not converged enough for implementation. | Do not start coding from this issue. | Closing criteria are met and scope is concrete. |
@@ -60,6 +61,7 @@ gh label create "area:admin-ui" --description "React admin UI" --color "1d76db"
60
61
  gh label create "area:docs" --description "Documentation and governance" --color "1d76db"
61
62
  gh label create "area:adapter" --description "Adapter boundary and future adapter work" --color "1d76db"
62
63
  gh label create "breaking-change" --description "Semver-relevant breaking change" --color "b60205"
64
+ gh label create "skip-release-notes" --description "Release bookkeeping only; omit from generated GitHub notes" --color "ededed"
63
65
  gh label create "needs-adr" --description "Requires an ADR or ADR-lite decision before merge" --color "d93f0b"
64
66
  gh label create "needs-grammar-revise" --description "Requires manifest grammar review before implementation" --color "d93f0b"
65
67
  gh label create "needs-discussion" --description "Not converged enough for implementation" --color "fbca04"
@@ -62,8 +62,9 @@ decision instead of starting another local redesign loop.
62
62
  ## Release PR
63
63
 
64
64
  1. Fetch Core and Starter remotes and choose the next unused version.
65
- 2. Read `CHANGELOG.md` completely. Add a dated Keep-a-Changelog entry from the
66
- merged commits since the previous tag; do not add an `[Unreleased]` bucket.
65
+ 2. Preview GitHub's generated notes for the merged commits since the previous
66
+ tag. Correct PR titles and labels before release; do not duplicate the notes
67
+ in `CHANGELOG.md`. Label the release-only PR `skip-release-notes`.
67
68
  3. Set that exact version in every workspace package and in all four agent
68
69
  plugin manifests. Set `.agents/plugins/marketplace.json` to the immutable
69
70
  `v<version>` ref.
@@ -77,6 +78,16 @@ decision instead of starting another local redesign loop.
77
78
  packed-consumer gate. Review and merge a same-repository PR into `develop`;
78
79
  the controller rejects a direct-push release commit.
79
80
 
81
+ Preview the native notes before merging the release PR:
82
+
83
+ ```bash
84
+ gh api --method POST repos/aotter/mantle/releases/generate-notes \
85
+ -f tag_name=vX.Y.Z \
86
+ -f target_commitish="$(git rev-parse origin/develop)" \
87
+ -f previous_tag_name=vPREVIOUS \
88
+ --jq .body
89
+ ```
90
+
80
91
  The five public packages publish in dependency order:
81
92
 
82
93
  1. `@aotter/mantle-spec`
@@ -179,7 +190,7 @@ deployment was started.
179
190
  ## Fix-forward policy
180
191
 
181
192
  - Broken public package or Starter bundle: publish the next alpha and explain
182
- the re-spin in `CHANGELOG.md`.
193
+ the re-spin in the fix PR and generated GitHub Release notes.
183
194
  - Use `npm deprecate` to steer consumers away from a broken version.
184
195
  - Unpublish only for secrets, private files, or similarly severe exposure;
185
196
  npm versions cannot be reused and registry metadata may remain unavailable
@@ -11,7 +11,7 @@ kind: Schema
11
11
  metadata: { name: account-members }
12
12
  spec:
13
13
  title: Account members
14
- lifecycle: none
14
+ lifecycle: operational
15
15
  schema:
16
16
  type: object
17
17
  properties: