create-ailk 0.13.0 → 0.15.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 (188) hide show
  1. package/README.md +12 -4
  2. package/component-catalog.json +250 -36
  3. package/component-catalog.md +10 -6
  4. package/dist/add-module.d.ts +7 -0
  5. package/dist/add-module.js +34 -2
  6. package/dist/add-route.js +18 -2
  7. package/dist/apply-design.d.ts +32 -2
  8. package/dist/apply-design.js +155 -22
  9. package/dist/cli.js +24 -3
  10. package/dist/derive-architecture.d.ts +15 -2
  11. package/dist/derive-architecture.js +34 -6
  12. package/dist/design-compile.d.ts +70 -2
  13. package/dist/design-compile.js +97 -1
  14. package/dist/design-home.d.ts +74 -0
  15. package/dist/design-home.js +256 -0
  16. package/dist/design-package.d.ts +39 -2
  17. package/dist/design-package.js +224 -9
  18. package/dist/design-presets.d.ts +17 -1
  19. package/dist/design-presets.js +17 -5
  20. package/dist/design-scaffold.d.ts +6 -25
  21. package/dist/design-scaffold.js +26 -74
  22. package/dist/index.d.ts +2 -1
  23. package/dist/index.js +1 -1
  24. package/dist/lib/chrome-readers.d.ts +21 -0
  25. package/dist/lib/chrome-readers.js +100 -0
  26. package/dist/lib/design-checks.d.ts +12 -1
  27. package/dist/lib/design-checks.js +38 -2
  28. package/dist/lib/extract-module-bundle.d.ts +11 -0
  29. package/dist/lib/extract-module-bundle.js +36 -6
  30. package/dist/lib/license-key-files.d.ts +6 -1
  31. package/dist/lib/license-key-files.js +9 -4
  32. package/dist/lib/module-license-gate.d.ts +9 -0
  33. package/dist/lib/module-license-gate.js +18 -0
  34. package/dist/module-architecture.d.ts +9 -0
  35. package/dist/module-architecture.js +6 -0
  36. package/dist/parse-args.d.ts +22 -0
  37. package/dist/parse-args.js +73 -0
  38. package/dist/programmatic.d.ts +20 -0
  39. package/dist/programmatic.js +180 -35
  40. package/dist/site-chrome.d.ts +42 -0
  41. package/dist/site-chrome.js +140 -2
  42. package/dist/site-fonts.d.ts +21 -0
  43. package/dist/site-fonts.js +54 -0
  44. package/dist/site-manifest.d.ts +12 -5
  45. package/dist/site-manifest.js +25 -11
  46. package/dist/site-roster.d.ts +16 -0
  47. package/dist/site-roster.js +81 -0
  48. package/dist/starter-grid.js +15 -0
  49. package/dist/surfaces.js +12 -0
  50. package/dist/sync-routes.js +11 -2
  51. package/dist/team-profiles.d.ts +29 -0
  52. package/dist/team-profiles.js +43 -0
  53. package/dist/topology.d.ts +131 -0
  54. package/dist/topology.js +533 -0
  55. package/package.json +3 -3
  56. package/release-scope.json +1 -1
  57. package/site-templates/_base/content/en/TeamList/team.mdx +68 -0
  58. package/site-templates/_base/content/en/TeamMember/first-member.mdx +22 -0
  59. package/site-templates/_base/content/en/TeamMember/second-member.mdx +22 -0
  60. package/site-templates/_base/content/en/TeamMember/third-member.mdx +22 -0
  61. package/site-templates/designs/minimalism/DESIGN.md +166 -36
  62. package/site-templates/designs/minimalism/home/local-business-service.mdx +119 -0
  63. package/site-templates/designs/minimalism/home/organization-product.mdx +132 -0
  64. package/site-templates/designs/minimalism/home/organization-service.mdx +131 -0
  65. package/site-templates/designs/minimalism/home/person-service.mdx +101 -0
  66. package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +34 -1
  67. package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +20 -0
  68. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +34 -1
  69. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +22 -1
  70. package/site-templates/marketing-site-free/ailk-template.json +1 -1
  71. package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.json +34 -1
  72. package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.md +26 -7
  73. package/site-templates/shapes/local-business-service/ailk-template.json +1 -1
  74. package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.json +34 -1
  75. package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.md +26 -7
  76. package/site-templates/shapes/organization-service/ailk-template.json +1 -1
  77. package/templates/README.md +2 -0
  78. package/templates/apps/api/CLAUDE.md +6 -1
  79. package/templates/apps/api/package.json +1 -0
  80. package/templates/apps/api/src/__tests__/factories/agent-caller.ts +5 -1
  81. package/templates/apps/api/src/lib/__mocks__/prisma.ts +10 -0
  82. package/templates/apps/api/src/lib/__tests__/agent-store.test.ts +114 -1
  83. package/templates/apps/api/src/lib/__tests__/site.test.ts +54 -0
  84. package/templates/apps/api/src/lib/agent-store.ts +95 -3
  85. package/templates/apps/api/src/lib/site.ts +37 -0
  86. package/templates/apps/api/src/lib/spoke-store.ts +18 -2
  87. package/templates/apps/api/src/middleware/__tests__/auth.bearer-path.test.ts +4 -0
  88. package/templates/apps/api/src/middleware/__tests__/default-tenant.test.ts +22 -0
  89. package/templates/apps/api/src/middleware/auth.ts +3 -0
  90. package/templates/apps/api/src/openapi/__tests__/openapi.test.ts +23 -0
  91. package/templates/apps/api/src/openapi/spec.ts +38 -2
  92. package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +50 -2
  93. package/templates/apps/api/src/routes/checkout/sessions.ts +7 -0
  94. package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +275 -0
  95. package/templates/apps/api/src/routes/leads/index.ts +69 -6
  96. package/templates/apps/api/src/routes/webhooks/README.md +39 -0
  97. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-site-filter.test.ts +366 -0
  98. package/templates/apps/api/src/routes/webhooks/stripe.ts +61 -1
  99. package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +199 -0
  100. package/templates/apps/api/src/services/default-tenant.ts +41 -7
  101. package/templates/apps/api/src/services/lead-routing.ts +148 -29
  102. package/templates/apps/api/src/services/public-site-resolver.ts +48 -0
  103. package/templates/apps/mcp/__tests__/index.test.ts +5 -5
  104. package/templates/apps/mcp/package.json +1 -1
  105. package/templates/apps/mcp/src/index.ts +6 -4
  106. package/templates/apps/mcp/src/lib/content-adapter.ts +2 -2
  107. package/templates/apps/mcp/src/lib/topology.generated.ts +24 -0
  108. package/templates/apps/web/CLAUDE.md +12 -0
  109. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/settings.test.tsx +10 -0
  110. package/templates/apps/web/app/[locale]/(authed)/account/settings/__tests__/theme-modes.test.ts +42 -0
  111. package/templates/apps/web/app/[locale]/(authed)/account/settings/theme-modes.ts +8 -0
  112. package/templates/apps/web/app/[locale]/[slug]/[detail]/__tests__/page.test.tsx +3 -0
  113. package/templates/apps/web/app/[locale]/[slug]/__tests__/page.test.tsx +3 -0
  114. package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +2 -2
  115. package/templates/apps/web/app/[locale]/case-studies/__tests__/page-renderer-migration.test.tsx +2 -2
  116. package/templates/apps/web/app/[locale]/clients/__tests__/page-renderer-migration.test.tsx +14 -1
  117. package/templates/apps/web/app/[locale]/contact/__tests__/page-renderer-migration.test.tsx +66 -14
  118. package/templates/apps/web/app/[locale]/contact/__tests__/site-contact-lines.test.ts +163 -0
  119. package/templates/apps/web/app/[locale]/contact/site-contact-lines.ts +68 -0
  120. package/templates/apps/web/app/[locale]/courses/__tests__/page-renderer-migration.test.tsx +4 -4
  121. package/templates/apps/web/app/[locale]/downloads/__tests__/page-renderer-migration.test.tsx +15 -3
  122. package/templates/apps/web/app/[locale]/events/__tests__/page-renderer-migration.test.tsx +17 -6
  123. package/templates/apps/web/app/[locale]/for/__tests__/page-renderer-migration.test.tsx +14 -1
  124. package/templates/apps/web/app/[locale]/how-to/__tests__/page-renderer-migration.test.tsx +2 -2
  125. package/templates/apps/web/app/[locale]/integrations/__tests__/page-renderer-migration.test.tsx +4 -6
  126. package/templates/apps/web/app/[locale]/layout.tsx +212 -57
  127. package/templates/apps/web/app/[locale]/newsletter/__tests__/page.test.tsx +3 -0
  128. package/templates/apps/web/app/[locale]/page.tsx +37 -0
  129. package/templates/apps/web/app/[locale]/partners/__tests__/page-renderer-migration.test.tsx +2 -2
  130. package/templates/apps/web/app/[locale]/products/__tests__/page-renderer-migration.test.tsx +4 -6
  131. package/templates/apps/web/app/[locale]/projects/__tests__/page-renderer-migration.test.tsx +5 -2
  132. package/templates/apps/web/app/[locale]/resources/__tests__/page-renderer-migration.test.tsx +15 -2
  133. package/templates/apps/web/app/[locale]/services/__tests__/page-renderer-migration.test.tsx +43 -6
  134. package/templates/apps/web/app/[locale]/services/page.tsx +14 -2
  135. package/templates/apps/web/app/[locale]/software/__tests__/page-renderer-migration.test.tsx +2 -2
  136. package/templates/apps/web/app/[locale]/team/[slug]/page.tsx +45 -0
  137. package/templates/apps/web/app/[locale]/team/__tests__/team-routes.test.tsx +155 -0
  138. package/templates/apps/web/app/[locale]/team/page.tsx +49 -0
  139. package/templates/apps/web/app/[locale]/tech-articles/__tests__/page-renderer-migration.test.tsx +2 -2
  140. package/templates/apps/web/app/[locale]/testimonials/__tests__/page-renderer-migration.test.tsx +14 -1
  141. package/templates/apps/web/app/[locale]/thanks/[slug]/__tests__/page.test.tsx +3 -0
  142. package/templates/apps/web/app/[locale]/whitepapers/__tests__/page-renderer-migration.test.tsx +2 -2
  143. package/templates/apps/web/app/not-found.tsx +47 -0
  144. package/templates/apps/web/app/openapi.json/__tests__/route.test.ts +89 -1
  145. package/templates/apps/web/app/openapi.json/route.ts +69 -1
  146. package/templates/apps/web/instrumentation.ts +7 -4
  147. package/templates/apps/web/lib/__tests__/closing-footer-path.test.ts +96 -0
  148. package/templates/apps/web/lib/__tests__/profile-back-link.test.ts +95 -0
  149. package/templates/apps/web/lib/__tests__/relay.test.ts +290 -0
  150. package/templates/apps/web/lib/__tests__/site-brand.test.ts +52 -0
  151. package/templates/apps/web/lib/__tests__/site-theme-head.test.ts +70 -0
  152. package/templates/apps/web/lib/__tests__/site-theme.test.ts +64 -1
  153. package/templates/apps/web/lib/closing-footer-path.ts +50 -0
  154. package/templates/apps/web/lib/profile-back-link.ts +51 -0
  155. package/templates/apps/web/lib/relay.ts +206 -2
  156. package/templates/apps/web/lib/site-brand.tsx +22 -1
  157. package/templates/apps/web/lib/site-chrome.ts +15 -0
  158. package/templates/apps/web/lib/site-fonts.ts +15 -0
  159. package/templates/apps/web/lib/site-logo.tsx +39 -1
  160. package/templates/apps/web/lib/site-theme-head.ts +33 -0
  161. package/templates/apps/web/lib/site-theme.ts +13 -1
  162. package/templates/apps/web/lib/topology.generated.ts +23 -0
  163. package/templates/apps/web/messages/en.json +7 -1
  164. package/templates/apps/web/package.json +1 -1
  165. package/templates/database/CHANGELOG.md +32 -0
  166. package/templates/database/__tests__/agents.db.test.ts +65 -0
  167. package/templates/database/__tests__/lead-idempotency-key.db.test.ts +92 -0
  168. package/templates/database/auth/schema.prisma +23 -0
  169. package/templates/database/inbox/schema.prisma +13 -0
  170. package/templates/database/jest.config.cjs +7 -11
  171. package/templates/database/migrations/20261008120000_user_last_workspace/migration.sql +35 -0
  172. package/templates/database/migrations/20261008130000_lead_idempotency_key/migration.sql +20 -0
  173. package/templates/database/migrations/20261009120000_agent_grants/migration.sql +33 -0
  174. package/templates/database/package.json +1 -1
  175. package/versions.json +35 -34
  176. package/site-templates/designs/minimalism/sample/README.md +0 -48
  177. package/site-templates/designs/minimalism/sample/content/en/HomePage/home.mdx +0 -244
  178. package/site-templates/designs/minimalism/sample/public/avatar-placeholder-1.svg +0 -7
  179. package/site-templates/designs/minimalism/sample/public/avatar-placeholder-2.svg +0 -7
  180. package/site-templates/designs/minimalism/sample/public/avatar-placeholder-3.svg +0 -7
  181. package/site-templates/designs/minimalism/sample/public/avatar-placeholder-4.svg +0 -7
  182. package/site-templates/designs/minimalism/sample/public/avatar-placeholder-5.svg +0 -7
  183. package/site-templates/designs/minimalism/sample/public/avatar-placeholder-6.svg +0 -7
  184. package/site-templates/designs/minimalism/sample/public/hero-backdrop.svg +0 -17
  185. package/site-templates/designs/minimalism/sample/public/logo-placeholder-1.svg +0 -6
  186. package/site-templates/designs/minimalism/sample/public/logo-placeholder-2.svg +0 -6
  187. package/site-templates/designs/minimalism/sample/public/logo-placeholder-3.svg +0 -6
  188. package/site-templates/designs/minimalism/sample/public/wordmark-placeholder.svg +0 -28
