@aotter/mantle 0.1.0-alpha.8 → 0.1.2-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 +38 -38
- package/dist/cli/generate.d.ts +10 -1
- package/dist/cli/generate.d.ts.map +1 -1
- package/dist/cli/generate.js +49 -4
- package/dist/cli/generate.js.map +1 -1
- package/dist/cli/main.js +0 -8
- package/dist/cli/main.js.map +1 -1
- package/docs/adapter-guide.md +20 -24
- package/docs/adr/0008-structured-diagnostic-shape.md +7 -2
- package/docs/adr/0014-auth-better-auth-and-multi-tenant-mcp.md +100 -0
- package/docs/adr/0018-core-starters-repository-boundary.md +3 -0
- package/docs/adr/0019-sealed-manifest-runtime-pipeline.md +37 -1
- package/docs/adr/0020-builtin-handler-contracts-and-matched-upsert.md +199 -0
- package/docs/adr/0021-retire-starter-scaffolding.md +30 -0
- package/docs/adr/0022-caller-observed-version-occ.md +212 -0
- package/docs/adr/0023-port-failure-contract.md +69 -0
- package/docs/adr/README.md +18 -3
- package/docs/adr/adr-lite-803-request-diagnostics.md +38 -0
- package/docs/adr/adr-lite-808-route-readiness.md +47 -0
- package/docs/adr/adr-lite-809-bounded-public-content.md +71 -0
- package/docs/adr/adr-lite-812-native-parity.md +149 -0
- package/docs/adr/adr-lite-823-home-statistics.md +63 -0
- package/docs/adr/adr-lite-842-mcp-authoring.md +30 -0
- package/docs/adr/adr-lite-861-admin-webmcp.md +42 -0
- package/docs/adr/adr-lite-909-admin-ui-kit.md +37 -0
- package/docs/api-mcp-authorization.md +3 -664
- package/docs/auth-hosting-model.md +4 -4
- package/docs/cloudflare-low-level-composition.md +3 -104
- package/docs/deferred-lifecycle-queues.md +2 -243
- package/docs/design-atoms.md +16 -796
- package/docs/direct-authoring.md +5 -0
- package/docs/examples/minimal-worker/README.md +37 -0
- package/docs/examples/minimal-worker/manifests/site.yaml +25 -0
- package/docs/examples/minimal-worker/package.json +26 -0
- package/docs/examples/minimal-worker/smoke.mjs +23 -0
- package/docs/examples/minimal-worker/src/index.ts +4 -0
- package/docs/examples/minimal-worker/tsconfig.json +17 -0
- package/docs/examples/minimal-worker/wrangler.jsonc +22 -0
- package/docs/handbook/cloudflare/authentication.md +167 -0
- package/docs/handbook/cloudflare/bindings.md +233 -0
- package/docs/handbook/cloudflare/conventional-worker.md +162 -0
- package/docs/handbook/cloudflare/deferred-hooks-queues.md +171 -0
- package/docs/handbook/cloudflare/deploy-and-operate.md +98 -0
- package/docs/handbook/cloudflare/low-level-composition.md +169 -0
- package/docs/handbook/cloudflare/media-r2.md +216 -0
- package/docs/handbook/cloudflare/public-web.md +144 -0
- package/docs/handbook/concepts/authorization.md +116 -0
- package/docs/handbook/concepts/four-atoms.md +173 -0
- package/docs/handbook/concepts/lifecycle-and-locales.md +142 -0
- package/docs/handbook/concepts/mcp-and-agents.md +109 -0
- package/docs/handbook/concepts/procedures-and-triggers.md +221 -0
- package/docs/handbook/concepts/runtime-and-adapters.md +177 -0
- package/docs/handbook/concepts/views.md +183 -0
- package/docs/handbook/examples/commerce-transaction.md +813 -0
- package/docs/handbook/examples/guarded-api.md +427 -0
- package/docs/handbook/examples/intake-form.md +319 -0
- package/docs/handbook/examples/legal-documents.md +218 -0
- package/docs/handbook/examples/procurement-approvals.md +240 -0
- package/docs/handbook/examples/publication.md +240 -0
- package/docs/handbook/examples/reservation.md +220 -0
- package/docs/handbook/navigation.json +63 -0
- package/docs/handbook/reference/authorization.md +295 -0
- package/docs/handbook/reference/diagnostics.md +183 -0
- package/docs/handbook/reference/manifest.md +132 -0
- package/docs/handbook/reference/procedure.md +263 -0
- package/docs/handbook/reference/schema.md +247 -0
- package/docs/handbook/reference/site-config.md +233 -0
- package/docs/handbook/reference/surface.md +207 -0
- package/docs/handbook/reference/trigger.md +193 -0
- package/docs/handbook/reference/view.md +256 -0
- package/docs/handbook/start/project-and-cli.md +122 -0
- package/docs/handbook/start/quickstart-worker.md +193 -0
- package/docs/labels.md +3 -1
- package/docs/media-uploads.md +3 -184
- package/docs/migration-0.1.2.md +34 -1
- package/docs/performance-harness.md +125 -8
- package/docs/release-process.md +85 -231
- package/docs/schema-indexes.md +3 -180
- package/docs/sealed-pipeline-ownership.md +5 -4
- package/docs/spec-only-host-adoption.md +158 -0
- package/docs/transaction-patterns.md +5 -0
- package/package.json +21 -25
- package/skills/README.md +4 -4
- package/skills/develop/SKILL.md +23 -28
- package/skills/install/SKILL.md +55 -147
- package/skills/provision/SKILL.md +4 -3
- package/skills/theme/SKILL.md +6 -6
- package/skills/update/SKILL.md +32 -68
- package/dist/cli/create.d.ts +0 -2
- package/dist/cli/create.d.ts.map +0 -1
- package/dist/cli/create.js +0 -243
- package/dist/cli/create.js.map +0 -1
- package/dist/cli/update.d.ts +0 -2
- package/dist/cli/update.d.ts.map +0 -1
- package/dist/cli/update.js +0 -413
- package/dist/cli/update.js.map +0 -1
- package/dist/provision/renderProvisionBundle.d.ts +0 -70
- package/dist/provision/renderProvisionBundle.d.ts.map +0 -1
- package/dist/provision/renderProvisionBundle.js +0 -367
- package/dist/provision/renderProvisionBundle.js.map +0 -1
- package/dist/provision.d.ts +0 -2
- package/dist/provision.d.ts.map +0 -1
- package/dist/provision.js +0 -2
- package/dist/provision.js.map +0 -1
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# ADR-0020: Builtin Handler Contracts and Matched Upsert
|
|
2
|
+
|
|
3
|
+
**Status:** Accepted + amended by [ADR-0022](0022-caller-observed-version-occ.md)
|
|
4
|
+
|
|
5
|
+
**Date:** 2026-09-01
|
|
6
|
+
|
|
7
|
+
**Related:** [#765](https://github.com/aotter/mantle/issues/765),
|
|
8
|
+
[#766](https://github.com/aotter/mantle/issues/766),
|
|
9
|
+
[#850](https://github.com/aotter/mantle/issues/850),
|
|
10
|
+
ADR-0008, ADR-0010, ADR-0014, ADR-0019, ADR-0022
|
|
11
|
+
|
|
12
|
+
> **Amendment (ADR-0022):** matched-upsert OCC no longer forbids caller
|
|
13
|
+
> `expectedVersion` or substitutes `preloaded.version`. Keep this ADR for
|
|
14
|
+
> builtin static contracts, `handler.match` / uniqueIndexes, `CONFLICT`
|
|
15
|
+
> without auto-retry, and natural-key lookup without system `id`. Read
|
|
16
|
+
> ADR-0022 for caller-observed version on the update branch, create vs
|
|
17
|
+
> update, reserved wire names, and first-party Admin bind.
|
|
18
|
+
|
|
19
|
+
## Context
|
|
20
|
+
|
|
21
|
+
Mantle Procedures can bind to standard entry-mutation operations using `handler.kind: builtin` (e.g. `create`, `update`, `delete`, `archive`, `upsert`).
|
|
22
|
+
|
|
23
|
+
Before this ADR:
|
|
24
|
+
1. **No static contract validation**: Procedure input JSON Schema contracts were not statically verified against the requirements of their builtin operations during manifest linking (`ValidateManifestsUseCase`). Invalid contracts (e.g. `update` missing `id` or `expectedVersion`, nullable union types like `type: ["string", "null"]`, or `archive` on operational Schemas) were only caught at runtime via `requireField` or runtime assertions, causing valid-looking manifests to fail during invocation.
|
|
25
|
+
2. **Missing natural key upsert (`handler.match`)**: Built-in `upsert` only supported legacy ID-based lookup (`id` + `expectedVersion`), requiring client knowledge of internal system IDs. Real-world domains require idempotent upserts matching natural keys (such as `slug`, `siteKey`, or composite unique keys like `[slug, locale]`).
|
|
26
|
+
3. **Concurrency and race conditions**: Matched upsert must handle concurrent writers safely. When two concurrent writers miss preflight lookup, storage-level unique constraints must catch the conflict and surface structured `CONFLICT` diagnostics without retrying or corrupting entries.
|
|
27
|
+
|
|
28
|
+
## Decision
|
|
29
|
+
|
|
30
|
+
### 1. Static Contract Validation for Builtin Handlers
|
|
31
|
+
|
|
32
|
+
Manifest linking (`ManifestGraphValidator.ts`) now validates Procedure `input` schemas against the requirements of their builtin `op`:
|
|
33
|
+
|
|
34
|
+
- **Diagnostic Code**: `BUILTIN_HANDLER_CONTRACT_INVALID` (phase: `validate`).
|
|
35
|
+
- **Common rule**: `input` must be an `object` schema (`type: "object"`).
|
|
36
|
+
- **`update`**:
|
|
37
|
+
- `properties.id` must be strict, non-nullable `string` (`type: "string"`, not union array, not `nullable: true`).
|
|
38
|
+
- `properties.expectedVersion` must be strict, non-nullable `number` (`type: "number"`, not union array, not `nullable: true`).
|
|
39
|
+
- `required` must include both `"id"` and `"expectedVersion"`.
|
|
40
|
+
- **`delete`**:
|
|
41
|
+
- `properties.id` must be strict, non-nullable `string`.
|
|
42
|
+
- `required` must include `"id"`.
|
|
43
|
+
- **`archive`**:
|
|
44
|
+
- Target Schema must have `lifecycle: "publishing"` (operational Schemas have no lifecycle transitions and cannot be archived).
|
|
45
|
+
- `properties.id` must be strict, non-nullable `string`.
|
|
46
|
+
- `required` must include `"id"`.
|
|
47
|
+
- **`upsert` (legacy mode without `match`)** (ADR-0022):
|
|
48
|
+
- `properties.expectedVersion` must be declared as strict `number`.
|
|
49
|
+
- If `id` is declared it must be strict `string`. Neither is in `required`
|
|
50
|
+
because the create branch accepts new entries without IDs or versions.
|
|
51
|
+
|
|
52
|
+
### 2. Matched Upsert Grammar and Static Contract (`handler.match`)
|
|
53
|
+
|
|
54
|
+
Procedures with `op: "upsert"` may declare `match`:
|
|
55
|
+
|
|
56
|
+
```yaml
|
|
57
|
+
apiVersion: cms.mantle.aotter.net/v1
|
|
58
|
+
kind: Procedure
|
|
59
|
+
metadata:
|
|
60
|
+
name: upsertArticleBySlug
|
|
61
|
+
spec:
|
|
62
|
+
input:
|
|
63
|
+
type: object
|
|
64
|
+
properties:
|
|
65
|
+
slug:
|
|
66
|
+
type: string
|
|
67
|
+
title:
|
|
68
|
+
type: string
|
|
69
|
+
body:
|
|
70
|
+
type: string
|
|
71
|
+
expectedVersion:
|
|
72
|
+
type: number
|
|
73
|
+
required:
|
|
74
|
+
- slug
|
|
75
|
+
- title
|
|
76
|
+
output:
|
|
77
|
+
type: object
|
|
78
|
+
handler:
|
|
79
|
+
kind: builtin
|
|
80
|
+
op: upsert
|
|
81
|
+
schema: articles
|
|
82
|
+
match:
|
|
83
|
+
- slug
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
**Grammar and parser validation rules (`ManifestParser.ts`)**:
|
|
87
|
+
- `match` is allowed in `spec.handler` only when `handler.kind === "builtin"` and `op === "upsert"`.
|
|
88
|
+
- `match` must be a non-empty array of unique, non-empty field names.
|
|
89
|
+
|
|
90
|
+
**Static manifest validation rules (`ManifestGraphValidator.ts`)**:
|
|
91
|
+
- `handler.match` must exactly match one declared unique index in the target Schema's `spec.uniqueIndexes` (matching length, field names, and order).
|
|
92
|
+
- Every field in `handler.match` must be declared in the target Schema's `spec.schema.properties`.
|
|
93
|
+
- Every field in `handler.match` must be declared in Procedure `input.properties` and listed in `input.required`.
|
|
94
|
+
- Procedure `input` must NOT declare `id` when using `match`.
|
|
95
|
+
- Procedure `input` **must** declare `expectedVersion` as strict `number`
|
|
96
|
+
(ADR-0022). It is not globally required: create has no version; update
|
|
97
|
+
requires the caller token at runtime. Do not overwrite that token with
|
|
98
|
+
`preloaded.version`.
|
|
99
|
+
|
|
100
|
+
### 3. Runtime Semantics and Atomic Conflict Handling (`InvokeBuiltinUseCase.ts`)
|
|
101
|
+
|
|
102
|
+
When executing matched upsert:
|
|
103
|
+
1. Extract matching field values from `input`.
|
|
104
|
+
2. Query existing entry using `entries.findByDataFields({ collection, fields })`.
|
|
105
|
+
3. **If found**: Execute update path (`opUpdate`):
|
|
106
|
+
- Merge input into existing data using `projectUpdateAndStamp`, preserving omitted fields and system bindings.
|
|
107
|
+
- Require the caller's `expectedVersion` and pass **that** token to the
|
|
108
|
+
atomic repository OCC check. Do not use `existing.version` as the token
|
|
109
|
+
(ADR-0022). Missing token → `INPUT_VALIDATION_FAILED` (do not overwrite).
|
|
110
|
+
- Pass caller's original input to lifecycle hooks (`before_update` / `after_update`).
|
|
111
|
+
4. **If not found**: Execute create path (`opCreate`) only when the caller
|
|
112
|
+
omitted `expectedVersion`. A versioned write for a missing row is
|
|
113
|
+
`NOT_FOUND` (do not recreate).
|
|
114
|
+
- Project and stamp data using `projectAndStamp`.
|
|
115
|
+
- Set status to `"published"` (if `lifecycle: "operational"`) or `"draft"` (if `lifecycle: "publishing"`).
|
|
116
|
+
- Pass caller's original input to lifecycle hooks (`before_create` / `after_create`).
|
|
117
|
+
5. **Concurrency & Race Conditions**:
|
|
118
|
+
- Both update and create operations are wrapped in `withConflictDiagnostic`.
|
|
119
|
+
- Storage-level unique constraint violations (e.g. SQLite `SQLITE_CONSTRAINT_UNIQUE`, Postgres `23505`) and version/status mismatches are converted into structured `CONFLICT` diagnostics (HTTP 409).
|
|
120
|
+
- The runtime does not automatically retry matched upsert on conflict, providing deterministic failure semantics under race conditions.
|
|
121
|
+
|
|
122
|
+
## Consequences
|
|
123
|
+
|
|
124
|
+
- **Fail-fast authoring**: Schema and Procedure mismatches are detected during `mantle validate` or test time rather than failing unpredictably at runtime.
|
|
125
|
+
- **Strict type safety**: Nullable union types cannot bypass static contract validation.
|
|
126
|
+
- **Natural key idempotency**: Clients can author natural-key upsert procedures for ingestion and sync pipelines without managing internal mantle IDs.
|
|
127
|
+
- **Safe concurrency across all adapters**: Concurrent writes are guarded by storage-level unique constraints (SQLite/D1 unique indexes, IndexedDB readwrite transaction assertions) and translated to standard `CONFLICT` diagnostics via typed `EntryUniqueConflict`.
|
|
128
|
+
|
|
129
|
+
## Alternatives considered
|
|
130
|
+
|
|
131
|
+
1. **Automatic retry loops on conflict**: Considered having `InvokeBuiltinUseCase` automatically re-query and retry upon catching unique collisions. Rejected because repeated attempts could fire lifecycle hooks (`before_create` / `before_update`) multiple times with unintended side effects (e.g. duplicate webhook notifications or rate limits) and mask true contention. Failing fast with a structured `CONFLICT` diagnostic gives caller control.
|
|
132
|
+
2. **Arbitrary field matching without Schema unique indexes**: Considered allowing `handler.match` on arbitrary Schema properties. Rejected because storage engines cannot enforce uniqueness without dedicated unique constraints/indexes, which would result in race conditions and duplicate entries under concurrent traffic.
|
|
133
|
+
3. **Permissive nullable unions**: Considered allowing `type: ["string", "null"]` in Procedure input schemas for `id`. Rejected because runtime entry mutations strictly require non-null scalar identifiers; allowing nullable schemas would let schema-valid requests crash inside builtin handlers.
|
|
134
|
+
|
|
135
|
+
## How to apply
|
|
136
|
+
|
|
137
|
+
1. **Declare Schema Unique Indexes**:
|
|
138
|
+
```yaml
|
|
139
|
+
apiVersion: cms.mantle.aotter.net/v1
|
|
140
|
+
kind: Schema
|
|
141
|
+
metadata:
|
|
142
|
+
name: site-settings
|
|
143
|
+
spec:
|
|
144
|
+
title: Site Settings
|
|
145
|
+
lifecycle: publishing
|
|
146
|
+
schema:
|
|
147
|
+
type: object
|
|
148
|
+
required: [siteKey, theme]
|
|
149
|
+
properties:
|
|
150
|
+
siteKey: { type: string }
|
|
151
|
+
theme: { type: string }
|
|
152
|
+
uniqueIndexes:
|
|
153
|
+
- [siteKey]
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
2. **Author a Matched Upsert Procedure**:
|
|
157
|
+
```yaml
|
|
158
|
+
apiVersion: cms.mantle.aotter.net/v1
|
|
159
|
+
kind: Procedure
|
|
160
|
+
metadata:
|
|
161
|
+
name: setSiteSetting
|
|
162
|
+
spec:
|
|
163
|
+
input:
|
|
164
|
+
type: object
|
|
165
|
+
required: [siteKey, theme]
|
|
166
|
+
properties:
|
|
167
|
+
siteKey: { type: string }
|
|
168
|
+
theme: { type: string }
|
|
169
|
+
expectedVersion: { type: number }
|
|
170
|
+
output:
|
|
171
|
+
type: object
|
|
172
|
+
handler:
|
|
173
|
+
kind: builtin
|
|
174
|
+
op: upsert
|
|
175
|
+
schema: site-settings
|
|
176
|
+
match: [siteKey]
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
3. **Validate**:
|
|
180
|
+
Run `mantle validate` to verify input contracts against the declared unique indexes and schema properties.
|
|
181
|
+
|
|
182
|
+
## Implementation status
|
|
183
|
+
|
|
184
|
+
- **`@aotter/mantle-spec`**:
|
|
185
|
+
- `ManifestGrammar.ts`: Added `match?: readonly string[]` to `HandlerBuiltinBinding`.
|
|
186
|
+
- `diagnostic.ts`: Added `BUILTIN_HANDLER_CONTRACT_INVALID` to `DIAGNOSTIC_CODES`.
|
|
187
|
+
- `ManifestParser.ts`: Parser-level validation for `handler.match`.
|
|
188
|
+
- `ManifestGraphValidator.ts`: Static input contract validation in `checkBuiltinHandler`.
|
|
189
|
+
- **`@aotter/mantle-runtime`**:
|
|
190
|
+
- `EntryRow.ts`: Added `EntryUniqueConflict` domain error.
|
|
191
|
+
- `EntryMutationDiagnostics.ts`: `withConflictDiagnostic` converts `EntryUniqueConflict` to `CONFLICT` diagnostic (HTTP 409).
|
|
192
|
+
- `DatabaseEntryRepository.ts`: SQLite/Postgres unique constraint violation translation to `EntryUniqueConflict`.
|
|
193
|
+
- `InvokeBuiltinUseCase.ts`: Matched upsert execution via `findByDataFields` and `projectUpdateAndStamp`.
|
|
194
|
+
- **`@aotter/mantle-indexeddb`**:
|
|
195
|
+
- `IndexedDbEntryRepository.ts`: Atomic transaction-level unique index enforcement and `EntryUniqueConflict` emission.
|
|
196
|
+
- **Documentation**:
|
|
197
|
+
- `docs/adr/0020-builtin-handler-contracts-and-matched-upsert.md` (this document).
|
|
198
|
+
- `docs/adr/README.md`.
|
|
199
|
+
- `docs/design-atoms.md`.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# ADR-0021: Author applications directly; retire Starter scaffolding
|
|
2
|
+
|
|
3
|
+
Status: Accepted by owner, 2026-09-08 (#786). Supersedes ADR-0018 for the 0.1.2 line.
|
|
4
|
+
|
|
5
|
+
Core is an embeddable manifest engine. Applications own package manifests,
|
|
6
|
+
Worker entries, provider configuration, business manifests and visitor UI.
|
|
7
|
+
Core no longer creates those files from a Starter bundle. Remove `create` and
|
|
8
|
+
the Starter three-way `update` CLI, their bundle renderer/export, and the
|
|
9
|
+
cross-repository Starter/Landing release gates. Legacy consumers retain the
|
|
10
|
+
immutable 0.1.0-alpha.17 packages, tags and URLs.
|
|
11
|
+
|
|
12
|
+
`generate` remains an explicit compiler for existing manifests and optional
|
|
13
|
+
installed Admin UI assets. It must not initialize a missing project or invent
|
|
14
|
+
a default Schema, frontend or home route. Web remains optional runtime
|
|
15
|
+
composition, with consumer-provided templates and routes.
|
|
16
|
+
|
|
17
|
+
Version-matched install/update skills describe direct authoring and reviewed
|
|
18
|
+
SDK dependency upgrades. Existing project source, provider identity, secrets
|
|
19
|
+
and legacy metadata remain user-owned. No replacement init/template command,
|
|
20
|
+
new preset registry or Builder/Platform implementation belongs to this change.
|
|
21
|
+
|
|
22
|
+
A small Core-owned reference consumer proves generation and a running Worker
|
|
23
|
+
against exact packed packages and public registry candidates. It is a test and
|
|
24
|
+
worked example, not a downloadable scaffolder or another launch product.
|
|
25
|
+
Useful transaction Queue/DO patterns remain documented with immutable sources.
|
|
26
|
+
|
|
27
|
+
Release ownership stays in the existing Core controller: source and packed
|
|
28
|
+
consumer gates → immutable tag → verified registry candidates → public-registry
|
|
29
|
+
consumer gate → monotonic channels → GitHub release. No downstream repo writer
|
|
30
|
+
is required. Wrong public artifacts still require fixing forward; no retagging.
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# ADR-0022: Caller-observed `expectedVersion` for Admin and builtin upsert
|
|
2
|
+
|
|
3
|
+
**Status:** Accepted
|
|
4
|
+
|
|
5
|
+
**Date:** 2026-09-14
|
|
6
|
+
|
|
7
|
+
**Related:** [#850](https://github.com/aotter/mantle/issues/850),
|
|
8
|
+
[ADR-0020](0020-builtin-handler-contracts-and-matched-upsert.md)
|
|
9
|
+
|
|
10
|
+
## Context
|
|
11
|
+
|
|
12
|
+
ADR-0020 remains the authority for builtin static contracts, `handler.match`
|
|
13
|
+
matching a declared unique index, natural-key lookup without a system `id`,
|
|
14
|
+
and `CONFLICT` without automatic retry.
|
|
15
|
+
|
|
16
|
+
Its **matched-upsert OCC** rule is outdated. Matched upsert forbade
|
|
17
|
+
`expectedVersion` on the Procedure input and, at runtime, substituted the
|
|
18
|
+
preloaded row's `version` for the caller's token. ID-based upsert did the same
|
|
19
|
+
whenever a preloaded row was supplied. The repository still rejected races
|
|
20
|
+
*after* the handler read, but a change made since the **client** read the row
|
|
21
|
+
was overwritten.
|
|
22
|
+
|
|
23
|
+
Admin rendered `expectedVersion` as an editable number. Operators had to type
|
|
24
|
+
a technical version, or a handler that re-read at submit time quietly dropped
|
|
25
|
+
read-time protection. Downstream (mantle-home organization quota) reproduced
|
|
26
|
+
this.
|
|
27
|
+
|
|
28
|
+
The intended contract: callers POST the **observed** native `entry.version`
|
|
29
|
+
from read time. Mantle enforces that token atomically. A successful write
|
|
30
|
+
still bumps storage to `expectedVersion + 1`. The token is **not**
|
|
31
|
+
`version + 1`.
|
|
32
|
+
|
|
33
|
+
## Decision
|
|
34
|
+
|
|
35
|
+
### 1. Wire semantics
|
|
36
|
+
|
|
37
|
+
`expectedVersion` is the observed native `entry.version` at read time.
|
|
38
|
+
|
|
39
|
+
- Successful repository writes still persist `expectedVersion + 1`.
|
|
40
|
+
- Callers must not send the next version.
|
|
41
|
+
- Core does **not** auto-inject `expectedVersion` into Manifests. Authors
|
|
42
|
+
declare it. Omit → fail-closed `BUILTIN_HANDLER_CONTRACT_INVALID`.
|
|
43
|
+
|
|
44
|
+
### 2. Closed reserved wire names
|
|
45
|
+
|
|
46
|
+
The reserved Procedure input wire name for OCC is `expectedVersion`. Business
|
|
47
|
+
fields must not collide with it. Schema `spec.schema.properties` must not
|
|
48
|
+
declare `expectedVersion`; validate fails closed (`INVALID_MANIFEST_ENVELOPE`)
|
|
49
|
+
so `projectUpdateAndStamp` cannot copy the OCC token into `data`. New reserved
|
|
50
|
+
wire names require an ADR. OCC behaviour must not depend on `x-mcp-hint`. The
|
|
51
|
+
magic word is the property name only.
|
|
52
|
+
|
|
53
|
+
### 3. Static contracts (amends ADR-0020 §2 matched upsert)
|
|
54
|
+
|
|
55
|
+
Keep ADR-0020 for `update` (`id` + `expectedVersion` both strict and
|
|
56
|
+
required), `delete`/`archive` (`id` required), `handler.match` vs
|
|
57
|
+
`uniqueIndexes`, and forbidding `id` on matched upsert.
|
|
58
|
+
|
|
59
|
+
**Matched upsert (`op: upsert` + `match`):**
|
|
60
|
+
|
|
61
|
+
- `input.properties.expectedVersion` **must** be a strict, non-nullable
|
|
62
|
+
`number`.
|
|
63
|
+
- `expectedVersion` is **not** globally required: the create branch has no
|
|
64
|
+
version. Authors may list it in `required` only when the Procedure is
|
|
65
|
+
update-only.
|
|
66
|
+
- `input` must still **not** declare `id`.
|
|
67
|
+
|
|
68
|
+
**ID-based upsert (`op: upsert` without `match`):**
|
|
69
|
+
|
|
70
|
+
- `input.properties.expectedVersion` **must** be a strict `number` (fail-closed
|
|
71
|
+
if omitted). It is not in `required`.
|
|
72
|
+
- If `id` is declared, it must be a strict `string`. Neither `id` nor
|
|
73
|
+
`expectedVersion` is required, so create can omit both.
|
|
74
|
+
|
|
75
|
+
### 4. Runtime (amends ADR-0020 §3)
|
|
76
|
+
|
|
77
|
+
Intent is the presence of a finite numeric `expectedVersion`, not the
|
|
78
|
+
presence of an id or a preload hit.
|
|
79
|
+
|
|
80
|
+
| Branch | Caller token | Existing row | Result |
|
|
81
|
+
|---|---|---|---|
|
|
82
|
+
| Create | absent | none | `create` (no version) |
|
|
83
|
+
| Create race | absent | unique collision after miss | `CONFLICT`; no silent overwrite |
|
|
84
|
+
| Update | present | found | atomic OCC with **caller** token; never `preloaded.version` |
|
|
85
|
+
| Update, stale | present | found, version ≠ token | `CONFLICT`; no retry |
|
|
86
|
+
| Update, missing | present | none (deleted / unknown) | `NOT_FOUND`; do **not** create |
|
|
87
|
+
| Create-intent vs existing | absent | found | `INPUT_VALIDATION_FAILED`; do **not** overwrite |
|
|
88
|
+
|
|
89
|
+
`opUpdate` always reads `expectedVersion` from caller input when a versioned
|
|
90
|
+
write is attempted. Preload supplies identity (`id`) and PATCH base data only.
|
|
91
|
+
|
|
92
|
+
There is still **no automatic retry**.
|
|
93
|
+
|
|
94
|
+
### 5. First-party Admin / SDK bind (not grammar)
|
|
95
|
+
|
|
96
|
+
When Procedure `input` declares `expectedVersion`, first-party Admin (and any
|
|
97
|
+
first-party SDK helper) treats the name as magic:
|
|
98
|
+
|
|
99
|
+
- Auto-bind the OCC target row's current `version` captured at read.
|
|
100
|
+
- Hide the field from the editable form (same UX idea as
|
|
101
|
+
`x-mcp-hint: idempotency-key`, keyed by reserved **name**).
|
|
102
|
+
- A resolvable OCC target (row menu, `id` in the form, or
|
|
103
|
+
`targetCollection` binding) **must** be bound before submit is enabled,
|
|
104
|
+
including matched upsert where `expectedVersion` is declared but not
|
|
105
|
+
globally required (opening from a row is update-path intent).
|
|
106
|
+
- If `expectedVersion` is in `input.required` and no OCC target can be
|
|
107
|
+
resolved, submit stays disabled (do not invent a version).
|
|
108
|
+
- Collection create / no-row dialogs may omit the token when it is not
|
|
109
|
+
required (create branch).
|
|
110
|
+
- Rebind when the selected target changes. An organization row must not
|
|
111
|
+
supply `expectedVersion` for a membership mutation.
|
|
112
|
+
- On `CONFLICT` / HTTP 409: keep operator business inputs; require an
|
|
113
|
+
explicit re-read; **no** auto-retry with the latest version.
|
|
114
|
+
- Background refetches of the same target must not replace a captured
|
|
115
|
+
token while the form still holds older values.
|
|
116
|
+
|
|
117
|
+
Other callers (HTTP Trigger, MCP, custom clients) supply the observed
|
|
118
|
+
version themselves. Magic bind/hide is first-party only.
|
|
119
|
+
|
|
120
|
+
OCC target for Admin bind:
|
|
121
|
+
|
|
122
|
+
1. If `input` declares `id`, the target is `form.id` (the membership / row
|
|
123
|
+
being mutated), never a merely contextual parent ref.
|
|
124
|
+
2. Else if the Procedure is builtin, prefer the `rowBindings` entry whose
|
|
125
|
+
`collection` equals `handler.schema`.
|
|
126
|
+
3. Else if another row binding (not the launch collection) identifies an
|
|
127
|
+
entry id, use that once filled.
|
|
128
|
+
4. Else use the launch row (quota-style actions bound only to that row).
|
|
129
|
+
|
|
130
|
+
A not-yet-existing target has no version: omit `expectedVersion` so upsert
|
|
131
|
+
takes the create branch.
|
|
132
|
+
|
|
133
|
+
### 6. Compatibility
|
|
134
|
+
|
|
135
|
+
This is a **breaking** change for:
|
|
136
|
+
|
|
137
|
+
- Matched-upsert Manifests that omitted `expectedVersion` (now fail
|
|
138
|
+
validate).
|
|
139
|
+
- Callers that updated via matched or ID-based upsert without sending an
|
|
140
|
+
observed version (preload OCC). Those writes now fail closed instead of
|
|
141
|
+
overwriting with the version read inside the handler.
|
|
142
|
+
- Matched-upsert Manifests that declared `expectedVersion` (previously
|
|
143
|
+
rejected; now required as a property).
|
|
144
|
+
|
|
145
|
+
Callers that already sent the observed version on `op: update` are
|
|
146
|
+
unchanged. HTTP and MCP share the Procedure input contract; only Admin/SDK
|
|
147
|
+
auto-fill the reserved name.
|
|
148
|
+
|
|
149
|
+
## Consequences
|
|
150
|
+
|
|
151
|
+
- Read-time OCC works through Admin forms, HTTP Triggers, and MCP for both
|
|
152
|
+
`update` and version-checked upsert.
|
|
153
|
+
- Create remains possible without a version; a versioned write cannot
|
|
154
|
+
silently recreate a deleted row.
|
|
155
|
+
- AI authors must declare `expectedVersion` as a strict number on OCC
|
|
156
|
+
builtins. Tool descriptions state **observed** version, not `+1`.
|
|
157
|
+
|
|
158
|
+
## Alternatives considered
|
|
159
|
+
|
|
160
|
+
1. **`x-mcp-hint: entry-version`.** Rejected. OCC must not depend on hints;
|
|
161
|
+
the reserved property name is the contract.
|
|
162
|
+
2. **Keep preload OCC for matched upsert.** Rejected. Accepting
|
|
163
|
+
`expectedVersion` and ignoring it is worse than forbidding it.
|
|
164
|
+
3. **Require `expectedVersion` in `input.required` for every upsert.**
|
|
165
|
+
Rejected: that makes create impossible at validate time.
|
|
166
|
+
4. **Auto-retry upsert on `CONFLICT`.** Rejected in ADR-0020; unchanged.
|
|
167
|
+
5. **Auto-inject the property into Manifests.** Rejected: fail-closed
|
|
168
|
+
grammar; authors (often agents) must see the field.
|
|
169
|
+
|
|
170
|
+
## How to apply
|
|
171
|
+
|
|
172
|
+
```yaml
|
|
173
|
+
apiVersion: cms.mantle.aotter.net/v1
|
|
174
|
+
kind: Procedure
|
|
175
|
+
metadata:
|
|
176
|
+
name: upsert-membership
|
|
177
|
+
spec:
|
|
178
|
+
input:
|
|
179
|
+
type: object
|
|
180
|
+
required: [organizationId, userId, role]
|
|
181
|
+
properties:
|
|
182
|
+
organizationId: { type: string, x-mantle-ref: organizations }
|
|
183
|
+
userId: { type: string }
|
|
184
|
+
role: { type: string }
|
|
185
|
+
expectedVersion: { type: number }
|
|
186
|
+
output:
|
|
187
|
+
type: object
|
|
188
|
+
handler:
|
|
189
|
+
kind: builtin
|
|
190
|
+
op: upsert
|
|
191
|
+
schema: organization-members
|
|
192
|
+
match: [organizationId, userId]
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
- Create: `{ organizationId, userId, role }` — no version.
|
|
196
|
+
- Update: the same fields plus `expectedVersion` equal to the membership
|
|
197
|
+
row's current `version`.
|
|
198
|
+
- Stale token → `CONFLICT` (409). Deleted membership + token → `NOT_FOUND`.
|
|
199
|
+
|
|
200
|
+
## Implementation status
|
|
201
|
+
|
|
202
|
+
- `@aotter/mantle-spec`: `ManifestGraphValidator` / parser fail-closed
|
|
203
|
+
`expectedVersion` on upsert; Schema `spec.schema.properties` must not
|
|
204
|
+
declare reserved Procedure input names; `EXPECTED_VERSION_PROPERTY`
|
|
205
|
+
reserved name.
|
|
206
|
+
- `@aotter/mantle-runtime`: `InvokeBuiltinUseCase` uses the caller token;
|
|
207
|
+
MCP catalog copy states observed version.
|
|
208
|
+
- `@aotter/mantle-admin` / `@aotter/mantle-admin-ui`: operations expose
|
|
209
|
+
builtin `targetCollection`; Admin binds and hides `expectedVersion`,
|
|
210
|
+
and does not enable submit on a resolvable OCC target until the
|
|
211
|
+
observed version is captured.
|
|
212
|
+
- Docs: this ADR, ADR-0020 amendment pointer, design-atoms, handbook.
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# ADR-0023: Storage and service failure contracts
|
|
2
|
+
|
|
3
|
+
Status: Accepted for alpha.17 under #857 (amends ADR-0008 and media deletion behavior).
|
|
4
|
+
|
|
5
|
+
This is the diagnostic grammar-revise record: add RESOURCE_EXHAUSTED (507),
|
|
6
|
+
RESOURCE_UNAVAILABLE (503), RATE_LIMITED (429), OUTCOME_UNKNOWN (503),
|
|
7
|
+
PARTIAL_FAILURE (503), PRECONDITION_FAILED (412), and optional safe `failure`
|
|
8
|
+
facts to Diagnostic. No Manifest keys, new ports, or parallel wire format.
|
|
9
|
+
Existing codes and envelopes remain compatible. DiagnosticError accepts internal
|
|
10
|
+
ErrorOptions.cause; causes never enter Diagnostic. Unexpected exceptions must not
|
|
11
|
+
be copied into public messages. Core owns no tenant, plan or provider quota policy.
|
|
12
|
+
|
|
13
|
+
## Contract
|
|
14
|
+
|
|
15
|
+
A port rejects with DiagnosticError for recognized failures. The diagnostic
|
|
16
|
+
contains stable code, safe message and optional failure facts: outcome is
|
|
17
|
+
not-applied/partial/unknown; retry is never/after-change/safe/reconcile; resource
|
|
18
|
+
is an optional non-sensitive logical resource name, not credentials or SQL.
|
|
19
|
+
A provider adapter or host policy owns classification. Unknown errors stay
|
|
20
|
+
unexpected; never guess quota from arbitrary exception prose. Existing specific
|
|
21
|
+
validation, not-found and atomic OCC diagnostics remain authoritative.
|
|
22
|
+
|
|
23
|
+
`safe` requires idempotence of the same operation against the same resource.
|
|
24
|
+
It does not request automatic retry. Timeouts after write/send default to unknown
|
|
25
|
+
outcome and reconciliation, not unconditional retry. Do not downgrade unknown
|
|
26
|
+
outcome to not-applied merely because no reply arrived. Not-found on an
|
|
27
|
+
idempotent object delete is success. Read absence may be null per the port.
|
|
28
|
+
|
|
29
|
+
## Operation matrix (public contracts)
|
|
30
|
+
|
|
31
|
+
| Contract | Success | Failure / effect / retry |
|
|
32
|
+
| --- | --- | --- |
|
|
33
|
+
| EntryReader / ViewQueryExecutor | documented rows/null/page | denied/unavailable/rate limit; reads have no business mutation; explicit retry after transient failure |
|
|
34
|
+
| EntryRepository | semantic mutation with atomic expectedVersion | existing NOT_FOUND/CONFLICT; host capacity/denial; unknown write requires readback by stable id/version, never latest-version substitution |
|
|
35
|
+
| SiteConfigRepository | validated settings/locale | reads as above; writes same conditional and uncertain-effect rules as entries |
|
|
36
|
+
| MediaAssetRepository / PendingUploadRepository | metadata persisted/deleted | capacity/availability/unknown write; reconcile by asset/upload id; removal of missing metadata is idempotent where specified |
|
|
37
|
+
| MantleStorageAdapter.prepare / migrations | ready prepared storage | invalid plan/readiness; completed migration steps may persist; journal/readback before retry, no cross-provider rollback promise |
|
|
38
|
+
| DatabaseDriver | driver rows/batch result | constraint/precondition or recognized provider failure; transaction guarantees remain driver-specific; ambiguous writes require readback, SQL is never public |
|
|
39
|
+
| MediaStorage.createUpload | authorized capabilities, not uploaded bytes | policy/size/denied/unavailable; adapter must state any reservations created; no general single-use guarantee |
|
|
40
|
+
| MediaStorage.commitUpload | verified asset, then use case stores metadata | missing/type/size/precondition or partial/unknown provider effect; reconcile same upload group and objects before retry |
|
|
41
|
+
| MediaStorage.deleteObject | object absent | idempotent same-key retry; unavailable/unknown; use case retains metadata if any variant fails and returns PARTIAL_FAILURE |
|
|
42
|
+
| EmailSender | provider accepted, not guaranteed recipient delivery | known rejection not-applied; lost acknowledgement unknown/reconcile; do not resend automatically without provider idempotency/readback |
|
|
43
|
+
| DeferredHookDispatcher | accepted for at-least-once delivery | ambiguous enqueue may duplicate; existing fallback preserves eventId + trigger idempotency identity; consumers must deduplicate |
|
|
44
|
+
| HandlerRegistry / custom handler | application output | DiagnosticError/InvokeFailure preserved; unknown exceptions internal only; handler owns side-effect semantics |
|
|
45
|
+
| Clock / IdGenerator | timestamp / unpredictable unique id | unexpected environmental/programming errors; never silently substitute insecure ids |
|
|
46
|
+
| selected Auth contract (outside Runtime) | authentication/session/admin operation | preserve supported validation/denial; provider/email/storage failure handled at Auth adapter boundary; no assumption that Procedure catches Auth errors |
|
|
47
|
+
|
|
48
|
+
HTTP and Admin use the existing central mapping; MCP retains the diagnostic
|
|
49
|
+
code/failure facts rather than deriving application recovery from HTTP status.
|
|
50
|
+
Admin OCC recovery applies to structured CONFLICT only, not all 409 responses.
|
|
51
|
+
Unknown provider bodies, SQL and causes remain internal. Host-supplied messages
|
|
52
|
+
and resource labels are explicitly public and must not include sensitive data.
|
|
53
|
+
|
|
54
|
+
## Media partial deletion change
|
|
55
|
+
|
|
56
|
+
Previously object errors were logged and the asset row removed. Now every
|
|
57
|
+
variant deletion is attempted; any failure retains the row and returns partial
|
|
58
|
+
failure. A retry deletes the same keys (including already absent objects), then
|
|
59
|
+
removes metadata. If metadata deletion itself has an uncertain result, reconcile
|
|
60
|
+
by asset id; no new object identities are created. No cross-store transaction is
|
|
61
|
+
implied. Remaining metadata may reference already removed bytes during recovery.
|
|
62
|
+
|
|
63
|
+
## Compatibility / verification
|
|
64
|
+
|
|
65
|
+
New failure fields are additive; old consumers still have code/message. Media
|
|
66
|
+
partial-delete clients now receive failure rather than misleading success.
|
|
67
|
+
Focused tests cover diagnostic serialization, transport redaction, host rejection
|
|
68
|
+
and retry of partial deletion. Concrete adapter conformance must preserve
|
|
69
|
+
provider constraints and no automatic retry of uncertain writes/sends.
|
package/docs/adr/README.md
CHANGED
|
@@ -18,8 +18,11 @@ Records of *why* mantle ended up shaped this way. The numbering preserves POC AD
|
|
|
18
18
|
| [0014](0014-auth-better-auth-and-multi-tenant-mcp.md) | The Cloudflare adapter owns one curated Better Auth 1.7 identity/OAuth/MCP authority with CIMD discovery. Verified callers are normalized into runtime context; mutable staff role and target authorization are re-evaluated per call. | Accepted + amended |
|
|
19
19
|
| [0016](0016-site-semantic-layer.md) | Site semantic layer: `AGENTS.md` (cross-tool entry) + `.mantle/launch-state.json` (deterministic install context). The older `mantle/site.md` letter surface is suspended from first-run scaffolds. | Accepted (slimmed) |
|
|
20
20
|
| [0017](0017-media-multi-variant-agent-side-optimization.md) | Multi-variant media assets with agent-side optimization and asset-id entry references. | Accepted |
|
|
21
|
-
| [0018](0018-core-starters-repository-boundary.md) | Core produces published SDK artifacts; the separate starters repository validates them as an external consumer. Revisit after release-contract simplification. |
|
|
21
|
+
| [0018](0018-core-starters-repository-boundary.md) | Core produces published SDK artifacts; the separate starters repository validates them as an external consumer. Revisit after release-contract simplification. | Superseded by 0021 |
|
|
22
22
|
| [0019](0019-sealed-manifest-runtime-pipeline.md) | One sealed source-to-runtime pipeline, semantic storage seam, and optional Web/Admin/platform dependency direction. | Accepted |
|
|
23
|
+
| [0020](0020-builtin-handler-contracts-and-matched-upsert.md) | Static builtin handler contracts and natural-key matched upsert (`handler.match`). | Accepted + amended by 0022 |
|
|
24
|
+
| [0021](0021-retire-starter-scaffolding.md) | Direct authoring; retire Starter scaffolding. | Accepted; supersedes 0018 |
|
|
25
|
+
| [0022](0022-caller-observed-version-occ.md) | Caller-observed `expectedVersion` for Admin bind and builtin upsert OCC. | Accepted |
|
|
23
26
|
|
|
24
27
|
## Reading order
|
|
25
28
|
|
|
@@ -31,7 +34,7 @@ If you're new to the codebase:
|
|
|
31
34
|
4. **0007** — what running the SDK feels like as an AI author (and as the operator agent).
|
|
32
35
|
5. **0011** — the boundary between the runtime and the adapter.
|
|
33
36
|
6. **0010** — how locale flows through the system.
|
|
34
|
-
7. **
|
|
37
|
+
7. **0021** — current direct-authoring boundary; **0013** is historical install-session context.
|
|
35
38
|
8. **0002, 0008** — the two ADRs that touch every diagnostic and every binding.
|
|
36
39
|
|
|
37
40
|
## What's NOT here (and why)
|
|
@@ -54,8 +57,20 @@ The rebuild's ADR-0011 (new) is the most load-bearing addition — the POC accum
|
|
|
54
57
|
|
|
55
58
|
## Contributing a new ADR
|
|
56
59
|
|
|
57
|
-
1. Pick the next number
|
|
60
|
+
1. Pick the next unused number after the highest existing ADR.
|
|
58
61
|
2. File: `docs/adr/<NNNN>-<kebab-title>.md`.
|
|
59
62
|
3. Sections: Status, Date, Context, Decision, Consequences, Alternatives, How to apply, Implementation status.
|
|
60
63
|
4. Link from this README's table.
|
|
61
64
|
5. Land it in a PR alongside (or before) the implementation it documents — ADR-as-design-artifact, not ADR-as-archaeology.
|
|
65
|
+
|
|
66
|
+
## ADR-lite records
|
|
67
|
+
|
|
68
|
+
These retain their issue-based filenames and original decision status.
|
|
69
|
+
|
|
70
|
+
| Record | Status |
|
|
71
|
+
|---|---|
|
|
72
|
+
| [#803 Request diagnostics](adr-lite-803-request-diagnostics.md) | Implemented |
|
|
73
|
+
| [#808 Route readiness](adr-lite-808-route-readiness.md) | Accepted |
|
|
74
|
+
| [#809 Bounded public content](adr-lite-809-bounded-public-content.md) | Implemented |
|
|
75
|
+
| [#812 Native parity](adr-lite-812-native-parity.md) | Implemented; deployment evidence linked in record |
|
|
76
|
+
| [#823 Home statistics](adr-lite-823-home-statistics.md) | Proposed; unmerged demo |
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# ADR-lite: request-scoped Cloudflare diagnostics
|
|
2
|
+
|
|
3
|
+
Status: implemented diagnostic record for #803; real-auth controls and measured
|
|
4
|
+
comparison acceptance belong to the #812 harness. This does not close the
|
|
5
|
+
separate starter provisioning/Smart Placement portion of #803.
|
|
6
|
+
|
|
7
|
+
Use native AsyncLocalStorage and a small versioned testing entry point instead of
|
|
8
|
+
a global current-request counter or a configurable production telemetry system.
|
|
9
|
+
Production mounts only check for an existing test context. With none, there are
|
|
10
|
+
no diagnostic clock reads, binding wrappers, records, observers or response headers.
|
|
11
|
+
Runtime, auth, role, catalog, dispatcher-build and dispatch retain their ownership
|
|
12
|
+
and authorization order. The ordinary runtime-ready promise keeps its identity.
|
|
13
|
+
|
|
14
|
+
Native D1 instrumentation sits beneath both Auth and Runtime and is idempotent.
|
|
15
|
+
It preserves receivers, bind chains, first-column behavior and native objects in
|
|
16
|
+
batches; no second driver observer is added for counting. Provider failures and
|
|
17
|
+
unknown metadata remain visible. Request context is captured when an operation
|
|
18
|
+
starts, so shared work belongs to its initiator. Catalog waiters inherit source
|
|
19
|
+
classification and wait time without inheriting the owner's binding counts.
|
|
20
|
+
|
|
21
|
+
The record freezes at response creation. Inclusive spans can overlap, and partial
|
|
22
|
+
metadata or deferred work must not be presented as a full total. A test-only cloned
|
|
23
|
+
MCP response supplies the JSON-RPC outcome without exposing its content; that
|
|
24
|
+
inspection is outside totalMs but remains instrumentation overhead for HTTP timing.
|
|
25
|
+
Observer errors never replace application results. D1/KV/R2 operations emit only
|
|
26
|
+
counts, sizes with their source/coverage, and durations; object keys, SQL, caller
|
|
27
|
+
identities and credentials never enter the record.
|
|
28
|
+
|
|
29
|
+
R2 GET bodies stay native. Successful direct stream-to-PUT completion confirms
|
|
30
|
+
payload bytes; canceled, incomplete and unconsumed reads remain unknown. This
|
|
31
|
+
avoids losing R2's known-length stream property or hiding buffering overhead.
|
|
32
|
+
|
|
33
|
+
Checks deliberately overlap MCP requests and share KV hit/failure loads, then
|
|
34
|
+
retry after failure. Native I/O is counted once per owner, and denied requests
|
|
35
|
+
leave unreached phases null. Additional checks preserve native D1 private receiver
|
|
36
|
+
and batch semantics, sync/async observer failure, immutable deferred snapshots,
|
|
37
|
+
R2 stream identity and partial-transfer uncertainty. The collector fixture labels
|
|
38
|
+
its deterministic auth explicitly; it is not evidence of native-auth parity.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Route-owned Cloudflare readiness (#808)
|
|
2
|
+
|
|
3
|
+
Status: accepted for the unreleased #806–812 implementation. Follows ADR-0019
|
|
4
|
+
(preparation owns migrations) and amends ADR-0014's unconditional HTTP boot rule.
|
|
5
|
+
|
|
6
|
+
The facade assembles immutable route projections once, then prepares only for
|
|
7
|
+
routes that consume canonical storage. It retains one retryable preparation
|
|
8
|
+
promise, the sealed plan and Better Auth's existing global AsyncLocalStorage,
|
|
9
|
+
`ready` observation, failed-assembly eviction and HTTP `waitUntil` ownership.
|
|
10
|
+
|
|
11
|
+
| Surface | Readiness, including an empty database |
|
|
12
|
+
| --- | --- |
|
|
13
|
+
| Consumer constant health, `/api/views` catalog | No content preparation. |
|
|
14
|
+
| Static Admin shell and assets | No content preparation; incomplete Auth still fails closed as before. |
|
|
15
|
+
| Manifest View/Trigger | Prepare before credential resolution, then use the same runtime invocation. |
|
|
16
|
+
| Admin API, configured Auth base path, OAuth UI/discovery | Prepare before session/provider work. Auth tables share canonical migrations. |
|
|
17
|
+
| MCP, including missing-token challenge | Prepare before the selected Auth verifier. A custom Auth implementation may require D1 even for a denied request; the facade cannot assume otherwise. |
|
|
18
|
+
| Public page/list/discovery and favicon fallback | Their existing `ref.get()` starts preparation when content is needed. |
|
|
19
|
+
| Consumer extension handlers | Call the supplied `getRuntime()` or `ref.get()` before using Mantle content or database-backed Auth. Pure handlers need neither. |
|
|
20
|
+
| Queue/scheduled `getRuntime(env)` | Await both runtime preparation and Auth initialization, unchanged. |
|
|
21
|
+
|
|
22
|
+
No deployment-time migration flag or KV readiness authority is introduced. A
|
|
23
|
+
current database's first protected route needs one fingerprint SELECT; an empty
|
|
24
|
+
or changed database performs canonical setup there. Concurrent first uses share
|
|
25
|
+
that work. A failed preparation returns the facade's redacted 500, and later
|
|
26
|
+
requests retry. Static availability is not a promise that data is ready.
|
|
27
|
+
|
|
28
|
+
The pre-change full-facade fixture recorded 106 SQL operations for its empty-DB
|
|
29
|
+
health request (its four Schemas include indexes), then 1 fingerprint operation
|
|
30
|
+
for each new-state health/catalog/shell/challenge. These are statement counts,
|
|
31
|
+
not network round trips. Its dry-run bundle was 3,921.40 KiB / 690.43 KiB gzip
|
|
32
|
+
with Wrangler 4.124.0 and compatibility date 2026-07-08. Esbuild attributed
|
|
33
|
+
1,342,428 emitted bytes to Better Auth, 76,078 to Admin, 24,624 to Web and
|
|
34
|
+
203,130 to Runtime; the rest includes transitive libraries and adapter code.
|
|
35
|
+
Byte attribution does not measure startup CPU. We retain static imports and
|
|
36
|
+
immutable mount-time projections: deferring those modules has not yet been
|
|
37
|
+
justified by a startup profile. The final #812 report owns the separate startup,
|
|
38
|
+
preparation-wall and warm-dispatch measurements.
|
|
39
|
+
|
|
40
|
+
Post-change full-facade workerd checks pass all 13 operation gates: empty-DB
|
|
41
|
+
health and fresh-state health/catalog/shell perform zero **content** queries;
|
|
42
|
+
the challenge retains its one fingerprint query. The fixture Auth is a stated
|
|
43
|
+
stub. A separate real Better Auth regression records one eager oauthResource
|
|
44
|
+
lookup while `ready` initializes; Better Auth 1.7.2 defers missing-table resource
|
|
45
|
+
seeding to first access. Subsequent static requests perform no additional Auth
|
|
46
|
+
queries. This background initialization is retained, not counted as eliminated
|
|
47
|
+
content preparation or hidden behind a zero-total-SQL claim.
|