@aotter/mantle 0.1.4 → 0.1.5-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.
Files changed (67) hide show
  1. package/README.md +16 -16
  2. package/dist/cli/generate-cloudflare.d.ts +7 -0
  3. package/dist/cli/generate-cloudflare.d.ts.map +1 -0
  4. package/dist/cli/generate-cloudflare.js +123 -0
  5. package/dist/cli/generate-cloudflare.js.map +1 -0
  6. package/dist/cli/generate-project.d.ts +25 -0
  7. package/dist/cli/generate-project.d.ts.map +1 -0
  8. package/dist/cli/generate-project.js +282 -0
  9. package/dist/cli/generate-project.js.map +1 -0
  10. package/dist/cli/generate-sites.d.ts +8 -0
  11. package/dist/cli/generate-sites.d.ts.map +1 -0
  12. package/dist/cli/generate-sites.js +273 -0
  13. package/dist/cli/generate-sites.js.map +1 -0
  14. package/dist/cli/generate.d.ts.map +1 -1
  15. package/dist/cli/generate.js +121 -10
  16. package/dist/cli/generate.js.map +1 -1
  17. package/dist/cli/main.d.ts +1 -1
  18. package/dist/cli/main.d.ts.map +1 -1
  19. package/dist/cli/main.js +10 -10
  20. package/docs/adr/0021-retire-starter-scaffolding.md +1 -1
  21. package/docs/adr/0026-progressive-generate.md +36 -0
  22. package/docs/adr/0027-scheduled-procedure-triggers.md +31 -0
  23. package/docs/adr/0028-schema-ttl-expiry.md +33 -0
  24. package/docs/adr/README.md +5 -2
  25. package/docs/agent-prompts.md +40 -78
  26. package/docs/examples/host-chatgpt-sites/README.md +1 -1
  27. package/docs/examples/host-chatgpt-sites/drizzle/0004_mantle.sql +10 -0
  28. package/docs/examples/host-chatgpt-sites/drizzle/0005_mantle.sql +24 -0
  29. package/docs/examples/host-chatgpt-sites/drizzle/meta/_journal.json +1 -1
  30. package/docs/examples/host-chatgpt-sites/drizzle/meta/mantle-state.json +60 -0
  31. package/docs/examples/host-chatgpt-sites/package.json +1 -1
  32. package/docs/examples/host-chatgpt-sites/scripts/migration.mjs +42 -8
  33. package/docs/examples/host-chatgpt-sites/src/chatgpt-auth.ts +9 -6
  34. package/docs/examples/host-local-admin-otp/package.json +2 -2
  35. package/docs/examples/host-minimal-worker/package.json +2 -2
  36. package/docs/handbook/chatgpt-sites/generated-app.md +84 -0
  37. package/docs/handbook/chatgpt-sites/index.md +21 -11
  38. package/docs/handbook/cloudflare/authentication.md +19 -2
  39. package/docs/handbook/cloudflare/bindings.md +4 -11
  40. package/docs/handbook/cloudflare/conventional-worker.md +7 -3
  41. package/docs/handbook/cloudflare/deploy-and-operate.md +1 -1
  42. package/docs/handbook/concepts/runtime-and-adapters.md +7 -0
  43. package/docs/handbook/guides/agent-setup.md +3 -3
  44. package/docs/handbook/navigation.json +3 -2
  45. package/docs/handbook/reference/diagnostics.md +5 -0
  46. package/docs/handbook/reference/procedure.md +52 -1
  47. package/docs/handbook/reference/schema.md +27 -1
  48. package/docs/handbook/reference/surface.md +3 -3
  49. package/docs/handbook/reference/trigger.md +21 -1
  50. package/docs/handbook/releases/index.md +55 -0
  51. package/docs/handbook/start/overview.md +5 -2
  52. package/docs/handbook/start/project-and-cli.md +103 -119
  53. package/docs/handbook/start/quickstart-admin.md +6 -4
  54. package/docs/handbook/start/quickstart-worker.md +17 -8
  55. package/docs/skills/develop/SKILL.md +14 -6
  56. package/docs/skills/integrate/SKILL.md +60 -0
  57. package/package.json +18 -17
  58. package/skills/README.md +3 -1
  59. package/skills/develop/SKILL.md +14 -6
  60. package/skills/install/SKILL.md +100 -137
  61. package/skills/integrate/SKILL.md +60 -0
  62. package/templates/chatgpt-sites/auth.ts +66 -0
  63. package/templates/chatgpt-sites/build.mjs +14 -0
  64. package/templates/chatgpt-sites/env.ts +1 -0
  65. package/templates/chatgpt-sites/mcp.ts +48 -0
  66. package/templates/chatgpt-sites/public-mcp.ts +29 -0
  67. package/templates/chatgpt-sites/smoke-local.mjs +20 -0
package/dist/cli/main.js CHANGED
@@ -25,15 +25,16 @@ async function main() {
25
25
  return 2;
26
26
  }
27
27
  }
28
- export const MANTLE_OVERVIEW = `mantle — compile manifests into a RuntimePlan and typed binding
28
+ export const MANTLE_OVERVIEW = `mantle — compile manifests and assemble selected application features
29
29
 
30
30
  Overview
31
- Optional surfaces — take only what you need. Admin is opt-in. You can ship a
32
- complete service with no Dev UI, no visitor frontend, and no extra packages.
31
+ New applications default to Spec, Runtime, API, MCP, Admin, and a blank Web
32
+ home. Pass --host cf or --host chatgpt-sites, or use --features for a smaller
33
+ positive selection. Existing authored applications keep compile-only mode.
33
34
 
34
- Minimal — Spec + generate
35
- validate + generate compile manifests into a sealed plan and typed binding.
36
- Embed that binding in an existing host. No Admin, no visitor UI.
35
+ Host-free — Spec only
36
+ mantle generate --features spec compiles a sealed plan and typed binding
37
+ without Runtime, Admin, visitor UI, or a host.
37
38
 
