create-ailk 0.7.0 → 0.8.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 (169) hide show
  1. package/README.md +44 -10
  2. package/component-catalog.json +520 -5
  3. package/component-catalog.md +9 -6
  4. package/dist/cli.js +22 -24
  5. package/dist/lib/extract-module-bundle.js +5 -1
  6. package/dist/lib/module-license-gate.d.ts +79 -0
  7. package/dist/lib/module-license-gate.js +160 -12
  8. package/dist/lib/module-tier.d.ts +45 -16
  9. package/dist/lib/module-tier.js +76 -0
  10. package/dist/lib/pro-callout.js +10 -2
  11. package/dist/parse-args.d.ts +78 -10
  12. package/dist/parse-args.js +204 -14
  13. package/dist/programmatic.d.ts +39 -9
  14. package/dist/programmatic.js +163 -25
  15. package/dist/site-roster.js +11 -6
  16. package/dist/starter-grid.d.ts +216 -0
  17. package/dist/starter-grid.js +449 -0
  18. package/dist/surfaces.d.ts +4 -4
  19. package/dist/surfaces.js +102 -43
  20. package/dist/template-manifest.d.ts +36 -17
  21. package/dist/template-manifest.js +121 -367
  22. package/dist/template-names.d.ts +14 -12
  23. package/dist/template-names.js +13 -17
  24. package/package.json +3 -3
  25. package/site-templates/_base/content/en/BookCall/book-call.mdx +0 -1
  26. package/site-templates/_base/content/en/Feature/feature-1.mdx +69 -0
  27. package/site-templates/_base/content/en/Feature/feature-2.mdx +69 -0
  28. package/site-templates/_base/content/en/Feature/feature-3.mdx +69 -0
  29. package/site-templates/_base/content/en/FeaturesList/features.mdx +91 -0
  30. package/site-templates/_base/content/en/GlossaryList/glossary.mdx +0 -1
  31. package/site-templates/_base/content/en/NewsletterSignup/newsletter.mdx +0 -1
  32. package/site-templates/_base/content/en/PortfolioItem/sample-four.mdx +105 -0
  33. package/site-templates/_base/content/en/PortfolioItem/sample-one.mdx +34 -0
  34. package/site-templates/_base/content/en/PortfolioItem/sample-three.mdx +94 -0
  35. package/site-templates/_base/content/en/PortfolioItem/sample-two.mdx +45 -0
  36. package/site-templates/_base/content/en/PortfolioList/portfolio.mdx +169 -0
  37. package/site-templates/_base/content/en/PrivacyPage/privacy.mdx +15 -0
  38. package/site-templates/_base/content/en/UseCase/persona-1.mdx +69 -0
  39. package/site-templates/_base/content/en/UseCase/persona-2.mdx +69 -0
  40. package/site-templates/_base/content/en/UseCase/persona-3.mdx +69 -0
  41. package/site-templates/designs/standard/DESIGN.md +5 -0
  42. package/site-templates/marketing-site/DESIGN.md +5 -0
  43. package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +89 -25
  44. package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +35 -14
  45. package/site-templates/marketing-site-free/DESIGN.md +5 -0
  46. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +95 -29
  47. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +74 -44
  48. package/site-templates/marketing-site-free/content/en/NewsletterSignup/newsletter.mdx +59 -0
  49. package/site-templates/marketing-site-free/content/en/Pricing/pricing.mdx +199 -0
  50. package/site-templates/minimalism-site/TEMPLATE-MANIFEST.md +17 -29
  51. package/site-templates/minimalism-site/content/en/BookCall/book-call.mdx +0 -1
  52. package/site-templates/minimalism-site/content/en/PurchasePolicyPage/purchase-policy.mdx +0 -1
  53. package/site-templates/saas-product/DESIGN.md +5 -0
  54. package/site-templates/saas-product/TEMPLATE-MANIFEST.md +9 -1
  55. package/site-templates/{simple-service → shapes/local-business-service}/TEMPLATE-MANIFEST.json +96 -76
  56. package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.md +302 -0
  57. package/site-templates/shapes/local-business-service/ailk-template.json +10 -0
  58. package/site-templates/{simple-service → shapes/local-business-service}/content/en/AboutPage/about.mdx +4 -4
  59. package/site-templates/shapes/local-business-service/content/en/HomePage/home.mdx +70 -0
  60. package/site-templates/shapes/local-business-service/content/en/NewsletterSignup/newsletter.mdx +59 -0
  61. package/site-templates/shapes/local-business-service/content/en/PortfolioList/portfolio.mdx +171 -0
  62. package/site-templates/{simple-service → shapes/local-business-service}/content/en/PurchasePolicyPage/purchase-policy.mdx +7 -8
  63. package/site-templates/shapes/local-business-service/content/en/Service/first-job.mdx +39 -0
  64. package/site-templates/shapes/local-business-service/content/en/Service/second-job.mdx +39 -0
  65. package/site-templates/shapes/local-business-service/content/en/Service/third-job.mdx +39 -0
  66. package/site-templates/shapes/local-business-service/content/en/ServicesList/services.mdx +61 -0
  67. package/site-templates/shapes/local-business-service/provenance.json +4 -0
  68. package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.json +362 -0
  69. package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.md +300 -0
  70. package/site-templates/shapes/organization-service/ailk-template.json +10 -0
  71. package/site-templates/shapes/organization-service/content/en/AboutPage/about.mdx +38 -0
  72. package/site-templates/shapes/organization-service/content/en/HomePage/home.mdx +85 -0
  73. package/site-templates/shapes/organization-service/content/en/NewsletterSignup/newsletter.mdx +59 -0
  74. package/site-templates/shapes/organization-service/content/en/PortfolioList/portfolio.mdx +171 -0
  75. package/site-templates/shapes/organization-service/content/en/PurchasePolicyPage/purchase-policy.mdx +123 -0
  76. package/site-templates/shapes/organization-service/content/en/Service/first-engagement.mdx +38 -0
  77. package/site-templates/shapes/organization-service/content/en/Service/second-engagement.mdx +38 -0
  78. package/site-templates/shapes/organization-service/content/en/Service/third-engagement.mdx +38 -0
  79. package/site-templates/shapes/organization-service/content/en/ServicesList/services.mdx +72 -0
  80. package/site-templates/shapes/organization-service/provenance.json +4 -0
  81. package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.json +369 -0
  82. package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.md +299 -0
  83. package/site-templates/shapes/person-service/ailk-template.json +10 -0
  84. package/site-templates/shapes/person-service/content/_site.mdx +85 -0
  85. package/site-templates/shapes/person-service/content/en/AboutPage/about.mdx +39 -0
  86. package/site-templates/shapes/person-service/content/en/HomePage/home.mdx +97 -0
  87. package/site-templates/shapes/person-service/content/en/NewsletterSignup/newsletter.mdx +59 -0
  88. package/site-templates/shapes/person-service/content/en/PortfolioList/portfolio.mdx +169 -0
  89. package/site-templates/shapes/person-service/content/en/PurchasePolicyPage/purchase-policy.mdx +123 -0
  90. package/site-templates/shapes/person-service/content/en/Service/first-engagement.mdx +38 -0
  91. package/site-templates/shapes/person-service/content/en/Service/second-engagement.mdx +38 -0
  92. package/site-templates/shapes/person-service/content/en/Service/third-engagement.mdx +38 -0
  93. package/site-templates/{simple-service → shapes/person-service}/content/en/ServicesList/services.mdx +16 -16
  94. package/site-templates/shapes/person-service/provenance.json +4 -0
  95. package/templates/.claude/skills/README.md +3 -2
  96. package/templates/.claude/skills/add-page/references/page-types.md +4 -0
  97. package/templates/.claude/skills/setup-database/SKILL.md +156 -0
  98. package/templates/.claude/skills/setup-database/scripts/verify-inbox.ts +154 -0
  99. package/templates/.claude/skills/setup-deploy/SKILL.md +64 -32
  100. package/templates/.claude/skills/setup-deploy/references/env-vars-template.md +19 -28
  101. package/templates/.claude/skills/setup-deploy/references/runbook-template.md +58 -102
  102. package/templates/DESIGN.md +5 -0
  103. package/templates/README.md +46 -38
  104. package/templates/apps/api/CLAUDE.md +3 -2
  105. package/templates/apps/api/src/config/__tests__/index.test.ts +115 -14
  106. package/templates/apps/api/src/config/index.ts +35 -16
  107. package/templates/apps/api/src/lib/__tests__/lead-capture-store.test.ts +30 -0
  108. package/templates/apps/api/src/lib/__tests__/lead-delivery.test.ts +96 -0
  109. package/templates/apps/api/src/lib/lead-capture-store.ts +12 -0
  110. package/templates/apps/api/src/lib/lead-delivery.ts +52 -0
  111. package/templates/apps/api/src/openapi/spec.ts +63 -12
  112. package/templates/apps/api/src/routes/leads/__tests__/capture.test.ts +88 -0
  113. package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +100 -0
  114. package/templates/apps/api/src/routes/leads/index.ts +46 -1
  115. package/templates/apps/api/src/server.ts +7 -0
  116. package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +194 -0
  117. package/templates/apps/api/src/services/__tests__/leads-inbox.test.ts +124 -0
  118. package/templates/apps/api/src/services/lead-routing.ts +140 -30
  119. package/templates/apps/api/src/services/leads-inbox.ts +48 -11
  120. package/templates/apps/mcp/__tests__/parity.test.ts +5 -4
  121. package/templates/apps/mcp/src/tools/create_project.ts +21 -6
  122. package/templates/apps/web/app/[locale]/contact/ConversationalIntakeWrapper.tsx +14 -6
  123. package/templates/apps/web/app/[locale]/contact/__tests__/ConversationalIntakeWrapper.test.tsx +10 -9
  124. package/templates/apps/web/app/[locale]/contact/__tests__/contact-form-text.test.ts +32 -0
  125. package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +129 -0
  126. package/templates/apps/web/app/[locale]/contact/__tests__/page-renderer-migration.test.tsx +227 -84
  127. package/templates/apps/web/app/[locale]/contact/contact-form-text.ts +50 -0
  128. package/templates/apps/web/app/[locale]/contact/contact.actions.ts +73 -20
  129. package/templates/apps/web/app/[locale]/contact/page.tsx +105 -15
  130. package/templates/apps/web/app/[locale]/contact/site-contact-email.ts +54 -0
  131. package/templates/apps/web/app/[locale]/features/[slug]/page.tsx +64 -0
  132. package/templates/apps/web/app/[locale]/features/page.tsx +52 -0
  133. package/templates/apps/web/app/[locale]/layout.tsx +37 -5
  134. package/templates/apps/web/app/[locale]/newsletter/__tests__/newsletter.actions.test.ts +75 -0
  135. package/templates/apps/web/app/[locale]/newsletter/__tests__/page.test.tsx +163 -0
  136. package/templates/apps/web/app/[locale]/newsletter/newsletter.actions.ts +66 -0
  137. package/templates/apps/web/app/[locale]/newsletter/page.tsx +106 -14
  138. package/templates/apps/web/app/[locale]/portfolio/[slug]/page.tsx +59 -0
  139. package/templates/apps/web/app/[locale]/portfolio/page.tsx +49 -0
  140. package/templates/apps/web/app/[locale]/use-cases/[slug]/page.tsx +18 -3
  141. package/templates/apps/web/lib/nav-config.ts +48 -36
  142. package/templates/apps/web/lib/site-identity.ts +14 -0
  143. package/templates/apps/web/messages/en.json +28 -2
  144. package/templates/content/_site.mdx +4 -2
  145. package/templates/content/en/BlogList/blog.mdx +37 -16
  146. package/templates/content/en/ContactPage/contact.mdx +14 -44
  147. package/templates/content/en/HomePage/home.mdx +8 -16
  148. package/templates/content/en/PurchasePolicyPage/purchase-policy.mdx +0 -1
  149. package/templates/content/en/UseCasesList/use-cases.mdx +6 -6
  150. package/templates/database/CHANGELOG.md +7 -0
  151. package/templates/database/package.json +1 -1
  152. package/versions.json +33 -33
  153. package/site-templates/_base/content/en/UseCase/first-use-case.mdx +0 -23
  154. package/site-templates/_base/content/en/UseCase/second-use-case.mdx +0 -23
  155. package/site-templates/_base/content/en/UseCase/third-use-case.mdx +0 -23
  156. package/site-templates/simple-service/DESIGN.md +0 -162
  157. package/site-templates/simple-service/TEMPLATE-MANIFEST.md +0 -333
  158. package/site-templates/simple-service/ailk-template.json +0 -11
  159. package/site-templates/simple-service/content/en/BookCall/book-call.mdx +0 -53
  160. package/site-templates/simple-service/content/en/ContactPage/contact.mdx +0 -32
  161. package/site-templates/simple-service/content/en/FAQPage/faq.mdx +0 -56
  162. package/site-templates/simple-service/content/en/HomePage/home.mdx +0 -85
  163. package/site-templates/simple-service/content/en/Service/landscape-design.mdx +0 -40
  164. package/site-templates/simple-service/content/en/Service/lawn-maintenance.mdx +0 -40
  165. package/site-templates/simple-service/content/en/Service/seasonal-cleanup.mdx +0 -40
  166. package/site-templates/simple-service/provenance.json +0 -4
  167. package/templates/apps/web/app/[locale]/contact/ContactForm.tsx +0 -211
  168. package/templates/apps/web/app/[locale]/contact/__tests__/ContactForm.test.tsx +0 -96
  169. package/templates/content/en/SoftwareProduct/product.mdx +0 -175