@@ -15,13 +15,13 @@ The component inventory of `@working-theory/ui` and the shells of `@working-theo
15
15
  3. Check the Primitives, Panels and Shells tables too — a low-level control, a collapsible layout container, or a route-level frame lives there, not under Sections or Blocks.
16
16
  4. Only then build. If you do build, you are declaring the capability is genuinely absent.
17
17
 
18
- Inventory: **307 component files** across 16 tiers — 55 sections, 99 blocks, 18 chrome, 79 primitives, 6 panels, 6 shells, 5 docs-blog-blocks, 1 docs-blog-chrome, 2 docs-blog-shells, 2 scheduling-sections, 5 scheduling-blocks, 5 signal-collectors-sections, 9 signal-collectors-blocks, 2 commerce-sections, 12 commerce-blocks, 1 commerce-panels. 304 distinct names: `SchedulerEmbed`, `Separator`, `Tabs` exist in more than one tier as genuinely different components, listed once per tier with its own source path.
18
+ Inventory: **311 component files** across 16 tiers — 55 sections, 101 blocks, 18 chrome, 79 primitives, 6 panels, 8 shells, 5 docs-blog-blocks, 1 docs-blog-chrome, 2 docs-blog-shells, 2 scheduling-sections, 5 scheduling-blocks, 5 signal-collectors-sections, 9 signal-collectors-blocks, 2 commerce-sections, 12 commerce-blocks, 1 commerce-panels. 308 distinct names: `SchedulerEmbed`, `Separator`, `Tabs` exist in more than one tier as genuinely different components, listed once per tier with its own source path.
19
19
 
20
- Description coverage: **303/307** components carry a capability description derived from source — 44 from the section registry's hand-authored when-to-use text, 228 from the component's own doc-comment header, 31 from its tier barrel's roster line. **4** have none; they are listed under [Undescribed components](#undescribed-components) rather than given a sentence invented from their name.
20
+ Description coverage: **307/311** components carry a capability description derived from source — 44 from the section registry's hand-authored when-to-use text, 232 from the component's own doc-comment header, 31 from its tier barrel's roster line. **4** have none; they are listed under [Undescribed components](#undescribed-components) rather than given a sentence invented from their name.
21
21
 
22
- Of those, **272** are *strong* — a capability sentence you could match a requirement against — and **31** are *weak*: either one short clause, or a long one that spends itself on wiring (client boundaries, class names, which primitives it composes) instead of on what the thing is for. Weak rows are marked `(weak)` and listed in full under [Weak descriptions](#weak-descriptions). Coverage is 99%, but **findable-without-luck coverage is 89%**.
22
+ Of those, **276** are *strong* — a capability sentence you could match a requirement against — and **31** are *weak*: either one short clause, or a long one that spends itself on wiring (client boundaries, class names, which primitives it composes) instead of on what the thing is for. Weak rows are marked `(weak)` and listed in full under [Weak descriptions](#weak-descriptions). Coverage is 99%, but **findable-without-luck coverage is 89%**.
23
23
 
24
- Per tier, strong out of total: **Sections** 54/55 · **Blocks** 93/99 · **Chrome** 10/18 · **Primitives** 64/79 (4 undescribed) · **Panels** 5/6 · **Shells** 5/6 · **Docs + Blog Blocks** 5/5 · **Docs + Blog Chrome** 1/1 · **Docs + Blog Shells** 0/2 · **Scheduling Sections** 2/2 · **Scheduling Blocks** 5/5 · **Signal-Collectors Sections** 5/5 · **Signal-Collectors Blocks** 8/9 · **Commerce Sections** 2/2 · **Commerce Blocks** 12/12 · **Commerce Panels** 1/1. A tier low on this line is a tier where a capability search is most likely to come up empty on something that exists.
24
+ Per tier, strong out of total: **Sections** 54/55 · **Blocks** 95/101 · **Chrome** 10/18 · **Primitives** 64/79 (4 undescribed) · **Panels** 5/6 · **Shells** 7/8 · **Docs + Blog Blocks** 5/5 · **Docs + Blog Chrome** 1/1 · **Docs + Blog Shells** 0/2 · **Scheduling Sections** 2/2 · **Scheduling Blocks** 5/5 · **Signal-Collectors Sections** 5/5 · **Signal-Collectors Blocks** 8/9 · **Commerce Sections** 2/2 · **Commerce Blocks** 12/12 · **Commerce Panels** 1/1. A tier low on this line is a tier where a capability search is most likely to come up empty on something that exists.
25
25
 
26
26
  That grade is **mechanical**. It counts what it can measure — how much a description adds beyond the component's own name, how much of it is spent on wiring, whether the extraction came out damaged — and it cannot tell a fluent paragraph about the wrong subject from a useful one. A person reading these tables will find more rows unusable than the weak count admits. Treat it as a floor on the problem, not a measurement of it, and read the source before concluding a capability is absent.
27
27
 
@@ -119,7 +119,7 @@ Import: `import { X } from "@working-theory/ui/sections";` (or from the `@workin
119
119
 
