create-ailk 0.8.0 → 0.10.0

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 (238) hide show
  1. package/README.md +71 -3
  2. package/component-catalog.json +279 -45
  3. package/dist/add-module.d.ts +20 -0
  4. package/dist/add-module.js +43 -7
  5. package/dist/add-route.d.ts +56 -0
  6. package/dist/add-route.js +286 -0
  7. package/dist/apply-design-md.d.ts +7 -0
  8. package/dist/apply-design-md.js +50 -0
  9. package/dist/apply-design.d.ts +64 -0
  10. package/dist/apply-design.js +273 -0
  11. package/dist/claude-md.d.ts +111 -0
  12. package/dist/claude-md.js +334 -0
  13. package/dist/cli.js +104 -5
  14. package/dist/derive-architecture.js +41 -36
  15. package/dist/design-compile.d.ts +71 -0
  16. package/dist/design-compile.js +98 -0
  17. package/dist/design-package.d.ts +83 -0
  18. package/dist/design-package.js +468 -0
  19. package/dist/design-presets.d.ts +86 -0
  20. package/dist/design-presets.js +642 -0
  21. package/dist/design-scaffold.d.ts +76 -0
  22. package/dist/design-scaffold.js +220 -0
  23. package/dist/index.d.ts +2 -1
  24. package/dist/index.js +4 -1
  25. package/dist/legacy-nav-renderers.d.ts +45 -0
  26. package/dist/legacy-nav-renderers.js +545 -0
  27. package/dist/lib/auth-secret-file.d.ts +19 -0
  28. package/dist/lib/auth-secret-file.js +112 -0
  29. package/dist/lib/design-checks.d.ts +78 -0
  30. package/dist/lib/design-checks.js +174 -0
  31. package/dist/lib/license-key-files.d.ts +9 -0
  32. package/dist/lib/license-key-files.js +2 -2
  33. package/dist/lib/module-license-gate.d.ts +48 -32
  34. package/dist/lib/module-license-gate.js +117 -8
  35. package/dist/lib/render-verdict.d.ts +52 -0
  36. package/dist/lib/render-verdict.js +77 -0
  37. package/dist/lib/scaffold-render-sweep.d.ts +105 -0
  38. package/dist/lib/scaffold-render-sweep.js +236 -0
  39. package/dist/module-architecture.d.ts +19 -16
  40. package/dist/module-architecture.js +67 -23
  41. package/dist/parse-args.d.ts +12 -2
  42. package/dist/parse-args.js +74 -13
  43. package/dist/programmatic.d.ts +30 -6
  44. package/dist/programmatic.js +216 -19
  45. package/dist/site-chrome.d.ts +19 -9
  46. package/dist/site-chrome.js +23 -13
  47. package/dist/site-manifest.d.ts +25 -0
  48. package/dist/site-manifest.js +254 -0
  49. package/dist/site-roster.d.ts +41 -0
  50. package/dist/site-roster.js +141 -8
  51. package/dist/starter-grid.d.ts +16 -63
  52. package/dist/starter-grid.js +102 -84
  53. package/dist/surfaces.d.ts +41 -9
  54. package/dist/surfaces.js +200 -83
  55. package/dist/template-manifest.d.ts +30 -13
  56. package/dist/template-manifest.js +53 -28
  57. package/package.json +6 -3
  58. package/release-scope.json +25 -0
  59. package/site-templates/_base/content/en/UseCase/persona-1.mdx +7 -6
  60. package/site-templates/_base/content/en/UseCase/persona-2.mdx +7 -6
  61. package/site-templates/_base/content/en/UseCase/persona-3.mdx +7 -6
  62. package/site-templates/designs/minimalism/DESIGN.md +98 -6
  63. package/site-templates/designs/minimalism/sample/README.md +48 -0
  64. package/site-templates/{minimalism-site → designs/minimalism/sample}/content/en/HomePage/home.mdx +1 -1
  65. package/site-templates/designs/standard/DESIGN.md +98 -6
  66. package/site-templates/marketing-site/DESIGN.md +98 -6
  67. package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +60 -27
  68. package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +50 -30
  69. package/site-templates/marketing-site-free/DESIGN.md +98 -6
  70. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +60 -24
  71. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +44 -27
  72. package/site-templates/marketing-site-free/ailk-template.json +1 -1
  73. package/site-templates/saas-product/DESIGN.md +98 -6
  74. package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.json +127 -18
  75. package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.md +74 -20
  76. package/site-templates/shapes/local-business-service/ailk-template.json +1 -1
  77. package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.json +127 -18
  78. package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.md +74 -20
  79. package/site-templates/shapes/organization-service/ailk-template.json +1 -1
  80. package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.json +127 -18
  81. package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.md +75 -21
  82. package/site-templates/shapes/person-service/ailk-template.json +1 -1
  83. package/site-templates/shapes/person-service/content/en/AboutPage/about.mdx +1 -1
  84. package/site-templates/shapes/person-service/content/en/HomePage/home.mdx +2 -2
  85. package/site-templates/shapes/person-service/content/en/PortfolioList/portfolio.mdx +4 -2
  86. package/site-templates/shapes/person-service/content/en/ServicesList/services.mdx +1 -1
  87. package/templates/.claude/agents/web.md +10 -8
  88. package/templates/.claude/rules/architecture.md +17 -7
  89. package/templates/.claude/skills/README.md +3 -2
  90. package/templates/.claude/skills/add-auth/SKILL.md +142 -90
  91. package/templates/.claude/skills/add-auth/references/env-vars.md +32 -17
  92. package/templates/.claude/skills/add-auth/references/oauth-app-setup.md +17 -8
  93. package/templates/.claude/skills/add-page/SKILL.md +241 -238
  94. package/templates/.claude/skills/add-page/references/page-types.md +94 -86
  95. package/templates/.claude/skills/add-page/scripts/site-pages.mjs +87 -0
  96. package/templates/.claude/skills/add-page/templates/comparison-page.mdx.tmpl +99 -0
  97. package/templates/.claude/skills/add-page/templates/content.mdx.tmpl +8 -18
  98. package/templates/.claude/skills/add-page/templates/flow-landing-page.mdx.tmpl +43 -0
  99. package/templates/.claude/skills/add-page/templates/report-page.mdx.tmpl +52 -0
  100. package/templates/.claude/skills/add-page/templates/reports-list.mdx.tmpl +38 -0
  101. package/templates/.claude/skills/check-site/SKILL.md +51 -18
  102. package/templates/.claude/skills/check-site/references/common-errors.md +12 -9
  103. package/templates/.claude/skills/check-site/references/findings-glossary.md +4 -4
  104. package/templates/.claude/skills/check-site/references/severity-levels.md +3 -3
  105. package/templates/.claude/skills/fill-page/SKILL.md +26 -7
  106. package/templates/.claude/skills/fill-page/profiles/plumbing.yaml +1 -1
  107. package/templates/.claude/skills/setup-deploy/SKILL.md +75 -38
  108. package/templates/.claude/skills/update-page/SKILL.md +117 -0
  109. package/templates/.claude/skills/update-page/scripts/update-page.ts +640 -0
  110. package/templates/.claude/skills/update-page/templates/redirects.mjs.tmpl +31 -0
  111. package/templates/.claude/skills/upgrade-plan/SKILL.md +12 -8
  112. package/templates/.env.example +23 -2
  113. package/templates/.github/workflows/ci.yml +4 -0
  114. package/templates/.prettierignore +1 -0
  115. package/templates/CLAUDE.md +146 -103
  116. package/templates/DESIGN.md +98 -6
  117. package/templates/README.md +50 -38
  118. package/templates/apps/api/.env.example +2 -1
  119. package/templates/apps/api/src/__tests__/commerce-claim-registration.test.ts +2 -0
  120. package/templates/apps/api/src/__tests__/server.test.ts +116 -0
  121. package/templates/apps/api/src/lib/stripe.ts +9 -0
  122. package/templates/apps/api/src/middleware/__tests__/auth.optional-cookie.test.ts +140 -0
  123. package/templates/apps/api/src/middleware/auth.ts +54 -0
  124. package/templates/apps/api/src/routes/checkout/__tests__/session.test.ts +5 -2
  125. package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +628 -0
  126. package/templates/apps/api/src/routes/checkout/sessions.ts +222 -24
  127. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +1 -1
  128. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +5 -1
  129. package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +521 -20
  130. package/templates/apps/api/src/routes/webhooks/stripe.ts +124 -32
  131. package/templates/apps/api/src/server.ts +28 -0
  132. package/templates/apps/api/src/services/__tests__/revalidation.test.ts +90 -15
  133. package/templates/apps/api/src/services/checkout-buyer.ts +72 -0
  134. package/templates/apps/api/src/services/revalidation.ts +60 -15
  135. package/templates/apps/mcp/__tests__/tools/create_project.test.ts +30 -24
  136. package/templates/apps/mcp/src/tools/purchase.ts +4 -2
  137. package/templates/apps/web/.env.example +13 -0
  138. package/templates/apps/web/CLAUDE.md +22 -28
  139. package/templates/apps/web/app/[locale]/(auth)/layout.tsx +1 -1
  140. package/templates/apps/web/app/[locale]/(authed)/leads/__tests__/page.test.tsx +24 -0
  141. package/templates/apps/web/app/[locale]/(authed)/leads/page.tsx +17 -6
  142. package/templates/apps/web/app/[locale]/[slug]/[detail]/__tests__/page.test.tsx +110 -0
  143. package/templates/apps/web/app/[locale]/[slug]/[detail]/page.tsx +60 -0
  144. package/templates/apps/web/app/[locale]/[slug]/page.tsx +1 -1
  145. package/templates/apps/web/app/[locale]/alternatives/__tests__/page.test.tsx +161 -0
  146. package/templates/apps/web/app/[locale]/alternatives/page.tsx +104 -0
  147. package/templates/apps/web/app/[locale]/{use-cases → for}/[slug]/page.tsx +4 -3
  148. package/templates/apps/web/app/[locale]/for/__tests__/page-renderer-migration.test.tsx +73 -0
  149. package/templates/apps/web/app/[locale]/{use-cases → for}/page.tsx +8 -7
  150. package/templates/apps/web/app/[locale]/glossary/page.tsx +1 -1
  151. package/templates/apps/web/app/[locale]/layout.tsx +35 -19
  152. package/templates/apps/web/app/[locale]/newsletter/page.tsx +1 -1
  153. package/templates/apps/web/app/[locale]/portfolio/[slug]/page.tsx +1 -1
  154. package/templates/apps/web/app/[locale]/portfolio/page.tsx +1 -1
  155. package/templates/apps/web/app/[locale]/vs/[slug]/__tests__/page.test.tsx +112 -0
  156. package/templates/apps/web/app/[locale]/vs/[slug]/page.tsx +70 -0
  157. package/templates/apps/web/app/api/revalidate/__tests__/route.test.ts +236 -44
  158. package/templates/apps/web/app/api/revalidate/route.ts +78 -36
  159. package/templates/apps/web/lib/__tests__/site-brand.test.ts +79 -0
  160. package/templates/apps/web/lib/__tests__/site-theme.test.ts +104 -11
  161. package/templates/apps/web/lib/__tests__/structured-data.test.tsx +21 -4
  162. package/templates/apps/web/lib/locale-guard.ts +18 -0
  163. package/templates/apps/web/lib/meet-placeholder.ts +5 -0
  164. package/templates/apps/web/lib/nav-config.ts +14 -13
  165. package/templates/apps/web/lib/site-brand.tsx +13 -9
  166. package/templates/apps/web/lib/site-logo.tsx +36 -0
  167. package/templates/apps/web/lib/site-theme.ts +46 -26
  168. package/templates/apps/web/messages/en.json +23 -9
  169. package/templates/apps/web/middleware.ts +28 -4
  170. package/templates/content/en/BookCall/book-a-call.mdx +80 -0
  171. package/templates/content/en/BookCall/onsite-estimate.mdx +56 -0
  172. package/templates/content/en/BookCall/onsite-service.mdx +56 -0
  173. package/templates/content/en/BookCall/start-project.mdx +56 -0
  174. package/templates/content/en/ContactPage/contact.mdx +40 -1
  175. package/templates/content/en/HomePage/home.mdx +2 -6
  176. package/templates/content/en/LandingPage/meet.mdx +59 -0
  177. package/templates/content/en/UseCasesList/{use-cases.mdx → for.mdx} +19 -18
  178. package/templates/database/CHANGELOG.md +7 -0
  179. package/templates/database/package.json +1 -1
  180. package/templates/package.json +4 -1
  181. package/templates/tools/check-claude-md.ts +128 -0
  182. package/versions.json +33 -33
  183. package/dist/modules/commerce/0.6.0.tar.gz +0 -0
  184. package/dist/modules/commerce/0.6.1.tar.gz +0 -0
  185. package/dist/modules/docs-blog/0.6.0.tar.gz +0 -0
  186. package/dist/modules/docs-blog/0.6.1.tar.gz +0 -0
  187. package/dist/modules/marketing-site/0.6.1.tar.gz +0 -0
  188. package/dist/modules/minimalism-site/0.6.1.tar.gz +0 -0
  189. package/dist/modules/multi-tenant/0.6.0.tar.gz +0 -0
  190. package/dist/modules/multi-tenant/0.6.1.tar.gz +0 -0
  191. package/dist/modules/saas-product/0.6.1.tar.gz +0 -0
  192. package/dist/modules/scheduling/0.6.0.tar.gz +0 -0
  193. package/dist/modules/scheduling/0.6.1.tar.gz +0 -0
  194. package/dist/modules/signal-collectors/0.6.0.tar.gz +0 -0
  195. package/dist/modules/signal-collectors/0.6.1.tar.gz +0 -0
  196. package/dist/modules/simple-service/0.6.1.tar.gz +0 -0
  197. package/dist/modules/workspace/0.6.0.tar.gz +0 -0
  198. package/dist/modules/workspace/0.6.1.tar.gz +0 -0
  199. package/site-templates/_base/content/en/BookCall/book-call.mdx +0 -52
  200. package/site-templates/minimalism-site/DESIGN.md +0 -162
  201. package/site-templates/minimalism-site/TEMPLATE-MANIFEST.json +0 -506
  202. package/site-templates/minimalism-site/TEMPLATE-MANIFEST.md +0 -458
  203. package/site-templates/minimalism-site/ailk-template.json +0 -23
  204. package/site-templates/minimalism-site/content/en/AboutPage/about.mdx +0 -110
  205. package/site-templates/minimalism-site/content/en/BookCall/book-call.mdx +0 -52
  206. package/site-templates/minimalism-site/content/en/ContactPage/contact.mdx +0 -34
  207. package/site-templates/minimalism-site/content/en/FAQPage/faq.mdx +0 -57
  208. package/site-templates/minimalism-site/content/en/LandingPage/referral.mdx +0 -77
  209. package/site-templates/minimalism-site/content/en/LandingPage/reports.mdx +0 -73
  210. package/site-templates/minimalism-site/content/en/PurchasePolicyPage/purchase-policy.mdx +0 -123
  211. package/site-templates/minimalism-site/content/en/ReportPage/annual-letter-2024.mdx +0 -52
  212. package/site-templates/minimalism-site/content/en/ReportPage/field-notes-2023.mdx +0 -52
  213. package/site-templates/minimalism-site/content/en/ReportPage/platform-shift-2025.mdx +0 -52
  214. package/site-templates/minimalism-site/content/en/ReportPage/state-of-the-market-2026.mdx +0 -52
  215. package/site-templates/minimalism-site/content/en/Service/first-engagement.mdx +0 -38
  216. package/site-templates/minimalism-site/content/en/Service/second-engagement.mdx +0 -38
  217. package/site-templates/minimalism-site/content/en/Service/third-engagement.mdx +0 -38
  218. package/site-templates/minimalism-site/content/en/ServicesList/services.mdx +0 -73
  219. package/site-templates/minimalism-site/flows/referral.flow.json +0 -71
  220. package/site-templates/minimalism-site/provenance.json +0 -4
  221. package/templates/.claude/skills/add-page/templates/route.tsx.tmpl +0 -28
  222. package/templates/.claude/skills/setup-deploy/references/env-vars-template.md +0 -175
  223. package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-31.552Z-fallback-1789916251590-037e9005.json +0 -8
  224. package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-33.840Z-fallback-1789916253842-b52659e9.json +0 -12
  225. package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-33.844Z-fallback-1789916253845-6b266f8c.json +0 -12
  226. package/templates/apps/web/app/[locale]/book-a-call/page.tsx +0 -50
  227. package/templates/apps/web/app/[locale]/use-cases/__tests__/page-renderer-migration.test.tsx +0 -72
  228. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/avatar-placeholder-1.svg +0 -0
  229. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/avatar-placeholder-2.svg +0 -0
  230. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/avatar-placeholder-3.svg +0 -0
  231. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/avatar-placeholder-4.svg +0 -0
  232. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/avatar-placeholder-5.svg +0 -0
  233. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/avatar-placeholder-6.svg +0 -0
  234. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/hero-backdrop.svg +0 -0
  235. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/logo-placeholder-1.svg +0 -0
  236. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/logo-placeholder-2.svg +0 -0
  237. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/logo-placeholder-3.svg +0 -0
  238. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/wordmark-placeholder.svg +0 -0