@@ -1,5 +1,7 @@
1
1
  import { classifyModules } from "./lib/module-tier.js";
2
2
  import { MODULE_SLICES, normalizeModules, } from "./module-architecture.js";
3
+ import { ALIASES, SHAPE_ROSTERS, SHAPE_STARTER_ALIASES, } from "./starter-grid.js";
4
+ import { RECOGNIZED_TEMPLATES } from "./template-names.js";
3
5
  /**
4
6
  * The closed **shape** enum (starter-template contract § The three axes,
5
7
  * operator-disposed 2026-09-24) — `<primary node>-<offering node>` in
@@ -69,386 +71,138 @@ export class TemplateError extends Error {
69
71
  }
70
72
  }
71
73
  /**
72
- * The three pages every starter carries whatever its shape (#6658 D5,
73
- * `docs/decisions/starter-roster-page-set.md` D4/D7): a glossary, a newsletter
74
- * signup and a book-a-call page. Each is a module-less `GATEABLE_SURFACES`
75
- * entry the roster enables, with its seed in `_base`. A template carrying the
76
- * scheduling module overrides `BookCall/book-call.mdx` in its own tree, so the
77
- * call to action opens `/meet` rather than `/contact`.
78
- */
79
- const SHAPE_INDEPENDENT_ROWS = [
80
- { route: "/glossary", content: ["content/en/GlossaryList/glossary.mdx"] },
81
- {
82
- route: "/newsletter",
83
- content: ["content/en/NewsletterSignup/newsletter.mdx"],
84
- },
85
- { route: "/book-a-call", content: ["content/en/BookCall/book-call.mdx"] },
86
- ];
87
- /**
88
- * The rows every `organization-product` template brings itself — one declared
89
- * page set, whichever layer carries each seed. Re-derived by #6658 from the
90
- * standard (`derive-roster.py --entity-type software --offering-type software
91
- * --primary-goal convert --scale global`, the `--shape organization-product`
92
- * equivalent), edited only by the deviations the Free manifest's § 3a records
93
- * with their reasons. With a template named, `scaffold()` keeps a `_base` or
94
- * overlay seed only when a merged roster row names it (#6646 D1): the roster
95
- * decides what installs, and the manifest generator's `--check` proves it.
96
- *
97
- * Two layers, one roster:
98
- *
99
- * - `/`, `/software/[slug]`, `/use-cases`, `/about`, `/faq`, `/contact` and
100
- * `/purchase-policy` take their content from the overlay
101
- * (`template-overlay/content/en/**`, copied whole into `templates/` by
102
- * `build-templates.mjs`). `/software/[slug]` is the product's own detail
103
- * page (`/software/product`, a SoftwareProduct), which replaced the
104
- * `/features` stand-in (#6658 D3); its route is core, like `/about`.
105
- * `/use-cases` lists the three UseCase pages at `/use-cases/[slug]`
106
- * (#6658 D4).
107
- * - `/pricing`, `/privacy`, `/terms`, `/glossary`, `/newsletter`,
108
- * `/book-a-call` and the three UseCase pages take their content from the
109
- * base layer (`site-templates/_base/content/**`, #5618 D1), which every
110
- * template reaches by declaring `extends` — installed before the module
111
- * bundles and the template's own tree, so a template may override a base
112
- * page by shipping the same path. Their surfaces are module-less by
113
- * construction (parent § D16, #6658 D5): a template enables them by
114
- * NAMING them in its roster — `scaffold()` selects their
115
- * `GATEABLE_SURFACES` entries from the merged roster's routes (#5469 D10).
116
- *
117
- * `/software`, `/sign-in` and `/sign-up` carry no `content`: they are code
118
- * routes (the SoftwareProduct listing, the SignInCard island, the sign-up
119
- * form), like the API-backed rows the scheduling module contributes — listed
120
- * so the manifest is a complete install record, where the generator marks
121
- * them `kind: route` (#6646 D2).
122
- *
123
- * `/purchase-policy` is the product-shape purchase policy (#6646 D3): its
124
- * overlay seed carries returns, shipping and refunds sections — the overlay,
125
- * not `_base`, because the route is core and a scaffold naming no template
126
- * serves it too. A service-shape template overrides that seed with its own
127
- * cancellation and refunds file.
74
+ * Compose one alias into its manifest (#6753 D5, extended #6757 D7): the
75
+ * shape's standard roster, minus the alias's `drop` routes, with its `add`
76
+ * rows inserted before the `before` route (appended when there is none),
77
+ * then each `insert` group's rows spliced before ITS OWN `before` route, in
78
+ * array order — the one alias with more than one anchor point
79
+ * (`minimalism-site`) uses this to interleave its own rows into the shape's
80
+ * row order. Rows are pushed by reference, never cloned, so
81
+ * `manifest.roster.includes(row)` identity checks downstream keep working.
82
+ * Every refusal names the alias, so a malformed table fails at load — the
83
+ * shipped one — or in the test that injected it.
128
84
  *
129
- * `/blog` is deliberately NOT here (#5469 D3): the docs-blog module
130
- * contributes its own `/blog` row, so a base row would make `mergeRoster`
131
- * refuse every template that takes that module. The Free teaser row is
132
- * declared on `marketing-site-free` alone, below.
85
+ * @throws TemplateError on a `drop`, `before` or an `insert` group's `before`
86
+ * naming a route the shape (or the roster built so far) does not carry, a
87
+ * `before` with nothing to add, an `insert` group with empty `rows`, an
88
+ * `add`/`insert` row whose route a kept shape row already claims, or two
89
+ * `add`/`insert` rows naming one route.
133
90
  */