38
39
  Runtime / adapter
39
40
  Bind Runtime through an adapter (Cloudflare Worker, Bun, Vercel, or yours).
@@ -41,10 +42,9 @@ Overview
41
42
  See docs/examples/host-minimal-worker or
42
43
  node_modules/@aotter/mantle/docs/examples/host-minimal-worker.
43
44
 
44
- Opt-in — Admin / Dev UI
45
- Add @aotter/mantle-admin and @aotter/mantle-admin-ui only when humans need
46
- a console. Then re-run generate, bind wrangler ASSETS, and open
47
- /admin/sign-in (local email OTP via ConsoleEmailSender).
45
+ Selected Admin / Dev UI
46
+ New full applications include the prebuilt UI. Reduced applications can
47
+ omit it; selected packages must be installed before generation completes.
48
48
  See docs/examples/host-local-admin-otp or
49
49
  node_modules/@aotter/mantle/docs/examples/host-local-admin-otp.
50
50
 
@@ -1,6 +1,6 @@
1
1
  # ADR-0021: Author applications directly; retire Starter scaffolding
2
2
 
3
- Status: Accepted by owner, 2026-09-08 (#786). Supersedes ADR-0018 for the 0.1.2 line.
3
+ Status: Accepted by owner, 2026-09-08 (#786). Supersedes ADR-0018 for the 0.1.2 line. Its no-initialization clause is superseded by [ADR-0026](0026-progressive-generate.md) for 0.1.5.
4
4
 
5
5
  Core is an embeddable manifest engine. Applications own package manifests,
6
6
  Worker entries, provider configuration, business manifests and visitor UI.
@@ -0,0 +1,36 @@
1
+ # ADR-0026: Progressive application generation
2
+
3
+ **Status:** Accepted by owner for the 0.1.5 line (#1081–#1086). Supersedes the no-initialization clause of ADR-0021; direct authorship and optional package boundaries remain.
4
+
5
+ Mantle is progressive. A new application defaults to Spec, Runtime, API, MCP, Admin, and an editable blank Web home so an agent does not forget the optional Admin composition. `--features` is a positive replacement list over `spec,runtime,api,mcp,admin,web`; dependencies are closed automatically. `spec` alone is host-free. The first generated hosts are `cf` and `chatgpt-sites`. Only host-dependent selections require `--host`; a TTY may prompt, while headless use must pass it explicitly.
6
+
7
+ | Feature | Required features | Direct package additions |
8
+ | --- | --- | --- |
9
+ | `spec` | — | `@aotter/mantle`, `zod` peer |
10
+ | `runtime` | `spec` | Host adapter; current CF adapter also brings Admin/Auth/Web transitively |
11
+ | `api` | `runtime` | Host route composition and `hono` peer |
12
+ | `mcp` | `runtime` | Host MCP route composition |
13
+ | `admin` | `runtime,api,mcp` | `@aotter/mantle-admin`, `@aotter/mantle-admin-ui`; CF owner auth also uses `@aotter/mantle-auth` |
14
+ | `web` | `runtime` | `@aotter/mantle-web` and a user-owned blank home |
15
+
16
+ The host adapter's declared peers (`better-auth`, `hono`, `zod`, `aws4fetch`) are installed for host-dependent selections until its package dependency graph is split. A reduced selection still omits unselected routes and assets; it does not claim smaller transitive installation today.
17
+
18
+ An application with only Git, installed skills, package metadata, a lockfile, README, and installed dependencies is new. Existing manifests or application code remain in direct-authoring compile mode. A saved `mantle.config.json` decides subsequent runs before that heuristic. V1 refuses host changes and feature-list changes after selection; application owners can make an explicit migration rather than have generation delete routes or data. An explicitly named missing manifest path is an error. A new default manifest directory may be absent or empty, yielding a valid empty plan without a fake business Schema.
19
+
20
+ Generation preflights selection, manifest validity, package/script conflicts, file ownership, and symlink escapes before writing. It saves the selection and adds only absent package declarations and scripts; it does not run a package manager. Selected Mantle packages use the running CLI's exact version. Missing selected packages produce an incomplete, nonzero result and an explicit install-and-rerun instruction. `--check` never writes, installs, prompts, or provisions. Generated code and assets remain CLI-owned; the editable frontend, host config, identity, secrets, handlers, and historical migrations are application-owned. A later SDK upgrade does not overwrite them. Clean builds remove only copied output.
21
+
22
+ Cloudflare currently has a facade with transitive Admin/Auth/Web dependencies and mounts OAuth/MCP unconditionally. Host generation must use a lower-level composition for reduced selections or change that facade; omitting dependencies from a manifest does not prove route omission. Cloudflare D1 remains runtime-managed. ChatGPT Sites uses reviewed, append-only managed migrations with applied-state checks (owned by #1086). Neither host generator provisions resources or deploys.
23
+
24
+ This decision restores a convenient default while keeping the sealed Manifest-to-RuntimePlan pipeline and user-owned application source. It does not reintroduce a remote Starter catalog or a `mantle create` command.
25
+
26
+ ## 2026-09-25 amendment — existing application integration
27
+
28
+ The original implementation added `--adopt` to convert an existing authored
29
+ application into the generated-project layout. This amendment removes that
30
+ mode. Applications with their own host shape continue direct authoring;
31
+ integrating Mantle is guided by the version-matched `integrate` consumer skill.
32
+ The agent may embed selected capabilities, replace one part at a time, or
33
+ build a fresh Mantle application and migrate frontend and data. The CLI owns
34
+ only deterministic generation and its declared checks; it does not certify
35
+ arbitrary user-owned host wiring. A saved generated project still reruns from
36
+ its own configuration, with file ownership and migration safeguards intact.
@@ -0,0 +1,31 @@
1
+ # ADR-0027: Scheduled Procedure Triggers
2
+
3
+ **Status:** Accepted for 0.1.5 (#1087). Revises the closed Trigger-source grammar from ADR-0001 and the host-only cron guidance of #322.
4
+
5
+ **Date:** 2026-09-25
6
+
7
+ ## Context
8
+
9
+ The earlier contract kept cron outside the Manifest graph. That forced an application Worker to invoke a Procedure by name with its own caller context, leaving scheduling invisible to validation and the compiled plan. A portable declaration is needed without pretending that every host can register a cron event.
10
+
11
+ ## Decision
12
+
13
+ `Trigger.spec.source` gains `kind: schedule`, a five-field Cloudflare UTC `cron` (weekday 1=Sunday), and optional `enabled` (default true). It targets one Procedure whose input accepts `{}` and requires no user or staff authorization. The compiled plan carries Trigger identity, expression, target, enabled state, and the required `cloudflare` host. No HTTP route or user identity is granted.
14
+
15
+ Cloudflare's `scheduled` entry finds enabled plan entries with the exact incoming expression and invokes them in Trigger-name order through the existing Procedure pipeline, continuing after individual failures and reporting them together. The system context has null user and staff, no credential, and `ctx.schedule` with stable `${trigger}:${scheduledTime}` identity. The application registers exact expressions with Wrangler. Cloudflare Cron Triggers do not document automatic retries; handlers still use the identity for possible duplicate/manual replay. Other hosts reject enabled schedules rather than silently omit them.
16
+
17
+ ## Consequences
18
+
19
+ The grammar and sealed plan version advance. Applications must rerun `mantle generate` after upgrading. A Procedure needing staff authorization cannot run from a schedule. Cron provisioning, retries, retention and exactly-once execution remain host/operator responsibilities; the runtime does not claim them.
20
+
21
+ ## Alternatives
22
+
23
+ Keeping host-only calls would preserve the old grammar but fail static validation and omit the feature from introspection. A general background-job framework would add storage and scheduling responsibilities without a second host implementation.
24
+
25
+ ## How to apply
26
+
27
+ Declare a schedule Trigger, export `worker.scheduled`, and register its expression in `wrangler.jsonc`. Use `ctx.schedule.id` to deduplicate writes. See [Trigger reference](../handbook/reference/trigger.md#schedule-source).
28
+
29
+ ## Implementation status
30
+
31
+ Implemented in the 0.1.5 scheduled Procedure PR, with parser, linker, compiler, runtime and Cloudflare adapter tests.
@@ -0,0 +1,33 @@
1
+ # ADR-0028: Logical Schema TTL before explicit physical cleanup
2
+
3
+ **Status:** Accepted for 0.1.5 (#1088).
4
+
5
+ **Date:** 2026-09-25
6
+
7
+ ## Context
8
+
9
+ An application needs a portable expiry rule across D1 and Bun SQLite. A cron-only delete task cannot guarantee that readers stop seeing expired data if it runs late or fails. Applying a new policy to a populated table must not silently bulk-delete rows.
10
+
11
+ ## Decision
12
+
13
+ `Schema.spec.ttl` names one top-level date-time field and a nonnegative `expireAfterSeconds`. At `timestamp + duration <= now`, semantic reads exclude the row. Missing, null and unparseable legacy dates never expire. Declarative Views include the same predicate. Native SQL Views are rejected while any TTL Schema exists, and shared caching of a TTL View is rejected. The compiled plan and introspection expose the policy. Public Cloudflare pages derived from TTL collections use `no-store` rather than shared caching.
14
+
15
+ SQLite storage implements an optional bounded `ExpirySweeper`. A sweep previews by default; `delete: true` is required for physical removal. It returns counts and a cursor, selects at most 100 expired IDs in stable order, and rechecks expiry in one bounded delete statement. A failed page can be retried with the previous cursor. Ref Procedures can call the semantic capability, including from a scheduled Trigger. No host automatically invokes it.
16
+
17
+ Physical TTL removal does not fire before/after entry lifecycle hooks. Expiry already changed logical visibility; cleanup reclaims storage rather than representing an editorial deletion. Consumers needing business actions at expiry must implement them in an idempotent scheduled Procedure before calling the sweep.
18
+
19
+ ## Consequences
20
+
21
+ Adding or shortening TTL changes read visibility immediately but never starts physical deletion. Owners preview counts and choose when to sweep; expired rows can still occupy unique indexes until removal. Other adapters must implement equivalent read filtering and a sweep or fail closed for TTL plans. The runtime does not promise an exact deletion time.
22
+
23
+ ## Alternatives
24
+
25
+ Scheduler-driven deletion alone leaves a visibility gap. Implicit migration-time cleanup risks irreversible bulk deletion. Firing ordinary deletion hooks during storage reclamation would cause delayed, retry-sensitive business effects after the row was already logically gone.
26
+
27
+ ## How to apply
28
+
29
+ Declare `ttl` on a Schema, use declarative Views, preview with `runtime.sweepExpired`, then explicitly request deletion page by page. See [Schema TTL](../handbook/reference/schema.md#ttl).
30
+
31
+ ## Implementation status
32
+
33
+ Implemented in the 0.1.5 TTL PR for the SQLite family, with Bun and local D1 conformance checks.
@@ -21,10 +21,13 @@ Records of *why* mantle ended up shaped this way. The numbering preserves POC AD
21
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
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 |
24
+ | [0021](0021-retire-starter-scaffolding.md) | Direct authoring; retire Starter scaffolding. | Accepted; no-initialization clause superseded by 0026 |
25
25
  | [0022](0022-caller-observed-version-occ.md) | Caller-observed `expectedVersion` for Admin bind and builtin upsert OCC. | Accepted |
26
26
  | [0024](0024-manifest-native-schema-tables.md) | Materialize each Schema as a native storage table and deploy reviewed migration artifacts. | Accepted; authored-native-name clauses superseded by 0025 |
27
27
  | [0025](0025-reserved-native-columns-and-published-only-public-views.md) | Reserve the six native column names, let `indexes` reference them, and compile public publishing Views published-only. | Accepted |
28
+ | [0026](0026-progressive-generate.md) | Progressive `mantle generate` with a full default, positive features, saved host selection, and user-owned source. | Accepted; supersedes part of 0021 |
29
+ | [0027](0027-scheduled-procedure-triggers.md) | Schedule Triggers in the sealed plan with Cloudflare-hosted invocation and system identity. | Accepted for 0.1.5 |
30
+ | [0028](0028-schema-ttl-expiry.md) | Logical TTL on Schema reads with preview-first, explicit bounded cleanup. | Accepted for 0.1.5 |
28
31
 
29
32
  ## Reading order
30
33
 
@@ -36,7 +39,7 @@ If you're new to the codebase:
36
39
  4. **0007** — what running the SDK feels like as an AI author (and as the operator agent).
37
40
  5. **0011** — the boundary between the runtime and the adapter.
38
41
  6. **0010** — how locale flows through the system.
39
- 7. **0021** — current direct-authoring boundary; **0013** is historical install-session context.
42
+ 7. **0026** — current progressive generation boundary; **0021** retains the direct-authoring history and **0013** is historical install-session context.
40
43
  8. **0002, 0008** — the two ADRs that touch every diagnostic and every binding.
41
44
 
42
45
  ## What's NOT here (and why)
@@ -1,102 +1,64 @@
1
1
  # Task-specific agent prompts
2
2
 
3
- Cold start from GitHub or a marketplace host is the install skill:
3
+ First install the small bootstrap skill:
4
4
 
5
5
  ```sh
6
6
  npx skills add aotter/mantle
7
7
  ```
8
8
 
9
- Copy one block into a coding agent after that skill is present. Paths below
10
- are relative to the Mantle docs root: `node_modules/@aotter/mantle/docs/`
11
- after `@aotter/mantle` is installed. A standalone `skills add` installation
12
- contains only the selected skill, not these docs.
13
- `npx --no-install mantle --help` is the layered CLI overview; it mirrors the
14
- authoring docs. A live `/mcp` catalog mirrors the Manifest → RuntimePlan, not
15
- the CLI. Resolve only missing requirements first; do not assume a host. There is no `mantle create`. Empty `generate` fails until
16
- manifests exist. Admin is opt-in.
9
+ The installed skill locates the target project, resolves its actual SDK
10
+ version, and sends the agent to that package's CLI and docs. A GitHub skill
11
+ may be newer than npm: published 0.1.4 uses the direct-authoring path;
12
+ 0.1.5-alpha.1 adds full generated projects. Never assume a new flag exists without
13
+ checking the installed CLI.
17
14
 
18
- ### Interview then build
15
+ ## New full Cloudflare app
19
16
 
20
17
  ```text
21
- Interview me about the service: host, who uses it, whether humans need a
22
- Dev UI, and whether we only embed Spec/Runtime. If the install skill is
23
- missing, run npx skills add aotter/mantle. Read the
24
- installer-reported skill, choose and install an exact SDK version, then read
25
- its embedded skills/install/SKILL.md,
26
- run npx --no-install mantle --help, and
27
- read handbook/start/overview.md.
28
- Use examples/README.md as the examples index; copy builtin-* Manifests only
29
- (not cf-primitives-*). Implement locally first. Take only the surfaces we
30
- chose. For Spec-only use, skip Runtime and code generation. For a Worker
31
- without Admin, follow examples/host-minimal-worker/. For a Worker with
32
- Dev UI, follow examples/host-local-admin-otp/README.md. ChatGPT Sites only
33
- if I name that host. No mantle create. Pin all @aotter/mantle* packages
34
- to one exact version.
18
+ Build a Mantle app on Cloudflare. Follow the installed mantle skill, pin one
19
+ exact stable SDK version, and use its project-generation path if supported.
20
+ Select the full default Spec, Runtime, API, MCP, Admin and blank home. Install
21
+ the CLI-declared dependencies and rerun generation. Ask for my actual data and
22
+ workflow before adding Manifests. For local Admin, copy .dev.vars.example,
23
+ set the real owner email and a random secret, and verify OTP, Admin assets,
24
+ unauthorized access, public API and MCP. Do not claim production Auth is set
25
+ up from a local OTP test.
35
26
  ```
36
27
 
37
- ### Embed Runtime with typed APIs
28
+ ## Small API or Spec-only app
38
29
 
39
30
  ```text
40
- Read handbook/start/project-and-cli.md and npx --no-install mantle --help.
41
- Pin @aotter/mantle at the exact version we agree, author manifests for
42
- this existing host, then run mantle generate. Embed
43
- the typed binding from .mantle/generated/mantle.ts into the current
44
- system. Do not add Admin, mantle-admin-ui, a visitor frontend, or a
45
- Cloudflare adapter unless I ask. Do not invent a default Schema.
31
+ Use the installed Mantle CLI and version-matched docs. For a Cloudflare API,
32
+ select --host cf --features spec,api if that CLI supports project generation.
33
+ For manifest parsing only, use --features spec without a host. Keep unselected
34
+ Admin, Web and MCP packages out. Author the Schema and View I request, then
35
+ run generate, validate, typecheck and a real route probe when a host exists.
36
+ Preserve an existing direct-authored application instead of regenerating it.
46
37
  ```
47
38
 
48
- ### Minimal API service locally
39
+ ## ChatGPT Sites
49
40
 
50
41
  ```text
51
- Read handbook/start/quickstart-worker.md and examples/host-minimal-worker/.
52
- Author a Cloudflare Worker from that official example or from scratch:
53
- one Schema, one public View (copy the contract, not the tree wholesale).
54
- Pin every @aotter/mantle* package to the same exact version. Run
55
- pnpm install && pnpm generate && pnpm dev (or wrangler dev --local).
56
- Probe GET /api/views/<name> with curl. GET / may 404. Do not install
57
- Admin or wrangler ASSETS unless I ask.
42
+ Build with ChatGPT Sites and Mantle. Start from the installed SDK's generated
43
+ blank app when supported; otherwise follow that version's Sites reference.
44
+ Pin all selected Mantle packages exactly, install declared dependencies,
45
+ review and apply the initial local D1 migration, then add my Manifests and
46
+ review each appended migration. Configure OWNER_EMAIL and PUBLIC_ORIGIN. The
47
+ local owner smoke simulates Sites identity; deployed ChatGPT sign-in needs a
48
+ separate check. Show how I will maintain content through Admin, browser
49
+ WebMCP, public MCP and Sites-session staff MCP. Add R2 only if my workflow
50
+ requires uploads. Never deploy the Worker directly or claim remote staff OAuth
51
+ MCP from a Sites browser session.
58
52
  ```
59
53
 
60
- ### Full local Dev UI (opt-in)
54
+ ## Existing project with typed queries
61
55
 
62
56
  ```text
63
- I want the optional Admin / Dev UI. Read examples/host-local-admin-otp/README.md
64
- (the procedural SSOT) and the thin pointer at handbook/start/quickstart-admin.md.
65
- Interview me for a bootstrap owner email, then follow that example locally.
66
- Prefer 127.0.0.1 over localhost. After pnpm check / smoke, restore .dev.vars
67
- from .dev.vars.example before pnpm dev.
57
+ Read this project's package.json, lockfile, installed Mantle package and
58
+ projected develop skill. Keep its host and selected surfaces. Use the installed
59
+ CLI's help and handbook to add the requested Schema/View/Procedure, then run
60
+ its generate/check, validation, TypeScript and host smoke. Do not use another
61
+ checkout's SDK docs or overwrite user-owned files.
68
62
  ```
69
63
 
70
- ### Later layer: MCP or public web (opt-in)
71
-
72
- ```text
73
- Do not add Admin unless it is already in this project. Read
74
- handbook/concepts/mcp-and-agents.md and/or
75
- handbook/cloudflare/public-web.md. Add only the surface I name: MCP
76
- Triggers at /mcp or /mcp/staff, or optional @aotter/mantle-web
77
- composition. Keep Core adapter-neutral. Probe the new route; do not
78
- claim Auth or Admin works from a public 200.
79
- ```
80
-
81
- ### Mantle on ChatGPT Sites, including media
82
-
83
- Use only when the user names ChatGPT Sites as the host.
84
-
85
- ```text
86
- Build with ChatGPT Sites; use Mantle for content management and publishing.
87
- Read handbook/chatgpt-sites/index.md and examples/host-chatgpt-sites/README.md.
88
- Use that runnable host as a reference and install its pinned
89
- dependencies from the registry. Derive Schema, View,
90
- Procedure and Trigger from my requirements and check the Admin editor/picker and public
91
- projections against them. Request both Sites D1 and R2 when my workflow
92
- includes uploads. Bind the R2 media port, declare media.purposes, and keep
93
- the same-origin PUT and committed-only public GET checks. Pin every
94
- @aotter/mantle* dependency to one exact version. Run the local smoke,
95
- show a draft-to-publish walkthrough, then save and deploy through Sites when
96
- requested. Verify the deployed owner/member role, R2 upload, published page, and anonymous draft 404. Treat public read-only
97
- MCP, Admin WebMCP, and staff OAuth MCP as separate acceptance gates; never
98
- claim staff MCP from a working browser session or connector URL alone.
99
- ```
100
-
101
-
102
- [Back to the Core README](../README.md#for-engineers-and-agents).
64
+ [Back to the Core README](../README.md).
@@ -49,7 +49,7 @@ Keep `.openai/hosting.json`; do not copy an existing Site's `project_id`. To use
49
49
 
50
50
  Review the entire [smoke script](./scripts/check.mjs) before adapting it. It covers D1 CRUD/version conflict, owner/member/role revocation, R2 read/write/delete, media create → PUT → commit → public read, both advertised MCP URLs, public MCP `initialize`/`tools/list`/View call, staff MCP authentication/catalog, draft isolation, published article HTML/Markdown/SEO, and negative auth/Origin/size checks. It creates and deletes only its own test records and objects.
51
51
 
52
- The checked-in `drizzle/` migrations and `src/storage-fingerprint.json` match the example manifest and runtime. Apply all pending D1 migrations before deploying updated code; managed storage does not apply runtime infrastructure migrations at boot, and a missing column can make every route return 500. `scripts/migration.mjs` shows the one-time generation mechanism; do **not** run it against a deployed database or overwrite an applied migration. For a new business manifest, generate/review an initial migration before the first deployment; for a later change, generate an additive migration from the previous schema state.
52
+ The checked-in `drizzle/` migrations and `src/storage-fingerprint.json` match the example manifest and runtime. Apply all pending D1 migrations before deploying updated code; managed storage refuses to boot against an older database. After changing a Schema, first apply the current migrations to local D1, then run `npm run generate` and `npm run migrate:generate`. Review the newly appended SQL and apply it with `wrangler d1 migrations apply mantle-sites-reference-local --local`. Commit the new migration, `drizzle/meta/mantle-state.json`, journal, and fingerprint together. The generator checks the local database fingerprint before appending; it never changes an applied SQL file or applies D1 migrations itself. Deploy the reviewed migration before deploying code that expects its fingerprint.
53
53
 
54
54
  ## Publish with Sites
55
55
 
@@ -0,0 +1,10 @@
1
+ CREATE TABLE IF NOT EXISTS _mantle_managed_runtime_state (
2
+ id INTEGER PRIMARY KEY CHECK (id = 1),
3
+ canonical_version TEXT NOT NULL
4
+ );
5
+ --> statement-breakpoint
6
+ INSERT INTO _mantle_schema_tables(name,projection) VALUES ('articles','{"columns":[["body","TEXT","string",false],["coverAssetId","TEXT","string",false],["summary","TEXT","string",false],["title","TEXT","string",false]],"indexes":[]}') ON CONFLICT(name) DO UPDATE SET projection=excluded.projection;
7
+ --> statement-breakpoint
8
+ INSERT INTO _mantle_managed_runtime_state(id,canonical_version) VALUES (1,'0006-managed-runtime-version') ON CONFLICT(id) DO UPDATE SET canonical_version=excluded.canonical_version;
9
+ --> statement-breakpoint
10
+ INSERT INTO _mantle_storage_state(id,fingerprint) VALUES (1,(SELECT CASE WHEN fingerprint='9ef85f9933e22328140e924045820af6be2a6985ed8ecc19b63a4f2a37826618' THEN '9ef85f9933e22328140e924045820af6be2a6985ed8ecc19b63a4f2a37826618' ELSE NULL END FROM _mantle_storage_state WHERE id=1)) ON CONFLICT(id) DO UPDATE SET fingerprint=excluded.fingerprint;
@@ -0,0 +1,24 @@
1
+ CREATE TABLE IF NOT EXISTS _mantle_schedule_runs (
2
+ run_id TEXT NOT NULL,
3
+ attempt INTEGER NOT NULL,
4
+ schedule_id TEXT NOT NULL,
5
+ scheduled_at INTEGER NOT NULL,
6
+ started_at INTEGER NOT NULL,
7
+ finished_at INTEGER,
8
+ status TEXT NOT NULL,
9
+ error_summary TEXT,
10
+ counts TEXT,
11
+ PRIMARY KEY (run_id, attempt)
12
+ );
13
+ --> statement-breakpoint
14
+ CREATE INDEX IF NOT EXISTS _mantle_schedule_runs_recent
15
+ ON _mantle_schedule_runs (started_at DESC);
16
+ --> statement-breakpoint
17
+ CREATE INDEX IF NOT EXISTS _mantle_schedule_runs_schedule_recent
18
+ ON _mantle_schedule_runs (schedule_id, started_at DESC);
19
+ --> statement-breakpoint
20
+ INSERT INTO _mantle_schema_tables(name,projection) VALUES ('articles','{"columns":[["body","TEXT","string",false],["coverAssetId","TEXT","string",false],["summary","TEXT","string",false],["title","TEXT","string",false]],"indexes":[]}') ON CONFLICT(name) DO UPDATE SET projection=excluded.projection;
21
+ --> statement-breakpoint
22
+ INSERT INTO _mantle_managed_runtime_state(id,canonical_version) VALUES (1,(SELECT CASE WHEN canonical_version='0006-managed-runtime-version' THEN '0007-schedule-run-observations' ELSE NULL END FROM _mantle_managed_runtime_state WHERE id=1)) ON CONFLICT(id) DO UPDATE SET canonical_version=excluded.canonical_version;
23
+ --> statement-breakpoint
24
+ INSERT INTO _mantle_storage_state(id,fingerprint) VALUES (1,(SELECT CASE WHEN fingerprint='9ef85f9933e22328140e924045820af6be2a6985ed8ecc19b63a4f2a37826618' THEN '9ef85f9933e22328140e924045820af6be2a6985ed8ecc19b63a4f2a37826618' ELSE NULL END FROM _mantle_storage_state WHERE id=1)) ON CONFLICT(id) DO UPDATE SET fingerprint=excluded.fingerprint;
@@ -1 +1 @@
1
- {"version":"7","dialect":"sqlite","entries":[{"idx":0,"version":"6","when":1790000000000,"tag":"0000_sites_users","breakpoints":true},{"idx":1,"version":"6","when":1790000000001,"tag":"0001_mantle","breakpoints":true},{"idx":2,"version":"6","when":1790000000002,"tag":"0002_article_cover","breakpoints":true},{"idx":3,"version":"6","when":1790000000003,"tag":"0003_store_instance_id","breakpoints":true}]}
1
+ {"version":"7","dialect":"sqlite","entries":[{"idx":0,"version":"6","when":1790000000000,"tag":"0000_sites_users","breakpoints":true},{"idx":1,"version":"6","when":1790000000001,"tag":"0001_mantle","breakpoints":true},{"idx":2,"version":"6","when":1790000000002,"tag":"0002_article_cover","breakpoints":true},{"idx":3,"version":"6","when":1790000000003,"tag":"0003_store_instance_id","breakpoints":true},{"idx":4,"version":"6","when":1790000000004,"tag":"0004_mantle","breakpoints":true},{"idx":5,"version":"6","when":1790300000000,"tag":"0005_mantle","breakpoints":true}]}
@@ -0,0 +1,60 @@
1
+ {
2
+ "fingerprint": "9ef85f9933e22328140e924045820af6be2a6985ed8ecc19b63a4f2a37826618",
3
+ "schemas": [
4
+ {
5
+ "apiVersion": "cms.mantle.aotter.net/v1",
6
+ "kind": "Schema",
7
+ "metadata": {
8
+ "name": "articles"
9
+ },
10
+ "spec": {
11
+ "title": "文章",
12
+ "schema": {
13
+ "type": "object",
14
+ "required": [
15
+ "title"
16
+ ],
17
+ "properties": {
18
+ "title": {
19
+ "type": "string",
20
+ "title": "標題",
21
+ "maxLength": 160
22
+ },
23
+ "summary": {
24
+ "type": "string",
25
+ "title": "摘要",
26
+ "maxLength": 1000
27
+ },
28
+ "body": {
29
+ "type": "string",
30
+ "title": "內容",
31
+ "x-mcp-hint": "markdown"
32
+ },
33
+ "coverAssetId": {
34
+ "type": "string",
35
+ "title": "封面圖片",
36
+ "x-mantle-ref": "media_assets",
37
+ "x-mcp-hint": "media-image"
38
+ }
39
+ }
40
+ },
41
+ "lifecycle": "publishing",
42
+ "uniqueIndexes": [],
43
+ "indexes": [],
44
+ "searchableFields": [],
45
+ "localized": false
46
+ }
47
+ }
48
+ ],
49
+ "appliedMigrationIds": [
50
+ "0001-init",
51
+ "0002-media-assets",
52
+ "0003-pending-media-uploads",
53
+ "0004-native-schema-storage",
54
+ "0005-store-instance-id",
55
+ "0006-managed-runtime-version",
56
+ "0007-schedule-run-observations"
57
+ ],
58
+ "lastIndex": 5,
59
+ "canonicalVersion": "0007-schedule-run-observations"
60
+ }
@@ -1 +1 @@
1
- {"name":"mantle-sites-reference","version":"0.0.1","private":true,"type":"module","scripts":{"build":"node scripts/build.mjs","dev":"wrangler dev","generate":"mantle generate","check":"tsc --noEmit","test":"node --experimental-strip-types scripts/check.mjs"},"dependencies":{"@aotter/mantle":"latest","@aotter/mantle-admin":"latest","@aotter/mantle-admin-ui":"latest","@aotter/mantle-cloudflare":"latest","@aotter/mantle-web":"latest","hono":"^4.9.0","micromark":"4.0.2","zod":"^4.0.0"},"devDependencies":{"@cloudflare/workers-types":"*","esbuild":"^0.28.0","typescript":"^5.9.0","wrangler":"^4.0.0"}}
1
+ {"name":"mantle-sites-reference","version":"0.0.1","private":true,"type":"module","scripts":{"build":"node scripts/build.mjs","dev":"wrangler dev","generate":"mantle generate","check":"tsc --noEmit","test":"node --experimental-strip-types scripts/check.mjs","migrate:generate":"node scripts/migration.mjs","migrate:check":"node scripts/migration.mjs --check"},"dependencies":{"@aotter/mantle":"latest","@aotter/mantle-admin":"latest","@aotter/mantle-admin-ui":"latest","@aotter/mantle-cloudflare":"latest","@aotter/mantle-web":"latest","hono":"^4.9.0","micromark":"4.0.2","zod":"^4.0.0"},"devDependencies":{"@cloudflare/workers-types":"*","esbuild":"^0.28.0","typescript":"^5.9.0","wrangler":"^4.0.0"}}
@@ -1,9 +1,43 @@
1
- // One-time, reviewable initial migration from Mantle's public migration planner.
2
- import { buildSqliteMigrationArtifact, splitSqlStatements } from '@aotter/mantle/runtime';
3
- import { plan } from '../.mantle/generated/mantle.ts';
4
- import { writeFile } from 'node:fs/promises';
5
- const artifact=await buildSqliteMigrationArtifact([],Object.values(plan.schemas).map(s=>s.manifest));
6
- const quote=value=>`'${value.replaceAll("'","''")}'`;
7
- const sql=[...artifact.migrations.flatMap(m=>splitSqlStatements(m.sql).map(s=>s+';')),...artifact.projections.map(p=>`INSERT INTO _mantle_schema_tables(name,projection) VALUES (${quote(p.name)},${quote(p.projection)});`),`INSERT INTO _mantle_storage_state(id,fingerprint) VALUES (1,${quote(artifact.targetFingerprint)});`].join('\n--> statement-breakpoint\n');
8
- await writeFile('drizzle/0001_mantle.sql',sql);
1
+ // Append a reviewed D1 migration only after the previous one is applied locally.
2
+ import { buildSqliteMigrationArtifact, renderSqliteManagedMigration } from '@aotter/mantle/runtime';
3
+ import { parseManifestSources, ValidateManifestsUseCase } from '@aotter/mantle/spec';
4
+ import { execFileSync } from 'node:child_process';
5
+ import { readFile, readdir, writeFile } from 'node:fs/promises';
6
+ const statePath='drizzle/meta/mantle-state.json';
7
+ const state=JSON.parse(await readFile(statePath,'utf8'));
8
+ const files=(await readdir('manifests')).filter(name=>/\.ya?ml$/i.test(name)).sort();
9
+ const parsed=parseManifestSources({sources:await Promise.all(files.map(async name=>({sourceId:name,text:await readFile(`manifests/${name}`,'utf8')})))});
10
+ if(!parsed.ok)throw Error(`Invalid manifest: ${parsed.diagnostics.map(d=>d.message).join('; ')}`);
11
+ const validated=ValidateManifestsUseCase.run({parsed:parsed.value});
12
+ if(validated.errorCount)throw Error(`Invalid manifest: ${validated.diagnostics.filter(d=>d.severity==='error').map(d=>d.message).join('; ')}`);
13
+ const target=parsed.value.entries.map(entry=>entry.manifest).filter(manifest=>manifest.kind==='Schema');
14
+ const artifact=await buildSqliteMigrationArtifact(state.schemas,target,{appliedMigrationIds:state.appliedMigrationIds});
15
+ if(artifact.sourceFingerprint!==state.fingerprint)throw Error('The saved migration source fingerprint is invalid. Restore the applied state before generating.');
16
+ if(artifact.destructive)throw Error('Destructive Schema change needs a reviewed migration; no D1 file was written.');
17
+ const unchanged=artifact.sourceFingerprint===artifact.targetFingerprint&&artifact.migrations.length===0;
18
+ const fingerprint=JSON.parse(await readFile('src/storage-fingerprint.json','utf8'));
19
+ if(fingerprint!==state.fingerprint)throw Error('Generated fingerprint and migration source disagree. Restore the reviewed migration state.');
20
+ if(process.argv.includes('--check')){
21
+ if(!unchanged)throw Error('Pending Schema or runtime migration. Run node scripts/migration.mjs and review the new D1 file.');
22
+ process.exit(0);
23
+ }
24
+ const output=execFileSync('node',['node_modules/wrangler/bin/wrangler.js','d1','execute','mantle-sites-reference-local','--local','--command','SELECT fingerprint FROM _mantle_storage_state WHERE id = 1','--json'],{encoding:'utf8'});
25
+ const active=JSON.parse(output)[0]?.results?.[0]?.fingerprint;
26
+ if(active!==state.fingerprint)throw Error(`Local D1 fingerprint ${active??'(missing)'} does not match the last migration ${state.fingerprint}. Apply pending D1 migrations first.`);
27
+ const versionOutput=execFileSync('node',['node_modules/wrangler/bin/wrangler.js','d1','execute','mantle-sites-reference-local','--local','--command',"SELECT name FROM sqlite_schema WHERE type='table' AND name='_mantle_managed_runtime_state'",'--json'],{encoding:'utf8'});
28
+ const versionTable=JSON.parse(versionOutput)[0]?.results?.[0]?.name;
29
+ const version=versionTable?(JSON.parse(execFileSync('node',['node_modules/wrangler/bin/wrangler.js','d1','execute','mantle-sites-reference-local','--local','--command','SELECT canonical_version FROM _mantle_managed_runtime_state WHERE id = 1','--json'],{encoding:'utf8'}))[0]?.results?.[0]?.canonical_version??null):null;
30
+ if(version!==null&&version!==state.canonicalVersion)throw Error(`Local D1 runtime version ${version} does not match migration source ${state.canonicalVersion}.`);
31
+ if(version===null&&state.canonicalVersion!=='0005-store-instance-id')throw Error('Local D1 is missing the managed runtime version marker. Apply pending migrations first.');
32
+ if(unchanged){console.log('D1 migrations are current.');process.exit(0);}
33
+ const sql=renderSqliteManagedMigration(artifact,{canonicalVersion:state.canonicalVersion});
34
+ const index=state.lastIndex+1;
35
+ const path=`drizzle/${String(index).padStart(4,'0')}_mantle.sql`;
36
+ const journalPath='drizzle/meta/_journal.json';
37
+ const journal=JSON.parse(await readFile(journalPath,'utf8'));
38
+ if(journal.entries.at(-1)?.idx!==state.lastIndex)throw Error('Drizzle journal does not match the saved migration state.');
39
+ await writeFile(path,sql,{flag:'wx'});
40
+ await writeFile(journalPath,JSON.stringify({...journal,entries:[...journal.entries,{idx:index,version:'6',when:Date.now(),tag:`${String(index).padStart(4,'0')}_mantle`,breakpoints:true}]})+'\n');
41
+ await writeFile(statePath,JSON.stringify({fingerprint:artifact.targetFingerprint,canonicalVersion:artifact.targetCanonicalVersion,schemas:target,appliedMigrationIds:[...new Set([...state.appliedMigrationIds,...artifact.migrations.map(m=>m.id)])],lastIndex:index},null,2)+'\n');
9
42
  await writeFile('src/storage-fingerprint.json',JSON.stringify(artifact.targetFingerprint)+'\n');
43
+ console.log(`Review ${path}, then apply it with wrangler d1 migrations apply mantle-sites-reference-local --local.`);
@@ -24,13 +24,16 @@ export function createChatGPTAuth(env: Env): AdminAuth {
24
24
  basePath:'/api/auth', methods:[],
25
25
  async getSession(request) {
26
26
  const who=readIdentity(request); if(!who)return null;
27
- // Only the configured owner can bootstrap privilege; a later role revocation persists.
28
- await db.prepare('INSERT OR IGNORE INTO sites_users (id,email,name,role,signed_in) VALUES (?,?,?,?,1)')
29
- .bind(who.id,who.email,who.name,who.email===env.OWNER_EMAIL?.trim().toLowerCase()?'owner':null).run();
30
- await db.prepare('UPDATE sites_users SET id=?,name=?,signed_in=1 WHERE email=? AND signed_in=0').bind(who.id,who.name,who.email).run();
31
- const row=await db.prepare('SELECT * FROM sites_users WHERE id=?').bind(who.id).first<UserRow>();
27
+ let row=await db.prepare('SELECT * FROM sites_users WHERE id=?').bind(who.id).first<UserRow>();
28
+ if(!row){
29
+ // Claim an invitation before inserting; a stable identity keeps its existing role if its email later changes.
30
+ await db.prepare('UPDATE sites_users SET id=?,name=?,signed_in=1 WHERE email=? AND signed_in=0').bind(who.id,who.name,who.email).run();
31
+ await db.prepare('INSERT OR IGNORE INTO sites_users (id,email,name,role,signed_in) VALUES (?,?,?,?,1)')
32
+ .bind(who.id,who.email,who.name,who.email===env.OWNER_EMAIL?.trim().toLowerCase()?'owner':null).run();
33
+ row=await db.prepare('SELECT * FROM sites_users WHERE id=?').bind(who.id).first<UserRow>();
34
+ }
32
35
  if(!row)return null;
33
- return {session:{id:`sites:${who.id}`},user:{...who,role:row.role,roleCurrent:true}};
36
+ return {session:{id:`sites:${who.id}`},user:{id:row.id,email:row.email,name:row.name,role:row.role,roleCurrent:true}};
34
37
  },
35
38
  getUserRole:roleFor,
36
39
  async handler(request) {
@@ -21,9 +21,9 @@
21
21
  "aws4fetch": "^1.0.20"
22
22
  },
23
23
  "devDependencies": {
24
- "@cloudflare/workers-types": "^5.20260907.1",
24
+ "@cloudflare/workers-types": "5.20260904.1",
25
25
  "typescript": "^6.0.3",
26
- "wrangler": "^4.125.0"
26
+ "wrangler": "4.129.0"
27
27
  },
28
28
  "packageManager": "pnpm@9.15.0"
29
29
  }
@@ -18,9 +18,9 @@
18
18
  "aws4fetch": "^1.0.20"
19
19
  },
20
20
  "devDependencies": {
21
- "@cloudflare/workers-types": "^5.20260907.1",
21
+ "@cloudflare/workers-types": "5.20260904.1",
22
22
  "typescript": "^6.0.3",
23
- "wrangler": "^4.125.0"
23
+ "wrangler": "4.129.0"
24
24
  },
25
25
  "packageManager": "pnpm@9.15.0"
26
26
  }