@@ -21,11 +21,14 @@
21
21
  * block, gets these defaults written, byte-identical to the monorepo's own
22
22
  * committed `apps/web/lib/site-chrome.ts`.
23
23
  *
24
- * A template's DESIGN supplies layout knobs too (#6638): `DESIGN_LAYOUTS` in
25
- * `template-manifest.ts`, read by `resolveSiteChrome`'s optional `design`
26
- * argument between an explicit `layout` and these defaults.
24
+ * A design supplies layout knobs too (#6638, widened #6813 D4): its OWN
25
+ * `layout` (a `DesignPackage`'s, read by `design-scaffold.ts`'s
26
+ * `resolveDesign`), passed as `resolveSiteChrome`'s optional `designLayout`
27
+ * argument between an explicit `layout` and these defaults. This module
28
+ * imports nothing from `template-manifest.ts` (the cycle that import used to
29
+ * close): an unrecognized design NAME now fails in `resolveDesign`, before
30
+ * this function ever sees one.
27
31
  */
28
- import { type StarterDesign } from "./template-manifest.js";
29
32
  /** The `nav` knob's vocabulary. `mark-only` → the layout passes Nav an empty
30
33
  * `navItems` (brand mark + the existing actions/theme/locale controls only). */
31
34
  export type SiteChromeNav = "full" | "mark-only";
@@ -75,12 +78,19 @@ export declare class TemplateLayoutError extends Error {
75
78
  */
76
79
  export declare function validateTemplateLayout(value: unknown, subject: string): TemplateLayout;
77
80
  /**
78
- * Fill every field a `TemplateLayout` leaves unset — from `design`'s
79
- * `DESIGN_LAYOUTS` entry when one is named (#6638), then from
80
- * `DEFAULT_SITE_CHROME`. The explicit `layout` wins field by field, so a
81
- * template can wear a design and still override one knob of it.
81
+ * Fill every field a `TemplateLayout` leaves unset — from `designLayout`
82
+ * (a design package's own resolved `layout`, #6638/#6813 D4) when one is
83
+ * given, then from `DEFAULT_SITE_CHROME`. The explicit `layout` wins field by
84
+ * field, so a template can wear a design and still override one knob of it.
85
+ *
86
+ * `designLayout` is typed `TemplateLayout`, but a `.mjs` caller
87
+ * (`build-template-manifest.mjs`) cannot be compile-time checked the way a
88
+ * `.ts` caller can, so the type alone cannot stop a caller passing a design
89
+ * NAME (a string) rather than its resolved layout: a runtime guard throws a
90
+ * `TypeError` naming what was given, rather than silently resolving to the
91
+ * defaults.
82
92
  */
83
- export declare function resolveSiteChrome(layout?: TemplateLayout, design?: StarterDesign): ResolvedSiteChrome;
93
+ export declare function resolveSiteChrome(layout?: TemplateLayout, designLayout?: TemplateLayout): ResolvedSiteChrome;
84
94
  /**
85
95
  * Render `apps/web/lib/site-chrome.ts` for a resolved layout — the same
86
96
  * GENERATED-header idiom `renderNavConfig` uses. `layout.tsx` imports
@@ -21,11 +21,14 @@
21
21
  * block, gets these defaults written, byte-identical to the monorepo's own
22
22
  * committed `apps/web/lib/site-chrome.ts`.
23
23
  *
24
- * A template's DESIGN supplies layout knobs too (#6638): `DESIGN_LAYOUTS` in
25
- * `template-manifest.ts`, read by `resolveSiteChrome`'s optional `design`
26
- * argument between an explicit `layout` and these defaults.
24
+ * A design supplies layout knobs too (#6638, widened #6813 D4): its OWN
25
+ * `layout` (a `DesignPackage`'s, read by `design-scaffold.ts`'s
26
+ * `resolveDesign`), passed as `resolveSiteChrome`'s optional `designLayout`
27
+ * argument between an explicit `layout` and these defaults. This module
28
+ * imports nothing from `template-manifest.ts` (the cycle that import used to
29
+ * close): an unrecognized design NAME now fails in `resolveDesign`, before
30
+ * this function ever sees one.
27
31
  */
28
- import { DESIGN_LAYOUTS, TemplateError, } from "./template-manifest.js";
29
32
  /** Defaults (#5612 D1; banner off since #6469). */
30
33
  export const DEFAULT_SITE_CHROME = {
31
34
  nav: "full",
@@ -92,17 +95,24 @@ export function validateTemplateLayout(value, subject) {
92
95
  return result;
93
96
  }
94
97
  /**
95
- * Fill every field a `TemplateLayout` leaves unset — from `design`'s
96
- * `DESIGN_LAYOUTS` entry when one is named (#6638), then from
97
- * `DEFAULT_SITE_CHROME`. The explicit `layout` wins field by field, so a
98
- * template can wear a design and still override one knob of it.
98
+ * Fill every field a `TemplateLayout` leaves unset — from `designLayout`
99
+ * (a design package's own resolved `layout`, #6638/#6813 D4) when one is
100
+ * given, then from `DEFAULT_SITE_CHROME`. The explicit `layout` wins field by
101
+ * field, so a template can wear a design and still override one knob of it.
102
+ *
103
+ * `designLayout` is typed `TemplateLayout`, but a `.mjs` caller
104
+ * (`build-template-manifest.mjs`) cannot be compile-time checked the way a
105
+ * `.ts` caller can, so the type alone cannot stop a caller passing a design
106
+ * NAME (a string) rather than its resolved layout: a runtime guard throws a
107
+ * `TypeError` naming what was given, rather than silently resolving to the
108
+ * defaults.
99
109
  */
100
- export function resolveSiteChrome(layout, design) {
101
- const fromDesign = design !== undefined ? DESIGN_LAYOUTS[design] : {};
102
- // A JS caller (or a cast) can reach here with an undeclared design.
103
- if (fromDesign === undefined) {
104
- throw new TemplateError(`unknown design "${design}". Declared: ${Object.keys(DESIGN_LAYOUTS).join(", ")}`);
110
+ export function resolveSiteChrome(layout, designLayout) {
111
+ if (designLayout !== undefined &&
112
+ (typeof designLayout !== "object" || designLayout === null || Array.isArray(designLayout))) {
113
+ throw new TypeError(`resolveSiteChrome: designLayout must be a layout object, got ${JSON.stringify(designLayout)}`);
105
114
  }
115
+ const fromDesign = designLayout ?? {};
106
116
  return {
107
117
  nav: layout?.nav ?? fromDesign.nav ?? DEFAULT_SITE_CHROME.nav,
108
118
  footer: layout?.footer ?? fromDesign.footer ?? DEFAULT_SITE_CHROME.footer,
@@ -0,0 +1,25 @@
1
+ import type { Level } from "./lib/module-tier.js";
2
+ import type { ModuleName } from "./parse-args.js";
3
+ import { type SiteRoster } from "./site-roster.js";
4
+ import type { MarkerMap } from "./substitute.js";
5
+ export interface PlanSiteManifestOptions {
6
+ targetDir: string;
7
+ /** The installed manifest, read after every layer landed. */
8
+ roster: SiteRoster;
9
+ /** The effective module set the scaffold installed. */
10
+ modules: readonly ModuleName[];
11
+ /** The level the key path derived, or undefined when it derived none. */
12
+ level: Level | undefined;
13
+ siteTemplatesDir: string;
14
+ templatesDir: string;
15
+ markers: MarkerMap;
16
+ }
17
+ /**
18
+ * The manifest's next text: `modules` (and `level`, when derived) after
19
+ * `requires`, and a row for every route present on disk.
20
+ *
21
+ * @throws AddModuleRefusedError when the file already records `modules` or
22
+ * `level`, or its layout is not the generator's (`rows` last, `requires`
23
+ * at the top level) — guessing would silently mis-record the site.
24
+ */
25
+ export declare function planSiteManifest(opts: PlanSiteManifestOptions): string;
@@ -0,0 +1,254 @@
1
+ /**
2
+ * The site manifest's record of what the scaffold IS (#6944) — written into
3
+ * `<site>/.ailk-site-manifest.json` by `scaffold()` once every layer has
4
+ * landed.
5
+ *
6
+ * A template scaffold's manifest arrives as its template's generated
7
+ * `TEMPLATE-MANIFEST.json` (#6658 D7): the template's own `requires` and
8
+ * rows, and nothing about the run that installed it. A Pro `--shape` run of a
9
+ * Free template recorded `requires: ["marketing"]` and the Free rows, missing
10
+ * every page its module bundles landed. `planSiteManifest` adds:
11
+ *
12
+ * - `modules` — the effective module set, the one `architecture.yaml`
13
+ * declares. Skills read it for what the site can render;
14
+ * - `level` — ONLY when the licence-key path derived one
15
+ * (`resolveModuleSet` / `deriveLevel`). Never inferred from `modules`:
16
+ * the level comes only from the key. An alias or `--modules` keep run
17
+ * derives none and records none;
18
+ * - a row for every route present on disk — each module's contributed rows
19
+ * through `planRosterAppend` (add-module's own append), the sample pages
20
+ * a module's bundle installs (`sampleRows`), and a code-route row for any
21
+ * other page route under `apps/web/app/[locale]/` (`codeRouteRows`).
22
+ *
23
+ * Every byte already in the file is kept; the new rows and keys are rendered
24
+ * the way prettier prints the generator's JSON, so the file stays formatted.
25
+ *
26
+ * Pure module: no `import.meta.url`. Package-rooted directories are supplied
27
+ * by the caller, the same split as `site-roster.ts` / `programmatic.ts`.
28
+ */
29
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
30
+ import { join, relative, sep } from "node:path";
31
+ import { isDeepStrictEqual } from "node:util";
32
+ import { AddModuleRefusedError } from "./add-module-core.js";
33
+ import { MODULE_SLICES, normalizeModules } from "./module-architecture.js";
34
+ import { moduleRosterRows, planRosterAppend, } from "./site-roster.js";
35
+ import { TEMPLATE_MANIFESTS, rosterDeclares, templateTreeDir, } from "./template-manifest.js";
36
+ /** Prettier's default print width — the generator formats the manifest with it. */
37
+ const PRINT_WIDTH = 80;
38
+ /** Where the site's App Router pages live. */
39
+ const ROUTE_ROOT = join("apps", "web", "app", "[locale]");
40
+ const PAGE_FILE = /^page\.(tsx|ts|jsx|js|mdx)$/;
41
+ /**
42
+ * The generator's row for a content-less route (`build-template-manifest.mjs`:
43
+ * every row renders under the locale layout's MarketingShell, chrome on).
44
+ */
45
+ const CODE_ROUTE_ROW = {
46
+ kind: "route",
47
+ pageType: null,
48
+ schema: null,
49
+ shell: "MarketingShell",
50
+ chrome: "on",
51
+ sections: [],
52
+ };
53
+ /**
54
+ * The manifest's next text: `modules` (and `level`, when derived) after
55
+ * `requires`, and a row for every route present on disk.
56
+ *
57
+ * @throws AddModuleRefusedError when the file already records `modules` or
58
+ * `level`, or its layout is not the generator's (`rows` last, `requires`
59
+ * at the top level) — guessing would silently mis-record the site.
60
+ */
61
+ export function planSiteManifest(opts) {
62
+ const { roster } = opts;
63
+ for (const key of ["modules", "level"]) {
64
+ if (key in roster.doc) {
65
+ throw new AddModuleRefusedError(`${roster.path} already records "${key}"`);
66
+ }
67
+ }
68
+ const plan = planRosterAppend({
69
+ targetDir: opts.targetDir,
70
+ roster,
71
+ modules: opts.modules,
72
+ references: opts.modules.flatMap((m) => moduleRosterRows(m, opts.siteTemplatesDir)),
73
+ siteTemplatesDir: opts.siteTemplatesDir,
74
+ templatesDir: opts.templatesDir,
75
+ markers: opts.markers,
76
+ });
77
+ const appended = plan.next ?? roster.text;
78
+ const declared = new Set(JSON.parse(appended).rows.map((row) => row["route"]));
79
+ const extra = [
80
+ ...sampleRows(opts, declared),
81
+ ...codeRouteRows(opts.targetDir, declared),
82
+ ];
83
+ const withRows = appendRows(appended, extra, roster);
84
+ return insertRecord(withRows, opts.modules, opts.level, roster.path);
85
+ }
86
+ /**
87
+ * The rows for the sample pages a module's bundle installs through its
88
+ * `installMap` (`signal-collectors`' /projects family) that no row names yet:
89
+ * each is the row a template requiring the module declares for that content,
90
+ * as the generator wrote it into the template's shipped manifest. A
91
+ * `contentDir` row lists the site's own seeds. `declared` gains each route.
92
+ */
93
+ function sampleRows(opts, declared) {
94
+ const out = [];
95
+ for (const moduleName of normalizeModules([...opts.modules])) {
96
+ const installed = Object.values(MODULE_SLICES[moduleName].installMap ?? {})
97
+ .filter((to) => to.startsWith("content/en/") && to.endsWith(".mdx"))
98
+ .filter((to) => existsSync(join(opts.targetDir, to)));
99
+ if (installed.length === 0)
100
+ continue;
101
+ for (const template of Object.values(TEMPLATE_MANIFESTS)) {
102
+ if (!template.requires.includes(moduleName))
103
+ continue;
104
+ const shippedPath = join(opts.siteTemplatesDir, templateTreeDir(template.name), "TEMPLATE-MANIFEST.json");
105
+ if (!existsSync(shippedPath))
106
+ continue;
107
+ const shipped = JSON.parse(readFileSync(shippedPath, "utf8")).rows;
108
+ for (const row of template.roster) {
109
+ if (declared.has(row.route))
110
+ continue;
111
+ if (!installed.some((path) => rosterDeclares([row], path)))
112
+ continue;
113
+ const value = shipped.find((r) => r["route"] === row.route);
114
+ if (value === undefined)
115
+ continue;
116
+ declared.add(row.route);
117
+ out.push(row.contentDir !== undefined && "seeds" in value
118
+ ? {
119
+ ...value,
120
+ seeds: listMdx(join(opts.targetDir, row.contentDir), opts.targetDir),
121
+ }
122
+ : value);
123
+ }
124
+ }
125
+ }
126
+ return out;
127
+ }
128
+ /** A code-route row for every page route on disk that `declared` does not name. */
129
+ function codeRouteRows(targetDir, declared) {
130
+ return routesOnDisk(targetDir)
131
+ .filter((route) => !declared.has(route))
132
+ .map((route) => ({ route, ...CODE_ROUTE_ROW }));
133
+ }
134
+ /**
135
+ * Every route the site's App Router serves from a page file, sorted: route
136
+ * groups `(x)` dropped, a trailing optional catch-all `[[...x]]` folded into
137
+ * its parent (docs-blog's `/blog/[[...slug]]` serves `/blog`). The root
138
+ * content renderers (`/[slug]`, `/[slug]/[detail]`) are not routes of their
139
+ * own — they are how the content rows render — so they are left out.
140
+ */
141
+ function routesOnDisk(targetDir) {
142
+ const root = join(targetDir, ROUTE_ROOT);
143
+ const routes = new Set();
144
+ const stack = existsSync(root) ? [root] : [];
145
+ for (let cur = stack.pop(); cur !== undefined; cur = stack.pop()) {
146
+ const entries = readdirSync(cur, { withFileTypes: true });
147
+ for (const entry of entries) {
148
+ if (entry.isDirectory())
149
+ stack.push(join(cur, entry.name));
150
+ }
151
+ if (!entries.some((entry) => entry.isFile() && PAGE_FILE.test(entry.name))) {
152
+ continue;
153
+ }
154
+ const segments = relative(root, cur)
155
+ .split(sep)
156
+ .filter((segment) => segment !== "" && !/^\(.+\)$/.test(segment));
157
+ const last = segments[segments.length - 1];
158
+ if (last !== undefined && /^\[\[\.\.\..+\]\]$/.test(last))
159
+ segments.pop();
160
+ if (segments[0]?.startsWith("["))
161
+ continue;
162
+ routes.add(`/${segments.join("/")}`);
163
+ }
164
+ return [...routes].sort();
165
+ }
166
+ /** Every `.mdx` under `dir`, as sorted `/`-separated paths relative to `base`. */
167
+ function listMdx(dir, base) {
168
+ const out = [];
169
+ const stack = existsSync(dir) ? [dir] : [];
170
+ for (let cur = stack.pop(); cur !== undefined; cur = stack.pop()) {
171
+ for (const entry of readdirSync(cur, { withFileTypes: true })) {
172
+ const abs = join(cur, entry.name);
173
+ if (entry.isDirectory())
174
+ stack.push(abs);
175
+ else if (entry.isFile() && entry.name.endsWith(".mdx")) {
176
+ out.push(relative(base, abs).split(sep).join("/"));
177
+ }
178
+ }
179
+ }
180
+ return out.sort();
181
+ }
182
+ /**
183
+ * `value` as prettier prints the generator's JSON with its value starting at
184
+ * `column` after a `prefix`-wide key: objects expanded one key per line, an
185
+ * array of scalars inline when the line fits, one element per line otherwise.
186
+ */
187
+ function renderJson(value, column, prefix = 0, trailing = 0) {
188
+ const pad = " ".repeat(column + 2);
189
+ const close = " ".repeat(column);
190
+ if (Array.isArray(value)) {
191
+ if (value.length === 0)
192
+ return "[]";
193
+ if (value.every((item) => item === null || typeof item !== "object")) {
194
+ const inline = `[${value.map((item) => JSON.stringify(item)).join(", ")}]`;
195
+ if (column + prefix + inline.length + trailing <= PRINT_WIDTH)
196
+ return inline;
197
+ }
198
+ const items = value.map((item, i) => pad + renderJson(item, column + 2, 0, i < value.length - 1 ? 1 : 0));
199
+ return `[\n${items.join(",\n")}\n${close}]`;
200
+ }
201
+ if (value !== null && typeof value === "object") {
202
+ const entries = Object.entries(value);
203
+ if (entries.length === 0)
204
+ return "{}";
205
+ const lines = entries.map(([key, item], i) => {
206
+ const name = `${JSON.stringify(key)}: `;
207
+ return (pad +
208
+ name +
209
+ renderJson(item, column + 2, name.length, i < entries.length - 1 ? 1 : 0));
210
+ });
211
+ return `{\n${lines.join(",\n")}\n${close}}`;
212
+ }
213
+ return JSON.stringify(value);
214
+ }
215
+ /** `text` with `rows` appended to its `rows` array — which the generator writes last. */
216
+ function appendRows(text, rows, roster) {
217
+ if (rows.length === 0)
218
+ return text;
219
+ const keys = Object.keys(JSON.parse(text));
220
+ if (keys[keys.length - 1] !== "rows") {
221
+ throw new AddModuleRefusedError(`${roster.path} does not end with its "rows" array, so rows cannot be appended`);
222
+ }
223
+ const close = text.lastIndexOf("]");
224
+ let end = close;
225
+ while (end > 0 && /\s/.test(text[end - 1]))
226
+ end--;
227
+ const empty = text[end - 1] === "[";
228
+ const column = roster.lead.length - roster.lead.lastIndexOf("\n") - 1;
229
+ const rendered = rows
230
+ .map((row) => roster.lead + renderJson(row, column))
231
+ .join(",");
232
+ return empty
233
+ ? text.slice(0, end) + rendered + roster.trail + text.slice(close)
234
+ : text.slice(0, end) + "," + rendered + text.slice(end);
235
+ }
236
+ /** `text` with `modules` (and `level`, when derived) inserted after the top-level `requires`. */
237
+ function insertRecord(text, modules, level, label) {
238
+ const anchor = '\n "requires": ';
239
+ const at = text.indexOf(anchor);
240
+ if (at === -1 || text.indexOf(anchor, at + 1) !== -1) {
241
+ throw new AddModuleRefusedError(`${label} has no single top-level "requires" key to record the module set beside`);
242
+ }
243
+ const end = text.indexOf("]", at) + 1;
244
+ const name = '"modules": ';
245
+ const record = `,\n ${name}${renderJson([...modules], 2, name.length, 1)}` +
246
+ (level !== undefined ? `,\n "level": ${JSON.stringify(level)}` : "");
247
+ const next = text.slice(0, end) + record + text.slice(end);
248
+ const doc = JSON.parse(next);
249
+ if (!isDeepStrictEqual(doc["modules"], [...modules]) ||
250
+ doc["level"] !== level) {
251
+ throw new AddModuleRefusedError(`${label}: the module set could not be recorded beside "requires"`);
252
+ }
253
+ return next;
254
+ }
@@ -1,5 +1,7 @@
1
+ import type { CompiledDesignTheme } from "./design-compile.js";
1
2
  import { type PageRosterRow } from "./module-architecture.js";
2
3
  import type { ModuleName } from "./parse-args.js";
4
+ import type { ResolvedSiteChrome } from "./site-chrome.js";
3
5
  import { type MarkerMap } from "./substitute.js";
4
6
  import { type TemplateManifest } from "./template-manifest.js";
5
7
  /** The site-root file that is the roster of record (#6658 D7). */
@@ -55,6 +57,25 @@ export interface ModuleRosterRow {
55
57
  * not declare is refused: guessing would silently mis-roster the site.
56
58
  */
57
59
  export declare function readSiteRoster(targetDir: string): SiteRoster | undefined;
60
+ /** What `planSiteManifestDesign` writes when it rewrites the manifest. */
61
+ export interface SiteManifestDesignNext {
62
+ design: string;
63
+ layout: ResolvedSiteChrome;
64
+ theme: CompiledDesignTheme;
65
+ }
66
+ /**
67
+ * Plan the site manifest's `design`/`layout`/`theme` keys as a design's
68
+ * applied values (#6813 D6) — `undefined` when the file already carries
69
+ * `next` (no rewrite, so the file keeps its bytes byte-for-byte), else the
70
+ * text with each CHANGED key's value spliced in place, located with the same
71
+ * `findKey`/`endOfValue` scanner `planRosterAppend` uses. `rows`,
72
+ * `generatedHash` and every other byte stay untouched.
73
+ *
74
+ * @throws AddModuleRefusedError when `text` is not a JSON object, or is
75
+ * missing `design`, `layout` or `theme` — a manifest predating #6658 D7
76
+ * never reaches this function (`scaffold()` guards on the file's presence).
77
+ */
78
+ export declare function planSiteManifestDesign(text: string, next: SiteManifestDesignNext, label: string): string | undefined;
58
79
  /**
59
80
  * The generator's row for each of a module's `pageContributions`, in
60
81
  * declaration order — read from the committed manifest of the first
@@ -63,6 +84,26 @@ export declare function readSiteRoster(targetDir: string): SiteRoster | undefine
63
84
  * of its forms. A route no shipped manifest carries is refused.
64
85
  */
65
86
  export declare function moduleRosterRows(moduleName: ModuleName, siteTemplatesDir: string): ModuleRosterRow[];
87
+ /**
88
+ * The Free `/meet` placeholder seeds on `route` (#6942): the keyless Free
89
+ * template's own `content` on a route the `scheduling` module contributes
90
+ * (`FREE_MEET_PLACEHOLDER_ROWS`, `starter-grid.ts`, read through the one
91
+ * registry that composes it). Empty for any other route. Throws when the Free
92
+ * template carries none at all, so a renamed template or a moved row fails
93
+ * loudly instead of re-opening the prune #6942 closed.
94
+ */
95
+ export declare function meetPlaceholderSeeds(route: string): readonly string[];
96
+ /**
97
+ * `roster` with the Free `/meet` placeholder seeds folded onto the
98
+ * `scheduling` module's code-route rows (#6942) — a row on a placeholder
99
+ * route that names no `content` of its own. Unconfigured, the module's
100
+ * `/meet` and `/meet/[slug]` pages hand off to the placeholder routes, so a
101
+ * seed there is still served and still declared: the scaffold's seed prune
102
+ * (`programmatic.ts`) and the site roster check read the roster through
103
+ * this. A row with its own `content` (the Free placeholder row itself) and a
104
+ * row on any other route are returned as is.
105
+ */
106
+ export declare function withMeetPlaceholderSeeds(roster: readonly PageRosterRow[]): PageRosterRow[];
66
107
  /** What becomes of a superseded template row's seed file. */
67
108
  export type SupersededSeed = {
68
109
  path: string;
@@ -30,7 +30,7 @@ import { AddModuleRefusedError, readArchitectureHeader, } from "./add-module-cor
30
30
  import { diffTwoStrings } from "./lib/extract-module-bundle.js";
31
31
  import { MODULE_SLICES, normalizeModules, } from "./module-architecture.js";
32
32
  import { substituteMarkers } from "./substitute.js";
33
- import { BASE_LAYER_DIR, SHAPE_STARTERS, TEMPLATE_MANIFESTS, resolveTemplate, rosterDeclares, templateTreeDir, } from "./template-manifest.js";
33
+ import { BASE_LAYER_DIR, SHAPE_STARTERS, TEMPLATE_MANIFESTS, resolveTemplate, rosterDeclares, shippedManifestPath, templateTreeDir, } from "./template-manifest.js";
34
34
  /** The site-root file that is the roster of record (#6658 D7). */
35
35
  export const SITE_ROSTER_PATH = ".ailk-site-manifest.json";
36
36
  /**
@@ -39,8 +39,6 @@ export const SITE_ROSTER_PATH = ".ailk-site-manifest.json";
39
39
  * `SITE_ROSTER_PATH` is absent, and written back in place.
40
40
  */
41
41
  export const LEGACY_SITE_ROSTER_PATH = "TEMPLATE-MANIFEST.json";
42
- /** A template tree's own generated manifest — the file the site copy is made from. */
43
- const SHIPPED_MANIFEST_PATH = "TEMPLATE-MANIFEST.json";
44
42
  /** Prettier's default print width — the generator formats the manifest with it. */
45
43
  const PRINT_WIDTH = 80;
46
44
  // ── A string-aware JSON scanner, for the text splice ──────────────────────
@@ -183,13 +181,67 @@ export function readSiteRoster(targetDir) {
183
181
  }
184
182
  return { ...file, template, path };
185
183
  }
184
+ /** `value`, rendered the way prettier prints an object-only JSON value nested one level under the manifest's root object. */
185
+ function renderManifestValue(value) {
186
+ return JSON.stringify(value, null, 2)
187
+ .split("\n")
188
+ .map((line, i) => (i === 0 ? line : ` ${line}`))
189
+ .join("\n");
190
+ }
191
+ /**
192
+ * Plan the site manifest's `design`/`layout`/`theme` keys as a design's
193
+ * applied values (#6813 D6) — `undefined` when the file already carries
194
+ * `next` (no rewrite, so the file keeps its bytes byte-for-byte), else the
195
+ * text with each CHANGED key's value spliced in place, located with the same
196
+ * `findKey`/`endOfValue` scanner `planRosterAppend` uses. `rows`,
197
+ * `generatedHash` and every other byte stay untouched.
198
+ *
199
+ * @throws AddModuleRefusedError when `text` is not a JSON object, or is
200
+ * missing `design`, `layout` or `theme` — a manifest predating #6658 D7
201
+ * never reaches this function (`scaffold()` guards on the file's presence).
202
+ */
203
+ export function planSiteManifestDesign(text, next, label) {
204
+ let doc;
205
+ try {
206
+ doc = JSON.parse(text);
207
+ }
208
+ catch (err) {
209
+ throw new AddModuleRefusedError(`${label} is not JSON (${err.message})`);
210
+ }
211
+ if (typeof doc !== "object" || doc === null || Array.isArray(doc)) {
212
+ throw new AddModuleRefusedError(`${label} is not a JSON object`);
213
+ }
214
+ const parsed = doc;
215
+ const keys = ["design", "layout", "theme"];
216
+ for (const key of keys) {
217
+ if (!(key in parsed)) {
218
+ throw new AddModuleRefusedError(`${label} has no "${key}" key`);
219
+ }
220
+ }
221
+ if (keys.every((key) => isDeepStrictEqual(parsed[key], next[key]))) {
222
+ return undefined;
223
+ }
224
+ let result = text;
225
+ for (const key of keys) {
226
+ if (isDeepStrictEqual(parsed[key], next[key]))
227
+ continue;
228
+ const at = findKey(result, result.indexOf("{"), key);
229
+ if (at === undefined) {
230
+ throw new AddModuleRefusedError(`${label} has a "${key}" key the scanner cannot locate`);
231
+ }
232
+ const end = endOfValue(result, at.valueStart);
233
+ result = result.slice(0, at.valueStart) + renderManifestValue(next[key]) + result.slice(end);
234
+ }
235
+ return result;
236
+ }
186
237
  // ── D2: a module's rows, from the shipped manifests ───────────────────────
187
238
  /** A template's shipped manifest, or undefined when its tree ships none. */
188
239
  function readShippedManifest(siteTemplatesDir, templateName) {
189
- const path = join(siteTemplatesDir, templateName, SHIPPED_MANIFEST_PATH);
240
+ const relPath = shippedManifestPath(templateName);
241
+ const path = join(siteTemplatesDir, relPath);
190
242
  if (!existsSync(path))
191
243
  return undefined;
192
- return parseRosterFile(readFileSync(path, "utf8"), `site-templates/${templateName}/${SHIPPED_MANIFEST_PATH}`);
244
+ return parseRosterFile(readFileSync(path, "utf8"), `site-templates/${relPath}`);
193
245
  }
194
246
  /**
195
247
  * The generator's row for each of a module's `pageContributions`, in
@@ -216,14 +268,53 @@ export function moduleRosterRows(moduleName, siteTemplatesDir) {
216
268
  };
217
269
  return contributions.map((contribution) => {
218
270
  const isDir = contribution.contentDir !== undefined;
219
- const row = find(contribution.route, isDir ? "route" : undefined);
220
271
  const seeded = isDir ? find(contribution.route, "page") : undefined;
272
+ let row = find(contribution.route, isDir ? "route" : undefined);
273
+ if (row === undefined && isDir && seeded !== undefined) {
274
+ const contentDir = contribution.contentDir;
275
+ const installMap = MODULE_SLICES[moduleName].installMap ?? {};
276
+ const selfSeeds = Object.values(installMap).some((dest) => dest.startsWith(`${contentDir}/`));
277
+ if (selfSeeds)
278
+ row = deriveRouteRow(contribution.route, seeded);
279
+ }
221
280
  if (row === undefined || (isDir && seeded === undefined)) {
222
281
  throw new AddModuleRefusedError(`module "${moduleName}" contributes ${contribution.route}, but no shipped template manifest carries ${isDir ? "both forms of " : ""}its row, so it cannot be added to ${SITE_ROSTER_PATH}`);
223
282
  }
224
283
  return { module: moduleName, contribution, row, ...(seeded && { seeded }) };
225
284
  });
226
285
  }
286
+ /**
287
+ * Derive a `contentDir` contribution's code-route form from its seeded page
288
+ * form (spec 6892 D9) — for a SELF-SEEDING contribution, one whose
289
+ * contributing module's own `installMap` lands at least one file under the
290
+ * directory, so no template supplies a route form for it (every template
291
+ * taking the module gets the module's seeds, never zero of them). Returns
292
+ * field-for-field the route form `build-template-manifest.mjs` emits for a
293
+ * template that supplies no seeds of its own: `kind: "route"`, no
294
+ * `pageType`/`schema`/`contentDir`/`seeds`, the seeded form's own `shell` and
295
+ * `chrome`, no sections. Rendered the way a generated manifest's row-array
296
+ * element is indented (four spaces beyond `JSON.stringify`'s own two), so a
297
+ * derived row's `text` is byte-identical to the route-form slice a template
298
+ * manifest carries for the same contribution.
299
+ */
300
+ function deriveRouteRow(route, seeded) {
301
+ const value = {
302
+ route,
303
+ kind: "route",
304
+ pageType: null,
305
+ schema: null,
306
+ shell: seeded.value["shell"],
307
+ chrome: seeded.value["chrome"],
308
+ sections: [],
309
+ };
310
+ return {
311
+ value,
312
+ text: JSON.stringify(value, null, 2)
313
+ .split("\n")
314
+ .map((line, i) => (i === 0 ? line : ` ${line}`))
315
+ .join("\n"),
316
+ };
317
+ }
227
318
  /**
228
319
  * Every `.mdx` under `dir`, as `/`-separated paths relative to `base`. Walked
229
320
  * by hand: `Dirent.parentPath` and a reliable recursive `readdirSync` post-date
@@ -281,6 +372,40 @@ function expectedRow(ref, targetDir) {
281
372
  const seeds = listMdx(join(targetDir, dir), targetDir);
282
373
  return seeds.length > 0 ? withSeeds(ref.seeded, seeds) : ref.row;
283
374
  }
375
+ /**
376
+ * The Free `/meet` placeholder seeds on `route` (#6942): the keyless Free
377
+ * template's own `content` on a route the `scheduling` module contributes
378
+ * (`FREE_MEET_PLACEHOLDER_ROWS`, `starter-grid.ts`, read through the one
379
+ * registry that composes it). Empty for any other route. Throws when the Free
380
+ * template carries none at all, so a renamed template or a moved row fails
381
+ * loudly instead of re-opening the prune #6942 closed.
382
+ */
383
+ export function meetPlaceholderSeeds(route) {
384
+ const scheduling = new Set((MODULE_SLICES.scheduling.pageContributions ?? []).map((row) => row.route));
385
+ const placeholders = (TEMPLATE_MANIFESTS["marketing-site-free"]?.roster ?? []).filter((row) => scheduling.has(row.route) && row.content !== undefined);
386
+ if (placeholders.length === 0) {
387
+ throw new Error('meetPlaceholderSeeds: "marketing-site-free" carries no /meet placeholder row on a scheduling route');
388
+ }
389
+ return placeholders.find((row) => row.route === route)?.content ?? [];
390
+ }
391
+ /**
392
+ * `roster` with the Free `/meet` placeholder seeds folded onto the
393
+ * `scheduling` module's code-route rows (#6942) — a row on a placeholder
394
+ * route that names no `content` of its own. Unconfigured, the module's
395
+ * `/meet` and `/meet/[slug]` pages hand off to the placeholder routes, so a
396
+ * seed there is still served and still declared: the scaffold's seed prune
397
+ * (`programmatic.ts`) and the site roster check read the roster through
398
+ * this. A row with its own `content` (the Free placeholder row itself) and a
399
+ * row on any other route are returned as is.
400
+ */
401
+ export function withMeetPlaceholderSeeds(roster) {
402
+ return roster.map((row) => {
403
+ if (row.content !== undefined || row.contentDir !== undefined)
404
+ return row;
405
+ const seeds = meetPlaceholderSeeds(row.route);
406
+ return seeds.length === 0 ? row : { ...row, content: seeds };
407
+ });
408
+ }
284
409
  /** The bytes a scaffold installed at `path` from the template, base or overlay layer. */
285
410
  function installedSeed(opts, path) {
286
411
  const layers = [
@@ -313,7 +438,7 @@ export function planRosterAppend(opts) {
313
438
  const inSet = new Set(opts.modules);
314
439
  const refs = opts.references.filter((ref) => inSet.has(ref.module));
315
440
  const contributed = new Set(refs.map((ref) => ref.contribution.route));
316
- const shipped = readShippedManifest(opts.siteTemplatesDir, templateTreeDir(roster.template.name));
441
+ const shipped = readShippedManifest(opts.siteTemplatesDir, roster.template.name);
317
442
  const templateRows = [];
318
443
  const moduleRows = new Map();
319
444
  const superseded = [];
@@ -350,7 +475,13 @@ export function planRosterAppend(opts) {
350
475
  }
351
476
  const seeds = [];
352
477
  for (const row of superseded) {
478
+ // The module's row on this route still declares a Free `/meet`
479
+ // placeholder seed (#6942): its unconfigured pages serve it, so the seed
480
+ // is kept, never removed or reported.
481
+ const kept = meetPlaceholderSeeds(row.route);
353
482
  for (const path of row.content ?? []) {
483
+ if (kept.includes(path))
484
+ continue;
354
485
  const abs = join(targetDir, path);
355
486
  if (!existsSync(abs))
356
487
  continue;
@@ -487,11 +618,13 @@ export function siteRosterProblems(opts) {
487
618
  }),
488
619
  };
489
620
  });
621
+ // #6942 — the Free /meet placeholder seeds the scheduling module's rows keep.
622
+ const declared = withMeetPlaceholderSeeds(effective);
490
623
  const installed = [...layerInstalledSeeds(roster.template, modules, opts)]
491
624
  .filter((path) => existsSync(join(opts.targetDir, path)))
492
625
  .sort();
493
626
  for (const path of installed) {
494
- if (!rosterDeclares(effective, path)) {
627
+ if (!rosterDeclares(declared, path)) {
495
628
  problems.push(`${path}: installed but not declared by any roster row`);
496
629
  }
497
630
  }