@aotter/mantle 0.1.0-alpha.9 → 0.1.2-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.
Files changed (109) hide show
  1. package/README.md +38 -38
  2. package/dist/cli/generate.d.ts +10 -1
  3. package/dist/cli/generate.d.ts.map +1 -1
  4. package/dist/cli/generate.js +49 -4
  5. package/dist/cli/generate.js.map +1 -1
  6. package/dist/cli/main.js +0 -8
  7. package/dist/cli/main.js.map +1 -1
  8. package/dist/codegen/emitMantleModule.js +8 -0
  9. package/dist/codegen/emitMantleModule.js.map +1 -1
  10. package/docs/adapter-guide.md +23 -26
  11. package/docs/adr/0008-structured-diagnostic-shape.md +7 -2
  12. package/docs/adr/0010-locale-and-translates.md +14 -16
  13. package/docs/adr/0011-adapter-port-spec.md +2 -1
  14. package/docs/adr/0014-auth-better-auth-and-multi-tenant-mcp.md +100 -0
  15. package/docs/adr/0018-core-starters-repository-boundary.md +3 -0
  16. package/docs/adr/0019-sealed-manifest-runtime-pipeline.md +38 -2
  17. package/docs/adr/0020-builtin-handler-contracts-and-matched-upsert.md +199 -0
  18. package/docs/adr/0021-retire-starter-scaffolding.md +30 -0
  19. package/docs/adr/0022-caller-observed-version-occ.md +212 -0
  20. package/docs/adr/0023-port-failure-contract.md +69 -0
  21. package/docs/adr/0024-manifest-native-schema-tables.md +260 -0
  22. package/docs/adr/README.md +19 -3
  23. package/docs/adr/adr-lite-803-request-diagnostics.md +38 -0
  24. package/docs/adr/adr-lite-808-route-readiness.md +47 -0
  25. package/docs/adr/adr-lite-809-bounded-public-content.md +71 -0
  26. package/docs/adr/adr-lite-812-native-parity.md +150 -0
  27. package/docs/adr/adr-lite-823-home-statistics.md +64 -0
  28. package/docs/adr/adr-lite-842-mcp-authoring.md +30 -0
  29. package/docs/adr/adr-lite-861-admin-webmcp.md +42 -0
  30. package/docs/adr/adr-lite-909-admin-ui-kit.md +37 -0
  31. package/docs/api-mcp-authorization.md +3 -664
  32. package/docs/auth-hosting-model.md +4 -4
  33. package/docs/cloudflare-low-level-composition.md +3 -104
  34. package/docs/deferred-lifecycle-queues.md +2 -243
  35. package/docs/design-atoms.md +16 -796
  36. package/docs/direct-authoring.md +5 -0
  37. package/docs/examples/minimal-worker/README.md +37 -0
  38. package/docs/examples/minimal-worker/manifests/site.yaml +25 -0
  39. package/docs/examples/minimal-worker/package.json +26 -0
  40. package/docs/examples/minimal-worker/smoke.mjs +23 -0
  41. package/docs/examples/minimal-worker/src/index.ts +4 -0
  42. package/docs/examples/minimal-worker/tsconfig.json +17 -0
  43. package/docs/examples/minimal-worker/wrangler.jsonc +22 -0
  44. package/docs/handbook/cloudflare/authentication.md +167 -0
  45. package/docs/handbook/cloudflare/bindings.md +236 -0
  46. package/docs/handbook/cloudflare/conventional-worker.md +162 -0
  47. package/docs/handbook/cloudflare/deferred-hooks-queues.md +171 -0
  48. package/docs/handbook/cloudflare/deploy-and-operate.md +105 -0
  49. package/docs/handbook/cloudflare/low-level-composition.md +169 -0
  50. package/docs/handbook/cloudflare/media-r2.md +216 -0
  51. package/docs/handbook/cloudflare/public-web.md +144 -0
  52. package/docs/handbook/concepts/authorization.md +116 -0
  53. package/docs/handbook/concepts/four-atoms.md +173 -0
  54. package/docs/handbook/concepts/lifecycle-and-locales.md +142 -0
  55. package/docs/handbook/concepts/mcp-and-agents.md +109 -0
  56. package/docs/handbook/concepts/procedures-and-triggers.md +221 -0
  57. package/docs/handbook/concepts/runtime-and-adapters.md +177 -0
  58. package/docs/handbook/concepts/views.md +191 -0
  59. package/docs/handbook/examples/commerce-transaction.md +813 -0
  60. package/docs/handbook/examples/guarded-api.md +427 -0
  61. package/docs/handbook/examples/intake-form.md +319 -0
  62. package/docs/handbook/examples/legal-documents.md +218 -0
  63. package/docs/handbook/examples/procurement-approvals.md +240 -0
  64. package/docs/handbook/examples/publication.md +240 -0
  65. package/docs/handbook/examples/reservation.md +220 -0
  66. package/docs/handbook/navigation.json +63 -0
  67. package/docs/handbook/reference/authorization.md +295 -0
  68. package/docs/handbook/reference/diagnostics.md +183 -0
  69. package/docs/handbook/reference/manifest.md +132 -0
  70. package/docs/handbook/reference/procedure.md +263 -0
  71. package/docs/handbook/reference/schema.md +250 -0
  72. package/docs/handbook/reference/site-config.md +233 -0
  73. package/docs/handbook/reference/surface.md +207 -0
  74. package/docs/handbook/reference/trigger.md +193 -0
  75. package/docs/handbook/reference/view.md +256 -0
  76. package/docs/handbook/start/project-and-cli.md +122 -0
  77. package/docs/handbook/start/quickstart-worker.md +193 -0
  78. package/docs/labels.md +3 -1
  79. package/docs/media-uploads.md +3 -184
  80. package/docs/migration-0.1.2.md +60 -3
  81. package/docs/performance-harness.md +127 -10
  82. package/docs/release-process.md +85 -231
  83. package/docs/schema-indexes.md +3 -180
  84. package/docs/sealed-pipeline-ownership.md +6 -5
  85. package/docs/spec-only-host-adoption.md +158 -0
  86. package/docs/transaction-patterns.md +5 -0
  87. package/package.json +21 -25
  88. package/skills/README.md +4 -4
  89. package/skills/develop/SKILL.md +23 -28
  90. package/skills/install/SKILL.md +55 -147
  91. package/skills/provision/SKILL.md +4 -3
  92. package/skills/theme/SKILL.md +6 -6
  93. package/skills/update/SKILL.md +32 -68
  94. package/dist/cli/create.d.ts +0 -2
  95. package/dist/cli/create.d.ts.map +0 -1
  96. package/dist/cli/create.js +0 -243
  97. package/dist/cli/create.js.map +0 -1
  98. package/dist/cli/update.d.ts +0 -2
  99. package/dist/cli/update.d.ts.map +0 -1
  100. package/dist/cli/update.js +0 -413
  101. package/dist/cli/update.js.map +0 -1
  102. package/dist/provision/renderProvisionBundle.d.ts +0 -70
  103. package/dist/provision/renderProvisionBundle.d.ts.map +0 -1
  104. package/dist/provision/renderProvisionBundle.js +0 -367
  105. package/dist/provision/renderProvisionBundle.js.map +0 -1
  106. package/dist/provision.d.ts +0 -2
  107. package/dist/provision.d.ts.map +0 -1
  108. package/dist/provision.js +0 -2
  109. package/dist/provision.js.map +0 -1
@@ -0,0 +1,207 @@
1
+ ---
2
+ description: Every surface Mantle exposes — Worker HTTP routes and their cache class, the MCP tool catalog, CLI flags, the generated module, packages and versions.
3
+ ---
4
+ # HTTP, MCP, CLI and package surface
5
+
6
+ What a Mantle deployment exposes, in one place: the routes the conventional Cloudflare Worker owns, the MCP tools it advertises, the commands the package installs, the module `mantle generate` writes, and which package holds what. Route behaviour follows [Conventional Worker](../cloudflare/conventional-worker.md); this page is the index.
7
+
8
+ ## HTTP routes
9
+
10
+ Every table below describes the conventional Cloudflare Worker assembled by `createMantleWorker`. The **cache** column is the class the final `applyCachePolicy` boundary assigns. Only an anonymous `200` `GET`/`HEAD` response that explicitly declares `public` plus a shared freshness lifetime stays cacheable, and it then varies on `Cookie` and `Authorization`; the request must carry neither header and the response must set no cookie. Everything else becomes `private, no-store` with any CDN cache override stripped.
11
+
12
+ ### Manifest routes
13
+
14
+ | Route | Response | Cache |
15
+ |---|---|---|
16
+ | `GET /api/views` | `{ ok: true, data: [ { name, title?, description, inputSchema } ] }` — one descriptor per public View. | `private, no-store` |
17
+ | `GET /api/views/<name>?page=&show=` | `{ ok: true, data: { rows, page, show, hasMore } }`. One route per View declaring `surface: public`. Query values are coerced against `params`; a bad value is `400`. | `private, no-store`, or the View's `cache.sharedMaxAge` for an anonymous eligible response |
18
+ | `<METHOD> <path>` | Every manifest HTTP Trigger, at its declared `POST`, `PUT`, `PATCH` or `DELETE` and path under `/api/`. The JSON body must be an object. Success is `{ ok: true, data: … }`. | `private, no-store` |
19
+
20
+ Staff Views are not mounted here; they live under `/admin/api/views/<name>`.
21
+
22
+ ### Admin
23
+
24
+ All `/admin/api/*` routes require a staff session, carry a 1 MiB JSON body limit, and are `private, no-store`.
25
+
26
+ | Route | Response |
27
+ |---|---|
28
+ | `GET /admin`, `/admin/`, `/admin/sign-in`, `/admin/c/:collection`, `/admin/c/:collection/:id`, `/admin/media`, `/admin/preferences`, `/admin/connected-apps`, `/admin/settings`, `/admin/staff`, `/admin/members`, `/admin/ops`, `/admin/dev`, `/admin/dev/model`, `/admin/dev/logic`, `/admin/dev/docs`, `/admin/views/:name` | The Admin SPA shell, read from `/_mantle/admin/index.html` through the assets binding. `503` with an explanatory body when the bundle is missing. |
29
+ | `GET /admin/api/views/<name>` | Staff execution of any declared View, public or staff, with Admin search and filtering applied before pagination. |
30
+ | `GET /admin/api/views/<name>/export` | The same query as CSV, covering every matching row rather than one page. |
31
+ | `GET /admin/api/views-manifest` | `{ views: … }` — the View manifest projection the SPA renders from. |
32
+ | `GET /admin/api/operations` | `{ operations: [ { name, title, description, input, uiSchema, triggers, rowBindings } ] }`, filtered per caller by re-evaluating each Procedure's `requires.auth.all`. |
33
+ | `POST /admin/api/operations/:name` | Invokes a staff-operable Procedure through the same use case the staff MCP surface uses. `404` when the name is not staff-operable. |
34
+ | `GET /admin/api/me`, `/collections`, `/collections/:name/statistics`, `/entries`, `/entries/export`, `/entries/:id`, `/site` | Session, catalog and entry reads. Entry detail requires `?collection=<schema>`. |
35
+ | `POST /admin/api/entries`, `PATCH /admin/api/entries/:id` | Create and edit. Entry mutation routes require `?collection=<schema>`; contributors are limited to drafts on publishing Schemas. |
36
+ | `POST /admin/api/entries/:id/publish`, `/unpublish`, `DELETE /admin/api/entries/:id` | Lifecycle. Requires `?collection=<schema>` and editor or above. |
37
+ | `POST /admin/api/media/uploads`, `POST /admin/api/media/uploads/:uploadGroupId/commit`, `GET /admin/api/media`, `GET`, `PATCH` and `DELETE /admin/api/media/:id` | Media lifecycle. Editor or above. |
38
+ | `GET /admin/api/staff`, `PATCH /admin/api/staff/:id/role`, `POST /admin/api/staff/invitations`, `DELETE /admin/api/staff/invitations/:id`, `GET /admin/api/developer-console`, `GET` and `PATCH /admin/api/site-settings` | Owner only. |
39
+ | `GET /admin/api/members` | Editor or above. |
40
+
41
+ `/_mantle/*` holds the static Admin bundle. It is served by the Worker's static-assets layer from `public/_mantle/admin/`, which `mantle generate` syncs when `@aotter/mantle-admin-ui` is installed. The Worker registers no route there; the prefix is reserved so extensions cannot claim it.
42
+
43
+ ### Auth, OAuth and MCP
44
+
45
+ | Route | Response | Cache |
46
+ |---|---|---|
47
+ | `GET /api/auth/methods` | `{ methods }` — the registered sign-in method kinds, no secrets. Explicitly `no-store` so a method change cannot be served stale. | `private, no-store` |
48
+ | `ALL /api/auth/*` | Better Auth: sign-in, callback, session, magic link, OTP. `/api/auth` is the default base path. | `private, no-store` |
49
+ | `ALL /api/auth/oauth2/*` | The site's OAuth provider endpoints, including consent. | `private, no-store` |
50
+ | `ALL /.well-known/oauth-authorization-server/*`, `/.well-known/oauth-protected-resource`, `/.well-known/oauth-protected-resource/*` | RFC 8414 and RFC 9728 discovery metadata, registered explicitly so no catch-all can swallow them. `/.well-known/oauth-protected-resource/mcp` is the resource metadata the MCP `WWW-Authenticate` challenge points at. | `private, no-store` |
51
+ | `ALL /oauth/*` | Consent and connected-apps pages: `/oauth/consent`, `/oauth/consent/data`, `/oauth/consents`, `/oauth/consents/data`, `/oauth/consents/revoke`. | `private, no-store` |
52
+ | `ALL /mcp` | Public MCP surface. JSON-RPC. | `private, no-store` |
53
+ | `ALL /mcp/staff` | Staff MCP surface. Rejects a verified caller with no staff row using `403` and `insufficient_scope`. | `private, no-store` |
54
+
55
+ Both MCP surfaces verify an OAuth access token against one canonical resource, `${PUBLIC_ORIGIN}/mcp`, and one scope, `mcp`. A missing or invalid token is `401` with a `Bearer` challenge naming the resource metadata URL; DPoP failures answer with a `DPoP` challenge. Misconfigured or partial auth environment variables keep public routes serving and return `503 setup_incomplete` from every Auth-owned route above — see [Authentication](../cloudflare/authentication.md).
56
+
57
+ ### Public pages
58
+
59
+ These are opt-in and application-declared. Public rendering needs three matching inputs: a `mountPublicRoutes(...)` call declaring the collection routes, a `TemplateRegistry` passed as `templates`, and a `publicPathResolver`. A headless deployment declares none of them and serves none of these routes; mounting every Schema automatically is deliberately not offered, because a collection can hold a slug and still be private. Bodies render from canonical database state.
60
+
61
+ | Route | Response | Cache |
62
+ |---|---|---|
63
+ | `GET /` | `302` to `/{canonical locale}`. | `private, no-store` |
64
+ | `GET /:locale` | Composed home page. | `public, max-age=0, s-maxage=300` + a deployment-scoped cache tag |
65
+ | `GET /:locale.md` | Markdown mirror of the home page. | public |
66
+ | `GET /:locale/:segment` | Collection list page. Opt-in per collection route. Paginates by `?cursor=` and adds `Link: <…>; rel="next"` plus an in-page next link. | public |
67
+ | `GET /:locale/:segment.md` | Markdown mirror of the list. | public |
68
+ | `GET /:locale/:segment/:slug` | Entry page. | public |
69
+ | `GET /:locale/:segment/:slug.md` | Markdown mirror of one entry. Registered before the bare slug route, and the slug group stays single-segment. | public |
70
+ | `GET /:locale/:segment/:slug?preview=1` | Live render of unpublished content. Gated on a staff session: `401` with no session, `403` for a non-staff user. | `private, no-store` |
71
+ | `GET /llms.txt` | Composed agent index across every locale, with `?cursor=` continuation and `Link: rel="next"`. | public |
72
+ | `GET /:locale/llms.txt` | The same, scoped to one locale. | public |
73
+ | `GET /sitemap.xml` | Sitemap. `?part=1` returns a single part; otherwise a sitemap index is returned whenever a continuation exists, and `?cursor=` fetches the next part. Part size is `min(2000, floor(40000 / locale count))`, at least 1. | public |
74
+ | `GET /robots.txt` | `User-agent: *`, `Allow: /`, and a `Sitemap:` pointer built from `siteDefaults.origin`. | public |
75
+ | `GET /favicon.ico` | A convention, not a reserved path: an existing host route wins. Picks the first themeless PNG icon, else the first themeless icon, else the first icon; serves it from the assets binding when it resolves to `/favicon.ico` on this origin, and otherwise redirects to the icon `src`. | `private, no-store` |
76
+
77
+ The first three rows register only when a home renderer is supplied; without one, `/` and `/{locale}` are not mounted at all. Unmatched paths fall through to the supplied not-found renderer. Setting `liveDev` switches entry and list HTML to `private, no-store`. `cacheScope` isolates each deployment's tag and optional KV key; without it the facade disables shared caching. Publishing-content and site-setting writes purge that scoped tag. Purge is best effort after the canonical write, and Cloudflare KV remains eventually consistent. Immutable assets and operational records stay outside that boundary. More in [Public web](../cloudflare/public-web.md).
78
+
79
+ ### Reserved paths
80
+
81
+ Extensions may add routes but may not replace Core surfaces. These are reserved:
82
+
83
+ - `/admin` and `/admin/*`
84
+ - `/_mantle` and `/_mantle/*`
85
+ - `/api/auth` and `/api/auth/*`
86
+ - `/api/views` and `/api/views/*`
87
+ - `/oauth` and `/oauth/*`
88
+ - `/mcp` and `/mcp/*`
89
+ - `/.well-known/oauth*`
90
+ - the global `*` and `/*` registrations
91
+
92
+ A custom Auth factory's `basePath` and every manifest-owned `(method, path)` pair are reserved as well. Static literal conflicts fail the consumer's TypeScript build; computed paths cannot be proven statically, so the facade inspects the assembled route table and fails closed before serving. A manifest HTTP Trigger under one of these prefixes is `TRIGGER_PATH_INVALID` at boot.
93
+
94
+ ## MCP tools
95
+
96
+ Tool names are the mangled `metadata.name`: lower-cased, with `-` replaced by `_`. Discovery is filtered per caller, but discovery is never the enforcement boundary — every `tools/call` re-evaluates the target's `requires.auth.all` and its guard.
97
+
98
+ | Tool | Surface | Registered when |
99
+ |---|---|---|
100
+ | `query_view_<segment>` | The View's own `surface` | One per declared View. `annotations.readOnlyHint` is `true`; the input schema is the View's `params.properties` plus `page` and `show`. |
101
+ | `<procedure segment>` | The Trigger's `surface` | One per `Trigger.source.kind: mcp`. A Procedure with no MCP Trigger is not exposed. |
102
+ | `request_publish` | staff | Always. Rejected at call time for an operational Schema. |
103
+ | `unpublish_entry` | staff | Always. Same restriction. |
104
+ | `archive_entry` | staff | Always. Same restriction. |
105
+ | `delete_entry` | staff | Always. |
106
+ | `create_draft_<segment>`, `update_draft_<segment>` | staff | Per publishing Schema whose `schema.readOnly` is not `true`. |
107
+ | `create_record_<segment>`, `update_record_<segment>` | staff | Per operational Schema whose `schema.readOnly` is not `true`. |
108
+ | `create_media_upload`, `commit_media_upload` | staff | Only when a `mediaStorage` port is bound **and** at least one `media.purposes` entry is declared. |
109
+
110
+ The public surface carries callable capabilities only — public Views and Procedures with a public MCP Trigger. No generic entry tool and no authoring tool is ever advertised there. Update tools add `id` and `expected_version` as required fields, and `x-mantle-bind` properties are stripped from authoring tool schemas because the server stamps them. Localized `title` and `description` collapse to their `en` value in tool schemas. Concepts are in [MCP and agents](../concepts/mcp-and-agents.md).
111
+
112
+ ## CLI
113
+
114
+ The `@aotter/mantle` package installs two binaries, `mantle` and `mantle-harness`. Exit codes are `0` for success, `1` for a diagnostic failure and `2` for an invocation problem.
115
+
116
+ | Command | Flags |
117
+ |---|---|
118
+ | `mantle generate` | `--manifests <dir>` (default `./manifests`), `-o, --output <dir>` (default `.mantle/generated`), `--namespace <name>` (default `Mantle`), `--check`, `-h, --help` |
119
+ | `mantle skills` | `--check`, `-h, --help` |
120
+ | `mantle validate` | `--manifests <dir>` (default `./manifests`), `--source <dir>` (default `./src`), `--no-source`, `--phase preview\|deploy` (default `preview`), `--format json\|text` (default by TTY), `--json`, `-h, --help` |
121
+ | `mantle emit-openapi` | `--manifests <dir>`, `--title <str>` (default `mantle`), `--version <str>` (default `0.1.0`), `--session-cookie-name <str>`, `-o, --output <file>`, `-h, --help` |
122
+ | `mantle-harness indexes` | `--manifests <dir>`, `--rows <n>`, `--require <view>` (repeatable), `--require-public`, `--format json\|text`, `-h, --help` |
123
+ | `mantle-harness http` | `--route <name=url>` (repeatable, required), `--base-url <url>`, `--rounds <n>`, `--warmup <n>`, `--format json\|text`, `-h, --help` |
124
+
125
+ `generate` validates and compiles the manifest directory, writes the typed module, and — when `@aotter/mantle-admin-ui` is installed — syncs the Admin SPA into `public/_mantle/admin/`, excluding `server.*` files. `--check` fails without writing when either output is stale. `skills` copies every skill the installed package marks `projection: project` into `.agents/skills/mantle-*` and `.claude/skills/mantle-*`; both layouts receive identical bytes. `validate --phase deploy` adds the pre-deploy-only gates on top of the grammar and cross-Schema checks. `emit-openapi` covers HTTP Triggers and View REST routes; MCP is out of scope. `mantle-harness indexes` executes compiled Views against crowded SQLite and inspects query plans; `http` samples a running Worker for p50 and p95. Day-to-day use is in [Project and CLI](../start/project-and-cli.md).
126
+
127
+ ## The generated module
128
+
129
+ `mantle generate` writes one file, `.mantle/generated/mantle.ts`. It is generated code: do not edit it, and regenerate after any manifest change.
130
+
131
+ | Export | Shape |
132
+ |---|---|
133
+ | `plan` | The sealed `RuntimePlan`, carrying a semantic fingerprint. |
134
+ | `Mantle` (or `--namespace`) | Type namespace holding `Entry_*`, `ViewRow_*`, `ViewParams_*`, `ProcInput_*` and `ProcOutput_*` for every atom. |
135
+ | `MantleViewOptions` | `{ page?, show?, ctx? }`. |
136
+ | `MantleHandlers<Env>` | Typed map of every `handler.kind: ref` key the manifests declare. |
137
+ | `CreateMantleOptions<Env>` | `BootMantleRuntimeArgs` without `plan` and `handlers`, plus the typed `handlers` map. |
138
+ | `createMantle(options)` | Boots the runtime and returns the bound facade. Eager: it prepares once and neither caches nor retries. |
139
+ | `bindMantle(runtime)` | Binds an already-booted runtime. Throws when `runtime.revision` does not equal the generated plan's fingerprint. |
140
+
141
+ The bound object is deterministic lower-camel property names over the authored wire names:
142
+
143
+ ```ts
144
+ import { createMantle } from "../.mantle/generated/mantle.js";
145
+
146
+ const mantle = await createMantle({ storage, handlers });
147
+
148
+ await mantle.views.publishedNotes({ page: 1, show: 20 });
149
+ await mantle.procedures.expireOrder({ orderId }, { user: null, staff: null, env });
150
+ await mantle.entries.orders.createDraft({ data, authorId: user.id });
151
+ mantle.triggers.expireOrderHttp; // { name, source, target }
152
+ await mantle.runtime.archive.execute({ id, ctx });
153
+ ```
154
+
155
+ `entries.<collection>` exposes `createDraft`, `get`, `list` and `delete`, supplying the required collection identity to Core. Generic MCP entry tools require a `collection` argument. `runtime` is the underlying Core runtime, so the typed projection never hides it. A host that owns its own lifecycle can skip generation entirely and call `runtime.executeView({ view: "published-notes" })` directly.
156
+
157
+ ## Packages
158
+
159
+ The umbrella installs Spec and Runtime only. Web, Admin, Admin UI, Bun, Vercel and Cloudflare are optional peers; install one before importing its subpath. Every sub-package is also directly installable.
160
+
161
+ | Package | Umbrella subpath | Holds |
162
+ |---|---|---|
163
+ | `@aotter/mantle` | root | Umbrella plus the `mantle` and `mantle-harness` binaries. |
164
+ | `@aotter/mantle-spec` | `/spec` | Manifest grammar, parser, validators, JSON Schema to zod, site-config contract, diagnostic catalog. No environment, no IO. |
165
+ | `@aotter/mantle-runtime` | `/runtime` | Hexagonal runtime: domain ports, use cases, MCP catalog, storage helpers. No adapter dependencies. |
166
+ | — | `/runtime/testing` | Node-only crowded-SQLite planner and HTTP sampling helpers used by `mantle-harness`. |
167
+ | — | `/codegen` | The pure linked-manifests to typed-module emitter, with no IO. |
168
+ | `@aotter/mantle-web` | `/web` | HTML, Markdown, `llms.txt`, sitemap, SEO and preview composition. No routes, no platform dependencies. |
169
+ | `@aotter/mantle-admin` | `/admin` | Admin API, auth route mounting, OAuth pages, static-asset composition. |
170
+ | `@aotter/mantle-admin-ui` | `/admin-ui` | Pre-built React 19 Admin SPA bundle. |
171
+ | `@aotter/mantle-bun` | `/bun` | Bun adapter over a caller-owned `bun:sqlite` database. |
172
+ | `@aotter/mantle-vercel` | `/vercel` | Vercel Functions adapter with injected durable storage and platform `waitUntil`. |
173
+ | — | `/vercel/libsql` | Optional application-owned Turso/libSQL driver. |
174
+ | `@aotter/mantle-cloudflare` | `/cloudflare` | Cloudflare Workers adapter: D1, Workers Cache, R2, Queues, Better Auth 1.7 MCP and CIMD. |
175
+ | `@aotter/mantle-indexeddb` | — | Browser IndexedDB adapter. Directly installable; no umbrella subpath. |
176
+
177
+ ## Versions
178
+
179
+ This handbook was added on the development branch after `v0.1.0-alpha.17`. It describes that development snapshot and will ship with a future release; it is not included in the published `0.1.0-alpha.17` package. For a registry installation, use the documentation at the matching release tag. For a source-built package, record the source commit as well as the package version: a development checkout can still carry the previous release version.
180
+
181
+ The documentation site pins its handbook commit in `docs/handbook.json` and records its vendored SDK source in `vendor/mantle/SOURCE.txt`. Those commits may differ when the intervening changes are behavior-neutral; the site verifies that condition during the build. The site's source-built tarballs are not the npm `alpha.17` artifacts.
182
+
183
+ `0.1.0-alpha.17` is immutable and no stable `0.1.0` is planned; the first stable target is `0.1.2`. That line removes the scaffolding path — the `mantle create` command, the bundle-oriented `mantle update` command, and the `@aotter/mantle/provision` subpath — with no aliases and no replacement scaffold command. `generate`, `skills`, `validate` and `emit-openapi` remain, and generation and runtime Web rendering keep their existing responsibilities. New projects are authored directly: write the manifests, run `generate`, wire the Worker. The reasoning is [ADR-0021](../../../docs/adr/0021-retire-starter-scaffolding.md).
184
+
185
+ Prerelease packages take their exact version from their own `package.json`, which is the authority; APIs may change between prereleases until `0.1.2`.
186
+
187
+ ## Source
188
+
189
+ - [`packages/mantle/README.md`](../../../packages/mantle/README.md)
190
+ - [`packages/mantle/src/cli/main.ts`](../../../packages/mantle/src/cli/main.ts)
191
+ - [`packages/mantle/src/cli/generate.ts`](../../../packages/mantle/src/cli/generate.ts)
192
+ - [`packages/mantle/src/cli/skills.ts`](../../../packages/mantle/src/cli/skills.ts)
193
+ - [`packages/mantle/src/cli/harness.ts`](../../../packages/mantle/src/cli/harness.ts)
194
+ - [`packages/mantle/src/codegen/emitMantleModule.ts`](../../../packages/mantle/src/codegen/emitMantleModule.ts)
195
+ - [`packages/mantle-spec/src/infrastructure/cli/ValidateCommand.ts`](../../../packages/mantle-spec/src/infrastructure/cli/ValidateCommand.ts)
196
+ - [`packages/mantle-spec/src/infrastructure/cli/EmitOpenapiCommand.ts`](../../../packages/mantle-spec/src/infrastructure/cli/EmitOpenapiCommand.ts)
197
+ - [`packages/mantle-runtime/src/infrastructure/mcp/McpToolCatalog.ts`](../../../packages/mantle-runtime/src/infrastructure/mcp/McpToolCatalog.ts)
198
+ - [`packages/mantle-admin/src/mountMantleAdmin.ts`](../../../packages/mantle-admin/src/mountMantleAdmin.ts)
199
+ - [`packages/mantle-admin/src/mountMantleOAuth.ts`](../../../packages/mantle-admin/src/mountMantleOAuth.ts)
200
+ - [`packages/adapters/cloudflare/README.md`](../../../packages/adapters/cloudflare/README.md)
201
+ - [`packages/adapters/cloudflare/src/worker/createMantleWorker.ts`](../../../packages/adapters/cloudflare/src/worker/createMantleWorker.ts)
202
+ - [`packages/adapters/cloudflare/src/mount/mountRuntimeEndpoints.ts`](../../../packages/adapters/cloudflare/src/mount/mountRuntimeEndpoints.ts)
203
+ - [`packages/adapters/cloudflare/src/mount/mountPublicRoutes.ts`](../../../packages/adapters/cloudflare/src/mount/mountPublicRoutes.ts)
204
+ - [`packages/adapters/cloudflare/src/mount/mountMcp.ts`](../../../packages/adapters/cloudflare/src/mount/mountMcp.ts)
205
+ - [`packages/adapters/cloudflare/src/oauth/cachePolicy.ts`](../../../packages/adapters/cloudflare/src/oauth/cachePolicy.ts)
206
+ - [`docs/adr/0021-retire-starter-scaffolding.md`](../../../docs/adr/0021-retire-starter-scaffolding.md)
207
+ - [`docs/migration-0.1.2.md`](../../../docs/migration-0.1.2.md)
@@ -0,0 +1,193 @@
1
+ ---
2
+ description: Trigger field reference — http, mcp and lifecycle sources, path and tool-name rules, the eight lifecycle hooks and their timing and error policy.
3
+ ---
4
+ # Trigger
5
+
6
+ A Trigger binds one source to one [Procedure](./procedure.md). Every external surface for a write — an HTTP endpoint, an MCP tool, an entry-lifecycle hook — is a Trigger, and there is no other way to expose a Procedure. This page is the field-level contract; the concepts are in [Procedures and Triggers](../concepts/procedures-and-triggers.md). Envelope rules are in [Manifest envelope and conventions](./manifest.md), and diagnostic codes are catalogued in [Diagnostics](./diagnostics.md).
7
+
8
+ ## Fields
9
+
10
+ `spec` accepts exactly two keys.
11
+
12
+ | Field | Type | Required | Rules |
13
+ |---|---|---|---|
14
+ | `source` | mapping | yes | Discriminated by `kind`; the accepted sibling keys depend on it. |
15
+ | `target` | `{ procedure }` | yes | Only the key `procedure`, naming a declared Procedure (`TRIGGER_TARGET_PROCEDURE_UNKNOWN`). |
16
+
17
+ | `source.kind` | Other keys | Binds |
18
+ |---|---|---|
19
+ | `http` | `method`, `path` | One REST endpoint under `/api/`. |
20
+ | `mcp` | `surface` | One tool on `/mcp` or `/mcp/staff`. |
21
+ | `lifecycle` | `schema`, `on`, `errorPolicy` | Entry-writer hooks on one Schema. |
22
+
23
+ An unknown `kind`, a missing `kind`, or a key that does not belong to the chosen kind is `INVALID_MANIFEST_ENVELOPE`. One Procedure may carry several Triggers — that is how the same handler becomes an HTTP endpoint and an MCP tool without duplicating logic.
24
+
25
+ ## `http` source
26
+
27
+ ```yaml
28
+ apiVersion: cms.mantle.aotter.net/v1
29
+ kind: Trigger
30
+ metadata:
31
+ name: inventory-level-http
32
+ spec:
33
+ source:
34
+ kind: http
35
+ method: PUT
36
+ path: /api/inventory/{sku}/level
37
+ target:
38
+ procedure: sync-inventory-level
39
+ ```
40
+
41
+ | Field | Rules |
42
+ |---|---|
43
+ | `method` | `POST`, `PUT`, `PATCH` or `DELETE`. |
44
+ | `path` | Non-empty string starting with `/`. OpenAPI `{param}` syntax for path params. No optional segments. |
45
+
46
+ `GET` is deliberately absent. Reads are [Views](./view.md), which mount themselves from `surface` and need no Trigger at all; a Procedure is a write.
47
+
48
+ ### Path rules
49
+
50
+ | Phase | Rule | Diagnostic |
51
+ |---|---|---|
52
+ | parse | `path` starts with `/`. | `INVALID_MANIFEST_ENVELOPE` |
53
+ | validate | `path` starts with `/api/`, so adapters can route public pages and Procedure endpoints without ambiguity. | `TRIGGER_PATH_INVALID` |
54
+ | validate | `(method, path)` is unique across every `http` Trigger. | `TRIGGER_PATH_COLLISION`, naming the Trigger that claimed it first. |
55
+ | boot | `path` falls outside the adapter's reserved prefixes. | `TRIGGER_PATH_INVALID` |
56
+
57
+ Only well-prefixed paths are tracked for collisions, so a path missing `/api/` produces one diagnostic rather than two. The Cloudflare Worker reserves `/admin`, `/_mantle`, `/api/auth`, `/api/views`, `/oauth`, `/mcp`, anything starting `/.well-known/oauth`, and the exact registrations `*` and `/*`; a prefix matches the path itself or a `/` or `{` boundary after it. See [Conventional Worker](../cloudflare/conventional-worker.md).
58
+
59
+ ### Routing and binding
60
+
61
+ | Behavior | Detail |
62
+ |---|---|
63
+ | Path params | Each `{param}` binds to the identically named field on the target Procedure's `input`, which must declare it. |
64
+ | Precedence | The invocation merges the body first and the path params second, so **the path wins** over a same-named body field. |
65
+ | Trailing slash | `/api/posts` and `/api/posts/` are the same route; the root `/` is preserved. |
66
+ | Percent-encoding | Each request segment is decoded once, so `/api/by%2Dtag` matches the literal `/api/by-tag`. Malformed encoding such as `%GG` is a routing miss (404), not a 500. |
67
+ | Body | Must be a JSON object. Anything else is `INPUT_VALIDATION_FAILED` (400) with *HTTP Trigger request body must be a JSON object*. A body over 1 MiB is the same code at 413. |
68
+ | Empty body | A `DELETE`, or any request without a JSON content type, is treated as `{}` — bind those inputs through path params. |
69
+
70
+ A success is `{ "ok": true, "data": <handler result> }`; a failure is `{ "ok": false, "diagnostic": ... }` at the diagnostic's mapped status. `http` Triggers are also what the OpenAPI emitter projects into operations.
71
+
72
+ ## `mcp` source
73
+
74
+ ```yaml
75
+ apiVersion: cms.mantle.aotter.net/v1
76
+ kind: Trigger
77
+ metadata:
78
+ name: approve-purchase-order-mcp
79
+ spec:
80
+ source:
81
+ kind: mcp
82
+ surface: staff
83
+ target:
84
+ procedure: approve-purchase-order
85
+ ```
86
+
87
+ | `surface` | Endpoint | Gate |
88
+ |---|---|---|
89
+ | `public` | `/mcp` | Bearer token. |
90
+ | `staff` | `/mcp/staff` | Bearer token plus a staff role read from storage on every invocation. |
91
+
92
+ `surface` is **discovery only**. It decides which tools appear in `tools/list` on which endpoint; it authorizes nothing. The target Procedure's `requires.auth.all` predicates and its optional guard are re-evaluated on every `tools/call` against the authenticated caller, exactly as they are over HTTP. A `public`-surface Procedure that requires `ctx.staff` is discoverable on `/mcp` and will still be denied there.
93
+
94
+ The tool name is derived from the **Procedure's** `metadata.name`, not the Trigger's: lower-cased, with `-` replaced by `_`. Only one Trigger may claim a given `(surface, tool name)` pair; a second is `MCP_TOOL_NAME_COLLISION`. The same code also fires when the mangled name hits a reserved generic tool name or prefix, or a Schema's or another Procedure's segment — see [Reserved names](./manifest.md#reserved-names).
95
+
96
+ The tool carries the Procedure's `title` and `description`, with a short authorization summary appended to the description. `output` is not surfaced; MCP clients infer the response shape from the `tools/call` result. See [MCP and agents](../concepts/mcp-and-agents.md).
97
+
98
+ ## `lifecycle` source
99
+
100
+ ```yaml
101
+ apiVersion: cms.mantle.aotter.net/v1
102
+ kind: Trigger
103
+ metadata:
104
+ name: 010-verify-purchase-token
105
+ spec:
106
+ source:
107
+ kind: lifecycle
108
+ schema: purchase-orders
109
+ on: [before_create]
110
+ errorPolicy: abort
111
+ target:
112
+ procedure: verify-purchase-token
113
+ ```
114
+
115
+ | Field | Type | Required | Default | Rules |
116
+ |---|---|---|---|---|
117
+ | `schema` | string | yes | — | A declared Schema (`LIFECYCLE_SCHEMA_UNKNOWN`). |
118
+ | `on` | `LifecycleHook[]` | yes | — | Non-empty; every entry from the closed list below. |
119
+ | `errorPolicy` | `abort` \| `continue` | no | `abort` for `before_*`, `continue` for `after_*` | See below. |
120
+
121
+ `errorPolicy: abort` is rejected at parse time when **any** `after_*` hook appears in `on`: an `after_*` hook runs once the response has already been sent, so an abort could never reach the caller. Split the `after_*` hooks into their own Trigger, or declare `continue`.
122
+
123
+ > **Info**
124
+ > `Schema.spec.lifecycle` (`publishing` / `operational`) is a different domain that shares the word. That setting governs which states an entry may be in; a lifecycle Trigger governs what fires around a mutation. See [Schema](./schema.md#lifecycle).
125
+
126
+ ### Hooks
127
+
128
+ | Hook | Fires |
129
+ |---|---|
130
+ | `before_create` | Before the insert. |
131
+ | `after_create` | After the insert. |
132
+ | `before_update` | Before an update **or** any status transition whose target is not `published` — this includes unpublish and archive. |
133
+ | `after_update` | After an update or such a transition. |
134
+ | `before_delete` | Before the delete. |
135
+ | `after_delete` | After the delete, only when a row was actually removed. |
136
+ | `before_publish` | Before a transition to `published`. |
137
+ | `after_publish` | After a transition to `published`. |
138
+
139
+ There are no unpublish-specific or archive-specific hooks. Do not read `before_update` / `after_update` as edit-only.
140
+
141
+ ### Error policy
142
+
143
+ | Phase | Default | Behavior |
144
+ |---|---|---|
145
+ | `before_*` | `abort` | A throwing hook cancels the surrounding mutation and the caller receives the hook's own diagnostic. A hook that rejects a write on purpose raises `LIFECYCLE_HOOK_REJECTED` (409). Under `continue` the failure is logged and the mutation proceeds. |
146
+ | `after_*` | `continue` | The committed mutation stands. On the inline or `waitUntil` path a failure is logged and swallowed. With a deferred dispatcher wired in, the failure reaches the delivery adapter so its at-least-once retry and dead-letter policy can run. Neither path ever rolls back. |
147
+
148
+ ### Handler input and `ctx.event`
149
+
150
+ Hook input is phase-specific.
151
+
152
+ | Phase | Handler input |
153
+ |---|---|
154
+ | `before_*` | The original **pre-projection** Procedure input, so a hook can read side-channel fields the row never stores — a CAPTCHA token, a client nonce. It falls back to the row's `data` when there is no caller input. |
155
+ | `after_*` | The persisted `entry.data` only. Deferred envelopes deliberately never carry arbitrary request input. |
156
+
157
+ Every hook handler also receives `ctx.event`:
158
+
159
+ | Field | Value |
160
+ |---|---|
161
+ | `id` | Stable event id, unchanged across enqueue fallback and deferred retries. |
162
+ | `trigger` | The firing Trigger's `metadata.name`. |
163
+ | `hook` | The hook name. |
164
+ | `schema` | The watched Schema name. |
165
+ | `entry` | `null` only on `before_create`; the pre-mutation row for the other `before_*` hooks; the persisted post-mutation row for every `after_*`. |
166
+
167
+ Deferred handlers key on `${ctx.event.id}:${ctx.event.trigger}`. See [Procedure](./procedure.md#conflicts-and-idempotency).
168
+
169
+ ### Ordering and coverage
170
+
171
+ When several lifecycle Triggers bind the same `(schema, hook)`, they fire **alphabetically by `Trigger.metadata.name`**. Choose names that sort the way you want them to run — the `010-`, `020-` convention exists for exactly this, and the code generator handles the leading digits.
172
+
173
+ Hooks are wired through a repository decorator that wraps the single entry-writer chokepoint, so **Staff MCP, Admin and builtin Procedure writes all fire the same hooks**. There is no write path that bypasses them.
174
+
175
+ For deferred `after_*` delivery, the ordered Trigger-name list is captured into one versioned envelope carrying the persisted row and a small identity snapshot. Every captured Trigger runs before a failure is reported back, so a retry may replay Triggers that already succeeded — hence the idempotency key. Queue acceptance is not transactional with the entry write, the `waitUntil` fallback is best-effort, and exactly-once is not promised. See [Deferred hooks on Queues](../cloudflare/deferred-hooks-queues.md).
176
+
177
+ ## Source
178
+
179
+ - [`packages/mantle-spec/src/domain/model/ManifestGrammar.ts`](../../../packages/mantle-spec/src/domain/model/ManifestGrammar.ts)
180
+ - [`packages/mantle-spec/src/domain/service/ManifestParser.ts`](../../../packages/mantle-spec/src/domain/service/ManifestParser.ts)
181
+ - [`packages/mantle-spec/src/domain/service/ManifestGraphValidator.ts`](../../../packages/mantle-spec/src/domain/service/ManifestGraphValidator.ts)
182
+ - [`packages/mantle-spec/src/domain/service/McpToolNaming.ts`](../../../packages/mantle-spec/src/domain/service/McpToolNaming.ts)
183
+ - [`packages/mantle-runtime/src/domain/service/PathMatcher.ts`](../../../packages/mantle-runtime/src/domain/service/PathMatcher.ts)
184
+ - [`packages/mantle-runtime/src/domain/service/TriggerIndex.ts`](../../../packages/mantle-runtime/src/domain/service/TriggerIndex.ts)
185
+ - [`packages/mantle-runtime/src/usecase/lifecycle/RunLifecycleHooksUseCase.ts`](../../../packages/mantle-runtime/src/usecase/lifecycle/RunLifecycleHooksUseCase.ts)
186
+ - [`packages/mantle-runtime/src/infrastructure/persistence/LifecycleHookingEntryRepository.ts`](../../../packages/mantle-runtime/src/infrastructure/persistence/LifecycleHookingEntryRepository.ts)
187
+ - [`packages/mantle-runtime/src/domain/port/DeferredHookDispatcher.ts`](../../../packages/mantle-runtime/src/domain/port/DeferredHookDispatcher.ts)
188
+ - [`packages/mantle-runtime/src/infrastructure/http/createMantleRequestHandler.ts`](../../../packages/mantle-runtime/src/infrastructure/http/createMantleRequestHandler.ts)
189
+ - [`packages/mantle-runtime/src/infrastructure/http/readJsonBody.ts`](../../../packages/mantle-runtime/src/infrastructure/http/readJsonBody.ts)
190
+ - [`packages/mantle-runtime/src/usecase/boot/ValidateBootUseCase.ts`](../../../packages/mantle-runtime/src/usecase/boot/ValidateBootUseCase.ts)
191
+ - [`packages/mantle/src/codegen/emitMantleModule.ts`](../../../packages/mantle/src/codegen/emitMantleModule.ts)
192
+ - [`packages/adapters/cloudflare/src/worker/createMantleWorker.ts`](../../../packages/adapters/cloudflare/src/worker/createMantleWorker.ts)
193
+ - [`packages/adapters/cloudflare/src/mount/mountMcp.ts`](../../../packages/adapters/cloudflare/src/mount/mountMcp.ts)