134
- const TEMPLATE_BASE_ROSTER = [
135
- { route: "/", content: ["content/en/HomePage/home.mdx"] },
136
- { route: "/software" },
137
- { route: "/software/[slug]", contentDir: "content/en/SoftwareProduct" },
138
- {
139
- route: "/use-cases",
140
- content: ["content/en/UseCasesList/use-cases.mdx"],
141
- },
142
- { route: "/use-cases/[slug]", contentDir: "content/en/UseCase" },
143
- { route: "/pricing", content: ["content/en/Pricing/pricing.mdx"] },
144
- { route: "/about", content: ["content/en/AboutPage/about.mdx"] },
145
- { route: "/faq", content: ["content/en/FAQPage/faq.mdx"] },
146
- { route: "/contact", content: ["content/en/ContactPage/contact.mdx"] },
147
- ...SHAPE_INDEPENDENT_ROWS,
148
- { route: "/sign-in" },
149
- { route: "/sign-up" },
150
- { route: "/privacy", content: ["content/en/PrivacyPage/privacy.mdx"] },
151
- { route: "/terms", content: ["content/en/TermsPage/terms.mdx"] },
152
- {
153
- route: "/purchase-policy",
154
- content: ["content/en/PurchasePolicyPage/purchase-policy.mdx"],
155
- },
156
- ];
157
- /**
158
- * The Free `/blog` teaser (#5469 D3) — one static BlogList page (schema `Blog`
159
- * with an empty `ItemList`; the correct page type, not a LandingPage) that
160
- * says what the Pro docs-blog module gives you. It is served by the generic
161
- * `[slug]` route — which probes `LandingPage` first, then `BlogList` — over
162
- * `content/en/BlogList/blog.mdx`, which ships in the always-copied overlay. `/blog` degrades by DIRECTORY PRECEDENCE, not
163
- * by a module gate: in a keyed docs-blog scaffold the module's own
164
- * `blog/[[...slug]]/page.tsx` directory exists and Next.js resolves the static
165
- * segment first, so the teaser file is inert; keyless, no `blog/` directory
166
- * exists and the teaser renders. Declared on the Free template only — on
167
- * `marketing-site` the docs-blog contribution claims the route.
168
- */
169
- const FREE_BLOG_TEASER_ROW = {
170
- route: "/blog",
171
- content: ["content/en/BlogList/blog.mdx"],
172
- };
91
+ function composeManifest(name, alias) {
92
+ const rows = SHAPE_ROSTERS[alias.shape];
93
+ const { drop = [], add = [], before, insert = [] } = alias.differences ?? {};
94
+ for (const route of drop) {
95
+ if (!rows.some((row) => row.route === route)) {
96
+ throw new TemplateError(`alias "${name}": differences.drop names "${route}", which shape "${alias.shape}" does not carry`);
97
+ }
98
+ }
99
+ const kept = rows.filter((row) => !drop.includes(row.route));
100
+ let roster;
101
+ if (before === undefined) {
102
+ roster = [...kept, ...add];
103
+ }
104
+ else {
105
+ const at = kept.findIndex((row) => row.route === before);
106
+ if (at === -1) {
107
+ throw new TemplateError(`alias "${name}": differences.before names "${before}", which shape "${alias.shape}" does not carry after its drops`);
108
+ }
109
+ if (add.length === 0) {
110
+ throw new TemplateError(`alias "${name}": differences.before names "${before}" but differences.add is empty`);
111
+ }
112
+ roster = [...kept.slice(0, at), ...add, ...kept.slice(at)];
113
+ }
114
+ for (const group of insert) {
115
+ const at = roster.findIndex((row) => row.route === group.before);
116
+ if (at === -1) {
117
+ throw new TemplateError(`alias "${name}": differences.insert names "${group.before}", which shape "${alias.shape}" does not carry after its drops`);
118
+ }
119
+ if (group.rows.length === 0) {
120
+ throw new TemplateError(`alias "${name}": differences.insert names "${group.before}" but its rows is empty`);
121
+ }
122
+ roster = [...roster.slice(0, at), ...group.rows, ...roster.slice(at)];
123
+ }
124
+ const shapeRoutes = new Set(kept.map((row) => row.route));
125
+ const added = new Set();
126
+ for (const row of [...add, ...insert.flatMap((group) => group.rows)]) {
127
+ if (shapeRoutes.has(row.route)) {
128
+ throw new TemplateError(`route "${row.route}" is claimed by both shape "${alias.shape}" and alias "${name}"`);
129
+ }
130
+ if (added.has(row.route)) {
131
+ throw new TemplateError(`alias "${name}": differences.add names route "${row.route}" twice`);
132
+ }
133
+ added.add(row.route);
134
+ }
135
+ return {
136
+ name,
137
+ requires: alias.modules,
138
+ ...(alias.layout && { layout: alias.layout }),
139
+ categories: alias.categories,
140
+ shape: alias.shape,
141
+ design: alias.design,
142
+ extends: BASE_LAYER_DIR,
143
+ roster,
144
+ };
145
+ }
173
146
  /**
174
- * The rows for the `signal-collectors` bundle's six sample pages (#6646
175
- * disposition (a)) — the project listing, its two project pages and the
176
- * three thank-you pages its `installMap` lands. The module contributes no
177
- * row for any of these routes (`module-architecture.ts`'s installMap
178
- * comment), so a template that takes the module declares them, and the
179
- * roster names every page the scaffold installs. `/projects` is a
180
- * module-less `GATEABLE_SURFACES` entry, so naming it here also copies the
181
- * route directory the samples render through (`rosterSurfaces`).
147
+ * The declared templates, each composed once at load from its `ALIASES` row
148
+ * (#6753) — no manifest here is hand-typed, so a template's shape, design,
149
+ * modules and roster have one authority: `starter-grid.ts`. Declaration order
150
+ * is `ALIASES`'s, which is `RECOGNIZED_TEMPLATES`'s.
151
+ *
152
+ * Frozen (#5507): this registry is also the issuer's authorization allowlist
153
+ * (spec 5480 § C7) — a mutable global there could be monkey-patched at
154
+ * runtime. A caller needing a different registry for a test injects its own
155
+ * fixture rather than mutating this one.
182
156
  */
