@aotter/mantle 0.0.11-alpha.73 → 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 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, KV, R2, Better Auth, MCP via `@cloudflare/workers-oauth-provider` |
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/KV/assets bindings, Auth, Admin,
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 / KV / R2) to the runtime's `domain/port/*` interfaces, so adding a new adapter is a port-implementation exercise, not a refactor.
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, cache HIT/MISS, and coding-agent guardrails)
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`
@@ -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 three runtime ports:
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 pre-rendered HTML, markdown,
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. New ADR. Replaces the architectural concerns previously tracked in POC ADR-0015 (`cms-astro` internal seam discipline).
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 to absorb ADR-0014; revised 2026-05-10 to remove obsolete pre-ADR-0014 port text).
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, KV, ASSETS, and supplies a Better Auth instance (per ADR-0014) for sign-in + MCP bearer validation.
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
- **Three required adapter ports**, defined as TypeScript interfaces in `@aotter/mantle-runtime/src/domain/port/`. Concrete adapters provide implementations and inject them into `createCmsRuntime`.
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 three required ports, then wires boot and HTTP/MCP surfaces. That's the contract. No hidden state, no implicit assumptions about the HTTP framework.
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 / KV / OAuth provider.
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, kv.get, assets.fetch, media.createUpload, …). **Rejected**: leaks the entire surface onto every adapter. Adapter authors who only want to swap KV would have to touch the mega-port impl. Discrete ports keep change blast radius per port.
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: `{ dbPrepare, kvGet, kvPut, sessionRead, … }`. **Rejected**: TypeScript interfaces are more discoverable (an adapter author IDE-jumps from `DatabaseDriver` to its surface; jumping from `dbPrepare` is harder). Interfaces also document grouping; functions don't.
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 / KV from runtime tests.
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, kv, assets, manifests, handlers, templates, siteDefaults, ... })`.
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. Future sentinels (`$now`, `$ctx.user`) follow the same `$<name>` shape; this ADR adds none of them.
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 },
@@ -72,7 +72,7 @@ function assemble(env: Env) {
72
72
  }
73
73
  ```
74
74
 
75
- Keep the conventional `DB`, `KV` and `OAUTH_KV` bindings and
75
+ Keep the conventional `DB` and `OAUTH_KV` bindings and
76
76
  `nodejs_compat`; add the Queue producer in `wrangler.jsonc`:
77
77
 
78
78
  ```jsonc
@@ -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,
@@ -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). Future grammar may upgrade it to enforced.
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. Public render hits KV cache, not D1.
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 is ~100–200 ms cold replica. Public reads should hit
725
- the SDK's KV render cache, not D1, so this rarely matters at the
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` | `HtmlPublishOrchestrator` plus the Cloudflare public-route cache policy | Reproducible derivatives live in KV. Settings updates invalidate through a runtime use case. |
17
- | D1/KV transport and optional query metrics | Cloudflare bindings | Bindings stay thin. Query/cache policy does not belong in a generic provider `BaseRepository`. |
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. KV contains only reproducible public artifacts.
28
- - A public KV hit checks the cache before loading full editable site settings.
29
- Locale policy is the small boot-seeded exception. A warm entry/page artifact
30
- therefore performs zero D1 queries.
31
- - A safe cache miss renders from canonical state and schedules KV write-back
32
- with the request execution context. It waits inline only when no execution
33
- context exists, such as a direct unit call.
34
- - Site-setting writes call the runtime settings use case, which completes
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, KV, Worker
80
- HTTP routing, View execution, and live page rendering. It compares 100 and
81
- 10,000 row fixtures, then samples page MISS and HIT separately. CI gates
82
- row-read scaling, endpoint query budgets, and zero-D1 warm hits, not absolute
83
- milliseconds.
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 KV hits read D1 first | Fixed. A 10,000-row warm page measured 0 queries / 0 rows read. |
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 KV write-back | Fixed. Reproducible artifacts write through `waitUntil`; regression coverage proves response completion does not await KV. |
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,
@@ -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
- - Pre-v0.1 alphas release directly from the merged `develop` release commit.
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 current `0.0.x-alpha` cadence, `latest` follows the current alpha
58
- while the `alpha` tag remains available.
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.11-alpha.73",
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.11-alpha.73",
59
- "@aotter/mantle-cloudflare": "0.0.11-alpha.73",
60
- "@aotter/mantle-runtime": "0.0.11-alpha.73",
61
- "@aotter/mantle-spec": "0.0.11-alpha.73"
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.0",
64
+ "@cloudflare/workers-oauth-provider": "^0.8.2",
65
65
  "aws4fetch": "^1.0.20",
66
- "better-auth": "^1.6.23",
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.0",
71
+ "@cloudflare/workers-oauth-provider": "^0.8.2",
72
72
  "@types/node": "^26",
73
73
  "aws4fetch": "^1.0.20",
74
- "better-auth": "^1.6.23",
75
- "hono": "^4.12.30",
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"
@@ -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
- `KvCache`, and `AssetServer`. Optional feature ports, such as `MediaStorage`
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