@aotter/mantle 0.1.0-alpha.9 → 0.1.2-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/dist/codegen/emitMantleModule.js +8 -0
- package/dist/codegen/emitMantleModule.js.map +1 -1
- package/docs/adapter-guide.md +23 -26
- package/docs/adr/0008-structured-diagnostic-shape.md +7 -2
- package/docs/adr/0010-locale-and-translates.md +14 -16
- package/docs/adr/0011-adapter-port-spec.md +2 -1
- 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 +38 -2
- 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/0024-manifest-native-schema-tables.md +260 -0
- package/docs/adr/README.md +19 -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 +150 -0
- package/docs/adr/adr-lite-823-home-statistics.md +64 -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 +236 -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 +105 -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 +191 -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 +250 -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 +60 -3
- package/docs/performance-harness.md +127 -10
- package/docs/release-process.md +85 -231
- package/docs/schema-indexes.md +3 -180
- package/docs/sealed-pipeline-ownership.md +6 -5
- 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
|
@@ -625,3 +625,103 @@ OAuth store and is not renamed by this decision.
|
|
|
625
625
|
This amendment adopts the 2026-07-28 CIMD authorization profile only. Updating
|
|
626
626
|
Mantle's JSON-RPC dispatcher to the complete MCP 2026-07-28 transport revision
|
|
627
627
|
is a separate decision.
|
|
628
|
+
|
|
629
|
+
## 2026-09-07 amendment — shared OAuth surfaces and revocation
|
|
630
|
+
|
|
631
|
+
OAuth product UI is not owned by a runtime adapter. `@aotter/mantle-admin`
|
|
632
|
+
owns `MantleOAuthAuth`, the consent/connected-app view models, and
|
|
633
|
+
`handleMantleOAuth(Request) -> Response | null`; `mountMantleOAuth` is a thin
|
|
634
|
+
Hono bridge. The Cloudflare adapter implements protocol actions using the
|
|
635
|
+
same Better Auth instance and D1. Future adapters reuse this contract, not
|
|
636
|
+
Cloudflare-specific UI glue. The old `mountAuthorize` export remains an alias.
|
|
637
|
+
|
|
638
|
+
`@aotter/mantle-admin-ui` owns the React/shadcn sign-in, consent and connected
|
|
639
|
+
apps surfaces, including i18n, theme and submit state. Auth pages initially
|
|
640
|
+
follow the system theme; the light/dark toggle persists an explicit override.
|
|
641
|
+
Connected apps has an Admin page, but managing one's own grants requires only
|
|
642
|
+
a session, never a staff role. The no-assets HTML fallback uses native forms
|
|
643
|
+
without JavaScript or a second implementation of the Admin design system.
|
|
644
|
+
Both mounts retain same-origin mutation checks and private/no-store responses.
|
|
645
|
+
The consent document's CSP permits only the provider-validated callback
|
|
646
|
+
origin for the browser's form redirect; it never trusts an unsigned query.
|
|
647
|
+
|
|
648
|
+
MCP authorization remains session-bound: the JWT's original Better Auth
|
|
649
|
+
session must still exist and be unexpired. Admin sign-out/session expiration
|
|
650
|
+
therefore also ends that session's MCP access. A refresh token is not an
|
|
651
|
+
independent authorization to bypass this check. Staff roles are still read
|
|
652
|
+
fresh on every protected request.
|
|
653
|
+
|
|
654
|
+
Disconnect is scoped to the authenticated user and the selected client. It
|
|
655
|
+
revokes refresh/opaque access tokens, removes pending authorization codes and
|
|
656
|
+
consent rows, and prevents existing JWTs from becoming valid when the client
|
|
657
|
+
is connected again. The verification-create hook captures the consent row ID
|
|
658
|
+
in Better Auth's existing authorization `referenceId`; Better Auth carries it
|
|
659
|
+
through the authorization code and every refresh rotation. MCP JWTs copy that
|
|
660
|
+
reference into `mantle_consent_id`, which must match the active consent row.
|
|
661
|
+
Never look up a new consent at token mint time: doing so could revive a refresh
|
|
662
|
+
lineage whose insertion raced the revoke batch. Same-second reconnect and a
|
|
663
|
+
delayed old refresh row are required regression cases, not clock delays.
|
|
664
|
+
|
|
665
|
+
MCP mode reserves authorization `referenceId` for this grant identity; future
|
|
666
|
+
curated configuration must not also expose Better Auth's `postLogin` reference
|
|
667
|
+
hooks. MCP access requires a persisted user consent, so `skipConsent` and
|
|
668
|
+
`cachedTrustedClients` must not bypass consent for MCP clients.
|
|
669
|
+
|
|
670
|
+
This alpha hotfix requires existing MCP clients to reconnect once: pre-hotfix
|
|
671
|
+
JWTs without the grant claim are rejected immediately. No account/session reset
|
|
672
|
+
is required. The unshipped watermark migration 0008 is removed; its unused
|
|
673
|
+
table in the phsu development database is harmless and is not queried or
|
|
674
|
+
deleted during deployment.
|
|
675
|
+
|
|
676
|
+
## 2026-09-08 amendment — request security boundaries
|
|
677
|
+
|
|
678
|
+
All Admin session mutations use the existing same-origin guard, including
|
|
679
|
+
same-site sibling origins. Admin HTML forbids framing and is private/no-store.
|
|
680
|
+
The SPA also refuses to render in frames, covering direct static-asset URLs
|
|
681
|
+
that bypass the server mount.
|
|
682
|
+
The Admin/auth mounts cap request bodies at 1 MiB using Hono's body limiter;
|
|
683
|
+
HTTP trigger and MCP dispatchers count streamed JSON bytes before parsing and
|
|
684
|
+
return 413 above the same limit. Media bytes continue through direct uploads.
|
|
685
|
+
This intentionally rejects previously accepted larger control-plane payloads.
|
|
686
|
+
|
|
687
|
+
Cloudflare `createAuth` explicitly enables Better Auth rate limits regardless
|
|
688
|
+
of `NODE_ENV`, including the OAuth provider's anonymous registration limit of
|
|
689
|
+
five requests per minute. Only `CF-Connecting-IP` supplies the client key.
|
|
690
|
+
The upstream memory store limits each isolate; deployments needing a shared
|
|
691
|
+
abuse quota must additionally enforce it at ingress. No D1 migration or new
|
|
692
|
+
runtime platform dependency is introduced.
|
|
693
|
+
|
|
694
|
+
Workers must retain initialization work through `ExecutionContext.waitUntil`
|
|
695
|
+
even when the initial challenge finishes or its client disconnects. Schema
|
|
696
|
+
boot precedes OAuth handling. The adapter's static AsyncLocalStorage seeding
|
|
697
|
+
is a version-pinned Better Auth 1.7.2 integration, with accessor-identity
|
|
698
|
+
regression coverage; it does not replace Better Auth's request context.
|
|
699
|
+
Failed Auth initialization evicts only the failed Worker assembly so a later
|
|
700
|
+
request can retry. Non-HTTP callers await Auth initialization with runtime boot;
|
|
701
|
+
HTTP requests anchor it without making public responses depend on Auth health.
|
|
702
|
+
|
|
703
|
+
The conventional `/favicon.ico` reflects the configured site icon, but is a
|
|
704
|
+
fallback after consumer routes, not a newly reserved namespace. Existing
|
|
705
|
+
consumer icon routes must continue to work after a package update.
|
|
706
|
+
|
|
707
|
+
## Amendment — 2026-09-14: Host-only control-plane cookies
|
|
708
|
+
|
|
709
|
+
`CreateAuthConfig.hostOnlyCookies` is an opt-in, curated auth field for a
|
|
710
|
+
control plane on a parent domain shared with tenants. It uses native `__Host-`
|
|
711
|
+
cookie names, `Secure`, `Path=/` and no `Domain`; HTTPS is required and enabling
|
|
712
|
+
cross-subdomain sharing at the same time is rejected. Existing defaults and
|
|
713
|
+
Apple's `SameSite=None` requirement remain unchanged. Consumers must treat
|
|
714
|
+
switching cookie names as an explicit sign-in migration, not rewrite request
|
|
715
|
+
or response cookies. This keeps cookie semantics in the auth adapter rather
|
|
716
|
+
than requiring each consumer to wrap Admin, member and OAuth routes.
|
|
717
|
+
|
|
718
|
+
## Amendment — 2026-09-16: deployment KV session reads
|
|
719
|
+
|
|
720
|
+
Cloudflare deployments may pass their deployment-owned KV namespace as
|
|
721
|
+
`CreateAuthConfig.sessionCacheKv`. Better Auth reads sessions from KV while
|
|
722
|
+
keeping the canonical session row in D1. OTP verification remains D1-backed
|
|
723
|
+
and rate limiting remains isolate-local because Workers KV does not provide
|
|
724
|
+
the atomic consume or increment operations those paths require. Auth keys use
|
|
725
|
+
the `better-auth:` prefix so the namespace can also hold Mantle projections.
|
|
726
|
+
Session revocation and user updates use Better Auth's cache invalidation and
|
|
727
|
+
therefore follow Workers KV's propagation model.
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# ADR-0018: Keep Core and public starters in separate repositories
|
|
2
2
|
|
|
3
|
+
> Superseded for 0.1.2 by [ADR-0021](0021-retire-starter-scaffolding.md).
|
|
4
|
+
> The contract below is retained as alpha.17 history, not current launch/release guidance.
|
|
5
|
+
|
|
3
6
|
**Status:** Accepted for now; revisit only under the triggers below
|
|
4
7
|
|
|
5
8
|
**Date:** 2026-08-02
|
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
**Status:** Accepted
|
|
4
4
|
|
|
5
|
-
**Date:** 2026-08-16
|
|
5
|
+
**Date:** 2026-08-16; last amended 2026-09-01
|
|
6
6
|
|
|
7
7
|
**Related:** [#656](https://github.com/aotter/mantle/issues/656),
|
|
8
8
|
[#662](https://github.com/aotter/mantle/issues/662),
|
|
9
|
+
[#759](https://github.com/aotter/mantle/issues/759),
|
|
10
|
+
[#760](https://github.com/aotter/mantle/issues/760),
|
|
9
11
|
[#546](https://github.com/aotter/mantle/issues/546), ADR-0008, ADR-0009,
|
|
10
12
|
ADR-0011, ADR-0018
|
|
11
13
|
|
|
@@ -67,7 +69,7 @@ semantic fingerprint.
|
|
|
67
69
|
| Parse + normalize | YAML syntax/alias limits, closed four-atom shape, primitive and atom-local rules, behavior-affecting defaults, source metadata | Cross-atom references, handlers, storage, optional routes |
|
|
68
70
|
| Link | Duplicate symbols, cross-atom references, guard graphs, translations, manifest-owned route/tool collisions | I/O, selected modules, handler availability |
|
|
69
71
|
| Compile | Immutable lookup records, authorization plans, Trigger indices, Procedure descriptors, logical View plans, semantic fingerprint | Connections, repositories, handlers, requests, templates, assets |
|
|
70
|
-
| Prepare | Selected storage migrations/indexes/
|
|
72
|
+
| Prepare | Selected storage migrations/native Schema tables and indexes/prepared Views, handler availability, selected capability/route checks, readiness revision | YAML interpretation or request execution |
|
|
71
73
|
| Bind/invoke | Semantic ports, handler dispatch, parameter binding, centralized authorization, content/View/Procedure/Trigger/lifecycle operations | DDL, route mounting, assets, HTTP/session/cache policy |
|
|
72
74
|
| Optional modules/adapters | Web/Admin composition and request/session/cache/platform translation | Re-parsing, re-linking, or a second authorization/runtime stack |
|
|
73
75
|
|
|
@@ -106,11 +108,45 @@ Concrete D1/SQLite drivers and SQL repositories remain implementation details.
|
|
|
106
108
|
An existing application may either pass its already-owned database/client to
|
|
107
109
|
an official adapter or implement the semantic ports over its own tables.
|
|
108
110
|
|
|
111
|
+
`@aotter/mantle-indexeddb` is the optional browser storage implementation of
|
|
112
|
+
the same semantic ports. One adapter exclusively owns one application-selected
|
|
113
|
+
database name, uses structured-clone persistence and IndexedDB transaction
|
|
114
|
+
completion as its write boundary, and exposes database deletion only on the
|
|
115
|
+
concrete adapter. Runtime Core gains no browser global, browser facade,
|
|
116
|
+
namespace, persistence request, reactive binding, or synchronization port.
|
|
117
|
+
Declarative browser Views use a documented O(n) scan until measured use proves
|
|
118
|
+
an IndexedDB query planner necessary.
|
|
119
|
+
|
|
109
120
|
Declarative Views compile to logical plans once. Storage preparation lowers
|
|
110
121
|
those plans to native queries. The v0.1 `View.spec.sql` form remains explicitly
|
|
111
122
|
SQLite-only and is rejected by unsupported storage during preparation; Mantle
|
|
112
123
|
does not guess a translation and does not add a universal query driver.
|
|
113
124
|
|
|
125
|
+
### Callable capability projection
|
|
126
|
+
|
|
127
|
+
Optional callable transports consume `projectCallableCapabilities(RuntimePlan)`.
|
|
128
|
+
Each Procedure capability retains the owning MCP Trigger name while its
|
|
129
|
+
input/output/title/description remain owned by the target Procedure; View
|
|
130
|
+
capabilities retain their read-only View contract. Discovery and invocation use
|
|
131
|
+
the same projected descriptor, and invocation routes through `invokeTrigger`
|
|
132
|
+
rather than bypassing Trigger identity with a direct Procedure call. Hosts may
|
|
133
|
+
select a surface and bind transport or browser lifecycle, but do not rescan raw
|
|
134
|
+
manifests or maintain a second registry.
|
|
135
|
+
|
|
136
|
+
`@aotter/mantle-web/webmcp` is an opt-in host binding over that projection. It
|
|
137
|
+
feature-detects `document.modelContext`, adds public capabilities without
|
|
138
|
+
replacing existing host tools, and uses the registration `AbortSignal` only for
|
|
139
|
+
teardown. A browser-local host supplies the projected capabilities and a
|
|
140
|
+
late-bound invoker; Procedure calls retain their explicit MCP Trigger identity.
|
|
141
|
+
For a server-backed host, the Cloudflare adapter publishes a minimal public View
|
|
142
|
+
catalog at `GET /api/views`; the binding validates that wire catalog and invokes
|
|
143
|
+
the existing same-origin `GET /api/views/<name>` route. The catalog contains no
|
|
144
|
+
raw manifest or staff capability.
|
|
145
|
+
Invocation hooks may drive host UI effects but cannot replace Runtime
|
|
146
|
+
validation, authorization, dispatch, or domain results. The binding does not
|
|
147
|
+
add browser globals to Runtime Core, expose staff capabilities, or introduce a
|
|
148
|
+
second manifest/runtime path.
|
|
149
|
+
|
|
114
150
|
### Naming and code generation
|
|
115
151
|
|
|
116
152
|
The Core execution unit is `MantleRuntime`, not a site. Optional TypeScript
|
|
@@ -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.
|