183
- const SIGNAL_COLLECTORS_SAMPLE_ROWS = [
184
- { route: "/projects", content: ["content/en/ProjectsList/projects.mdx"] },
185
- { route: "/projects/[slug]", contentDir: "content/en/Project" },
186
- { route: "/thanks/[slug]", contentDir: "content/en/ThankYouPage" },
187
- ];
157
+ export const TEMPLATE_MANIFESTS = Object.freeze(Object.fromEntries(RECOGNIZED_TEMPLATES.map((name) => [
158
+ name,
159
+ composeManifest(name, ALIASES[name]),
160
+ ])));
188
161
  /**
189
- * `minimalism-site`'s own rows (#5589) — the one declared template that does
190
- * not reuse `TEMPLATE_BASE_ROSTER`. Its page set is the issue's ROSTER, and
191
- * the rows it does NOT carry are as deliberate as the ones it does: no
192
- * `/how-it-works`, product page or `/pricing` (a reports-first site sells
193
- * nothing from a pricing table), and no `/sign-in` (a code route every
194
- * scaffold carries regardless — `CORE_ROUTE_DIRS` — which this template
195
- * simply does not advertise as one of its pages). It does carry the three
196
- * pages every starter carries (`SHAPE_INDEPENDENT_ROWS`, #6658), with its
197
- * own `/book-a-call` seed pointing at the scheduling module's `/meet`.
198
- *
199
- * `/purchase-policy` is the service-shape purchase policy (#6646 D3) — this
200
- * template's own cancellation and refunds seed, overriding `_base`'s.
201
- *
202
- * `/meet`, `/meet/[slug]`, `/meet/confirmed`, `/reports/[slug]` and the three
203
- * `/flows/*` rows are absent here on purpose: they are the `scheduling` and
204
- * `signal-collectors` modules' own `pageContributions`, merged in by
205
- * `mergeRoster`. Declaring one here would make that merge refuse the
206
- * template outright (§ D8's both-claimants refusal).
162
+ * The composed FREE manifest per shape with no `--template` alias of its own
163
+ * (#6757 D5) — the coordinates a keyless `--shape S` builds. `Object.freeze`d,
164
+ * composed once at load by the same module-private `composeManifest` over
165
+ * `SHAPE_STARTER_ALIASES`, each manifest's `name` the shape. Data holding
166
+ * composed manifests, the status D5 gives `TEMPLATE_MANIFESTS`;
167
+ * `resolveTemplate` stays the only exported function that yields a
168
+ * template's roster, and its behavior is unchanged — it does NOT resolve a
169
+ * shape name (NFR-3, the issuer allowlist stays `TEMPLATE_MANIFESTS`-only).
207
170
  */