120
120
  ---
121
121
 
122
- ## Blocks (99)
122
+ ## Blocks (101)
123
123
 
124
124
  Intermediate composition tiles — the pieces sections are built from. Compose primitives and panels; never import a section.
125
125
 
@@ -158,6 +158,7 @@ Import: `import { X } from "@working-theory/ui/blocks";` (or from the `@working-
158
158
  | `DescriptionList` | Semantic <dl> of aligned term/definition rows (rows + inline layouts). A semantic <dl> rendering label/value rows from an `items` prop. Each item is a <dt> (muted term) paired with a <dd> (value). The value is a ReactNode, so callers compose richer values (spans, lists, mono text) inside it. | `import { DescriptionList } from "@working-theory/ui/blocks"` |
159
159
  | `DetailCard` | The collapsible console / data-panel card: a composable card with a header (title + optional actions + collapse toggle), an optional tab bar, a body slot, and an optional footer. The "deferred SidePanel DetailCard" from, built as a general content card any console / detail view can compose without inventing its own card structure. | `import { DetailCard } from "@working-theory/ui/blocks"` |
160
160
  | `DetailHeader` | The entity-bearing sibling of PageHeader: a Breadcrumb wayfinding trail above an EntityHeader identity row, plus a forwarded actions slot. | `import { DetailHeader } from "@working-theory/ui/blocks"` |
161
+ | `DisplayList` | Centred display-size rows of short labels, each optionally a link, with no rules, cards or dividers between them — the bondcap.com "what we offer" list and its report archive grid. | `import { DisplayList } from "@working-theory/ui/blocks"` |
161
162
  | `DistributionList` | Per-option answer-share list over the Progress primitive. One question's answer distribution: a title, an "answered" count line, and one row per declared option (a label, a pre-formatted share `display` string, and a `Progress` bar). `display` is pre-formatted by the caller (Rule 10 — this block never formats numbers itself); `share` drives the `Progress` fill, scaled to the primitive's 0–100 range. | `import { DistributionList } from "@working-theory/ui/blocks"` |
162
163
  | `EntityHeader` | The single identity-row source: an optional leading avatar, a title/name (Heading), an optional subtitle/meta line (Text), an optional type-badge slot above the title, and an optional trailing actions slot. | `import { EntityHeader } from "@working-theory/ui/blocks"` |
163
164
  | `FaqItem` | Q&A pair block for FAQ section families (expanded + accordion). Defaults reproduce the current render except the deliberate item-spacing-asymmetry fix. | `import { FaqItem } from "@working-theory/ui/blocks"` |
@@ -208,6 +209,7 @@ Import: `import { X } from "@working-theory/ui/blocks";` (or from the `@working-
208
209
  | `SectionHeader` | Alignment-parameterized section header (eyebrow/subtitle/description/supportLink). The same component serves two ranks (headingLevel="section" \| "subsection") and three alignments (align="start" \| "center" \| "end"). It knows nothing about columns/splits/carousels — it is width-flexible and fills whatever container the section places it in. | `import { SectionHeader } from "@working-theory/ui/blocks"` |
209
210
  | `ShareBar` | The reader-facing counterpart to DocsActionBar. Both surfaces share the same SplitButton *affordance + slot* (outline variant, right-justified on the post title row) but fill it with different actions: DocsActionBar carries the AI-ingestion set (Copy page + Open in …), ShareBar carries the social-share set (X · LinkedIn · Bluesky · Copy link). One primitive, two configs. | `import { ShareBar } from "@working-theory/ui/blocks"` |
210
211
  | `SinglePriceCard` | 2-column split card for the Pricing 'single-price-with-details' variant. *(weak)* | `import { SinglePriceCard } from "@working-theory/ui/blocks"` |
212
+ | `SlideAdvance` | The deck's slide-advance control. One ghost Button holding a hairline chevron: a down chevron moves to the next slide, an up chevron to the first. A native button, so a click, Enter and Space all fire the same move, and nothing else listens: no wheel, touch, scroll or key handler is added, so native scroll-snap still settles every user scroll (the argument against a scroll-hijacking deck). | `import { SlideAdvance } from "@working-theory/ui/blocks"` |
211
213
  | `SocialIcons` | Horizontal row of social icon links. Renders brand icons (via Icon primitive + simple-icons paths) inside accessible Link primitives. Each platform link gets an aria-label. Shared by company-mission, simple-centered, and newsletter-below footer variants, and composed by TeamMemberCard for member social links. *(weak)* | `import { SocialIcons } from "@working-theory/ui/blocks"` |
212
214
  | `SortableList` | The one @dnd-kit/sortable reorder block: mouse, touch and keyboard drag behind a six-dot grip, announced, controlled. `@dnd-kit/sortable` wrapped ONCE behind our six-dot grip. The ONLY file in the monorepo that imports `@dnd-kit/*` — enforced by tests/architecture/no-dnd-kit-outside-sortablelist.test.ts (SD-2). | `import { SortableList } from "@working-theory/ui/blocks"` |
213
215
  | `StatTicker` | Static, horizontally overflow-scrollable workspace stat strip with an optional trailing CTA; no auto-scroll marquee (ploy-capture recomposition). The Overview stat strip captured off ploy.ai (behavior spec: -2, — `overview-ticker` "-viewport"/"-track"/"-segment"/"-value"/"-cta"). | `import { StatTicker } from "@working-theory/ui/blocks"` |
@@ -365,7 +367,7 @@ Import: `import { X } from "@working-theory/ui/panels";` (or from the `@working-
365
367
 
366
368
  ---
367
369
 
368
- ## Shells (6)
370
+ ## Shells (8)
369
371
 
370
372
  Route-level slot-frames — the outermost layout a page is placed into. They supply geometry and slots (nav / sidebar / main / footer), not content: you inject the real Nav, Footer and sidebar. A different package from every tier above: `@working-theory/templates`.
371
373
 
@@ -377,6 +379,8 @@ Import: `import { X } from "@working-theory/templates/shells";` (or from the `@w
377
379
  | `AppShell` | Shared authed-app base frame. Layout ownership: AppShell is the SINGLE owner of the authed sidebar frame layout. The `sidebar` prop is CONTENT (e.g. a <SidebarNav>), never a layout component. Do NOT pass SidebarShell or FixedPanel as the sidebar — that nests two incompatible layout systems (a fixed-width shell vs a variable-width panel). | `import { AppShell } from "@working-theory/templates/shells"` |
378
380
  | `AuthShell` | The frame the five auth pages (/sign-in, /sign-up, /forgot-password, /reset-password, /verify-email) share: a brand mark top-start, one centered content column, and a slim footer. No nav — an auth page has nothing to navigate to, and the marketing Nav is exactly what this shell exists to replace on those routes. | `import { AuthShell } from "@working-theory/templates/shells"` |
379
381
  | `DeckHeroObserver` | Internal client island for MarketingShell's `deckHero` option. | internal — compose from `packages/templates/src/shells/DeckHeroObserver.tsx` |
382
+ | `DeckPresentation` | Internal client island for MarketingShell's `presentation` option. Not exported from the shells barrel (`./index.ts`) — MarketingShell renders it internally under `snap="mandatory"` with `presentation`, beside DeckHeroObserver, whose idiom it follows: a zero-size anchor, no React state, and every write made through the DOM after hydration. | internal — compose from `packages/templates/src/shells/DeckPresentation.tsx` |
383
+ | `DeckSlideAdvance` | Internal client island that puts one working slide-advance button at the foot of every slide of MarketingShell's scroll-snap deck: down to the next slide, and up from the last slide back to the first. | internal — compose from `packages/templates/src/shells/DeckSlideAdvance.tsx` |
380
384
  | `MarketingShell` | Public-facing marketing site **slot-frame**. Server Component (RSC default). A shell is a slot-defining frame: it carves out *where chrome goes* (top + bottom) and *where sections go* (the body), owning **geometry only**. Chrome (Nav/Footer) and sections **fill** the slots — nothing is baked into the component. See (first instance). *(weak)* | `import { MarketingShell } from "@working-theory/templates/shells"` |
381
385
  | `SignedInShell` | Every AILK signed-in area runs on this frame: the admin app's workspace (AppFrame supplies its switcher, Library and gating as content) and a site's own /account area. | `import { SignedInShell } from "@working-theory/templates/shells"` |
382
386
 
@@ -4,6 +4,13 @@ import { type ModulePatch } from "./module-architecture.js";
4
4
  import { type ModuleName } from "./parse-args.js";
5
5
  import type { MarkerMap, WorkspaceVersionMap } from "./substitute.js";
6
6
  import { type SyncRoutesResult } from "./sync-routes.js";
7
+ import { type Topology } from "./topology.js";
8
+ /**
9
+ * The site's topology for add-module (#7426 D13): an unreadable manifest is a
10
+ * refusal this command names (exit 2), before the first write — the same
11
+ * posture as its unreadable-roster refusal.
12
+ */
13
+ export declare function addModuleSiteTopology(targetDir: string): Topology;
7
14
  export { AddModuleRefusedError, readArchitectureHeader };
8
15
  /**
9
16
  * One report row: a bundle file written, patched or skipped, or a seed the
@@ -39,6 +39,22 @@ import { ParseError, parseModules } from "./parse-args.js";
39
39
  import { SITE_ROSTER_PATH, moduleRosterRows, planRosterAppend, readSiteRoster, siteRosterProblems, } from "./site-roster.js";
40
40
  import { MODULE_SURFACES, NAV_CONFIG_PATH, readNavConfigStamp, renderNavConfig, resolveSurfaces, } from "./surfaces.js";
41
41
  import { syncRoutes } from "./sync-routes.js";
42
+ import { TopologyError, isSpokeExcluded, readSiteTopology, spokeModuleRefusal, } from "./topology.js";
43
+ /**
44
+ * The site's topology for add-module (#7426 D13): an unreadable manifest is a
45
+ * refusal this command names (exit 2), before the first write — the same
46
+ * posture as its unreadable-roster refusal.
47
+ */
48
+ export function addModuleSiteTopology(targetDir) {
49
+ try {
50
+ return readSiteTopology(targetDir);
51
+ }
52
+ catch (err) {
53
+ if (err instanceof TopologyError)
54
+ throw new AddModuleRefusedError(err.message);
55
+ throw err;
56
+ }
57
+ }
42
58
  export { AddModuleRefusedError, readArchitectureHeader };
43
59
  /** `package.json` paths a `json` patch may write under the plus-config policy. */
44
60
  const PACKAGE_JSON_PATCH_KEYS = new Set([
@@ -86,6 +102,17 @@ export function checkPlusConfigPatches(patches) {
86
102
  export async function applyModuleToScaffold(opts) {
87
103
  const targetAbs = resolve(opts.targetDir);
88
104
  const archPath = join(targetAbs, "architecture.yaml");
105
+ // #7426 D13 — a spoke stays a spoke: a module it does not admit is refused
106
+ // before the first write, and an admitted one lands through the spoke
107
+ // filter. `mcp` follows whether the spoke still carries apps/mcp.
108
+ const spoke = addModuleSiteTopology(targetAbs) === "spoke"
109
+ ? { mcp: existsSync(join(targetAbs, "apps", "mcp")) }
110
+ : undefined;
111
+ if (spoke !== undefined) {
112
+ const refusal = spokeModuleRefusal(opts.moduleName);
113
+ if (refusal !== undefined)
114
+ throw new AddModuleRefusedError(refusal);
115
+ }
89
116
  const current = readArchitectureHeader(readFileSync(archPath, "utf8"));
90
117
  const modules = normalizeModules([...current.modules, opts.moduleName]);
91
118
  // Resolved before the first write: a header surface that is unknown, or
@@ -112,6 +139,9 @@ export async function applyModuleToScaffold(opts) {
112
139
  markers: opts.markers,
113
140
  existingFilePolicy: "skip-existing",
114
141
  checkPatches: checkPlusConfigPatches,
142
+ ...(spoke !== undefined
143
+ ? { skip: (destination) => isSpokeExcluded(destination, spoke) }
144
+ : {}),
115
145
  });
116
146
  const routes = syncRoutes({
117
147
  targetDir: targetAbs,
@@ -123,10 +153,12 @@ export async function applyModuleToScaffold(opts) {
123
153
  // #6107 D8 — a module that brings `apps/admin` brings its auth hard-fail;
124
154
  // the Admin app gets the web app's effective secret, and a secret already
125
155
  // held in any Next-loaded env file is never shadowed.
126
- writeAuthSecretFile(targetAbs);
156
+ // A spoke has no sign-in (#7426 D13), so it gets no secret.
157
+ if (spoke === undefined)
158
+ writeAuthSecretFile(targetAbs);
127
159
  // The same composition `syncArchitectureSync` writes — a pure function of
128
160
  // the module set, so an unchanged set rewrites identical bytes.
129
- writeFileSync(archPath, deriveArchitecture(modules, surfaces));
161
+ writeFileSync(archPath, deriveArchitecture(modules, surfaces, spoke !== undefined ? { topology: "spoke", mcp: spoke.mcp } : {}));
130
162
  // #7536 D3 — the paid-sections registry follows `apps/web/package.json`,
131
163
  // which the bundle's `patches` just edited. Generated and always
132
164
  // overwritten, like `architecture.yaml` above.
package/dist/add-route.js CHANGED
@@ -39,6 +39,7 @@ import { substituteMarkers } from "./substitute.js";
39
39
  import { GATEABLE_SURFACES, MODULE_SURFACES, parseSurfaces, resolveSurfaces, } from "./surfaces.js";
40
40
  import { syncRoutes } from "./sync-routes.js";
41
41
  import { BASE_LAYER_DIR } from "./template-manifest.js";
42
+ import { TopologyError, readSiteTopology } from "./topology.js";
42
43
  /** A refusal the CLI exits 2 on, thrown before anything is written. */
43
44
  export class AddRouteRefusedError extends Error {
44
45
  exitCode = 2;
@@ -64,6 +65,20 @@ export function addRouteToScaffold(opts) {
64
65
  if (!existsSync(archPath)) {
65
66
  throw new Error(`not a scaffolded repo: no architecture.yaml at ${targetAbs}. Run \`create-ailk <target-dir>\` to scaffold first.`);
66
67
  }
68
+ // #7426 D13 — a spoke stays a spoke. Read before the first write: an
69
+ // unreadable manifest is a refusal this command names (exit 2).
70
+ let spoke;
71
+ try {
72
+ spoke =
73
+ readSiteTopology(targetAbs) === "spoke"
74
+ ? { mcp: existsSync(join(targetAbs, "apps", "mcp")) }
75
+ : undefined;
76
+ }
77
+ catch (err) {
78
+ if (err instanceof TopologyError)
79
+ throw new AddRouteRefusedError(err.message);
80
+ throw err;
81
+ }
67
82
  const route = canonicalRoute(opts.route);
68
83
  const owner = Object.entries(MODULE_SURFACES).find(([, surfaces]) => surfaces.includes(route))?.[0];
69
84
  let current;
@@ -106,8 +121,9 @@ export function addRouteToScaffold(opts) {
106
121
  if (navRow !== undefined)
107
122
  report.push(navRow);
108
123
  // Last: the declared header is what makes a rerun a no-op, so it is
109
- // written only once every other step has landed.
110
- writeFileSync(archPath, deriveArchitecture(current.modules, surfaces));
124
+ // written only once every other step has landed. A spoke's `syncRoutes`
125
+ // call above already skipped what it lacks (#7426 D13).
126
+ writeFileSync(archPath, deriveArchitecture(current.modules, surfaces, spoke !== undefined ? { topology: "spoke", mcp: spoke.mcp } : {}));
111
127
  return { targetDir: targetAbs, route, surfaces, report, routes };
112
128
  }
113
129
  /** The canonical id for one route argument, refusing an unknown one. */
@@ -1,3 +1,5 @@
1
+ import { type HomeSeedOutcome } from "./design-home.js";
2
+ import { type UnreadChromeKnob } from "./lib/chrome-readers.js";
1
3
  export interface ApplyDesignOptions {
2
4
  /** The design: a shipped name, or a directory holding a DESIGN.md. */
3
5
  design: string;
@@ -6,7 +8,11 @@ export interface ApplyDesignOptions {
6
8
  /** Package-rooted inputs, supplied by the caller (cli.ts from its PKG_ROOT). */
7
9
  designsDir: string;
8
10
  siteTemplatesDir: string;
11
+ /** The overlay's `templates/` (#7743 D7) — where a treeless shape's home lives. Default: `templates` beside `siteTemplatesDir`. */
12
+ templatesDir?: string;
9
13
  catalogPath: string;
14
+ /** #7781 — apply the design even when a reader file of the site ignores a chrome knob it sets; the pairs are reported instead of refused. */
15
+ allowUnreadChrome?: boolean;
10
16
  /** Where a relative design path resolves from. Default process.cwd(). */
11
17
  cwd?: string;
12
18
  /** Read for AILK_LICENSE_KEY only (D6). Default process.env. */
@@ -21,6 +27,9 @@ export type ApplyDesignChange = {
21
27
  } | {
22
28
  path: string;
23
29
  kind: "layout";
30
+ } | {
31
+ path: string;
32
+ kind: "fonts";
24
33
  } | {
25
34
  path: string;
26
35
  kind: "design";
@@ -28,6 +37,11 @@ export type ApplyDesignChange = {
28
37
  path: string;
29
38
  kind: "preset";
30
39
  fields: string[];
40
+ } | {
41
+ path: string;
42
+ kind: "home";
43
+ action: "seeded" | "restored";
44
+ fields: string[];
31
45
  } | {
32
46
  path: string;
33
47
  kind: "manifest";
@@ -39,15 +53,31 @@ export interface ApplyDesignResult {
39
53
  /** In write order (D4); [] when nothing changed. */
40
54
  changes: ApplyDesignChange[];
41
55
  manifest: "recorded" | "unchanged" | "no-roster" | "legacy-roster";
56
+ /** #7743 D7 — what the design did to the home page's sections. */
57
+ home: HomeSeedOutcome;
58
+ /** #7743 D7 — the design ships a home arc for this site, so a home it leaves alone is reported. */
59
+ homeArc?: true;
42
60
  /** The site-relative roster file, when there is one. */
43
61
  rosterPath?: string;
62
+ /** #7720 D4.2 — the distinct families written to site-fonts.ts, display first; set only when faces are written. */
63
+ fonts?: string[];
64
+ /** #7720 D4.2 — faces were written, but the site's layout.tsx does not read lib/site-fonts. */
65
+ fontsUnwired?: true;
66
+ /** #7781 — chrome knobs the design set that a reader file of the site does not read; present only when non-empty, which only `allowUnreadChrome` lets through. */
67
+ unreadChrome?: UnreadChromeKnob[];
68
+ }
69
+ /** #7781 — thrown before any write when the design sets a chrome knob that a reader file of the site does not read. */
70
+ export declare class UnreadChromeError extends Error {
71
+ readonly unread: readonly UnreadChromeKnob[];
72
+ constructor(unread: readonly UnreadChromeKnob[]);
44
73
  }
45
74
  export interface ApplyDesignArgs {
46
75
  design: string | undefined;
47
76
  target: string | undefined;
77
+ allowUnreadChrome: boolean;
48
78
  help: boolean;
49
79
  }
50
- export declare const APPLY_DESIGN_HELP = "create-ailk apply-design \u2014 make an existing site wear a design.\n\nUsage:\n create-ailk apply-design <name> [--target <site-dir>]\n create-ailk apply-design <path> [--target <site-dir>]\n\nArguments:\n <name|path> A shipped design (standard, minimalism), or a\n directory holding a DESIGN.md. A relative path\n resolves from the current directory.\n\nOptions:\n --target <site-dir> An already-scaffolded repo (contains\n architecture.yaml). Defaults to the current dir.\n --help, -h Print this message.\n\nWrites only the look: DESIGN.md (replaced whole by the design's, keeping the\nsite's Source template and Version lines), the theme: block of\ncontent/_site.mdx, the generated apps/web/lib/site-chrome.ts, the look fields\nof sections the site's pages already place, and the design/layout/theme record\nin .ailk-site-manifest.json.\nNever adds, removes or renames a route or page, never touches a content field\nor the brand: block, and leaves every page's structured data unchanged.\n\nA design other than standard needs a Pro or Pro Plus key: the site's own\nailk.key (put one there with create-ailk license set <key>), else\nAILK_LICENSE_KEY. A malformed package, a preset the validator refuses or a\nmissing key is refused before any file is written.\n\nA preset sets the look fields it names and never unsets one: a field it does\nnot name keeps whatever value the page has. Running it again changes nothing.\n";
80
+ export declare const APPLY_DESIGN_HELP = "create-ailk apply-design \u2014 make an existing site wear a design.\n\nUsage:\n create-ailk apply-design <name> [--target <site-dir>]\n create-ailk apply-design <path> [--target <site-dir>]\n\nArguments:\n <name|path> A shipped design (standard, minimalism), or a\n directory holding a DESIGN.md. A relative path\n resolves from the current directory.\n\nOptions:\n --target <site-dir> An already-scaffolded repo (contains\n architecture.yaml). Defaults to the current dir.\n --allow-unread-chrome Apply the design even when a file of the site does\n not read a chrome setting the design sets (a site\n scaffolded before the setting existed). Without it\n that is refused; with it each such file is reported.\n --help, -h Print this message.\n\nWrites only the look: DESIGN.md (replaced whole by the design's, keeping the\nsite's Source template and Version lines), the theme: block of\ncontent/_site.mdx, the generated apps/web/lib/site-chrome.ts, the generated\napps/web/lib/site-fonts.ts, the look fields of sections the site's pages\nalready place, and the design/layout/theme record in .ailk-site-manifest.json.\nNever adds, removes or renames a route or page, never touches a content field\n(except a placeholder home's sections, below) or the brand: block, and leaves\nevery other page's structured data unchanged.\n\nA design may seed the placeholder home's sections \u2014 its home arc \u2014 while they\nare still a placeholder: the shape's own, or a design's arc of them. It never\nchanges a home whose sections were edited; it reports it and leaves it. A\nseeded home takes the design's looks; look values on the sections it replaced\nare not kept. A design that ships no arc restores the shape's own sections on\na home that still wears another design's untouched arc.\n\nA design other than standard needs a Pro or Pro Plus key: the site's own\nailk.key (put one there with create-ailk license set <key>), else\nAILK_LICENSE_KEY. A malformed package, a preset the validator refuses or a\nmissing key is refused before any file is written.\n\nA preset sets the look fields it names and never unsets one: a field it does\nnot name keeps whatever value the page has. Running it again changes nothing.\n";
51
81
  /**
52
82
  * Parse the argv AFTER the `apply-design` word (D1). Strict `node:util`
53
83
  * parseArgs, like `parseSyncRoutesArgs`: exactly one positional, the design
@@ -57,7 +87,7 @@ export declare function parseApplyDesignArgs(argv: string[]): ApplyDesignArgs;
57
87
  /**
58
88
  * Apply a design to an existing site (D2–D7). Refuses before any write
59
89
  * (C1–C6); otherwise writes only the files whose text changes — page files,
60
- * `site-chrome.ts`, `DESIGN.md`, `_site.mdx`, then the manifest last.
90
+ * `site-chrome.ts`, `site-fonts.ts`, `DESIGN.md`, `_site.mdx`, then the manifest last.
61
91
  */
62
92
  export declare function applyDesign(opts: ApplyDesignOptions): Promise<ApplyDesignResult>;
63
93
  /** The changed-file report (D9). */
@@ -7,7 +7,9 @@
7
7
  * resolver and theme splicer, #6816's preset validator and applier, #6760's
8
8
  * design gate and #6813's manifest writer. Every refusal happens before the
9
9
  * first write (D3); every new text is planned in memory, then written in one
10
- * pass (D4). Routes, content fields, `brand:` and structured data never move.
10
+ * pass (D4). Routes, content fields, `brand:` and structured data never move —
11
+ * except a placeholder home's `sections:`, which a design's home arc may
12
+ * replace (#7743 D7).
11
13
  *
12
14
  * Reads no module URL of its own — every package directory it reads is passed in
13
15
  * — so Jest's CJS transform can import it, unlike `programmatic.ts`.
@@ -16,15 +18,31 @@ import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSy
16
18
  import { dirname, join, relative, resolve, sep } from "node:path";
17
19
  import { parseArgs } from "node:util";
18
20
  import { planDesignMd } from "./apply-design-md.js";
19
- import { compileDesignTheme } from "./design-compile.js";
21
+ import { compileDesignFonts, compileDesignTheme } from "./design-compile.js";
22
+ import { HOME_PAGE_PATH, designHomeSeeds, planDesignHome, shapeHomePath, shippedHomeSeeds, } from "./design-home.js";
20
23
  import { applyDesignPresets, readSectionLookCatalog, shippedPageTypes, validateDesignPresets, } from "./design-presets.js";
21
24
  import { resolveDesign, spliceThemeBlock } from "./design-scaffold.js";
25
+ import { chromeKnobReaders, unreadChromeKnobs } from "./lib/chrome-readers.js";
22
26
  import { LICENSE_KEY_FILE } from "./lib/license-key-files.js";
23
27
  import { CONVERSION_HINT_URL, LicenseRequiredError, assertDesignAllowed, deriveLevel, } from "./lib/module-license-gate.js";
24
28
  import { LEVEL_LABEL } from "./lib/module-tier.js";
25
29
  import { ParseError } from "./parse-args.js";
26
30
  import { SITE_CHROME_PATH, renderSiteChrome, resolveSiteChrome, } from "./site-chrome.js";
27
- import { SITE_ROSTER_PATH, planSiteManifestDesign, readSiteRoster, } from "./site-roster.js";
31
+ import { SITE_FONTS_PATH, renderSiteFonts } from "./site-fonts.js";
32
+ import { SITE_ROSTER_PATH, planHomeRecord, planSiteManifestDesign, readSiteRoster, } from "./site-roster.js";
33
+ /** #7781 — thrown before any write when the design sets a chrome knob that a reader file of the site does not read. */
34
+ export class UnreadChromeError extends Error {
35
+ unread;
36
+ constructor(unread) {
37
+ super(`design chrome would not render: the site's own copy of these files does not read the knob the design sets (update them from the current create-ailk overlay, or pass --allow-unread-chrome to apply anyway):\n${unread
38
+ .map(({ knob, file }) => ` ${knob} — ${file}`)
39
+ .join("\n")}`);
40
+ this.unread = unread;
41
+ this.name = "UnreadChromeError";
42
+ }
43
+ }
44
+ /** The site's locale layout — the one reader of the generated site-fonts.ts. */
45
+ const SITE_LAYOUT_PATH = "apps/web/app/[locale]/layout.tsx";
28
46
  export const APPLY_DESIGN_HELP = `create-ailk apply-design — make an existing site wear a design.
29
47
 
30
48
  Usage:
@@ -39,15 +57,27 @@ Arguments:
39
57
  Options:
40
58
  --target <site-dir> An already-scaffolded repo (contains
41
59
  architecture.yaml). Defaults to the current dir.
60
+ --allow-unread-chrome Apply the design even when a file of the site does
61
+ not read a chrome setting the design sets (a site
62
+ scaffolded before the setting existed). Without it
63
+ that is refused; with it each such file is reported.
42
64
  --help, -h Print this message.
43
65
 
44
66
  Writes only the look: DESIGN.md (replaced whole by the design's, keeping the
45
67
  site's Source template and Version lines), the theme: block of
46
- content/_site.mdx, the generated apps/web/lib/site-chrome.ts, the look fields
47
- of sections the site's pages already place, and the design/layout/theme record
48
- in .ailk-site-manifest.json.
68
+ content/_site.mdx, the generated apps/web/lib/site-chrome.ts, the generated
69
+ apps/web/lib/site-fonts.ts, the look fields of sections the site's pages
70
+ already place, and the design/layout/theme record in .ailk-site-manifest.json.
49
71
  Never adds, removes or renames a route or page, never touches a content field
50
- or the brand: block, and leaves every page's structured data unchanged.
72
+ (except a placeholder home's sections, below) or the brand: block, and leaves
73
+ every other page's structured data unchanged.
74
+
75
+ A design may seed the placeholder home's sections — its home arc — while they
76
+ are still a placeholder: the shape's own, or a design's arc of them. It never
77
+ changes a home whose sections were edited; it reports it and leaves it. A
78
+ seeded home takes the design's looks; look values on the sections it replaced
79
+ are not kept. A design that ships no arc restores the shape's own sections on
80
+ a home that still wears another design's untouched arc.
51
81
 
52
82
  A design other than standard needs a Pro or Pro Plus key: the site's own
53
83
  ailk.key (put one there with create-ailk license set <key>), else
@@ -69,6 +99,7 @@ export function parseApplyDesignArgs(argv) {
69
99
  allowPositionals: true,
70
100
  options: {
71
101
  target: { type: "string" },
102
+ "allow-unread-chrome": { type: "boolean", default: false },
72
103
  help: { type: "boolean", default: false, short: "h" },
73
104
  },
74
105
  });
@@ -80,7 +111,12 @@ export function parseApplyDesignArgs(argv) {
80
111
  if (design === undefined && !help) {
81
112
  throw new ParseError("apply-design needs a design. Usage: create-ailk apply-design <name|path> [--target <site-dir>]");
82
113
  }
83
- return { design, target: parsed.values.target, help };
114
+ return {
115
+ design,
116
+ target: parsed.values.target,
117
+ allowUnreadChrome: parsed.values["allow-unread-chrome"] === true,
118
+ help,
119
+ };
84
120
  }
85
121
  /** `abs` relative to `root`, with `/` separators. */
86
122
  function siteRelative(root, abs) {
@@ -118,7 +154,7 @@ function readSiteKey(targetAbs, env) {
118
154
  /**
119
155
  * Apply a design to an existing site (D2–D7). Refuses before any write
120
156
  * (C1–C6); otherwise writes only the files whose text changes — page files,
121
- * `site-chrome.ts`, `DESIGN.md`, `_site.mdx`, then the manifest last.
157
+ * `site-chrome.ts`, `site-fonts.ts`, `DESIGN.md`, `_site.mdx`, then the manifest last.
122
158
  */
123
159
  export async function applyDesign(opts) {
124
160
  const targetAbs = resolve(opts.targetDir);
@@ -146,13 +182,18 @@ export async function applyDesign(opts) {
146
182
  const { pkg } = resolved;
147
183
  const hasPresets = Object.keys(pkg.sections).length > 0;
148
184
  // C4 — the presets, validated exactly as scaffold() validates them.
185
+ let lookCatalog;
186
+ const catalog = () => (lookCatalog ??= readSectionLookCatalog(opts.catalogPath));
149
187
  if (hasPresets) {
150
188
  validateDesignPresets(pkg.sections, {
151
- catalog: readSectionLookCatalog(opts.catalogPath),
189
+ catalog: catalog(),
152
190
  pageTypes: shippedPageTypes(opts.siteTemplatesDir),
153
191
  file: join(resolved.dir, "DESIGN.md"),
154
192
  });
155
193
  }
194
+ // #7743 D7 — the home seeds (the design's and every shipped design's), validated beside the presets.
195
+ const designSeeds = designHomeSeeds(resolved, { designsDir: opts.designsDir, catalog });
196
+ const shippedSeeds = shippedHomeSeeds({ designsDir: opts.designsDir, catalog });
156
197
  // C5 — the design gate, fed the site's own key (D6). A Free design reads no key.
157
198
  if (!resolved.free) {
158
199
  const key = readSiteKey(targetAbs, env);
@@ -170,20 +211,60 @@ export async function applyDesign(opts) {
170
211
  throw new LicenseRequiredError(`design "${resolved.display}" is a Pro feature — this site's licence level is ${LEVEL_LABEL[level]}; put a Pro or Pro Plus key in the site with create-ailk license set <key>, or set AILK_LICENSE_KEY → ${CONVERSION_HINT_URL}`);
171
212
  }
172
213
  }
214
+ // #7781 — after the licence gate, before any plan: every chrome knob the design sets must be read by
215
+ // the site's own copy of each overlay file that reads it. The reader map is derived from the overlay.
216
+ const roster = readSiteRoster(targetAbs);
217
+ const chrome = resolveSiteChrome(roster?.template.layout, pkg.layout);
218
+ const templatesDir = opts.templatesDir ?? join(dirname(opts.siteTemplatesDir), "templates");
219
+ const unreadChrome = unreadChromeKnobs(chrome, chromeKnobReaders(templatesDir), targetAbs);
220
+ if (unreadChrome.length > 0 && opts.allowUnreadChrome !== true)
221
+ throw new UnreadChromeError(unreadChrome);
173
222
  // C6 / D4 — plan every write in memory.
174
223
  const theme = compileDesignTheme(pkg);
175
224
  const siteMdxCurrent = readFileSync(siteMdxPath, "utf8");
176
225
  const siteMdxNext = spliceThemeBlock(siteMdxCurrent, theme, siteMdxPath);
177
- const roster = readSiteRoster(targetAbs);
178
- const chrome = resolveSiteChrome(roster?.template.layout, pkg.layout);
179
226
  const chromePath = join(targetAbs, SITE_CHROME_PATH);
180
227
  const chromeCurrent = existsSync(chromePath) ? readFileSync(chromePath, "utf8") : undefined;
181
228
  const chromeNext = renderSiteChrome(chrome);
229
+ // #7720 D4.2 — a face-less design never adds the file to a site that lacks it.
230
+ const fonts = compileDesignFonts(pkg);
231
+ const fontsPath = join(targetAbs, SITE_FONTS_PATH);
232
+ const fontsCurrent = existsSync(fontsPath) ? readFileSync(fontsPath, "utf8") : undefined;
233
+ const fontsNext = renderSiteFonts(fonts);
234
+ const writeFonts = fontsNext !== fontsCurrent && (fontsCurrent !== undefined || fonts !== undefined);
235
+ const fontFamilies = writeFonts && fonts !== undefined
236
+ ? [...new Set([fonts.display?.family, fonts.body?.family])].filter((family) => family !== undefined)
237
+ : undefined;
238
+ const layoutPath = join(targetAbs, SITE_LAYOUT_PATH);
239
+ const fontsUnwired = fontFamilies !== undefined &&
240
+ !(existsSync(layoutPath) && readFileSync(layoutPath, "utf8").includes("lib/site-fonts"));
182
241
  const designMdPath = join(targetAbs, "DESIGN.md");
183
242
  const designMdCurrent = existsSync(designMdPath) ? readFileSync(designMdPath, "utf8") : undefined;
184
243
  const designMdNext = planDesignMd(resolved.dir, targetAbs);
244
+ // #7743 D7 — the home is planned before the presets, so a seeded home is
245
+ // written once, carrying both. The one `planDesignHome` call site here.
246
+ const shape = roster?.template.shape;
247
+ const target = designSeeds.find((seed) => seed.shape === shape);
248
+ // The arc this site would take: the shape's seed, or (with no shape to pick one) any seed at all.
249
+ const homeArc = target !== undefined || (shape === undefined && designSeeds.length > 0);
250
+ const homePath = join(targetAbs, ...HOME_PAGE_PATH.split("/"));
251
+ const current = existsSync(homePath) ? readFileSync(homePath, "utf8") : undefined;
252
+ const homePlan = planDesignHome({
253
+ current,
254
+ shape,
255
+ target,
256
+ shapeHome: shape === undefined || current === undefined
257
+ ? ""
258
+ : readFileSync(shapeHomePath(shape, {
259
+ siteTemplatesDir: opts.siteTemplatesDir,
260
+ templatesDir,
261
+ }), "utf8"),
262
+ shippedSeeds,
263
+ catalog: catalog(),
264
+ });
265
+ const homeAction = homePlan.outcome === "seeded" || homePlan.outcome === "restored" ? homePlan.outcome : undefined;
185
266
  const pageWrites = [];
186
- if (hasPresets && roster !== undefined) {
267
+ if (roster !== undefined && (hasPresets || homeAction !== undefined)) {
187
268
  const pageTypes = new Set();
188
269
  for (const row of roster.rows) {
189
270
  const pageType = row.value["pageType"];
@@ -191,17 +272,28 @@ export async function applyDesign(opts) {
191
272
  pageTypes.add(pageType);
192
273
  }
193
274
  const contentDir = join(targetAbs, "content");
275
+ // An authored home kept from a design's arc is reported as kept, so no preset dresses it either (D3, F4).
276
+ const homeKept = homePlan.outcome === "authored" && homeArc;
194
277
  const pages = listMdxFiles(contentDir)
195
- .filter((path) => path !== siteMdxPath)
196
- .map((path) => ({ path, text: readFileSync(path, "utf8") }));
197
- const presetResult = applyDesignPresets(pages, pkg.sections, { pageTypes });
198
- for (const page of presetResult.pages) {
278
+ .filter((path) => path !== siteMdxPath && !(homeKept && path === homePath))
279
+ .map((path) => ({
280
+ path,
281
+ text: path === homePath && homePlan.text !== undefined ? homePlan.text : readFileSync(path, "utf8"),
282
+ }));
283
+ const presetResult = hasPresets
284
+ ? applyDesignPresets(pages, pkg.sections, { pageTypes })
285
+ : { pages: [], applied: [] };
286
+ for (const page of pages) {
287
+ const preset = presetResult.pages.find((p) => p.path === page.path);
288
+ const home = page.path === homePath ? homeAction : undefined;
289
+ if (preset === undefined && home === undefined)
290
+ continue;
199
291
  const fields = [
200
292
  ...new Set(presetResult.applied
201
293
  .filter((a) => a.path === page.path)
202
294
  .map((a) => `${a.sectionType}.${a.field}`)),
203
295
  ].sort();
204
- pageWrites.push({ path: page.path, text: page.text, fields });
296
+ pageWrites.push({ path: page.path, text: preset?.text ?? page.text, fields, ...(home !== undefined ? { home } : {}) });
205
297
  }
206
298
  }
207
299
  let manifest = "no-roster";
@@ -210,23 +302,37 @@ export async function applyDesign(opts) {
210
302
  if (roster !== undefined && rosterAbs !== undefined) {
211
303
  if (roster.path === SITE_ROSTER_PATH) {
212
304
  manifestNext = planSiteManifestDesign(readFileSync(rosterAbs, "utf8"), { design: resolved.recorded, layout: chrome, theme }, rosterAbs);
305
+ // #7743 D6 — the `/` row records the home as written, in the same single write.
306
+ const home = pageWrites.find((page) => page.home !== undefined);
307
+ if (home !== undefined) {
308
+ manifestNext =
309
+ planHomeRecord(manifestNext ?? readFileSync(rosterAbs, "utf8"), home.text, catalog()) ?? manifestNext;
310
+ }
213
311
  manifest = manifestNext === undefined ? "unchanged" : "recorded";
214
312
  }
215
313
  else {
216
314
  manifest = "legacy-roster";
217
315
  }
218
316
  }
219
- // Write — page files, site-chrome.ts, DESIGN.md, _site.mdx, the manifest last (D4).
317
+ // Write — page files, site-chrome.ts, site-fonts.ts, DESIGN.md, _site.mdx, the manifest last (D4).
220
318
  const changes = [];
221
319
  for (const page of pageWrites) {
222
320
  writeFileSync(page.path, page.text);
223
- changes.push({ path: siteRelative(targetAbs, page.path), kind: "preset", fields: page.fields });
321
+ const path = siteRelative(targetAbs, page.path);
322
+ changes.push(page.home !== undefined
323
+ ? { path, kind: "home", action: page.home, fields: page.fields }
324
+ : { path, kind: "preset", fields: page.fields });
224
325
  }
225
326
  if (chromeNext !== chromeCurrent) {
226
327
  mkdirSync(dirname(chromePath), { recursive: true });
227
328
  writeFileSync(chromePath, chromeNext);
228
329
  changes.push({ path: SITE_CHROME_PATH, kind: "layout" });
229
330
  }
331
+ if (writeFonts) {
332
+ mkdirSync(dirname(fontsPath), { recursive: true });
333
+ writeFileSync(fontsPath, fontsNext);
334
+ changes.push({ path: SITE_FONTS_PATH, kind: "fonts" });
335
+ }
230
336
  if (designMdNext !== designMdCurrent) {
231
337
  writeFileSync(designMdPath, designMdNext);
232
338
  changes.push({ path: "DESIGN.md", kind: "design" });
@@ -245,7 +351,12 @@ export async function applyDesign(opts) {
245
351
  display: resolved.display,
246
352
  changes,
247
353
  manifest,
354
+ home: homePlan.outcome,
355
+ ...(homeArc ? { homeArc: true } : {}),
248
356
  ...(roster !== undefined ? { rosterPath: roster.path } : {}),
357
+ ...(fontFamilies !== undefined ? { fonts: fontFamilies } : {}),
358
+ ...(fontsUnwired ? { fontsUnwired: true } : {}),
359
+ ...(unreadChrome.length > 0 ? { unreadChrome } : {}),
249
360
  };
250
361
  }
251
362
  /** The changed-file report (D9). */
@@ -257,11 +368,33 @@ export function formatApplyDesignReport(result) {
257
368
  else {
258
369
  lines.push(`[create-ailk] Applied design "${result.design}" to ${result.targetDir}`);
259
370
  for (const change of result.changes) {
260
- const suffix = change.kind === "preset" ? ` (${change.fields.join(", ")})` : "";
371
+ const suffix = change.kind === "preset"
372
+ ? ` (${change.fields.join(", ")})`
373
+ : change.kind === "home"
374
+ ? ` (${[change.action, change.fields.join(", ")].filter((part) => part !== "").join("; ")})`
375
+ : "";
261
376
  lines.push(` ${change.kind.padEnd(8)} ${change.path}${suffix}`);
262
377
  }
263
378
  const n = result.changes.length;
264
- lines.push(`${n} file${n === 1 ? "" : "s"} changed. Routes, page content and structured data are unchanged.`);
379
+ const homeWritten = result.changes.some((change) => change.kind === "home");
380
+ lines.push(`${n} file${n === 1 ? "" : "s"} changed. ${homeWritten
381
+ ? "Routes are unchanged; the home page's sections were replaced."
382
+ : "Routes and structured data are unchanged."}`);
383
+ }
384
+ if (result.home === "authored" && result.homeArc === true) {
385
+ lines.push(`home kept: ${HOME_PAGE_PATH} has authored sections — the ${result.design} home arc was not applied`);
386
+ }
387
+ else if (result.home === "no-shape" && result.homeArc === true) {
388
+ lines.push("home not seeded: the site records no shape");
389
+ }
390
+ if (result.fonts !== undefined) {
391
+ lines.push(`fonts: ${result.fonts.join(", ")} load at next build through next/font/google — a family that is not a variable Google font fails there`);
392
+ }
393
+ if (result.fontsUnwired === true) {
394
+ lines.push(`fonts not loaded: ${SITE_LAYOUT_PATH} predates lib/site-fonts — update the layout from the current create-ailk overlay`);
395
+ }
396
+ for (const { knob, file } of result.unreadChrome ?? []) {
397
+ lines.push(`chrome not rendered: ${knob} — ${file} does not read it`);
265
398
  }
266
399
  if (result.manifest === "legacy-roster") {
267
400
  lines.push(`manifest not recorded: ${result.rosterPath} predates the design record`);