@aotter/mantle 0.1.3 → 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 (105) hide show
  1. package/README.md +20 -20
  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 +11 -11
  20. package/dist/cli/skills.js +1 -1
  21. package/dist/cli/skills.js.map +1 -1
  22. package/docs/adr/0001-four-atom-manifest-model.md +3 -2
  23. package/docs/adr/0013-agent-provisioned-consumer-projects.md +4 -4
  24. package/docs/adr/0016-site-semantic-layer.md +5 -4
  25. package/docs/adr/0018-core-starters-repository-boundary.md +1 -1
  26. package/docs/adr/0021-retire-starter-scaffolding.md +1 -1
  27. package/docs/adr/0026-progressive-generate.md +36 -0
  28. package/docs/adr/0027-scheduled-procedure-triggers.md +31 -0
  29. package/docs/adr/0028-schema-ttl-expiry.md +33 -0
  30. package/docs/adr/README.md +7 -4
  31. package/docs/agent-prompts.md +41 -78
  32. package/docs/assets/mantle-admin-developer.png +0 -0
  33. package/docs/examples/README.md +1 -1
  34. package/docs/examples/host-chatgpt-sites/README.md +29 -12
  35. package/docs/examples/host-chatgpt-sites/drizzle/0003_store_instance_id.sql +1 -0
  36. package/docs/examples/host-chatgpt-sites/drizzle/0004_mantle.sql +10 -0
  37. package/docs/examples/host-chatgpt-sites/drizzle/0005_mantle.sql +24 -0
  38. package/docs/examples/host-chatgpt-sites/drizzle/meta/_journal.json +1 -1
  39. package/docs/examples/host-chatgpt-sites/drizzle/meta/mantle-state.json +60 -0
  40. package/docs/examples/host-chatgpt-sites/package.json +1 -1
  41. package/docs/examples/host-chatgpt-sites/public/site.css +1 -1
  42. package/docs/examples/host-chatgpt-sites/scripts/build.mjs +2 -1
  43. package/docs/examples/host-chatgpt-sites/scripts/check.mjs +2 -0
  44. package/docs/examples/host-chatgpt-sites/scripts/migration.mjs +42 -8
  45. package/docs/examples/host-chatgpt-sites/src/chatgpt-auth.ts +9 -6
  46. package/docs/examples/host-chatgpt-sites/src/index.ts +1 -1
  47. package/docs/examples/host-chatgpt-sites/src/web.ts +1 -1
  48. package/docs/examples/host-local-admin-otp/package.json +2 -2
  49. package/docs/examples/host-minimal-worker/README.md +5 -5
  50. package/docs/examples/host-minimal-worker/package.json +2 -2
  51. package/docs/handbook/{sites → chatgpt-sites}/equipment-checkout.md +1 -1
  52. package/docs/handbook/chatgpt-sites/generated-app.md +84 -0
  53. package/docs/handbook/{sites → chatgpt-sites}/index.md +35 -13
  54. package/docs/handbook/cloudflare/authentication.md +19 -2
  55. package/docs/handbook/cloudflare/bindings.md +4 -11
  56. package/docs/handbook/cloudflare/chatgpt-sites.md +6 -6
  57. package/docs/handbook/cloudflare/conventional-worker.md +7 -3
  58. package/docs/handbook/cloudflare/deploy-and-operate.md +4 -3
  59. package/docs/handbook/cloudflare/media-r2.md +2 -2
  60. package/docs/handbook/concepts/four-atoms.md +1 -1
  61. package/docs/handbook/concepts/lifecycle-and-locales.md +1 -1
  62. package/docs/handbook/concepts/mcp-and-agents.md +24 -4
  63. package/docs/handbook/concepts/procedures-and-triggers.md +1 -1
  64. package/docs/handbook/concepts/runtime-and-adapters.md +8 -1
  65. package/docs/handbook/concepts/views.md +1 -1
  66. package/docs/handbook/guides/agent-setup.md +8 -7
  67. package/docs/handbook/navigation.json +7 -6
  68. package/docs/handbook/reference/diagnostics.md +5 -0
  69. package/docs/handbook/reference/procedure.md +52 -1
  70. package/docs/handbook/reference/schema.md +27 -1
  71. package/docs/handbook/reference/surface.md +3 -3
  72. package/docs/handbook/reference/trigger.md +21 -1
  73. package/docs/handbook/releases/index.md +81 -1
  74. package/docs/handbook/start/overview.md +6 -3
  75. package/docs/handbook/start/project-and-cli.md +103 -119
  76. package/docs/handbook/start/quickstart-admin.md +6 -4
  77. package/docs/handbook/start/quickstart-worker.md +17 -11
  78. package/docs/release-process.md +3 -2
  79. package/docs/sealed-pipeline-ownership.md +1 -1
  80. package/docs/skills/develop/SKILL.md +258 -0
  81. package/docs/skills/integrate/SKILL.md +60 -0
  82. package/docs/skills/media-gc/SKILL.md +85 -0
  83. package/docs/skills/plugin/SKILL.md +132 -0
  84. package/docs/skills/provision/SKILL.md +194 -0
  85. package/docs/skills/theme/SKILL.md +77 -0
  86. package/docs/skills/update/SKILL.md +51 -0
  87. package/docs/spec-only-host-adoption.md +1 -1
  88. package/package.json +19 -18
  89. package/skills/README.md +28 -21
  90. package/skills/develop/SKILL.md +16 -8
  91. package/skills/install/SKILL.md +103 -128
  92. package/skills/integrate/SKILL.md +60 -0
  93. package/skills/media-gc/SKILL.md +2 -2
  94. package/skills/plugin/SKILL.md +2 -2
  95. package/skills/provision/SKILL.md +3 -3
  96. package/skills/theme/SKILL.md +2 -2
  97. package/skills/update/SKILL.md +2 -2
  98. package/templates/chatgpt-sites/auth.ts +66 -0
  99. package/templates/chatgpt-sites/build.mjs +14 -0
  100. package/templates/chatgpt-sites/env.ts +1 -0
  101. package/templates/chatgpt-sites/mcp.ts +48 -0
  102. package/templates/chatgpt-sites/public-mcp.ts +29 -0
  103. package/templates/chatgpt-sites/smoke-local.mjs +20 -0
  104. package/skills/.keep +0 -0
  105. /package/docs/handbook/{sites → chatgpt-sites}/host-reference.md +0 -0