208
- const MINIMALISM_ROSTER = [
209
- { route: "/", content: ["content/en/HomePage/home.mdx"] },
210
- { route: "/reports", content: ["content/en/LandingPage/reports.mdx"] },
211
- { route: "/about", content: ["content/en/AboutPage/about.mdx"] },
212
- { route: "/services", content: ["content/en/ServicesList/services.mdx"] },
213
- { route: "/services/[slug]", contentDir: "content/en/Service" },
214
- { route: "/contact", content: ["content/en/ContactPage/contact.mdx"] },
215
- { route: "/referral", content: ["content/en/LandingPage/referral.mdx"] },
216
- { route: "/faq", content: ["content/en/FAQPage/faq.mdx"] },
217
- ...SHAPE_INDEPENDENT_ROWS,
218
- { route: "/privacy", content: ["content/en/PrivacyPage/privacy.mdx"] },
219
- { route: "/terms", content: ["content/en/TermsPage/terms.mdx"] },
220
- {
221
- route: "/purchase-policy",
222
- content: ["content/en/PurchasePolicyPage/purchase-policy.mdx"],
223
- },
224
- ...SIGNAL_COLLECTORS_SAMPLE_ROWS,
225
- ];
171
+ export const SHAPE_STARTERS = Object.freeze(Object.fromEntries(Object.entries(SHAPE_STARTER_ALIASES).map(([name, alias]) => [
172
+ name,
173
+ composeManifest(name, alias),
174
+ ])));
226
175
  /**
227
- * `simple-service`'s own rows (#6087) — the T1 lawn-care roster of Epic #5778.
228
- * The second declared template that does not reuse `TEMPLATE_BASE_ROSTER`: a
229
- * lawn-care site sells three named services from `/services`, not from a
230
- * pricing table, so `/how-it-works`, the product page and `/pricing` are absent —
231
- * and so are their seeds: `scaffold()` installs a `_base` or overlay seed
232
- * only for a row that names it (#6646 D1), exactly as for `minimalism-site`.
233
- *
234
- * `/meet`, `/meet/[slug]`, `/meet/confirmed` and `/appointments` are absent
235
- * here on purpose: they are the `scheduling` module's own
236
- * `pageContributions`, merged in by `mergeRoster`. Declaring one here would
237
- * make that merge refuse the template outright (§ D8's both-claimants
238
- * refusal).
239
- *
240
- * `/purchase/success` and `/purchase/cancel` ARE declared here, and that is a
241
- * documented divergence from spec § D3. The spec was written at a pin where
242
- * #6084 was expected to add the pair to `MODULE_SLICES.commerce`'s
243
- * `pageContributions`. It did not: the operator's shape-2 disposition
244
- * (#6084 D12) ships the return pair BASE TREE — `CORE_ROUTE_DIRS` carries
245
- * `[locale]/purchase`, and neither `GATEABLE_SURFACES` nor
246
- * `MODULE_SURFACES.commerce` gained an entry — so the `commerce` slice
247
- * contributes no rows at all and nothing else claims these two routes. The
248
- * issue's AC-1 requires the merged roster to carry them; with no module
249
- * claimant, the template's own roster is the only place that can declare
250
- * them, and doing so trips no collision.
251
- *
252
- * `/thanks/[slug]` is declared here for the same reason (#6646 D2): the
253
- * post-purchase thank-you route ships base tree and no module claims it.
254
- *
255
- * `/purchase-policy` is the service-shape purchase policy (#6646 D3) — its
256
- * cancellation and refunds sections replace the three #5947 policy rows.
257
- *
258
- * `/account` carries no `content`: it is a code route (the `(authed)` group),
259
- * like `/sign-in` on the base roster and like the API-backed rows the
260
- * scheduling module contributes. `/services/[slug]` declares its seed
261
- * DIRECTORY (`contentDir`, `content/en/Service`) — the three service pages.
176
+ * Where a starter's files live under `site-templates/` (#6757 D5) — the one
177
+ * place that says so. A `SHAPE_STARTERS` key (a bare-shape starter with no
178
+ * `--template` alias) lives at `shapes/<name>`, out of the template showcase
179
+ * (§ 1.6); every other name (a declared `--template` alias) is unchanged.
262
180
  */
