@aotter/mantle 0.0.11-alpha.72 → 0.1.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/docs/adapter-guide.md +2 -5
- package/docs/adr/0011-adapter-port-spec.md +15 -27
- package/docs/adr/0012-views-as-public-rest.md +1 -1
- package/docs/api-mcp-authorization.md +50 -2
- package/docs/cloudflare-low-level-composition.md +1 -1
- package/docs/deferred-lifecycle-queues.md +0 -3
- package/docs/design-atoms.md +11 -6
- package/docs/performance-harness.md +18 -19
- package/docs/release-process.md +5 -3
- package/package.json +10 -10
- package/skills/develop/SKILL.md +12 -3
- package/skills/provision/SKILL.md +1 -1
package/README.md
CHANGED
|
@@ -22,7 +22,7 @@ Adopters install this one package and import from subpaths. Sub-packages remain
|
|
|
22
22
|
| `@aotter/mantle/spec` (or root) | Manifest grammar, validators, JSON-Schema→Zod, diagnostic catalog (no env / no IO) |
|
|
23
23
|
| `@aotter/mantle/runtime` | Hexagonal runtime: domain ports, use cases, infrastructure helpers (no adapter deps) |
|
|
24
24
|
| `@aotter/mantle/runtime/testing` | Node-only crowded SQLite planner and HTTP sampling helpers |
|
|
25
|
-
| `@aotter/mantle/cloudflare` | Cloudflare Workers adapter — D1,
|
|
25
|
+
| `@aotter/mantle/cloudflare` | Cloudflare Workers adapter — D1, Workers Cache, R2, Better Auth, MCP via `@cloudflare/workers-oauth-provider` |
|
|
26
26
|
| `@aotter/mantle/admin-ui` | Pre-built React 19 admin SPA bundle |
|
|
27
27
|
|
|
28
28
|
```ts
|
|
@@ -80,7 +80,7 @@ import { manifest } from "../.mantle/generated/site.js";
|
|
|
80
80
|
export default createMantleWorker({ manifest });
|
|
81
81
|
```
|
|
82
82
|
|
|
83
|
-
`createMantleWorker` owns conventional D1/
|
|
83
|
+
`createMantleWorker` owns conventional D1/assets bindings, Auth, Admin,
|
|
84
84
|
manifest REST/HTTP routes, OAuth, MCP, cache safety, and rejection-safe
|
|
85
85
|
per-isolate boot. Use its single `extend` seam for application handlers and
|
|
86
86
|
new Hono routes; use the public low-level exports when the deployment does not
|
|
@@ -166,7 +166,7 @@ Core-owned `mantle:plugin` skill and records it in `.mantle/plugins.json` plus
|
|
|
166
166
|
| Cloudflare Workers | ✅ shipping |
|
|
167
167
|
| Netlify | 📋 README stub — engineering forcing function for v0.2 (`@aotter/mantle-netlify`) |
|
|
168
168
|
|
|
169
|
-
The `mantle-runtime` package never imports Cloudflare-specific types — adapters bind concrete drivers (D1 /
|
|
169
|
+
The `mantle-runtime` package never imports Cloudflare-specific types — adapters bind concrete drivers (D1 / R2) to the runtime's `domain/port/*` interfaces, so adding a new adapter is a port-implementation exercise, not a refactor.
|
|
170
170
|
|
|
171
171
|
## Documentation
|
|
172
172
|
|
|
@@ -181,7 +181,7 @@ The `mantle-runtime` package never imports Cloudflare-specific types — adapter
|
|
|
181
181
|
- `node_modules/@aotter/mantle/docs/schema-indexes.md` (ordered composite
|
|
182
182
|
JSON-field indexes, D1 query plans, and the safe Procedure SQL helper)
|
|
183
183
|
- `node_modules/@aotter/mantle/docs/performance-harness.md` (crowded SQLite,
|
|
184
|
-
Wrangler-local D1
|
|
184
|
+
Wrangler-local D1 origin paths and coding-agent guardrails)
|
|
185
185
|
- `node_modules/@aotter/mantle/docs/adr/`
|
|
186
186
|
- `node_modules/@aotter/mantle/skills/develop/SKILL.md`
|
|
187
187
|
- `node_modules/@aotter/mantle/skills/plugin/SKILL.md`
|
package/docs/adapter-guide.md
CHANGED
|
@@ -8,12 +8,11 @@ Adapter packages live under `packages/adapters/<platform>/` using a plural `adap
|
|
|
8
8
|
|
|
9
9
|
## Required runtime ports
|
|
10
10
|
|
|
11
|
-
A first-run adapter must implement exactly these
|
|
11
|
+
A first-run adapter must implement exactly these two runtime ports:
|
|
12
12
|
|
|
13
13
|
| Contract | Source | Cloudflare example |
|
|
14
14
|
|---|---|---|
|
|
15
15
|
| `DatabaseDriver` plus `PreparedStatement` and `MigrationRunner` | `packages/mantle-runtime/src/domain/port/DatabaseDriver.ts` | `packages/adapters/cloudflare/src/bindings/D1DatabaseDriver.ts` |
|
|
16
|
-
| `KvCache` | `packages/mantle-runtime/src/domain/port/KvCache.ts` | `packages/adapters/cloudflare/src/bindings/KvCacheBinding.ts` |
|
|
17
16
|
| `AssetServer` | `packages/mantle-runtime/src/domain/port/AssetServer.ts` | `packages/adapters/cloudflare/src/bindings/AssetsAssetServer.ts` |
|
|
18
17
|
|
|
19
18
|
The runtime must not import platform types such as `D1Database`, `KVNamespace`, Cloudflare `Fetcher`, Netlify request objects, Postgres pools, or adapter SDK types. Those live in adapter packages.
|
|
@@ -49,7 +48,6 @@ const runtime = createCmsRuntime({
|
|
|
49
48
|
templates,
|
|
50
49
|
siteDefaults,
|
|
51
50
|
db,
|
|
52
|
-
kv,
|
|
53
51
|
assets,
|
|
54
52
|
publicPathResolver,
|
|
55
53
|
mediaStorage,
|
|
@@ -116,7 +114,7 @@ response policy covers admin, auth, API, OAuth, MCP, redirects, and errors.
|
|
|
116
114
|
Those responses receive `Cache-Control: private, no-store`; Cloudflare-specific
|
|
117
115
|
CDN cache overrides are removed.
|
|
118
116
|
|
|
119
|
-
`mountPublicRoutes(...)` opts only successful
|
|
117
|
+
`mountPublicRoutes(...)` renders canonical D1 state and opts only successful HTML, markdown,
|
|
120
118
|
`llms.txt`, and sitemap responses into the shared cache with
|
|
121
119
|
`Cache-Control: public, max-age=0, s-maxage=300`. The top-level policy preserves
|
|
122
120
|
that opt-in only for anonymous `GET`/`HEAD` responses with status 200, explicit
|
|
@@ -149,7 +147,6 @@ Minimum auth/MCP behavior:
|
|
|
149
147
|
## Implementation checklist
|
|
150
148
|
|
|
151
149
|
- [ ] Implement `DatabaseDriver`, including canonical migration tracking.
|
|
152
|
-
- [ ] Implement `KvCache`, including prefix listing and opaque cursors.
|
|
153
150
|
- [ ] Implement `AssetServer` for the admin UI assets.
|
|
154
151
|
- [ ] Compose `createCmsRuntime` with manifests, handlers, templates, site defaults, and required ports.
|
|
155
152
|
- [ ] Call `bootInit()` before serving CMS traffic.
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
# ADR-0011: Adapter port spec
|
|
2
2
|
|
|
3
|
-
**Status:** Accepted for v0.1.0.
|
|
3
|
+
**Status:** Accepted for v0.1.0. Amended 2026-08-11 to remove the rendered-artifact `KvCache` port.
|
|
4
4
|
|
|
5
|
-
**Date:** 2026-05-04 (revised 2026-05-09
|
|
5
|
+
**Date:** 2026-05-04 (revised 2026-05-09, 2026-05-10, and 2026-08-11).
|
|
6
6
|
|
|
7
7
|
## Context
|
|
8
8
|
|
|
9
9
|
`@aotter/mantle-runtime` is adapter-agnostic. It owns dispatcher, entry-writer, view executor, content-ops, render pipeline, boot validation, and MCP JSON-RPC dispatch. It depends only on `@aotter/mantle-spec` and a small set of TypeScript interfaces it defines itself.
|
|
10
10
|
|
|
11
|
-
`@aotter/mantle-cloudflare` is the only adapter shipping in v0.1.0. It binds the runtime's interfaces against Cloudflare Workers' D1
|
|
11
|
+
`@aotter/mantle-cloudflare` is the only adapter shipping in v0.1.0. It binds the runtime's interfaces against Cloudflare Workers' D1 and ASSETS, and supplies a Better Auth instance (per ADR-0014) for sign-in + MCP bearer validation. OAuth grant KV remains adapter-owned infrastructure and is not a runtime port.
|
|
12
12
|
|
|
13
13
|
`@aotter/mantle-netlify` is a v0.2 stub — README only. It exists in the package layout as an engineering forcing function: with N=1 adapter, "adapter-agnostic" silently rots in PR review (a `D1Database` import slips into runtime, then a second, then five). With a second adapter visible in the workspace (even if its impl is a TODO), reviewers have somewhere to point when blocking the slip.
|
|
14
14
|
|
|
@@ -21,14 +21,18 @@ The POC accumulated multiple half-decisions about this seam (POC ADR-0015 docume
|
|
|
21
21
|
|
|
22
22
|
## Decision
|
|
23
23
|
|
|
24
|
-
**
|
|
24
|
+
**Two required adapter ports**, defined as TypeScript interfaces in `@aotter/mantle-runtime/src/domain/port/`. Concrete adapters provide implementations and inject them into `createCmsRuntime`.
|
|
25
25
|
|
|
26
26
|
| Port | Surface |
|
|
27
27
|
|---|---|
|
|
28
28
|
| `DatabaseDriver` | All persistent state — `entries`, `site_config`, `staff`, `users`, `approvals`, plus migrations. |
|
|
29
|
-
| `KvCache` | Publish-pipeline cache — pre-rendered HTML, `.md` mirrors, `llms.txt` per locale. Read-mostly, written by the publish pipeline. |
|
|
30
29
|
| `AssetServer` | Static-asset serving for the admin SPA. The runtime hands the adapter an asset path + `Request`; the adapter returns a `Response` with the right MIME and caching. |
|
|
31
30
|
|
|
31
|
+
Rendered public artifacts are not a second storage model. D1 stays canonical;
|
|
32
|
+
adapters render on origin misses and may use their native HTTP response cache.
|
|
33
|
+
The Cloudflare adapter uses version-local Workers Cache, which runs before the
|
|
34
|
+
Worker for eligible anonymous responses.
|
|
35
|
+
|
|
32
36
|
Optional feature ports may also live in `domain/port/`, but they are
|
|
33
37
|
not part of the first-run adapter contract until a feature is enabled.
|
|
34
38
|
For v0.1.x media hosting and deferred lifecycle dispatch:
|
|
@@ -121,20 +125,6 @@ The runtime never sees `D1Database`, `Pool` (postgres), or any concrete driver.
|
|
|
121
125
|
|
|
122
126
|
The CF adapter's impl is a thin proxy over `env.DB` (D1). A future Postgres-via-Hyperdrive adapter wraps `pg` to the same shape; a Netlify adapter could wrap Neon, Supabase, or PlanetScale.
|
|
123
127
|
|
|
124
|
-
### `KvCache`
|
|
125
|
-
|
|
126
|
-
```ts
|
|
127
|
-
export interface KvCache {
|
|
128
|
-
get(key: string): Promise<string | null>;
|
|
129
|
-
put(key: string, value: string, opts?: { expirationTtl?: number }): Promise<void>;
|
|
130
|
-
delete(key: string): Promise<void>;
|
|
131
|
-
/** List keys with a prefix — used by sitemap / llms.txt aggregation. */
|
|
132
|
-
list(prefix: string, cursor?: string | null): Promise<{ keys: string[]; cursor: string | null }>;
|
|
133
|
-
}
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
CF adapter: Workers KV. Future: Redis, FS, S3-compatible store.
|
|
137
|
-
|
|
138
128
|
### `AssetServer`
|
|
139
129
|
|
|
140
130
|
```ts
|
|
@@ -164,7 +154,6 @@ import {
|
|
|
164
154
|
mountServerEndpoints,
|
|
165
155
|
AssetsAssetServer,
|
|
166
156
|
D1DatabaseDriver,
|
|
167
|
-
KvCacheBinding,
|
|
168
157
|
} from "@aotter/mantle-cloudflare";
|
|
169
158
|
|
|
170
159
|
const auth = createAuth({
|
|
@@ -188,7 +177,6 @@ const cms = createCmsRef({
|
|
|
188
177
|
handlers,
|
|
189
178
|
bindings: {
|
|
190
179
|
db: new D1DatabaseDriver(env.DB),
|
|
191
|
-
kv: new KvCacheBinding(env.KV),
|
|
192
180
|
assets: new AssetsAssetServer(env.ASSETS),
|
|
193
181
|
},
|
|
194
182
|
auth,
|
|
@@ -224,10 +212,10 @@ There's no module-global state holding adapter-specific bindings.
|
|
|
224
212
|
- Removing a port is also possible (if a port is found to overlap or be unnecessary), again by amending this ADR.
|
|
225
213
|
|
|
226
214
|
**Discoverability for adapter authors**:
|
|
227
|
-
- A future Bun/Deno/Vercel/Netlify port author reads this ADR + [`docs/adapter-guide.md`](../adapter-guide.md), implements the
|
|
215
|
+
- A future Bun/Deno/Vercel/Netlify port author reads this ADR + [`docs/adapter-guide.md`](../adapter-guide.md), implements the two required ports, then wires boot and HTTP/MCP surfaces. That's the contract. No hidden state, no implicit assumptions about the HTTP framework.
|
|
228
216
|
|
|
229
217
|
**Test ergonomics**:
|
|
230
|
-
- Each port is small and isolated. Tests can mock individual ports without spinning up D1
|
|
218
|
+
- Each port is small and isolated. Tests can mock individual ports without spinning up D1 or an OAuth provider.
|
|
231
219
|
- The runtime's test suite exercises against in-memory port impls; the adapter's test suite exercises the binding against real CF resources via `wrangler dev` or live deploy.
|
|
232
220
|
|
|
233
221
|
**The Netlify stub's job**:
|
|
@@ -235,11 +223,11 @@ There's no module-global state holding adapter-specific bindings.
|
|
|
235
223
|
|
|
236
224
|
## Alternatives considered
|
|
237
225
|
|
|
238
|
-
**(a) Single mega-port** — One `RuntimePorts` interface containing every method (db.prepare,
|
|
226
|
+
**(a) Single mega-port** — One `RuntimePorts` interface containing every method (db.prepare, assets.fetch, media.createUpload, …). **Rejected**: leaks the entire surface onto every adapter. Discrete ports keep change blast radius per port.
|
|
239
227
|
|
|
240
228
|
**(b) Concrete CF types in runtime** — Just `import type { D1Database } from "@cloudflare/workers-types"` directly into `mantle-runtime`. Treat "CF-only" as a v0.1.0 reality, defer the abstraction. **Rejected**: this is what the POC did (via `cms-server` having implicit assumptions about D1 shape) and it's the trap the rebuild exists to escape. Once concrete CF types land in runtime, removing them is a multi-PR uplift later. Cheaper to do it right at v0.1.0.
|
|
241
229
|
|
|
242
|
-
**(c) Function-injection (no interfaces, just functions)** — Runtime accepts a record of functions
|
|
230
|
+
**(c) Function-injection (no interfaces, just functions)** — Runtime accepts a record of functions such as `{ dbPrepare, assetFetch, sessionRead, … }`. **Rejected**: TypeScript interfaces are more discoverable and document grouping.
|
|
243
231
|
|
|
244
232
|
**(d) Plugin pattern (each port is a separate package)** — `@aotter/mantle-port-database`, `@aotter/mantle-port-kv`, etc., and runtime depends on one package per port. **Rejected**: the port set is too small to warrant per-port packages. The current 5-package structure (spec / runtime / admin-ui / cloudflare / netlify) is already at the boundary of "too many"; splitting further increases the maintenance tax without useful benefit. Ports are TS interfaces in `mantle-runtime`'s `src/domain/port/` directory — that's enough.
|
|
245
233
|
|
|
@@ -251,12 +239,12 @@ When you're authoring `@aotter/mantle-runtime` code:
|
|
|
251
239
|
|
|
252
240
|
1. If you reach for a CF-specific type, **stop**. Define a method on a port instead.
|
|
253
241
|
2. If a port is missing the method you need, **amend this ADR first** in the same PR, then add the method. Adapters in the same PR.
|
|
254
|
-
3. Tests must use port mocks (in-memory implementations) — never reach into a real D1
|
|
242
|
+
3. Tests must use port mocks (in-memory implementations) — never reach into a real D1 from runtime tests.
|
|
255
243
|
|
|
256
244
|
When you're authoring an adapter (`@aotter/mantle-cloudflare` for v0.1.0; future `mantle-netlify`, `mantle-bun`, …):
|
|
257
245
|
|
|
258
246
|
1. Read `mantle-runtime/src/domain/port/`. Implement each required port against your runtime's primitives.
|
|
259
|
-
2. Compose the runtime via `createCmsRuntime({ db,
|
|
247
|
+
2. Compose the runtime via `createCmsRuntime({ db, assets, manifests, handlers, templates, siteDefaults, ... })`.
|
|
260
248
|
3. Call `runtime.bootInit()` once before serving CMS traffic.
|
|
261
249
|
4. Bind to your HTTP framework — Hono on CF, Netlify Functions handler, raw `fetch` Worker, …
|
|
262
250
|
5. Provide adapter-owned auth and map sessions/scopes/roles into runtime handler context.
|
|
@@ -88,7 +88,7 @@ spec:
|
|
|
88
88
|
eq: { field: locale, value: { $param: locale } }
|
|
89
89
|
```
|
|
90
90
|
|
|
91
|
-
The `$param` discriminator key was chosen to match the JSON Schema `$ref` convention.
|
|
91
|
+
The `$param` discriminator key was chosen to match the JSON Schema `$ref` convention. The later closed `{ "$ctx.user": "id" }` sentinel follows the same `$<name>` shape; `$now` remains unimplemented.
|
|
92
92
|
|
|
93
93
|
Boot validator gates:
|
|
94
94
|
- `View.spec.params` MUST be `type: object` with `properties` declared (`VIEW_PARAMS_INVALID_SHAPE`).
|
|
@@ -78,6 +78,56 @@ Missing/invalid credentials return `401`; a verified caller missing a required
|
|
|
78
78
|
role or scope returns `403`; a site guard may return
|
|
79
79
|
`ENTITLEMENT_REQUIRED`/`402`. Guards run on every call and are not cached.
|
|
80
80
|
|
|
81
|
+
### Identity-bound Views
|
|
82
|
+
|
|
83
|
+
Use the closed `{ "$ctx.user": "id" }` filter sentinel for rows owned by the
|
|
84
|
+
current site-local Better Auth user. The caller never supplies this value, so
|
|
85
|
+
the same View is safe on both REST and public MCP:
|
|
86
|
+
|
|
87
|
+
```yaml
|
|
88
|
+
apiVersion: cms.mantle.aotter.net/v1
|
|
89
|
+
kind: Schema
|
|
90
|
+
metadata: { name: orders }
|
|
91
|
+
spec:
|
|
92
|
+
schema:
|
|
93
|
+
type: object
|
|
94
|
+
properties:
|
|
95
|
+
userId: { type: string, x-mantle-bind: ctx.user }
|
|
96
|
+
orderNumber: { type: string }
|
|
97
|
+
orderStatus: { type: string }
|
|
98
|
+
totalMinor: { type: integer }
|
|
99
|
+
placedAt: { type: integer }
|
|
100
|
+
indexes: [[userId, placedAt]]
|
|
101
|
+
---
|
|
102
|
+
apiVersion: cms.mantle.aotter.net/v1
|
|
103
|
+
kind: View
|
|
104
|
+
metadata: { name: my-orders }
|
|
105
|
+
spec:
|
|
106
|
+
surface: public
|
|
107
|
+
from: orders
|
|
108
|
+
requires:
|
|
109
|
+
auth:
|
|
110
|
+
all: [ctx.user]
|
|
111
|
+
filter:
|
|
112
|
+
and:
|
|
113
|
+
- { eq: { field: status, value: published } }
|
|
114
|
+
- { eq: { field: userId, value: { "$ctx.user": id } } }
|
|
115
|
+
fields: [orderNumber, orderStatus, totalMinor, placedAt]
|
|
116
|
+
orderBy: [{ field: placedAt, direction: desc }]
|
|
117
|
+
limit: 50
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Core rejects this sentinel unless the View requires `ctx.user` and the bound
|
|
121
|
+
field is the leftmost field of a declared Schema index. Missing identity fails
|
|
122
|
+
with `401`; it never drops the filter or falls back to all rows. REST exposes
|
|
123
|
+
`GET /api/views/my-orders`; public MCP exposes `query_view_my_orders`. Both
|
|
124
|
+
call `ExecuteViewUseCase` and bind the same `ctx.user.id`.
|
|
125
|
+
|
|
126
|
+
The id belongs to the customer site's Better Auth user row. It is not a
|
|
127
|
+
Mantle Platform user id, Hosted Auth upstream subject, email, or provider id.
|
|
128
|
+
Hosted Auth may establish the site session, but Platform is not part of the
|
|
129
|
+
View query path.
|
|
130
|
+
|
|
81
131
|
## Cloudflare consumer wiring
|
|
82
132
|
|
|
83
133
|
Pass one site-owned resolver to `createCmsRef`. Return `not-handled` when the
|
|
@@ -173,7 +223,6 @@ import {
|
|
|
173
223
|
createMcpApiHandler,
|
|
174
224
|
createOAuthProvider,
|
|
175
225
|
D1DatabaseDriver,
|
|
176
|
-
KvCacheBinding,
|
|
177
226
|
mountServerEndpoints,
|
|
178
227
|
} from "@aotter/mantle/cloudflare";
|
|
179
228
|
|
|
@@ -182,7 +231,6 @@ const runtimeRef = createCmsRef({
|
|
|
182
231
|
handlers,
|
|
183
232
|
bindings: {
|
|
184
233
|
db: new D1DatabaseDriver(env.DB),
|
|
185
|
-
kv: new KvCacheBinding(env.KV),
|
|
186
234
|
assets: env.ASSETS
|
|
187
235
|
? new AssetsAssetServer(env.ASSETS)
|
|
188
236
|
: { fetch: async () => null },
|
|
@@ -103,7 +103,6 @@ import type { DeferredHookEnvelope } from "@aotter/mantle/runtime";
|
|
|
103
103
|
import {
|
|
104
104
|
AssetsAssetServer,
|
|
105
105
|
D1DatabaseDriver,
|
|
106
|
-
KvCacheBinding,
|
|
107
106
|
WorkersQueueHookDispatcher,
|
|
108
107
|
createCmsRef,
|
|
109
108
|
createQueueHandler,
|
|
@@ -112,7 +111,6 @@ import {
|
|
|
112
111
|
|
|
113
112
|
interface Env {
|
|
114
113
|
DB: D1Database;
|
|
115
|
-
KV: KVNamespace;
|
|
116
114
|
ASSETS: Fetcher;
|
|
117
115
|
MANTLE_INTERNAL_QUEUE: Queue<DeferredHookEnvelope>;
|
|
118
116
|
}
|
|
@@ -124,7 +122,6 @@ function buildWorker(env: Env) {
|
|
|
124
122
|
auth: createSiteAuth(env),
|
|
125
123
|
bindings: {
|
|
126
124
|
db: new D1DatabaseDriver(env.DB),
|
|
127
|
-
kv: new KvCacheBinding(env.KV),
|
|
128
125
|
assets: new AssetsAssetServer(env.ASSETS),
|
|
129
126
|
deferredHookDispatcher: new WorkersQueueHookDispatcher(
|
|
130
127
|
env.MANTLE_INTERNAL_QUEUE,
|
package/docs/design-atoms.md
CHANGED
|
@@ -237,7 +237,12 @@ properties:
|
|
|
237
237
|
Informational FK marker on a string-typed field that holds an ID
|
|
238
238
|
referencing rows in another Schema. Not enforced in v0.1 (the SDK
|
|
239
239
|
passes it through; no foreign-key constraint, no cascade, no orphan
|
|
240
|
-
detection).
|
|
240
|
+
detection). The admin uses declared refs to show related rows and to nest
|
|
241
|
+
required child collections; it does not infer relationships from field names.
|
|
242
|
+
Declare a single-field `indexes` entry for the ref field (for example,
|
|
243
|
+
`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.
|
|
241
246
|
|
|
242
247
|
**Example**:
|
|
243
248
|
```yaml
|
|
@@ -714,16 +719,16 @@ grammar, leftmost-prefix rules, SQL helper, and query-plan examples.
|
|
|
714
719
|
### Practical scale envelope on D1
|
|
715
720
|
|
|
716
721
|
- **Blog-scale (< 10k entries / collection)**: today's design is
|
|
717
|
-
comfortable.
|
|
722
|
+
comfortable. Anonymous public responses can hit version-local Workers
|
|
723
|
+
Cache before the Worker; origin misses use indexed D1 reads.
|
|
718
724
|
- **Mid-scale (10k – 100k entries)**: declare measured list/filter
|
|
719
725
|
access paths with ordered `indexes`. Cross-collection JSON-path joins
|
|
720
726
|
may eventually gain `x-mantle-ref` auto-lift; that remains DRAFT.
|
|
721
727
|
- **Hard D1 limits**: 1 MB max row size; 5,000 rows per query result;
|
|
722
728
|
single-writer per database (concurrent writes serialize).
|
|
723
|
-
- **Cross-region read**: D1 is region-pinned; first hit from a
|
|
724
|
-
far region
|
|
725
|
-
|
|
726
|
-
CMS layer.
|
|
729
|
+
- **Cross-region read**: D1 is region-pinned; first origin hit from a
|
|
730
|
+
far region may pay replica latency. Eligible anonymous responses are
|
|
731
|
+
then served by version-local Workers Cache.
|
|
727
732
|
|
|
728
733
|
### Scale-up path: D1 → Postgres via Cloudflare Hyperdrive
|
|
729
734
|
|
|
@@ -13,8 +13,8 @@ make a normal content/API/page change.
|
|
|
13
13
|
| Manifest View execution | `ExecuteViewUseCase` + `ViewSqlCompiler` | The deliberate compiled-query exception; it still resolves declared Schema indexes. |
|
|
14
14
|
| Editable settings and code-owned locale/media policy | `DatabaseSiteConfigRepository` | Editable values and dynamic media tool policy are read fresh; boot-seeded locale policy may be memoized within the runtime instance. |
|
|
15
15
|
| Pending media uploads | `DatabasePendingUploadRepository` | Canonical, read-after-write D1 state; never publish-cache state. |
|
|
16
|
-
| Rendered HTML, Markdown, and `llms.txt` |
|
|
17
|
-
| D1
|
|
16
|
+
| Rendered HTML, Markdown, and `llms.txt` | Request-time render use cases plus the Cloudflare public-route cache policy | D1 is canonical; version-local Workers Cache stores anonymous HTTP responses. |
|
|
17
|
+
| D1 transport and optional query metrics | Cloudflare bindings | Bindings stay thin. Query/cache policy does not belong in a generic provider `BaseRepository`. |
|
|
18
18
|
|
|
19
19
|
`CmsRuntime.db` remains deprecated compatibility surface. New site code uses
|
|
20
20
|
Manifests, runtime use cases, `entryReader`, and `siteConfig`. A site may own
|
|
@@ -24,16 +24,14 @@ must not query Mantle-owned tables through `runtime.db`.
|
|
|
24
24
|
## Cache contract
|
|
25
25
|
|
|
26
26
|
- D1 is canonical for entries, site settings, media metadata, and pending
|
|
27
|
-
uploads.
|
|
28
|
-
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
public-artifact invalidation before reporting success. HTTP routes do not
|
|
36
|
-
scan/delete KV prefixes themselves.
|
|
27
|
+
uploads. Core stores no rendered artifact copies.
|
|
28
|
+
- Public routes render canonical state and return
|
|
29
|
+
`Cache-Control: public, max-age=0, s-maxage=300`.
|
|
30
|
+
- Cloudflare Workers Cache checks eligible anonymous responses before invoking
|
|
31
|
+
the Worker. Cache keys are version-local, so a deploy starts with no stale
|
|
32
|
+
response from the previous Worker version.
|
|
33
|
+
- Site-setting and content writes only persist canonical state. They do not
|
|
34
|
+
wait for render work or scan/delete cache prefixes.
|
|
37
35
|
- Do not cache every repository read. Cross-isolate correctness for editable
|
|
38
36
|
data wins unless a read has a measured hot-path contract and explicit
|
|
39
37
|
invalidation.
|
|
@@ -76,11 +74,12 @@ Timing always reports p50/p95/max. A test-only Worker wrapper may also return
|
|
|
76
74
|
`x-mantle-query-count` and `x-mantle-rows-read`; those become distributions in
|
|
77
75
|
the same report. Do not expose these diagnostic headers in production.
|
|
78
76
|
|
|
79
|
-
Core CI runs `pnpm bench:wrangler` against real Wrangler-local D1,
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
77
|
+
Core CI runs `pnpm bench:wrangler` against real Wrangler-local D1, Worker HTTP
|
|
78
|
+
routing, View execution, and origin page rendering. It compares 100 and 10,000
|
|
79
|
+
row fixtures and gates row-read scaling plus endpoint query budgets, not
|
|
80
|
+
absolute milliseconds. Wrangler-local does not emulate the new entrypoint
|
|
81
|
+
Workers Cache, so cache hits are a deployment-level smoke check rather than a
|
|
82
|
+
fabricated local metric.
|
|
84
83
|
|
|
85
84
|
## Seven findings: measured disposition
|
|
86
85
|
|
|
@@ -89,12 +88,12 @@ diagnostic, while query/row counts are the stable assertions.
|
|
|
89
88
|
|
|
90
89
|
| Finding | Disposition |
|
|
91
90
|
|---|---|
|
|
92
|
-
| Public
|
|
91
|
+
| Public cache hits read D1 first | Removed from Worker code. Cloudflare's entrypoint Workers Cache runs before the Worker; Core has no inner render cache. |
|
|
93
92
|
| Slug/locale reads bypass generated indexes | Fixed by the shared schema-aware entry-read boundary. A 10,000-row page MISS measured 2 queries / 5 rows read. |
|
|
94
93
|
| OFFSET pagination | Accepted for the v0.1 bounded-result surfaces: every response is capped at 500 rows and public hot paths must stay shallow. Deep/export workloads require a purpose-shaped cursor API before they are declared hot. |
|
|
95
94
|
| Admin substring search scans | Accepted only for the authenticated Admin collection browser, with a 500-row response cap. Large/search-heavy sites should add a purpose-shaped indexed View or dedicated search service; do not expose this scan publicly. |
|
|
96
95
|
| Published list/sitemap/llms paths lack system indexes | Fixed with measured partial indexes for published global, locale, collection, and collection+locale ordering. The 100-row and 10,000-row API runs both measured 1 query / 20 rows read. |
|
|
97
|
-
| Page MISS waits for
|
|
96
|
+
| Page MISS waits for cache write-back | Removed. Origin rendering returns directly; Workers Cache owns response storage outside the Worker. |
|
|
98
97
|
| Benchmark stops at fake in-process dispatch | Fixed by the Node planner and Wrangler-local Worker/API/page layers. The old dispatch microbenchmark remains a narrow CPU signal only. |
|
|
99
98
|
|
|
100
99
|
The retained OFFSET and substring-search trade-offs are visible exceptions,
|
package/docs/release-process.md
CHANGED
|
@@ -48,14 +48,16 @@ decision instead of starting another local redesign loop.
|
|
|
48
48
|
## Branches and channels
|
|
49
49
|
|
|
50
50
|
- Feature and release PRs target `develop`.
|
|
51
|
-
-
|
|
51
|
+
- Alpha prereleases before stable v0.1.0 release directly from the merged
|
|
52
|
+
`develop` release commit.
|
|
52
53
|
- Beta, RC, and stable promotion to `main` remains a deliberate human decision;
|
|
53
54
|
it is not part of the alpha controller.
|
|
54
55
|
- Alpha, beta, and RC GitHub releases are prereleases.
|
|
55
56
|
- npm dist-tags follow the suffix: `alpha`, `beta`, `rc`, or `latest` for
|
|
56
57
|
stable versions.
|
|
57
|
-
- During the
|
|
58
|
-
|
|
58
|
+
- During the legacy `0.0.x-alpha` cadence, `latest` follows the current alpha.
|
|
59
|
+
The final `0.1.0-alpha.N` candidates advance only `alpha`; `latest` moves to
|
|
60
|
+
`0.1.0` after the stable gate passes.
|
|
59
61
|
|
|
60
62
|
## Release PR
|
|
61
63
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aotter/mantle",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0-alpha.1",
|
|
4
4
|
"description": "Umbrella entry for @aotter/mantle. Adopters install this one package and import from subpaths: /spec, /runtime, /cloudflare, /admin-ui. Sub-packages remain individually installable on npm for tooling / alt-adapter authors. The Netlify adapter ships as a private workspace stub in v0.1 — its subpath will be added when the impl lands in v0.2.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://mantle.tools/",
|
|
@@ -55,24 +55,24 @@
|
|
|
55
55
|
"README.md"
|
|
56
56
|
],
|
|
57
57
|
"dependencies": {
|
|
58
|
-
"@aotter/mantle-admin-ui": "0.0
|
|
59
|
-
"@aotter/mantle-
|
|
60
|
-
"@aotter/mantle-spec": "0.0
|
|
61
|
-
"@aotter/mantle-
|
|
58
|
+
"@aotter/mantle-admin-ui": "0.1.0-alpha.1",
|
|
59
|
+
"@aotter/mantle-cloudflare": "0.1.0-alpha.1",
|
|
60
|
+
"@aotter/mantle-spec": "0.1.0-alpha.1",
|
|
61
|
+
"@aotter/mantle-runtime": "0.1.0-alpha.1"
|
|
62
62
|
},
|
|
63
63
|
"peerDependencies": {
|
|
64
|
-
"@cloudflare/workers-oauth-provider": "^0.8.
|
|
64
|
+
"@cloudflare/workers-oauth-provider": "^0.8.2",
|
|
65
65
|
"aws4fetch": "^1.0.20",
|
|
66
|
-
"better-auth": "^1.6.
|
|
66
|
+
"better-auth": "^1.6.24",
|
|
67
67
|
"hono": "^4.12.0",
|
|
68
68
|
"zod": "^4.0.0"
|
|
69
69
|
},
|
|
70
70
|
"devDependencies": {
|
|
71
|
-
"@cloudflare/workers-oauth-provider": "^0.8.
|
|
71
|
+
"@cloudflare/workers-oauth-provider": "^0.8.2",
|
|
72
72
|
"@types/node": "^26",
|
|
73
73
|
"aws4fetch": "^1.0.20",
|
|
74
|
-
"better-auth": "^1.6.
|
|
75
|
-
"hono": "^4.12.
|
|
74
|
+
"better-auth": "^1.6.24",
|
|
75
|
+
"hono": "^4.12.34",
|
|
76
76
|
"typescript": "^6.0.3",
|
|
77
77
|
"vitest": "^4.1.10",
|
|
78
78
|
"zod": "^4.4.2"
|
package/skills/develop/SKILL.md
CHANGED
|
@@ -16,7 +16,7 @@ docs govern runtime/API behavior.
|
|
|
16
16
|
## First Read
|
|
17
17
|
|
|
18
18
|
1. `package.json` for the installed `@aotter/mantle*` versions.
|
|
19
|
-
2. `manifests
|
|
19
|
+
2. `manifests/`, the active adapter config, and `src/auth.ts` when present. If the project is older, check `src/mantleConfig.ts`.
|
|
20
20
|
3. The active `.mantle/overlays/<type>/seed.json`, when present; generated
|
|
21
21
|
homepages commonly import visible copy and form structure from it.
|
|
22
22
|
4. Optional local context: `.mantle/launch-state.json`, `.mantle/handoff.md`,
|
|
@@ -108,8 +108,8 @@ the atoms cannot express the behavior.
|
|
|
108
108
|
|
|
109
109
|
## Adapter Boundary
|
|
110
110
|
|
|
111
|
-
The runtime is adapter-neutral. Required runtime ports are `DatabaseDriver
|
|
112
|
-
|
|
111
|
+
The runtime is adapter-neutral. Required runtime ports are `DatabaseDriver`
|
|
112
|
+
and `AssetServer`. Optional feature ports, such as `MediaStorage`
|
|
113
113
|
or `DeferredHookDispatcher`, are enabled only when the current adapter wires
|
|
114
114
|
them.
|
|
115
115
|
|
|
@@ -125,6 +125,15 @@ composition root. If a normal feature cannot be expressed through a
|
|
|
125
125
|
purpose-shaped surface, treat that as a Core abstraction gap instead of
|
|
126
126
|
teaching the project Mantle internals.
|
|
127
127
|
|
|
128
|
+
## Auth Composition
|
|
129
|
+
|
|
130
|
+
When a generated repo contains `src/auth.ts`, the repo owns its auth
|
|
131
|
+
composition. Preserve the explicit mode recorded in its launch state and
|
|
132
|
+
Worker config; do not infer a mode from whichever credentials happen to be
|
|
133
|
+
present or configure competing paths. Keep provider secrets out of source.
|
|
134
|
+
Follow the repo handoff for provider-specific setup rather than adding product
|
|
135
|
+
policy to Core.
|
|
136
|
+
|
|
128
137
|
## Performance Loop
|
|
129
138
|
|
|
130
139
|
After changing a Schema index, View filter/order, public API, or rendered page,
|
|
@@ -107,7 +107,7 @@ Follow the landing handoff and generated client configuration. Hosted
|
|
|
107
107
|
configuration remains in landing-managed Cloudflare Worker bindings; do not
|
|
108
108
|
write client secrets into `wrangler.toml`.
|
|
109
109
|
|
|
110
|
-
Verify that admin sign-in redirects to Mantle
|
|
110
|
+
Verify that admin sign-in redirects to Mantle Hosted Auth and Staff MCP
|
|
111
111
|
authenticates, then skip the self-hosted flow.
|
|
112
112
|
|
|
113
113
|
## Smoke Test
|