@@ -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 +1 @@
1
- :root{font-family:Inter,"Noto Sans TC",ui-sans-serif,system-ui,sans-serif;color:#172320;background:#f7f8f4;font-synthesis:none}*{box-sizing:border-box}body{margin:0}a{color:inherit;text-decoration:none}a:hover{text-decoration:underline}.topbar{height:78px;max-width:1440px;margin:auto;padding:0 clamp(24px,5vw,80px);display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid #d9dfd8}.brand{display:flex;align-items:center;gap:12px;font-size:18px;font-weight:760;letter-spacing:-.04em}.brand em{color:#95aa8c;font-style:normal}.mark{width:37px;height:37px;border-radius:11px;background:#183c30;color:#d9eadb;display:grid;place-items:center;font-size:19px;font-family:Georgia,serif}.topbar nav{display:flex;gap:28px;font-size:14px;color:#43564e}.hero{max-width:1440px;margin:auto;padding:100px clamp(24px,8vw,128px) 116px}.hero.small{padding-bottom:75px}.eyebrow,.feature-index{font-size:11px;letter-spacing:.19em;font-weight:750;color:#68806f}.hero h1,.article-page h1{font-family:Georgia,"Noto Serif TC",serif;font-weight:500;letter-spacing:-.055em;line-height:1.09;margin:26px 0 28px;font-size:clamp(48px,6.1vw,94px)}.dot{color:#a2b888}.hero-copy{max-width:620px;color:#52665a;font-size:18px;line-height:1.8}.actions{display:flex;align-items:center;gap:30px;margin-top:38px}.button{display:inline-flex;justify-content:space-between;gap:44px;min-width:184px;padding:17px 19px;border-radius:7px;color:#fff;background:#214f3c;font-size:14px;font-weight:650}.button:hover{background:#173c2d;text-decoration:none}.text-link{font-size:14px;color:#315640}.feature-grid{max-width:1440px;margin:auto;padding:0 clamp(24px,8vw,128px) 110px;display:grid;grid-template-columns:repeat(3,1fr);gap:18px}.feature-grid>div{border-top:1px solid #bfcac0;padding-top:24px}.feature-grid h2{font-family:Georgia,"Noto Serif TC",serif;font-size:29px;font-weight:500;margin:29px 0 10px}.feature-grid p{font-size:14px;line-height:1.75;color:#597064;max-width:270px}.content-wrap{max-width:1184px;margin:0 auto;padding:0 24px 100px}.article-row{padding:25px 0;border-top:1px solid #cbd5ca;display:grid;grid-template-columns:160px 1fr 36px;gap:24px;align-items:start}.article-row:last-child{border-bottom:1px solid #cbd5ca}.article-number{font-size:12px;color:#708477;margin-top:11px}.article-row h2{font-family:Georgia,"Noto Serif TC",serif;font-size:clamp(24px,3vw,35px);font-weight:500;letter-spacing:-.035em;margin:0 0 8px}.article-row p{font-size:14px;color:#617468;line-height:1.65;margin:0}.article-row>span{color:#739279;font-size:25px}.empty{min-height:220px;padding:52px 0}.empty h2{font-size:27px;font-family:Georgia,"Noto Serif TC",serif;font-weight:500}.empty p{color:#66796c}.pagination{margin:26px 0}.article-page{max-width:900px;padding:75px 30px 140px;margin:auto}.back{font-size:13px;color:#5c7768}.article-page article{margin-top:75px}.article-page h1{font-size:clamp(44px,5vw,75px)}.lede{font-size:21px;line-height:1.75;color:#52685a}.prose{border-top:1px solid #cbd5ca;margin-top:45px;padding-top:28px;line-height:1.95;font-size:17px;color:#33473c}.prose p{margin:0 0 1.2em}.markdown-link{display:inline-block;margin-top:40px;font-size:13px;color:#4f765b}footer{border-top:1px solid #d5ddd3;max-width:1440px;margin:auto;padding:28px clamp(24px,5vw,80px);display:flex;justify-content:space-between;font-size:12px;color:#788b7d}@media(max-width:700px){.topbar{height:68px}.topbar nav{gap:16px;font-size:12px}.hero{padding-top:74px;padding-bottom:70px}.hero-copy{font-size:16px}.feature-grid{grid-template-columns:1fr;gap:26px}.article-row{grid-template-columns:1fr 26px;gap:10px}.article-number{grid-column:1/-1;margin:0}.article-page{padding-top:55px}.article-page article{margin-top:50px}footer{gap:20px}}
1
+ :root{font-family:Inter,"Noto Sans TC",ui-sans-serif,system-ui,sans-serif;color:#172320;background:#f7f8f4;font-synthesis:none}*{box-sizing:border-box}body{margin:0}a{color:inherit;text-decoration:none}a:hover{text-decoration:underline}.topbar{height:78px;max-width:1440px;margin:auto;padding:0 clamp(24px,5vw,80px);display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid #d9dfd8}.brand{display:flex;align-items:center;gap:12px;font-size:18px;font-weight:760;letter-spacing:-.04em}.brand em{color:#95aa8c;font-style:normal}.mark{width:37px;height:37px;border-radius:11px;background:#183c30;color:#d9eadb;display:grid;place-items:center;font-size:19px;font-family:Georgia,serif}.topbar nav{display:flex;gap:28px;font-size:14px;color:#43564e}.hero{max-width:1440px;margin:auto;padding:100px clamp(24px,8vw,128px) 116px}.hero.small{padding-bottom:75px}.eyebrow,.feature-index{font-size:11px;letter-spacing:.19em;font-weight:750;color:#68806f}.hero h1,.article-page h1{font-family:Georgia,"Noto Serif TC",serif;font-weight:500;letter-spacing:-.055em;line-height:1.09;margin:26px 0 28px;font-size:clamp(48px,6.1vw,94px)}.dot{color:#a2b888}.hero-copy{max-width:620px;color:#52665a;font-size:18px;line-height:1.8}.actions{display:flex;align-items:center;gap:30px;margin-top:38px}.button{display:inline-flex;justify-content:space-between;gap:44px;min-width:184px;padding:17px 19px;border-radius:7px;color:#fff;background:#214f3c;font-size:14px;font-weight:650}.button:hover{background:#173c2d;text-decoration:none}.text-link{font-size:14px;color:#315640}.feature-grid{max-width:1440px;margin:auto;padding:0 clamp(24px,8vw,128px) 110px;display:grid;grid-template-columns:repeat(3,1fr);gap:18px}.feature-grid>div{border-top:1px solid #bfcac0;padding-top:24px}.feature-grid h2{font-family:Georgia,"Noto Serif TC",serif;font-size:29px;font-weight:500;margin:29px 0 10px}.feature-grid p{font-size:14px;line-height:1.75;color:#597064;max-width:270px}.content-wrap{max-width:1184px;margin:0 auto;padding:0 24px 100px}.article-row{padding:25px 0;border-top:1px solid #cbd5ca;display:grid;grid-template-columns:160px 1fr 36px;gap:24px;align-items:start}.article-row:last-child{border-bottom:1px solid #cbd5ca}.article-number{font-size:12px;color:#708477;margin-top:11px}.article-row h2{font-family:Georgia,"Noto Serif TC",serif;font-size:clamp(24px,3vw,35px);font-weight:500;letter-spacing:-.035em;margin:0 0 8px}.article-row p{font-size:14px;color:#617468;line-height:1.65;margin:0}.article-row>span{color:#739279;font-size:25px}.empty{min-height:220px;padding:52px 0}.empty h2{font-size:27px;font-family:Georgia,"Noto Serif TC",serif;font-weight:500}.empty p{color:#66796c}.pagination{margin:26px 0}.article-page{max-width:900px;padding:75px 30px 140px;margin:auto}.back{font-size:13px;color:#5c7768}.article-page article{margin-top:75px}.article-page h1{font-size:clamp(44px,5vw,75px)}.lede{font-size:21px;line-height:1.75;color:#52685a}.prose{border-top:1px solid #cbd5ca;margin-top:45px;padding-top:28px;line-height:1.95;font-size:17px;color:#33473c}.prose p{margin:0 0 1.2em}footer{border-top:1px solid #d5ddd3;max-width:1440px;margin:auto;padding:28px clamp(24px,5vw,80px);display:flex;justify-content:space-between;font-size:12px;color:#788b7d}@media(max-width:700px){.topbar{height:68px}.topbar nav{gap:16px;font-size:12px}.hero{padding-top:74px;padding-bottom:70px}.hero-copy{font-size:16px}.feature-grid{grid-template-columns:1fr;gap:26px}.article-row{grid-template-columns:1fr 26px;gap:10px}.article-number{grid-column:1/-1;margin:0}.article-page{padding-top:55px}.article-page article{margin-top:50px}footer{gap:20px}}
@@ -1,6 +1,7 @@
1
1
  import { build } from 'esbuild';
2
- import { cp, mkdir } from 'node:fs/promises';
2
+ import { cp, mkdir, rm } from 'node:fs/promises';
3
3
  import { execFileSync } from 'node:child_process';
4
+ await rm('dist',{recursive:true,force:true});
4
5
  execFileSync('node',['node_modules/@aotter/mantle/dist/cli/main.js','generate'],{stdio:'inherit'});
5
6
  // Reject older published SDKs that cannot advertise the mounted MCP endpoints.
6
7
  execFileSync('node',['node_modules/typescript/bin/tsc','--noEmit'],{stdio:'inherit'});
@@ -76,6 +76,8 @@ assert.match(publicPage,new RegExp(mediaUrl));
76
76
  assert.doesNotMatch(publicPage,/href="javascript:/);
77
77
  assert.match(publicPage,/<meta property="og:image"/);
78
78
  assert.match(publicPage,/<link rel="canonical"/);
79
+ assert.match(publicPage,/<link rel="alternate" type="text\/markdown"/);
80
+ assert.doesNotMatch(publicPage,/<a\b[^>]*href="[^"]+\.md(?:[?#][^"]*)?"/);
79
81
  assert.match(publicPage,/application\/ld\+json/);
80
82
  assert.match(await (await call(`/articles/${entry.id}.md`)).text(),/Published body from Mantle/);
81
83
  assert.match(await (await call('/articles')).text(),new RegExp(entry.id));
@@ -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) {
@@ -23,7 +23,7 @@ function assemble(env:Env) {
23
23
  mountWeb(app,get);
24
24
  mountMcp(app,get,auth);
25
25
  mountMedia(app,auth,env);
26
- app.get('/health',async()=>{await get();return Response.json({ok:true,storage:'D1',auth:'ChatGPT Sites',mantle:'0.1.2'});});
26
+ app.get('/health',async()=>{await get();return Response.json({ok:true,storage:'D1',auth:'ChatGPT Sites'});});
27
27
  app.get('/admin/sign-in',async c=>{
28
28
  if(c.req.header('cookie')?.split(';').some(v=>v.trim()==='mantle-sites-signout=1')) {
29
29
  c.header('Set-Cookie','mantle-sites-signout=; Path=/admin/sign-in; HttpOnly; SameSite=Strict; Max-Age=0'+(env.PUBLIC_ORIGIN.startsWith('https:')?'; Secure':''));
@@ -26,7 +26,7 @@ export function mountWeb(app:Hono<{Bindings:Env}>,get:()=>Promise<MantleAdminRun
26
26
  const title=String(entry.data.title??site.title);
27
27
  const image=coverUrl?`<figure class="article-cover"><img src="${escape(coverUrl)}" alt="${escape(cover?.alt||title)}" loading="eager"></figure>`:'';
28
28
  const body=micromark(String(entry.data.body??''),{allowDangerousHtml:false,allowDangerousProtocol:false});
29
- return c.html(shell(`${title}|${site.title}`,renderSeoTagsHtml(seo),`<main class="article-page"><a class="back" href="/articles">← 返回文章</a><article><p class="eyebrow">ARTICLE · ${new Date(entry.updatedAt).toLocaleDateString('zh-TW',{timeZone:'UTC'})}</p><h1>${escape(title)}</h1>${entry.data.summary?`<p class="lede">${escape(entry.data.summary)}</p>`:''}${image}<div class="prose">${body}</div>${seo.alternateMarkdown?`<a class="markdown-link" href="${articlePath(entry.id)}.md">閱讀 Markdown 版本 ↗</a>`:''}</article></main>`));});
29
+ return c.html(shell(`${title}|${site.title}`,renderSeoTagsHtml(seo),`<main class="article-page"><a class="back" href="/articles">← 返回文章</a><article><p class="eyebrow">ARTICLE · ${new Date(entry.updatedAt).toLocaleDateString('zh-TW',{timeZone:'UTC'})}</p><h1>${escape(title)}</h1>${entry.data.summary?`<p class="lede">${escape(entry.data.summary)}</p>`:''}${image}<div class="prose">${body}</div></article></main>`));});
30
30
  // ponytail: one discovery page is enough for this lab; expose cursor pages when published articles exceed the SDK page limit.
31
31
  app.get('/llms.txt',async c=>{const {site,web}=await load();const page=await web.composeLlmsTxt.execute({collections:['articles'],locale:null,site,pathFor:e=>articlePath(e.id)});const body=page?.body??`# ${site.title}\n\n文章索引:${site.origin}/articles\n`;return c.text(body,200,{'Content-Type':'text/plain; charset=utf-8'});});
32
32
  // ponytail: first 1000 published URLs; add sitemap index parts only when the catalogue reaches that ceiling.
@@ -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
  }
@@ -9,15 +9,15 @@ Schema/View is example business data; `mantle generate` never invents it.
9
9
 
10
10
  For your own project, author package.json, manifests, Worker/provider config
11
11
  and TypeScript settings for your requirements. Pin all selected `@aotter/mantle*`
12
- dependencies to the same intended release. This reference depends on them via
13
- the `latest` dist-tag instead, so a fresh install always resolves the current
14
- stable release; Core's test runner substitutes its exact candidate in a
15
- disposable copy.
12
+ dependencies to the same intended release. The checked-in `latest` values are
13
+ placeholders for Core's disposable release test; resolve and pin one exact
14
+ version before installing a copied application.
16
15
 
17
16
  Outside the SDK workspace, with Node 22+ and pnpm 9+:
18
17
 
19
18
  ```sh
20
- pnpm install
19
+ MANTLE_VERSION=$(pnpm view @aotter/mantle@latest version)
20
+ pnpm add --save-exact "@aotter/mantle@$MANTLE_VERSION" "@aotter/mantle-cloudflare@$MANTLE_VERSION"
21
21
  pnpm check
22
22
  pnpm dev
23
23
  ```
@@ -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
  }
@@ -208,7 +208,7 @@ because the underlying host composition or a different provider succeeded.
208
208
 
209
209
  ```text
210
210
  Extend my existing ChatGPT Site with Mantle equipment checkout. Read the matching
211
- SDK's docs/handbook/sites/index.md, equipment-checkout.md and host-reference.md.
211
+ SDK's docs/handbook/chatgpt-sites/index.md, equipment-checkout.md and host-reference.md.
212
212
  Preserve current content, audience and R2 media. This guide is a design, not an
213
213
  installed app: implement and test the missing manifests, handlers and routes.
214
214
  Start with one identified kit and one fixed half-day slot per request. Include
@@ -0,0 +1,84 @@
1
+ ---
2
+ description: Generate a blank Mantle app for ChatGPT Sites, apply D1 migrations, and verify local Admin and MCP.
3
+ ---
4
+ # Generate a ChatGPT Sites app
5
+
6
+ `mantle generate --host chatgpt-sites` creates a blank home, a D1-backed Runtime,
7
+ public API and MCP, and Mantle Admin. It adds no article Schema, media bucket, or
8
+ demo content. Edit `src/home.ts` and add your own manifests and handlers as needed.
9
+
10
+ ```sh
11
+ mkdir my-site && cd my-site
12
+ npm init -y
13
+ npm pkg set type=module
14
+ npm install --save-exact @aotter/mantle
15
+ npx mantle generate --host chatgpt-sites
16
+ npm install
17
+ npx mantle generate
18
+ npm run build
19
+ npx wrangler d1 migrations apply my-site --local
20
+ ```
21
+
22
+ The first generate declares the exact Mantle package versions it needs; the
23
+ second runs after those packages are installed. Review `drizzle/0000_mantle.sql`
24
+ before applying it. The D1 name comes from `wrangler.jsonc`. Generation does not
25
+ apply a migration, provision a Site, or publish one. `npm run build` removes the
26
+ old `dist` before copying the current migrations and Admin assets.
27
+
28
+ For local Admin, copy `.dev.vars.example` to `.dev.vars`, set `OWNER_EMAIL` to
29
+ the email of the ChatGPT account that will own this Site, and keep
30
+ `PUBLIC_ORIGIN` equal to the local URL. Run `npm run dev`.
31
+ The plain local browser has no ChatGPT Sites identity, so it will redirect to
32
+ Sites sign-in and cannot prove owner access. To exercise the identity contract
33
+ locally, set `MANTLE_TEST_OWNER_EMAIL` to the same email and run
34
+ `npm run smoke:local`. That script refuses non-loopback
35
+ targets. Its identity headers are a local simulation; the production Worker
36
+ contains no identity-injection path and must run behind Sites' trusted
37
+ identity-stripping dispatcher. The generated Wrangler config disables the
38
+ independent `workers.dev` URL; do not add a direct Worker route. A passing
39
+ local smoke does not verify a deployed ChatGPT sign-in.
40
+
41
+ Without `OWNER_EMAIL`, sign-in grants no Mantle staff role. If the intended
42
+ owner signed in before configuration, their existing `sites_users` row remains
43
+ unprivileged. Configure the owner email and deliberately assign `role='owner'`
44
+ to that account's exact email and `chatgpt:` ID in D1, or reset a disposable
45
+ local database and sign in again. Mantle never gives ownership to the first
46
+ visitor.
47
+
48
+ The anonymous public MCP endpoint is `/api/mcp`; it discovers only public
49
+ manifest capabilities. `/api/mcp/staff` uses the current Sites browser session
50
+ and a Mantle staff role. Admin advertises these endpoints, and Admin WebMCP
51
+ works in supporting browsers. The staff endpoint is not a remote OAuth MCP
52
+ connector. A remote agent needs a separately implemented verified OAuth flow.
53
+
54
+ When a Schema changes, run `npx mantle generate --check` to detect drift, then
55
+ `npx mantle generate`. Review the newly appended SQL and apply pending D1
56
+ migrations before deploying code that expects the new Schema. Previous
57
+ migration files remain untouched; an unapplied migration keeps the old database
58
+ fingerprint, and managed boot refuses to use an older database. Sites owns
59
+ `.openai/hosting.json` project metadata and production D1 migration delivery.
60
+
61
+ Replacing a Schema's `uniqueIndexes` tuple requires an explicit reviewed
62
+ migration. With the old local D1 migration applied, edit the Manifest and run
63
+ `npx mantle generate --review-unique-indexes --check` to inspect the proposed
64
+ source/target fingerprints, affected tuples and checksum without writing. Then
65
+ run `npx mantle generate --review-unique-indexes`, review the new
66
+ `drizzle/meta/000N_mantle.review.json` and matching SQL, and apply that SQL
67
+ through the normal Sites D1 migration path. The CLI checks **local D1 only**
68
+ for duplicate target tuples and reports a count and sample before writing;
69
+ inspect production data separately before applying there. SQLite also refuses
70
+ to create the new unique index if production has conflicts; do
71
+ not deploy the target Worker until the migration has succeeded. Old Workers
72
+ cannot safely reactivate after the old uniqueness guarantee is removed.
73
+ This option is limited to managed Sites SQLite and unique tuple changes;
74
+ field conversions and runtime-managed CF boot still reject destructive
75
+ changes.
76
+
77
+ For a complete article, media, and publishing example, use the
78
+ [Sites reference](../../examples/host-chatgpt-sites/README.md).
79
+
80
+ ## Source
81
+
82
+ - [Sites generator](../../../packages/mantle/src/cli/generate-sites.ts)
83
+ - [Generated Sites templates](../../../packages/mantle/templates/chatgpt-sites)
84
+ - [Runnable Sites reference](../../examples/host-chatgpt-sites/README.md)
@@ -16,6 +16,10 @@ repository. Sites owns hosting, visitor access, and ChatGPT sign-in; Mantle owns
16
16
  the content model, staff roles, and publishing workflow. For platform access and
17
17
  sharing options, see [OpenAI's Sites documentation](https://learn.chatgpt.com/docs/sites).
18
18
 
19
+ For a new project, start with the [blank generated app](./generated-app.md).
20
+ The article reference below demonstrates a complete editorial Site with R2
21
+ media after you choose that content model.
22
+
19
23
  ## What you get
20
24
 
21
25
  | Capability | In the Mantle Sites reference |
@@ -34,12 +38,17 @@ boundaries.
34
38
 
35
39
  ## Start with the supported SDK
36
40
 
37
- You need Node.js 22+, pnpm 9+, and Sites access with D1 and R2 available for your
38
- project. Follow the reference's local build and smoke test before deployment.
39
- Keep your application outside the SDK checkout. The included article schema is
40
- a working example; adapt its fields and lifecycle to your team's content.
41
+ You need Node.js 22+ and Sites access with D1. Start with the
42
+ [blank generated app](./generated-app.md) to choose your own content model;
43
+ it requires no R2 bucket. The article reference also needs R2 for cover
44
+ images. Build and smoke-test locally before deployment, and keep your
45
+ application outside the SDK checkout.
46
+
47
+ ## Follow the article reference when it fits
41
48
 
42
- ## Publish your first article
49
+ The steps below apply to the optional prebuilt article example. A new blank
50
+ generated Site has no article Schema, media bucket or content; add those only
51
+ when your requirements call for them.
43
52
 
44
53
  1. **Prepare the application.** Follow the
45
54
  [runnable reference](../../examples/host-chatgpt-sites/README.md#install-and-run).
@@ -56,7 +65,8 @@ a working example; adapt its fields and lifecycle to your team's content.
56
65
  Markdown body. Upload an image in the media library and select it as the
57
66
  article's cover. The draft's public article URL should return 404.
58
67
  5. **Publish and read.** Publish the article and open `/articles`. Follow its
59
- link to check the body and cover, then open its `.md` version. For a public
68
+ link to check the body and cover, then confirm the page head advertises
69
+ its `.md` version with `rel="alternate"` and `type="text/markdown"`. For a public
60
70
  Site, repeat this while signed out. A restricted Site still enforces its
61
71
  audience settings before visitors reach these pages.
62
72
  6. **Verify control.** Unpublish the article and confirm its detail page returns
@@ -67,23 +77,34 @@ For later content edits, return to Mantle Admin. Changes to the application's
67
77
  schema, code, or deployment settings follow the reference's migration and Sites
68
78
  deployment workflow.
69
79
 
80
+ ## Maintain content with an agent
81
+
82
+ The owner can use Admin WebMCP in a browser that supports it to let an agent
83
+ discover staff tools for drafts, publishing, and other permitted operations.
84
+ Open `/admin/dev/docs/webmcp` to see that browser surface and
85
+ `/admin/dev/docs/mcp` to see the Site's `/api/mcp` public read-only endpoint and `/api/mcp/staff`
86
+ staff endpoint. The latter uses the current Sites browser session and checks
87
+ the Mantle staff role on each request. It is useful to same-origin browser
88
+ code, but is not a remote OAuth MCP connector for a desktop agent. A remote
89
+ staff connector needs its own verified OAuth flow; see the [host reference](./host-reference.md#remote-mcp-is-a-separate-gate).
90
+
70
91
  ## Ask your agent to set it up
71
92
 
72
- Install the Mantle agent plugin, or `@aotter/mantle` itself, then describe your
73
- audience and content requirements:
93
+ Install the Mantle skill and exact SDK version, then describe your audience
94
+ and content requirements:
74
95
 
75
96
  ```text
76
97
  Build a content site with ChatGPT Sites and Mantle. Read
77
- handbook/sites/index.md and examples/host-chatgpt-sites/README.md from the
78
- installed Mantle docs, and follow that reference's install and run steps.
79
- Adapt the article example to my content requirements. Include ChatGPT
80
- sign-in, Mantle staff roles, cover uploads, and published article pages.
98
+ handbook/chatgpt-sites/generated-app.md from the installed Mantle docs, and
99
+ start from the blank generated Site when that CLI supports it. Use the article
100
+ reference only if its content model matches my requirements. Include ChatGPT
101
+ sign-in and Mantle staff roles; add cover uploads only if I request media.
81
102
  Run the local checks and show me a draft-to-publish walkthrough before
82
103
  deploying through Sites. Keep remote staff OAuth MCP outside this scope.
83
104
  ```
84
105
 
85
106
  For business rules beyond content, add: "use custom `ref` handlers and
86
- application-owned tables as described in handbook/sites/equipment-checkout.md."
107
+ application-owned tables as described in handbook/chatgpt-sites/equipment-checkout.md."
87
108
 
88
109
  ## Beyond content: operational workflows
89
110
 
@@ -100,6 +121,7 @@ evidence from the equipment and Slack work still required in your application.
100
121
  ## Integration details
101
122
 
102
123
  - [Host reference](./host-reference.md): identity, migrations, R2 routes, MCP, and deployment checks.
124
+ - [Generated blank app](./generated-app.md): local composition, D1 migration, Admin, and MCP.
103
125
  - [Equipment checkout](./equipment-checkout.md): custom business rules and external API delivery without payments.
104
126
  - [Runnable application](../../examples/host-chatgpt-sites/README.md): installation, build artifacts, and smoke tests.
105
127
  - [Conventional Cloudflare Worker](../cloudflare/conventional-worker.md): the separate path for a Worker you deploy directly.
@@ -7,11 +7,28 @@ Conventional Auth is chosen by one variable, `MANTLE_AUTH_MODE`, and fails close
7
7
 
8
8
  ## Local Admin: email OTP
9
9
 
10
- The local human path does not use `MANTLE_AUTH_MODE` or GitHub. Pass `auth` to `createMantleWorker` with `email-otp`, `ConsoleEmailSender`, and `bootstrapOwner.match: "email"`. The one-time code is printed on the wrangler log. See [Quickstart: local Admin](../start/quickstart-admin.md). `ConsoleEmailSender` is for `wrangler dev` only; production needs a real sender. The official example binds `127.0.0.1:8787` and sets `PUBLIC_ORIGIN` to that origin; Better Auth rejects OTP with `INVALID_ORIGIN` when they diverge from the origin wrangler prints.
10
+ The generated CF app writes `.dev.vars.example`. Copy it to `.dev.vars` for
11
+ local use, then set `MANTLE_AUTH_MODE=local-otp`, the real `ADMIN_EMAIL`, a
12
+ random `BETTER_AUTH_SECRET`, and the exact loopback `PUBLIC_ORIGIN`. It configures
13
+ its email-OTP auth factory only for a
14
+ loopback origin. If you author the Worker yourself, pass `auth` to
15
+ `createMantleWorker` with `email-otp`, `ConsoleEmailSender`, and
16
+ `bootstrapOwner.match: "email"`; that factory does not read the conventional
17
+ mode matrix. The one-time code is printed in wrangler logs. See
18
+ [Quickstart: local Admin](../start/quickstart-admin.md). `ConsoleEmailSender`
19
+ is for `wrangler dev` only; production needs a real sender. Match
20
+ `PUBLIC_ORIGIN` to wrangler's actual `127.0.0.1:8787` origin or Better Auth
21
+ will reject OTP with `INVALID_ORIGIN`.
22
+
23
+ For production, the generated Worker's local OTP branch is disabled outside
24
+ loopback. Configure the conventional `self-managed` or `hosted` Auth mode,
25
+ including its required provider credentials and `ADMIN_GITHUB_LOGIN`, instead
26
+ of editing the CLI-owned Worker. An application that needs production email
27
+ delivery should author its own Worker Auth factory and real `EmailSender`.
11
28
 
12
29
  With `auth` set, the mode matrix below is not read. Core still owns `/admin` and `/api/auth/*`.
13
30
 
14
- For production email OTP, keep the custom `createAuth()` factory, replace
31
+ For production email OTP in a manually authored Worker, keep the custom `createAuth()` factory, replace
15
32
  `ConsoleEmailSender` with the application's production `EmailSender`, and keep
16
33
  `bootstrapOwner: { match: "email", value: <owner email> }`. Store the sender
17
34
  credentials and `BETTER_AUTH_SECRET` as Worker secrets. If the application has
@@ -186,26 +186,19 @@ the interface is one `record(event)` method.
186
186
  "triggers": { "crons": ["*/5 * * * *"] }
187
187
  ```
188
188
 
189
- A `scheduled()` handler reuses the fetch path's runtime and invokes a Procedure that has no Trigger of its own:
189
+ A `schedule` Trigger declares the Procedure, and the Worker export reuses the same runtime as `fetch`:
190
190
 
191
191
  ```ts
192
- import { bindMantle } from "../.mantle/generated/mantle.js";
193
-
194
192
  const worker = createMantleWorker<Env>({ plan, handlers });
195
193
 
196
194
  export default {
197
195
  fetch: worker.fetch,
198
- async scheduled(_controller, env, ctx) {
199
- const mantle = bindMantle(await worker.getRuntime(env));
200
- const result = await mantle.procedures.sweepExpiredOrders(
201
- { now: Date.now() },
202
- { user: null, staff: null, env, waitUntil: (p) => ctx.waitUntil(p) },
203
- );
204
- if (!result.ok) throw new Error(`sweep failed: ${result.diagnostic.code}`);
205
- },
196
+ scheduled: worker.scheduled,
206
197
  } satisfies ExportedHandler<Env>;
207
198
  ```
208
199
 
200
+ Register the exact enabled `cron` expression in `wrangler.jsonc`; Mantle does not create provider triggers. The Procedure takes `{}` input and sees `ctx.schedule.id` for retry-safe idempotency. See [Trigger](../reference/trigger.md#schedule-source).
201
+
209
202
  ## Durable Objects (application-owned)
210
203
 
211
204
  Durable Objects are not a Core primitive. Use one as an application-owned coordinator when a decision must be serialized, for example reserving inventory exactly once. Export the class from the Worker entry, then bind and migrate it:
@@ -4,26 +4,26 @@ description: "Mantle on ChatGPT Sites now has its own integration guide."
4
4
  # Mantle on ChatGPT Sites
5
5
 
6
6
  ChatGPT Sites has a dedicated section in the Mantle handbook. Start with
7
- [Mantle on ChatGPT Sites](../sites/index.md) for installation, supported
7
+ [Mantle on ChatGPT Sites](../chatgpt-sites/index.md) for installation, supported
8
8
  capabilities, and your first published article. This page preserves earlier
9
9
  links to the host checklist.
10
10
 
11
11
  ## First deployment
12
12
 
13
- See [the host deployment checklist](../sites/host-reference.md#first-deployment).
13
+ See [the host deployment checklist](../chatgpt-sites/host-reference.md#first-deployment).
14
14
 
15
15
  ## Media with only an R2 binding
16
16
 
17
- See [R2 media integration](../sites/host-reference.md#media-with-only-an-r2-binding).
17
+ See [R2 media integration](../chatgpt-sites/host-reference.md#media-with-only-an-r2-binding).
18
18
 
19
19
  ## Remote MCP is a separate gate
20
20
 
21
- See [MCP support and authentication](../sites/host-reference.md#remote-mcp-is-a-separate-gate).
21
+ See [MCP support and authentication](../chatgpt-sites/host-reference.md#remote-mcp-is-a-separate-gate).
22
22
 
23
23
  ## Capability boundaries
24
24
 
25
- See [host capability boundaries](../sites/host-reference.md#capability-boundaries).
25
+ See [host capability boundaries](../chatgpt-sites/host-reference.md#capability-boundaries).
26
26
 
27
27
  ## Source
28
28
 
29
- See [integration sources](../sites/host-reference.md#source).
29
+ See [integration sources](../chatgpt-sites/host-reference.md#source).
@@ -3,7 +3,10 @@ description: Assemble a Mantle Worker with createMantleWorker, add handlers and
3
3
  ---
4
4
  # The conventional Worker
5
5
 
6
- `createMantleWorker` is the normal Cloudflare entry for a Mantle application. This page covers its options, what the facade owns, how to add application handlers and routes, and the readiness and reserved-path rules that keep those extensions safe.
6
+ New applications can start with `mantle generate --host cf`, which writes a
7
+ blank home, Worker entry and selected Admin/API/MCP wiring. This page covers
8
+ the lower-level `createMantleWorker` facade for applications that author their
9
+ own Worker entry: its options, extension seam, readiness and reserved paths.
7
10
 
8
11
  ## Minimal entry
9
12
 
@@ -14,7 +17,7 @@ import { plan } from "../.mantle/generated/mantle.js";
14
17
  export default createMantleWorker({ plan, cacheScope: "my-site-production" });
15
18
  ```
16
19
 
17
- `plan` is the sealed plan that `mantle generate` writes to `.mantle/generated/mantle.ts`. With only `plan`, the Worker serves public View REST, HTTP Triggers, Admin, Auth, OAuth and MCP. It renders no public pages and `/` is a 404; see [Public web](./public-web.md). Local Admin with email OTP replaces Auth construction; see [Quickstart: local Admin](../start/quickstart-admin.md). Admin still requires the `ASSETS` binding.
20
+ `plan` is the sealed plan that `mantle generate` writes to `.mantle/generated/mantle.ts`. With only `plan`, this manually assembled Worker serves public View REST, HTTP Triggers, Admin, Auth, OAuth and MCP. It renders no public pages and `/` is a 404; see [Public web](./public-web.md). A newly generated full CF project wires a separate editable blank home. Local Admin with email OTP replaces Auth construction; see [Quickstart: local Admin](../start/quickstart-admin.md). Admin still requires the `ASSETS` binding.
18
21
 
19
22
  ## Options
20
23
 
@@ -64,9 +67,10 @@ If `auth.ready` rejects, the memoized assembly is evicted so the next request re
64
67
  const worker = createMantleWorker<Env>({ plan });
65
68
  worker.fetch(request, env, ctx); // HTTP entry
66
69
  worker.getRuntime(env); // the runtime fetch uses, resolved after auth.ready
70
+ worker.scheduled(controller, env, ctx); // declared scheduled Procedure Triggers
67
71
  ```
68
72
 
69
- Queue and scheduled handlers call `worker.getRuntime(env)` and then generated `bindMantle(runtime)` so they reuse the assembled runtime and never write Mantle tables directly. See [Bindings](./bindings.md#cron-triggers) and [Deferred hooks](./deferred-hooks-queues.md).
73
+ Queue handlers call `worker.getRuntime(env)` and then generated `bindMantle(runtime)` so they reuse the assembled runtime. Scheduled Procedure Triggers use `worker.scheduled` with expressions registered in Wrangler. See [Bindings](./bindings.md#cron-triggers), [Trigger](../reference/trigger.md#schedule-source), and [Deferred hooks](./deferred-hooks-queues.md).
70
74
 
71
75
  ## Readiness rule
72
76
 
@@ -10,7 +10,7 @@ This page is the production checklist for a Mantle Worker on Cloudflare: what to
10
10
  - Pin every `@aotter/mantle*` package to one exact version and commit the lockfile. Install with `pnpm install --frozen-lockfile` (or `npm ci`) from then on. See [Project and CLI](../start/project-and-cli.md).
11
11
  - Set `PUBLIC_ORIGIN` to the real HTTPS origin, without a trailing slash. It drives canonical URLs, `.md` mirrors, `llms.txt`, the MCP resource and the OAuth callback. If a static documentation build also emits absolute URLs, give it the same value.
12
12
  - Set the production D1 `database_id` (and `account_id` if your deployment needs it) in `wrangler.jsonc`. A local `database_name` is not a production identifier, and local D1 is not production data.
13
- - Choose production Auth: replace local console email delivery with a real `EmailSender`, or configure `MANTLE_AUTH_MODE` for GitHub OAuth or hosted Auth. Store secrets with `wrangler secret put`. See [Authentication](./authentication.md).
13
+ - Choose production Auth: a generated CF app uses `MANTLE_AUTH_MODE=self-managed` or `hosted` with `ADMIN_GITHUB_LOGIN` and the matching provider credentials. Local `local-otp` works only on loopback. If you author your own Worker Auth factory, replace console email delivery with a real `EmailSender`. Store secrets with `wrangler secret put`. See [Authentication](./authentication.md).
14
14
  - Keep `compatibility_flags: ["nodejs_compat", "global_fetch_strictly_public"]`.
15
15
  - Enable observability:
16
16
 
@@ -47,7 +47,8 @@ wrangler deploy
47
47
 
48
48
  ## Post-deploy verification
49
49
 
50
- Probe the deployed origin, not `wrangler dev`:
50
+ Probe the deployed origin, not `wrangler dev`. Run only the probes for surfaces
51
+ the application mounts and configures:
51
52
 
52
53
  | Probe | Expect |
53
54
  |---|---|
@@ -55,7 +56,7 @@ Probe the deployed origin, not `wrangler dev`:
55
56
  | `GET /<locale>/<segment>/<slug>` and `GET /<locale>/<segment>/<slug>.md` | `200` HTML and Markdown for a published entry |
56
57
  | `GET /llms.txt`, `GET /sitemap.xml`, `GET /robots.txt` | `200` |
57
58
  | `GET /<locale>/<segment>/does-not-exist` | `404` from your `notFoundRenderer` |
58
- | `GET /mcp` without credentials | `401` with `WWW-Authenticate` |
59
+ | `POST /mcp/staff` without credentials | `401` with `WWW-Authenticate`; `GET /mcp` is `405` |
59
60
  | `GET /admin` | Sign-in page; sign in with the `ADMIN_GITHUB_LOGIN` account |
60
61
 
61
62
  Then check the cache: a second anonymous `GET` of a public page should show `cf-cache-status: HIT`; publish a change in Admin and the next request should be a `MISS`. Sample latency with: