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
package/dist/cli.js CHANGED
@@ -201,7 +201,9 @@ async function main() {
201
201
  targetDir: args.target ?? process.cwd(),
202
202
  designsDir: resolve(PKG_ROOT, "site-templates", "designs"),
203
203
  siteTemplatesDir: resolve(PKG_ROOT, "site-templates"),
204
+ templatesDir: resolve(PKG_ROOT, "templates"),
204
205
  catalogPath: resolve(PKG_ROOT, "component-catalog.json"),
206
+ allowUnreadChrome: args.allowUnreadChrome,
205
207
  });
206
208
  process.stdout.write(formatApplyDesignReport(result));
207
209
  process.exit(0);
@@ -317,7 +319,8 @@ async function main() {
317
319
  // marketing-site-free` still rides the no-key case (record D11): its
318
320
  // `requires` is all-free, so it installs from the shipped
319
321
  // `site-templates/` tree with no key and no request.
320
- const { targetDir: targetAbs, surfaces, starter, modules: effectiveModules, } = await scaffold(toScaffoldOptions(opts));
322
+ const { targetDir: targetAbs, surfaces, starter, modules: effectiveModules, droppedModules, hubOrigin, } = await scaffold(toScaffoldOptions(opts));
323
+ const isSpoke = opts.topology === "spoke";
321
324
  // The starter scaffold() resolved — from --template, or from --shape (#6754 D4).
322
325
  const templateName = starter?.template ?? opts.template;
323
326
  if (opts.install) {
@@ -345,6 +348,22 @@ async function main() {
345
348
  ...(starter !== undefined
346
349
  ? [`Starter: shape ${starter.shape}, design ${starter.design}.`]
347
350
  : []),
351
+ // #7426 — a spoke says so, and names what its hosts must carry (D3).
352
+ ...(isSpoke
353
+ ? [
354
+ `Topology: spoke — no apps/api, no database, no sign-in${opts.mcp === false ? ", no apps/mcp" : "; apps/mcp is a stateless front"}.`,
355
+ ...(droppedModules !== undefined && droppedModules.length > 0
356
+ ? [
357
+ `Not carried on a spoke: ${droppedModules.join(", ")} (your licence includes ${droppedModules.length === 1 ? "it" : "them"}; a spoke does not admit ${droppedModules.length === 1 ? "it" : "them"} yet).`,
358
+ ]
359
+ : []),
360
+ "",
361
+ `Every host serving this spoke's ${opts.mcp === false ? "apps/web" : "apps/web and apps/mcp"} must carry:`,
362
+ ` AILK_API_URL=${hubOrigin ?? "<your hub origin>"} # written to apps/web/.env.local`,
363
+ " AILK_SPOKE_KEY # issued by the hub; never written here",
364
+ " AILK_VISITOR_FORWARD_KEY # issued by the hub; never written here",
365
+ ]
366
+ : []),
348
367
  "",
349
368
  ];
350
369
  // Prerequisites (#6440 AC4) — every one the scaffold enforces, and nothing
@@ -373,13 +392,15 @@ async function main() {
373
392
  lines.push("Next steps:");
374
393
  lines.push(` cd ${targetAbs}`);
375
394
  if (!opts.install) {
376
- lines.push(" pnpm install # also generates the Prisma client");
395
+ lines.push(isSpoke ? " pnpm install" : " pnpm install # also generates the Prisma client");
377
396
  }
378
397
  lines.push(" pnpm dev # http://localhost:3000");
379
398
  lines.push(" pnpm build # production build, when you are ready to deploy");
380
399
  lines.push("");
381
400
  lines.push("Open http://localhost:3000 to see the home page.");
382
- lines.push("Adding auth, saved flows, checkout or database-backed content later? README.md \u00a7 \"When you need a database\" has the one-command setup.");
401
+ if (!isSpoke) {
402
+ lines.push("Adding auth, saved flows, checkout or database-backed content later? README.md \u00a7 \"When you need a database\" has the one-command setup.");
403
+ }
383
404
  lines.push("");
384
405
  lines.push("Claude Code: the AI Launch Kit skills come from the ailk plugin. Trust this folder and Claude Code loads it, or run in a session:");
385
406
  for (const line of PLUGIN_INSTALL_LINES)
@@ -1,3 +1,11 @@
1
+ import type { Topology } from "./topology.js";
2
+ /** The topology a document is derived for (#7426 D8). */
3
+ export interface DeriveArchitectureOptions {
4
+ /** `"hub"` (the default) or `"spoke"`. */
5
+ topology?: Topology;
6
+ /** Spoke only: whether the spoke keeps `apps/mcp` (default true). */
7
+ mcp?: boolean;
8
+ }
1
9
  /**
2
10
  * Derive the `architecture.yaml` document for a module set.
3
11
  *
@@ -6,8 +14,13 @@
6
14
  * @param surfaces the enabled gateable route surfaces. Omitted ⟹ derived from
7
15
  * the module set alone (`resolveSurfaces`), so a caller that passes only
8
16
  * modules gets exactly what the modules imply.
17
+ * @param options the topology (#7426 D8). Absent or `"hub"` ⟹ the output is
18
+ * byte-identical to the two-argument form. `"spoke"` ⟹ only the spoke's own
19
+ * apps (`web`, and `mcp` unless `mcp: false`), no `namespaces`, no
20
+ * `database` root, a top-level `topology: spoke` and the hub-facing
21
+ * composition tiers.
9
22
  * @returns the complete `architecture.yaml` file content (a deterministic
10
- * function of the module SET and the surface SET).
23
+ * function of the module SET, the surface SET and the topology).
11
24
  * @throws Error on an unrecognized module (defensive; parse-args rejects first).
12
25
  */
13
- export declare function deriveArchitecture(modules?: string[], surfaces?: string[]): string;
26
+ export declare function deriveArchitecture(modules?: string[], surfaces?: string[], options?: DeriveArchitectureOptions): string;
@@ -15,6 +15,12 @@
15
15
  */
16
16
  import { ARCHITECTURE_BASE, MODULE_SLICES, normalizeModules, } from "./module-architecture.js";
17
17
  import { CORE_SURFACE_PATHS, RECOGNIZED_SURFACES, resolveSurfaces, } from "./surfaces.js";
18
+ /** A spoke's composition tiers (#7426 D8): its data and API are the hub's. */
19
+ const SPOKE_TIERS = {
20
+ data: "none — the hub's database",
21
+ api: "the hub at AILK_API_URL (apps/mcp is a stateless front)",
22
+ presentation: "apps/web",
23
+ };
18
24
  function isScalar(v) {
19
25
  return typeof v === "string" || typeof v === "number";
20
26
  }
@@ -150,11 +156,18 @@ function namespacesToYaml(ns) {
150
156
  * @param surfaces the enabled gateable route surfaces. Omitted ⟹ derived from
151
157
  * the module set alone (`resolveSurfaces`), so a caller that passes only
152
158
  * modules gets exactly what the modules imply.
159
+ * @param options the topology (#7426 D8). Absent or `"hub"` ⟹ the output is
160
+ * byte-identical to the two-argument form. `"spoke"` ⟹ only the spoke's own
161
+ * apps (`web`, and `mcp` unless `mcp: false`), no `namespaces`, no
162
+ * `database` root, a top-level `topology: spoke` and the hub-facing
163
+ * composition tiers.
153
164
  * @returns the complete `architecture.yaml` file content (a deterministic
154
- * function of the module SET and the surface SET).
165
+ * function of the module SET, the surface SET and the topology).
155
166
  * @throws Error on an unrecognized module (defensive; parse-args rejects first).
156
167
  */
157
- export function deriveArchitecture(modules = ["marketing"], surfaces) {
168
+ export function deriveArchitecture(modules = ["marketing"], surfaces, options = {}) {
169
+ const spoke = options.topology === "spoke";
170
+ const keepMcp = options.mcp !== false;
158
171
  const selected = normalizeModules(modules);
159
172
  const slices = selected.map((m) => MODULE_SLICES[m]);
160
173
  // The declared route surface (#4154). A scaffold's live-route set is a
@@ -180,12 +193,20 @@ export function deriveArchitecture(modules = ["marketing"], surfaces) {
180
193
  };
181
194
  for (const s of slices)
182
195
  Object.assign(declared, s.apps ?? {});
196
+ // A spoke carries only its own apps (#7426 D8): web, and mcp unless
197
+ // `--without-mcp` — never api, never a module's admin.
198
+ if (spoke) {
199
+ for (const key of Object.keys(declared)) {
200
+ if (key !== "web" && !(key === "mcp" && keepMcp))
201
+ delete declared[key];
202
+ }
203
+ }
183
204
  // A scaffold carries no `packages/` tree — `@working-theory/*` install from
184
205
  // npm — so an entry naming a monorepo folder is dropped rather than shipped
185
206
  // as a rule the linter can never apply (#6940). What survives names a path
186
207
  // the scaffold has: a declared app, or `database/`.
187
208
  const roots = [
188
- "database",
209
+ ...(spoke ? [] : ["database"]),
189
210
  ...Object.values(declared).map((a) => a.path),
190
211
  ];
191
212
  const inScaffold = (p) => {
@@ -209,8 +230,11 @@ export function deriveArchitecture(modules = ["marketing"], surfaces) {
209
230
  forbidden_patterns: mergeForbidden([...ARCHITECTURE_BASE.appForbiddenPatterns], a.forbidden_patterns ?? []).map(scoped),
210
231
  });
211
232
  }
212
- // namespaces — present only if a data-tier module contributes one.
213
- const nsSlice = slices.find((s) => s.namespaces)?.namespaces;
233
+ // namespaces — present only if a data-tier module contributes one, and
234
+ // never on a spoke, which has no database/ of its own.
235
+ const nsSlice = spoke
236
+ ? undefined
237
+ : slices.find((s) => s.namespaces)?.namespaces;
214
238
  // forbidden_patterns — base floor first, then the selected modules' patterns.
215
239
  const extraForbidden = [];
216
240
  for (const s of slices)
@@ -218,8 +242,11 @@ export function deriveArchitecture(modules = ["marketing"], surfaces) {
218
242
  const forbidden = mergeForbidden([...ARCHITECTURE_BASE.forbiddenPatterns], extraForbidden).map(scoped);
219
243
  const doc = {
220
244
  version: ARCHITECTURE_BASE.version,
245
+ ...(spoke ? { topology: "spoke" } : {}),
221
246
  composition_model: {
222
- tiers: { ...ARCHITECTURE_BASE.composition_model.tiers },
247
+ tiers: spoke
248
+ ? { ...SPOKE_TIERS }
249
+ : { ...ARCHITECTURE_BASE.composition_model.tiers },
223
250
  },
224
251
  surfaces: {
225
252
  core: [...CORE_SURFACE_PATHS],
@@ -240,6 +267,7 @@ export function deriveArchitecture(modules = ["marketing"], surfaces) {
240
267
  "#",
241
268
  `# Modules: ${selected.join(", ")}`,
242
269
  `# Surfaces: ${enabledSurfaces.length > 0 ? enabledSurfaces.join(", ") : "core only"}`,
270
+ ...(spoke ? ["# Topology: spoke"] : []),
243
271
  "",
244
272
  ].join("\n");
245
273
  return header + emitDocument(doc);
@@ -15,7 +15,7 @@
15
15
  * this compiles to what `SiteThemeSchema` accepts is test-only, by relative
16
16
  * path, in `design-compile.test.ts`.
17
17
  */
18
- import type { DesignPackage } from "./design-package.js";
18
+ import { type DesignPackage } from "./design-package.js";
19
19
  /** The nine runtime colour slots this compile emits, per mode (D4). */
20
20
  export type RuntimeColorSlot = "bg" | "bgMuted" | "surface" | "border" | "text" | "textMuted" | "primary" | "primaryFg" | "accent";
21
21
  /** One mode's compiled palette — the shape `SiteThemeSchema`'s partial palette accepts. */
@@ -28,11 +28,72 @@ export interface CompiledDesignTheme {
28
28
  eyebrowCase?: "upper" | "sentence";
29
29
  placeholders?: "outline" | "fill";
30
30
  checkmarks?: "circle" | "plain";
31
+ /** #7741 D7 — the button shape, copied from `treatments.buttonShape`. */
32
+ buttonShape?: "control" | "pill";
33
+ /** #7739 D2 — the ink of headings and labels on a dark ground, verbatim from the package. */
34
+ darkInk?: "fg" | "accent";
35
+ /** #7739 D2 — the stat-numeral treatment, verbatim from the package. */
36
+ figures?: "platform" | "display";
37
+ /**
38
+ * #7720 D2 — the display leading pair, present only when the display role
39
+ * declares `lineHeightMobile`; or, under `typeScale: design` (#7739 D2.2),
40
+ * every declared field of the five compiled roles.
41
+ */
42
+ typography?: CompiledTypography;
43
+ /**
44
+ * #7719 D6 — `"site"` paints dark-scoped bands with the site's own dark
45
+ * colours. Derived (never authored): emitted when the package names the
46
+ * `solid-dark` surface or declares `darkInk: accent` (#7739 D2.3); absent otherwise.
47
+ */
48
+ darkBands?: "platform" | "site";
31
49
  colors: {
32
50
  light: CompiledColorPalette;
33
51
  dark: CompiledColorPalette;
34
52
  };
35
53
  }
54
+ /**
55
+ * The compiled type roles (#7720, #7739 D2.2) — the shape `SiteThemeSchema`'s
56
+ * `typography` accepts. Each role is present only when it carries a field.
57
+ */
58
+ export interface CompiledTypography {
59
+ display?: {
60
+ fontSize?: string;
61
+ fontSizeMobile?: string;
62
+ fontWeight?: number;
63
+ lineHeight?: number;
64
+ lineHeightMobile?: number;
65
+ };
66
+ "heading-section"?: {
67
+ fontSize?: string;
68
+ fontSizeMobile?: string;
69
+ fontWeight?: number;
70
+ };
71
+ "heading-item"?: {
72
+ fontSize?: string;
73
+ fontSizeMobile?: string;
74
+ fontWeight?: number;
75
+ };
76
+ body?: {
77
+ fontSize?: string;
78
+ lineHeight?: number;
79
+ };
80
+ eyebrow?: {
81
+ fontSize?: string;
82
+ fontWeight?: number;
83
+ };
84
+ }
85
+ /** One sourced face (#7720 D3.1): the first family, unquoted, and the rest of the authored stack. */
86
+ export interface CompiledFace {
87
+ source: "google";
88
+ family: string;
89
+ /** The authored stack after the first comma, trimmed and verbatim; "" when there is none. */
90
+ fallback: string;
91
+ }
92
+ /** `compileDesignFonts`'s return shape — the faces `apps/web/lib/site-fonts.ts` loads. */
93
+ export interface CompiledDesignFonts {
94
+ display?: CompiledFace;
95
+ body?: CompiledFace;
96
+ }
36
97
  /** Every authored colour key the map may read from (design-package.ts D3). */
37
98
  type DesignAuthoredColorKey = "bg" | "fg" | "surface" | "surface-secondary" | "border" | "text-muted" | "brand" | "fg-on-brand" | "accent" | "bg-dark" | "fg-dark" | "border-dark" | "brand-dark" | "surface-dark" | "surface-secondary-dark" | "text-muted-dark" | "fg-on-brand-dark" | "accent-dark";
38
99
  /** The five named lossy cases with a row (D4) — an authored key absent falls back. */
@@ -59,7 +120,7 @@ export declare const DESIGN_COLOR_MAP: readonly DesignColorMapRow[];
59
120
  */
60
121
  export declare const DESIGN_COMPILE_LOSSY: {
61
122
  readonly L6: "accentFg, destructive and destructiveFg have no authored source and are never emitted by this compile — the partial palette inherits the platform default.";
62
- readonly L7: "typography, rounded and spacing are carried in the package and not compiled — SiteThemeSchema rejects fonts/radius/spacing by name.";
123
+ readonly L7: "typography, rounded and spacing are carried in the package and not compiled — SiteThemeSchema rejects fonts/radius/spacing by name. Three exceptions: the display role's leading pair (lineHeight, lineHeightMobile) compiles into theme.typography.display when lineHeightMobile is declared (#7720); the display/body faces a fontSource names compile to apps/web/lib/site-fonts.ts (#7720); and under treatments.typeScale: design the display, heading-section, heading-item, body and eyebrow roles' sizes, phone sizes, weights and leadings compile into theme.typography (#7739) — never their families or letterSpacing.";
63
124
  readonly L8: "an absent treatments key is omitted from the compiled theme block, not defaulted.";
64
125
  };
65
126
  /**
@@ -68,4 +129,11 @@ export declare const DESIGN_COMPILE_LOSSY: {
68
129
  * performs no I/O.
69
130
  */
70
131
  export declare function compileDesignTheme(pkg: DesignPackage): CompiledDesignTheme;
132
+ /**
133
+ * The faces a package's display and body roles source (#7720 D3.1), or
134
+ * `undefined` when neither declares `fontSource`. Pure, like
135
+ * `compileDesignTheme`; `readDesignPackage` has already constrained every
136
+ * stack it reads here.
137
+ */
138
+ export declare function compileDesignFonts(pkg: DesignPackage): CompiledDesignFonts | undefined;
71
139
  export {};
@@ -1,3 +1,32 @@
1
+ /**
2
+ * Compile a `DesignPackage` (design-package.ts) into the `theme:` block a
3
+ * site's `_site.mdx` carries (#6812 D4, spec docs/specs/6812-the-design-package-design-md-spec.md).
4
+ *
5
+ * `compileDesignTheme` is pure and idempotent: it reads `pkg` only, never
6
+ * mutates it, and has no I/O. The authored→runtime key map is the single
7
+ * exported table `DESIGN_COLOR_MAP` below — every lossy translation (a
8
+ * runtime slot the authored quartet has no direct source for) is named as a
9
+ * `fallback` + `lossy` case, and the cases with no row at all are named in
10
+ * `DESIGN_COMPILE_LOSSY`.
11
+ *
12
+ * This module imports nothing from `@working-theory/site-config` — the
13
+ * `theme:` block's shape is reproduced structurally here so `create-ailk`
14
+ * gains no runtime dependency (§ 1.5); the cross-package read that proves
15
+ * this compiles to what `SiteThemeSchema` accepts is test-only, by relative
16
+ * path, in `design-compile.test.ts`.
17
+ */
18
+ import { unquoteFamily } from "./design-package.js";
19
+ /**
20
+ * #7739 D2.2 — the fields `typeScale: design` compiles, per role, in emission
21
+ * order. Roles are emitted in this table's order.
22
+ */
23
+ const COMPILED_TYPE_FIELDS = {
24
+ display: ["fontSize", "fontSizeMobile", "fontWeight", "lineHeight", "lineHeightMobile"],
25
+ "heading-section": ["fontSize", "fontSizeMobile", "fontWeight"],
26
+ "heading-item": ["fontSize", "fontSizeMobile", "fontWeight"],
27
+ body: ["fontSize", "lineHeight"],
28
+ eyebrow: ["fontSize", "fontWeight"],
29
+ };
1
30
  /**
2
31
  * The authored→runtime key map (D4) — 18 rows, one per emitted runtime slot
3
32
  * per mode, in the overlay block's key order (bg bgMuted surface border text
@@ -62,9 +91,46 @@ export const DESIGN_COLOR_MAP = [
62
91
  */
63
92
  export const DESIGN_COMPILE_LOSSY = {
64
93
  L6: "accentFg, destructive and destructiveFg have no authored source and are never emitted by this compile — the partial palette inherits the platform default.",
65
- L7: "typography, rounded and spacing are carried in the package and not compiled — SiteThemeSchema rejects fonts/radius/spacing by name.",
94
+ L7: "typography, rounded and spacing are carried in the package and not compiled — SiteThemeSchema rejects fonts/radius/spacing by name. Three exceptions: the display role's leading pair (lineHeight, lineHeightMobile) compiles into theme.typography.display when lineHeightMobile is declared (#7720); the display/body faces a fontSource names compile to apps/web/lib/site-fonts.ts (#7720); and under treatments.typeScale: design the display, heading-section, heading-item, body and eyebrow roles' sizes, phone sizes, weights and leadings compile into theme.typography (#7739) — never their families or letterSpacing.",
66
95
  L8: "an absent treatments key is omitted from the compiled theme block, not defaulted.",
67
96
  };
97
+ /** Whether a package names the `solid-dark` surface — in its deck grounds or any section preset (#7719 D6). */
98
+ function usesSolidDark(pkg) {
99
+ if (pkg.layout.deckGrounds?.includes("solid-dark"))
100
+ return true;
101
+ return Object.values(pkg.sections).some((sections) => Object.values(sections).some((preset) => typeof preset === "object" &&
102
+ preset !== null &&
103
+ preset.surface === "solid-dark"));
104
+ }
105
+ /** The `typography` group a package compiles (#7720 D2, #7739 D2.2), or `undefined`. */
106
+ function compileTypography(pkg) {
107
+ if (pkg.treatments?.typeScale === "design") {
108
+ const result = {};
109
+ for (const [role, fields] of Object.entries(COMPILED_TYPE_FIELDS)) {
110
+ const def = pkg.typography[role];
111
+ if (def === undefined)
112
+ continue;
113
+ const compiled = {};
114
+ for (const field of fields) {
115
+ const value = def[field];
116
+ if (value !== undefined)
117
+ compiled[field] = value;
118
+ }
119
+ if (Object.keys(compiled).length > 0)
120
+ result[role] = compiled;
121
+ }
122
+ return Object.keys(result).length > 0 ? result : undefined;
123
+ }
124
+ const display = pkg.typography["display"];
125
+ if (display?.lineHeightMobile === undefined)
126
+ return undefined;
127
+ return {
128
+ display: {
129
+ ...(display.lineHeight !== undefined && { lineHeight: display.lineHeight }),
130
+ lineHeightMobile: display.lineHeightMobile,
131
+ },
132
+ };
133
+ }
68
134
  /**
69
135
  * Translate a `DesignPackage` into the `theme:` block `SiteThemeSchema.parse`
70
136
  * accepts (D4). Pure and idempotent — reads `pkg` only, mutates nothing, and
@@ -88,11 +154,41 @@ export function compileDesignTheme(pkg) {
88
154
  target[row.runtime] = value;
89
155
  }
90
156
  const treatments = pkg.treatments ?? {};
157
+ const typography = compileTypography(pkg);
158
+ // #7739 D2.3 — accent ink needs the site's own palette on the band, or the
159
+ // accent there would be the platform's.
160
+ const darkBands = usesSolidDark(pkg) || treatments.darkInk === "accent";
91
161
  return {
92
162
  ...(treatments.corners !== undefined && { corners: treatments.corners }),
93
163
  ...(treatments.eyebrowCase !== undefined && { eyebrowCase: treatments.eyebrowCase }),
94
164
  ...(treatments.placeholders !== undefined && { placeholders: treatments.placeholders }),
95
165
  ...(treatments.checkmarks !== undefined && { checkmarks: treatments.checkmarks }),
166
+ ...(treatments.buttonShape !== undefined && { buttonShape: treatments.buttonShape }),
167
+ ...(treatments.darkInk !== undefined && { darkInk: treatments.darkInk }),
168
+ ...(treatments.figures !== undefined && { figures: treatments.figures }),
169
+ ...(typography !== undefined && { typography }),
170
+ ...(darkBands && { darkBands: "site" }),
96
171
  colors: { light, dark },
97
172
  };
98
173
  }
174
+ /**
175
+ * The faces a package's display and body roles source (#7720 D3.1), or
176
+ * `undefined` when neither declares `fontSource`. Pure, like
177
+ * `compileDesignTheme`; `readDesignPackage` has already constrained every
178
+ * stack it reads here.
179
+ */
180
+ export function compileDesignFonts(pkg) {
181
+ const fonts = {};
182
+ for (const role of ["display", "body"]) {
183
+ const def = pkg.typography[role];
184
+ if (def?.fontSource === undefined || def.fontFamily === undefined)
185
+ continue;
186
+ const comma = def.fontFamily.indexOf(",");
187
+ fonts[role] = {
188
+ source: def.fontSource,
189
+ family: unquoteFamily(comma === -1 ? def.fontFamily : def.fontFamily.slice(0, comma)),
190
+ fallback: comma === -1 ? "" : def.fontFamily.slice(comma + 1).trim(),
191
+ };
192
+ }
193
+ return fonts.display !== undefined || fonts.body !== undefined ? fonts : undefined;
194
+ }
@@ -0,0 +1,74 @@
1
+ import { type SectionLookCatalog } from "./design-presets.js";
2
+ import type { ResolvedDesign } from "./design-scaffold.js";
3
+ /** Where a site's home page lives, site-relative. */
4
+ export declare const HOME_PAGE_PATH = "content/en/HomePage/home.mdx";
5
+ /** One `home/<shape>.mdx` file of a design package (D1). */
6
+ export interface HomeSeed {
7
+ readonly shape: string;
8
+ /** Absolute path, for refusals. */
9
+ readonly file: string;
10
+ readonly text: string;
11
+ }
12
+ export type HomeSeedOutcome = "seeded" | "restored" | "unchanged" | "authored" | "no-shape" | "no-home";
13
+ /**
14
+ * Read and validate `<dir>/home/*` (D1). Refuses with a `DesignPackageError`
15
+ * naming the file, before any write: a basename that is not a recognized
16
+ * shape, a page whose `type` is not `HomePage`, `sections` that cannot be
17
+ * located (or are not in the flow form a carry can splice), a section type
18
+ * the catalog does not carry, or a `logoCloud` whose `logos` is not `[]`.
19
+ */
20
+ export declare function readDesignHomeSeeds(dir: string, opts: {
21
+ catalog: () => SectionLookCatalog;
22
+ }): HomeSeed[];
23
+ /**
24
+ * The seeds a resolved design uses (D1): its own `home/`, else — a
25
+ * byte-identical copy of a shipped design IS that design — the shipped
26
+ * design's, else none.
27
+ */
28
+ export declare function designHomeSeeds(resolved: ResolvedDesign, opts: {
29
+ designsDir: string;
30
+ catalog: () => SectionLookCatalog;
31
+ }): HomeSeed[];
32
+ /** Every shipped design's seeds (D3's placeholder set reads them all). */
33
+ export declare function shippedHomeSeeds(opts: {
34
+ designsDir: string;
35
+ catalog: () => SectionLookCatalog;
36
+ }): HomeSeed[];
37
+ /** The shape's own home: its shape tree's, else the overlay's (spec § 1.4). */
38
+ export declare function shapeHomePath(shape: string, opts: {
39
+ siteTemplatesDir: string;
40
+ templatesDir: string;
41
+ }): string;
42
+ /**
43
+ * A home's content fingerprint (D3): per section in order, its type and its
44
+ * non-look `data` entries as `[key, normalized raw value]`, sorted by key.
45
+ * A reflow, a look-field change or a preset leaves it unchanged; ids, the
46
+ * title, the description and the body are not in it.
47
+ */
48
+ export declare function fingerprint(text: string, catalog: SectionLookCatalog, file?: string): string;
49
+ /**
50
+ * The seed's `sections:` value with D4's carry applied from `home`: a seed
51
+ * `logoCloud` (its `logos: []`) takes the raw `logos` of the home's first
52
+ * `logoCloud`, or is dropped when the home has none.
53
+ */
54
+ export declare function applySeed(home: string, seed: HomeSeed, file?: string): string;
55
+ /**
56
+ * Plan a site home under a design (D3). Writes nothing: `text` is the new
57
+ * home when the outcome is `seeded` or `restored`, and absent otherwise.
58
+ */
59
+ export declare function planDesignHome(opts: {
60
+ /** The site's content/en/HomePage/home.mdx, if any. */
61
+ current: string | undefined;
62
+ /** The roster's template shape. */
63
+ shape: string | undefined;
64
+ /** The applied design's seed for that shape (D1). */
65
+ target: HomeSeed | undefined;
66
+ /** The text of the shape's own home file (spec § 1.4). */
67
+ shapeHome: string;
68
+ /** Every shipped design's seeds (filtered to `shape` here). */
69
+ shippedSeeds: readonly HomeSeed[];
70
+ catalog: SectionLookCatalog;
71
+ }): {
72
+ outcome: HomeSeedOutcome;
73
+ text?: string;
74
+ };