263
- const SIMPLE_SERVICE_ROSTER = [
264
- { route: "/", content: ["content/en/HomePage/home.mdx"] },
265
- { route: "/services", content: ["content/en/ServicesList/services.mdx"] },
266
- { route: "/services/[slug]", contentDir: "content/en/Service" },
267
- { route: "/about", content: ["content/en/AboutPage/about.mdx"] },
268
- { route: "/contact", content: ["content/en/ContactPage/contact.mdx"] },
269
- { route: "/faq", content: ["content/en/FAQPage/faq.mdx"] },
270
- ...SHAPE_INDEPENDENT_ROWS,
271
- {
272
- route: "/purchase-policy",
273
- content: ["content/en/PurchasePolicyPage/purchase-policy.mdx"],
274
- },
275
- { route: "/privacy", content: ["content/en/PrivacyPage/privacy.mdx"] },
276
- { route: "/terms", content: ["content/en/TermsPage/terms.mdx"] },
277
- { route: "/account" },
278
- { route: "/purchase/success" },
279
- { route: "/purchase/cancel" },
280
- { route: "/thanks/[slug]" },
281
- ];
181
+ export function templateTreeDir(name) {
182
+ return Object.prototype.hasOwnProperty.call(SHAPE_STARTERS, name)
183
+ ? `shapes/${name}`
184
+ : name;
185
+ }
282
186
  /**
283
- * The three settled templates and their fixed `requires` lists — settled by
284
- * Sam, 2026-09-14 (parent § D12). The plain name is the Pro one: it is the
285
- * flagship and the demo.
187
+ * Look up a declared template — the one exported function that yields a
188
+ * template's roster (#6753 AC-1); `TEMPLATE_MANIFESTS`, the frozen record it
189
+ * reads, holds only manifests composed from `ALIASES`.
286
190
  *
287
- * `workspace` appears in none of them. It is a Pro module, but an authed app
288
- * surface contributing no PUBLIC page to a marketing site, and `requires`
289
- * names what the roster uses (parent § D15). A Pro key unlocks it regardless
290
- * of any template. `ModuleName` could express it either way — `workspace` is
291
- * one of the seven `RECOGNIZED_MODULES` names; it is simply not one any
292
- * template's fixed `requires` list names.
191
+ * With no `aliases`, returns the frozen `TEMPLATE_MANIFESTS` entry itself.
192
+ * With an injected alias table — the `resolveTemplateLineage` precedent
193
+ * (#5507) — composes `aliases[name]` against the shipped shape rosters, so a
194
+ * test exercises the composer's refusals without touching the shipped table.
293
195
  *
294
- * `commerce` is likewise in none of them: it is a per-site add-on available at
295
- * any level, so a template for it would contradict the offering model and
296
- * multiply the set combinatorially (parent § D13). It still contributes rows
297
- * to whichever template takes it, through `mergeRoster` below.
298
- *
299
- * Frozen (#5507): this registry is also the issuer's authorization allowlist
300
- * (spec 5480 § C7) — a mutable global there could be monkey-patched at
301
- * runtime. A caller needing a different registry for a test injects its own
302
- * fixture rather than mutating this one.
196
+ * @throws TemplateError on an undeclared name, or a malformed injected alias.
303
197
  */
304
- export const TEMPLATE_MANIFESTS = Object.freeze({
305
- /**
306
- * The Free template, and the mechanism's own proof (parent § D11): it
307
- * requires only `marketing`, so the keyless merge path is exercised by a
308
- * template we ship anyway rather than by a throwaway fixture.
309
- */
310
- "marketing-site-free": {
311
- name: "marketing-site-free",
312
- requires: ["marketing"],
313
- // The demo site carries the "Get started with this template" banner
314
- // (operator ruling 2026-09-23); every other layout knob keeps its default.
315
- layout: { announcementBanner: true },
316
- categories: ["accounting", "legal", "technology"],
317
- shape: "organization-product",
318
- design: "standard",
319
- extends: BASE_LAYER_DIR,
320
- roster: [...TEMPLATE_BASE_ROSTER, FREE_BLOG_TEASER_ROW],
321
- },
322
- /**
323
- * The Pro flagship. Its module list is OFFERINGS § 4 offering 3's tiers line
324
- * — "docs, scheduling and flows modules" — where "flows" is
325
- * `signal-collectors` (`MODULE_SURFACES` maps it to `["flows"]`).
326
- */
327
- "marketing-site": {
328
- name: "marketing-site",
329
- requires: ["marketing", "docs-blog", "scheduling", "signal-collectors"],
330
- // The flagship is the general-purpose template: it carries the whole
331
- // registry, so every group tile on the hub has at least one template.
332
- categories: [
333
- "accounting",
334
- "legal",
335
- "hvac",
336
- "roofing",
337
- "plumbing",
338
- "electrical",
339
- "landscaping",
340
- "auto-repair",
341
- "real-estate",
342
- "technology",
343
- "medical-specialty",
344
- ],
345
- shape: "organization-product",
346
- design: "standard",
347
- extends: BASE_LAYER_DIR,
348
- roster: [...TEMPLATE_BASE_ROSTER, ...SIGNAL_COLLECTORS_SAMPLE_ROWS],
349
- },
350
- /**
351
- * Pro Plus — the Pro list plus `multi-tenant`, which is correctly in the
352
- * list because it DOES contribute pages (the org and workspace surfaces).
353
- * Its `pageContributions` is nonetheless unset today, deferred to its own
354
- * instance, so this template's merged roster carries the same contributed
355
- * rows as `marketing-site`'s and no more. That is a module which has
356
- * declared no rows yet — visible in the diff — not a merge that dropped
357
- * declared rows.
358
- */
359
- "saas-product": {
360
- name: "saas-product",
361
- requires: [
362
- "marketing",
363
- "docs-blog",
364
- "scheduling",
365
- "signal-collectors",
366
- "multi-tenant",
367
- ],
368
- categories: ["technology", "medical-specialty"],
369
- shape: "organization-product",
370
- design: "standard",
371
- extends: BASE_LAYER_DIR,
372
- roster: [...TEMPLATE_BASE_ROSTER, ...SIGNAL_COLLECTORS_SAMPLE_ROWS],
373
- },
374
- /**
375
- * The second Pro template (#5589) — the reports-first, scroll-snap deck
376
- * site. NOT `marketing-site` plus a module: its `requires` omits
377
- * `docs-blog`, so the two Pro templates are siblings under `_base`
378
- * (#5618 D2), never a lineage. Level still derives from `requires`:
379
- * `scheduling` and `signal-collectors` are paid, so `templateDelivery`
380
- * returns `"paid"` exactly as it does for `marketing-site`.
381
- *
382
- * It declares its OWN roster rather than reusing `TEMPLATE_BASE_ROSTER`
383
- * (the only declared template that does): the deck home replaces the
384
- * document-scroll home, and `/how-it-works`, the product page, `/pricing` and
385
- * `/sign-in` are not pages this site has. The roster is what the
386
- * template IS, so an unrostered page has neither a route directory
387
- * (`rosterSurfaces`) nor its `_base`/overlay seed (#6646 D1).
388
- *
389
- * It declares NO `layout` block (#6638): the deck home, mark-only nav and
390
- * centered footer are the `minimalism` design's, supplied by
391
- * `DESIGN_LAYOUTS` from `design:` — the look any shape can wear. The
392
- * roster rows the `organization-service` shape does not derive
393
- * (`/reports`, `/referral`) are recorded with their reasons in this
394
- * template's TEMPLATE-MANIFEST.md § 3a.
395
- *
396
- * `/reports` and `/referral` carry no `GATEABLE_SURFACES` entry and need
397
- * none: both are single-segment `LandingPage` slugs served by the generic
398
- * `[slug]` route (#5469 D2), the same mechanism Free's `/how-it-works`
399
- * uses. `/reports/[slug]` (the reader) is the signal-collectors module's
400
- * own contributed row.
401
- *
402
- * `/services/[slug]` declares its seed DIRECTORY as `contentDir`
403
- * (`content/en/Service`, as `routes-map.ts` records it). The three
404
- * samples ride the template tree unmapped, landing at that directory.
405
- */
406
- "minimalism-site": {
407
- name: "minimalism-site",
408
- requires: ["marketing", "scheduling", "signal-collectors"],
409
- categories: ["real-estate", "technology", "legal"],
410
- shape: "organization-service",
411
- design: "minimalism",
412
- extends: BASE_LAYER_DIR,
413
- roster: MINIMALISM_ROSTER,
414
- },
415
- /**
416
- * The T1 lawn-care template (#6087, Epic #5778) — the first site shape
417
- * that composes `scheduling` and `commerce`: three priced services, a
418
- * booker, a checkout return pair and the three #5947 policy pages.
419
- *
420
- * `requires` lists `marketing` for the registry convention every other
421
- * manifest keeps: it is free (`module-tier.ts`'s `FREE_MODULES`), so it
422
- * cannot change `templateDelivery`'s verdict, and listing it keeps the
423
- * template's own declaration — not `DEFAULT_MODULES` — the authority for
424
- * what a bare `--template simple-service` run installs. The order is
425
- * `RECOGNIZED_MODULES` order, which is also `normalizeModules`' output
426
- * order, so the declared list and the merge order read the same.
427
- *
428
- * `scheduling` and `commerce` are both paid, so `templateDelivery`
429
- * returns `"paid"` from either one alone.
430
- *
431
- * It declares NO `layout` block: the default chrome (full nav, column
432
- * footer, no announcement banner) is the one a lawn-care site wants, and
433
- * the issue's STOP boundary excludes layout knobs the spec does not name.
434
- */
435
- "simple-service": {
436
- name: "simple-service",
437
- requires: ["marketing", "scheduling", "commerce"],
438
- categories: ["landscaping"],
439
- shape: "local-business-service",
440
- design: "standard",
441
- extends: BASE_LAYER_DIR,
442
- roster: SIMPLE_SERVICE_ROSTER,
443
- },
444
- });
445
- /** Look up a declared template. @throws TemplateError on an undeclared name. */
446
- export function resolveTemplate(name) {
447
- const manifest = TEMPLATE_MANIFESTS[name];
448
- if (!manifest) {
449
- throw new TemplateError(`unknown template "${name}". Declared: ${Object.keys(TEMPLATE_MANIFESTS).join(", ")}`);
198
+ export function resolveTemplate(name, aliases) {
199
+ const declared = aliases ?? TEMPLATE_MANIFESTS;
200
+ if (!Object.prototype.hasOwnProperty.call(declared, name)) {
201
+ throw new TemplateError(`unknown template "${name}". Declared: ${Object.keys(declared).join(", ")}`);
450
202
  }
451
- return manifest;
203
+ return aliases
204
+ ? composeManifest(name, aliases[name])
205
+ : TEMPLATE_MANIFESTS[name];
452
206
  }
453
207
  /**
454
208
  * Resolve a template's `extends` chain — `[name, …ancestors, "_base"]` — in
@@ -20,22 +20,24 @@
20
20
  * ("Deliberately imports nothing from `parse-args.ts`: that module imports
21
21
  * THIS one … so a back-import would close a cycle"), and `surfaces.ts` is the
22
22
  * precedent this file follows: the flag's vocabulary lives in a module the
23
- * parser can import because it imports nothing at all.
23
+ * parser can import because it reaches nothing that imports the parser.
24
24
  *
25
- * THE REGISTRY IS STILL `template-manifest.ts` (parent record D2). This file
26
- * declares NAMES; that one declares what a template IS — `requires`, the
27
- * roster, the `installMap`, the free-vs-paid derivation — and it stays the
28
- * single place a template is defined. The two cannot drift: the names here
29
- * are asserted equal to `Object.keys(TEMPLATE_MANIFESTS)` by
30
- * `__tests__/template-scaffold.test.ts`, so adding a template without listing
31
- * it here (or the reverse) fails the suite rather than shipping a flag that
32
- * accepts a name the registry does not know.
25
+ * THE NAMES ARE DERIVED, NOT TYPED (#6753 D1). They are the key set of
26
+ * `ALIASES` in `starter-grid.ts` — the alias table `template-manifest.ts`
27
+ * composes every manifest from — so the flag's vocabulary and the registry
28
+ * cannot drift. `starter-grid.ts` is the one file this module imports, and it
29
+ * imports nothing at runtime (every import there is `import type`), so
30
+ * `parse-args.ts` → here → `starter-grid.ts` still reaches no module that
31
+ * imports `parse-args.ts` back. `ALIASES` does not live in
32
+ * `template-manifest.ts` for exactly that reason: importing it from there
33
+ * would re-open the cycle above.
33
34
  *
34
35
  * Rejected: lazifying `module-tier.ts`'s `PAID_MODULES` to break the cycle at
35
36
  * its source. It is the correct structural fix and it is not this unit's to
36
37
  * make — `module-tier.ts` is a READ row in spec 5481 § 4, and `PAID_MODULES`
37
38
  * is consumed as a plain array across the package.
38
39
  */
39
- /** The declared site templates, in `TEMPLATE_MANIFESTS` declaration order. */
40
- export declare const RECOGNIZED_TEMPLATES: readonly ["marketing-site-free", "marketing-site", "saas-product", "minimalism-site", "simple-service"];
41
- export type TemplateName = (typeof RECOGNIZED_TEMPLATES)[number];
40
+ import { ALIASES } from "./starter-grid.js";
41
+ export type TemplateName = keyof typeof ALIASES;
42
+ /** The declared site templates, in `ALIASES` (and so `TEMPLATE_MANIFESTS`) declaration order. */
43
+ export declare const RECOGNIZED_TEMPLATES: readonly TemplateName[];
@@ -20,27 +20,23 @@
20
20
  * ("Deliberately imports nothing from `parse-args.ts`: that module imports
21
21
  * THIS one … so a back-import would close a cycle"), and `surfaces.ts` is the
22
22
  * precedent this file follows: the flag's vocabulary lives in a module the
23
- * parser can import because it imports nothing at all.
23
+ * parser can import because it reaches nothing that imports the parser.
24
24
  *
25
- * THE REGISTRY IS STILL `template-manifest.ts` (parent record D2). This file
26
- * declares NAMES; that one declares what a template IS — `requires`, the
27
- * roster, the `installMap`, the free-vs-paid derivation — and it stays the
28
- * single place a template is defined. The two cannot drift: the names here
29
- * are asserted equal to `Object.keys(TEMPLATE_MANIFESTS)` by
30
- * `__tests__/template-scaffold.test.ts`, so adding a template without listing
31
- * it here (or the reverse) fails the suite rather than shipping a flag that
32
- * accepts a name the registry does not know.
25
+ * THE NAMES ARE DERIVED, NOT TYPED (#6753 D1). They are the key set of
26
+ * `ALIASES` in `starter-grid.ts` — the alias table `template-manifest.ts`
27
+ * composes every manifest from — so the flag's vocabulary and the registry
28
+ * cannot drift. `starter-grid.ts` is the one file this module imports, and it
29
+ * imports nothing at runtime (every import there is `import type`), so
30
+ * `parse-args.ts` → here → `starter-grid.ts` still reaches no module that
31
+ * imports `parse-args.ts` back. `ALIASES` does not live in
32
+ * `template-manifest.ts` for exactly that reason: importing it from there
33
+ * would re-open the cycle above.
33
34
  *
34
35
  * Rejected: lazifying `module-tier.ts`'s `PAID_MODULES` to break the cycle at
35
36
  * its source. It is the correct structural fix and it is not this unit's to
36
37
  * make — `module-tier.ts` is a READ row in spec 5481 § 4, and `PAID_MODULES`
37
38
  * is consumed as a plain array across the package.
38
39
  */
39
- /** The declared site templates, in `TEMPLATE_MANIFESTS` declaration order. */
40
- export const RECOGNIZED_TEMPLATES = [
41
- "marketing-site-free",
42
- "marketing-site",
43
- "saas-product",
44
- "minimalism-site",
45
- "simple-service",
46
- ];
40
+ import { ALIASES } from "./starter-grid.js";
41
+ /** The declared site templates, in `ALIASES` (and so `TEMPLATE_MANIFESTS`) declaration order. */
42
+ export const RECOGNIZED_TEMPLATES = Object.keys(ALIASES);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-ailk",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "Scaffolder for AI Launch Kit — `pnpm create ailk@latest <dir>` produces a starter repo consuming @working-theory/* from npm (per ADR 0007).",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -25,8 +25,8 @@
25
25
  "tar": "^7.5.21"
26
26
  },
27
27
  "devDependencies": {
28
- "@working-theory/schema": "0.27.0",
29
- "@working-theory/templates": "0.27.0"
28
+ "@working-theory/templates": "0.28.0",
29
+ "@working-theory/schema": "0.28.0"
30
30
  },
31
31
  "repository": {
32
32
  "type": "git",